Skip to content

Commit efb61f3

Browse files
committed
Refactor
1 parent 8b6eafa commit efb61f3

File tree

4 files changed

+185
-140
lines changed

4 files changed

+185
-140
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { workspace, RelativePattern, CancellationToken, Uri, WorkspaceFolder } from 'vscode'
2+
import braces from 'braces'
3+
import { CONFIG_GLOB, CSS_GLOB } from '@tailwindcss/language-server/src/lib/constants'
4+
import { getExcludePatterns } from './exclusions'
5+
6+
export interface SearchOptions {
7+
folders: readonly WorkspaceFolder[]
8+
token: CancellationToken
9+
}
10+
11+
export async function anyWorkspaceFoldersNeedServer({ folders, token }: SearchOptions) {
12+
// An explicit config file setting means we need the server
13+
for (let folder of folders) {
14+
let settings = workspace.getConfiguration('tailwindCSS', folder)
15+
if (settings.get('experimental.configFile') !== null) return true
16+
}
17+
18+
let configs: Array<() => Thenable<Uri[]>> = []
19+
let stylesheets: Array<() => Thenable<Uri[]>> = []
20+
21+
for (let folder of folders) {
22+
let exclusions = getExcludePatterns(folder).flatMap((pattern) => braces.expand(pattern))
23+
let exclude = `{${exclusions.join(',').replace(/{/g, '%7B').replace(/}/g, '%7D')}}`
24+
25+
configs.push(() =>
26+
workspace.findFiles(
27+
new RelativePattern(folder, `**/${CONFIG_GLOB}`),
28+
exclude,
29+
undefined,
30+
token,
31+
),
32+
)
33+
34+
stylesheets.push(() =>
35+
workspace.findFiles(new RelativePattern(folder, `**/${CSS_GLOB}`), exclude, undefined, token),
36+
)
37+
}
38+
39+
// If we find a config file then we need the server
40+
let configUrls = await Promise.all(configs.map((fn) => fn()))
41+
for (let group of configUrls) {
42+
if (group.length > 0) {
43+
return true
44+
}
45+
}
46+
47+
// If we find a possibly-related stylesheet then we need the server
48+
// The step is done last because it requires reading individual files
49+
// to determine if the server should be started.
50+
//
51+
// This is also, unfortunately, prone to starting the server unncessarily
52+
// in projects that don't use TailwindCSS so we do this one-by-one instead
53+
// of all at once to keep Disk I/O low.
54+
let stylesheetUrls = await Promise.all(stylesheets.map((fn) => fn()))
55+
for (let group of stylesheetUrls) {
56+
for (let file of group) {
57+
if (await fileMayBeTailwindRelated(file)) {
58+
return true
59+
}
60+
}
61+
}
62+
}
63+
64+
let HAS_CONFIG = /@config\s*['"]/
65+
let HAS_IMPORT = /@import\s*['"]/
66+
let HAS_TAILWIND = /@tailwind\s*[^;]+;/
67+
let HAS_THEME = /@theme\s*\{/
68+
69+
export async function fileMayBeTailwindRelated(uri: Uri) {
70+
let buffer = await workspace.fs.readFile(uri)
71+
let contents = buffer.toString()
72+
73+
return (
74+
HAS_CONFIG.test(contents) ||
75+
HAS_IMPORT.test(contents) ||
76+
HAS_TAILWIND.test(contents) ||
77+
HAS_THEME.test(contents)
78+
)
79+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { workspace, CancellationTokenSource, OutputChannel, ExtensionContext, Uri } from 'vscode'
2+
import { anyWorkspaceFoldersNeedServer, fileMayBeTailwindRelated } from './analyze'
3+
4+
interface ApiOptions {
5+
context: ExtensionContext
6+
outputChannel: OutputChannel
7+
}
8+
9+
export async function createApi({ context, outputChannel }: ApiOptions) {
10+
async function workspaceNeedsLanguageServer() {
11+
let source: CancellationTokenSource | null = new CancellationTokenSource()
12+
source.token.onCancellationRequested(() => {
13+
source?.dispose()
14+
source = null
15+
16+
outputChannel.appendLine(
17+
'Server was not started. Search for Tailwind CSS-related files was taking too long.',
18+
)
19+
})
20+
21+
// Cancel the search after roughly 15 seconds
22+
setTimeout(() => source?.cancel(), 15_000)
23+
context.subscriptions.push(source)
24+
25+
folderAnalysis = anyWorkspaceFoldersNeedServer({
26+
token: source.token,
27+
folders: workspace.workspaceFolders ?? [],
28+
})
29+
30+
let result = await folderAnalysis
31+
source?.dispose()
32+
return result
33+
}
34+
35+
async function stylesheeNeedsLanguageServer(uri: Uri) {
36+
outputChannel.appendLine(`Checking if ${uri.fsPath} may be Tailwind-related…`)
37+
38+
return fileMayBeTailwindRelated(uri)
39+
}
40+
41+
return {
42+
workspaceNeedsLanguageServer,
43+
stylesheeNeedsLanguageServer,
44+
}
45+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
workspace,
3+
type WorkspaceConfiguration,
4+
type ConfigurationScope,
5+
type WorkspaceFolder,
6+
} from 'vscode'
7+
import picomatch from 'picomatch'
8+
import * as path from 'node:path'
9+
10+
function getGlobalExcludePatterns(scope: ConfigurationScope | null): string[] {
11+
return Object.entries(workspace.getConfiguration('files', scope)?.get('exclude') ?? [])
12+
.filter(([, value]) => value === true)
13+
.map(([key]) => key)
14+
.filter(Boolean)
15+
}
16+
17+
export function getExcludePatterns(scope: ConfigurationScope | null): string[] {
18+
return [
19+
...getGlobalExcludePatterns(scope),
20+
...(<string[]>workspace.getConfiguration('tailwindCSS', scope).get('files.exclude')).filter(
21+
Boolean,
22+
),
23+
]
24+
}
25+
26+
export function isExcluded(file: string, folder: WorkspaceFolder): boolean {
27+
for (let pattern of getExcludePatterns(folder)) {
28+
let matcher = picomatch(path.join(folder.uri.fsPath, pattern))
29+
30+
if (matcher(file)) {
31+
return true
32+
}
33+
}
34+
35+
return false
36+
}
37+
38+
export function mergeExcludes(
39+
settings: WorkspaceConfiguration,
40+
scope: ConfigurationScope | null,
41+
): any {
42+
return {
43+
...settings,
44+
files: {
45+
...settings.files,
46+
exclude: getExcludePatterns(scope),
47+
},
48+
}
49+
}

packages/vscode-tailwindcss/src/extension.ts

Lines changed: 12 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import type {
44
TextDocument,
55
WorkspaceFolder,
66
ConfigurationScope,
7-
WorkspaceConfiguration,
87
Selection,
9-
CancellationToken,
108
} from 'vscode'
119
import {
1210
workspace as Workspace,
@@ -16,8 +14,6 @@ import {
1614
SymbolInformation,
1715
Position,
1816
Range,
19-
RelativePattern,
20-
CancellationTokenSource,
2117
} from 'vscode'
2218
import type {
2319
DocumentFilter,
@@ -34,11 +30,11 @@ import { languages as defaultLanguages } from '@tailwindcss/language-service/src
3430
import * as semver from '@tailwindcss/language-service/src/util/semver'
3531
import isObject from '@tailwindcss/language-service/src/util/isObject'
3632
import namedColors from 'color-name'
37-
import picomatch from 'picomatch'
3833
import { CONFIG_GLOB, CSS_GLOB } from '@tailwindcss/language-server/src/lib/constants'
39-
import braces from 'braces'
4034
import normalizePath from 'normalize-path'
4135
import * as servers from './servers/index'
36+
import { isExcluded, mergeExcludes } from './exclusions'
37+
import { createApi } from './api'
4238

4339
const colorNames = Object.keys(namedColors)
4440

@@ -52,60 +48,6 @@ function getUserLanguages(folder?: WorkspaceFolder): Record<string, string> {
5248
return isObject(langs) ? langs : {}
5349
}
5450

55-
function getGlobalExcludePatterns(scope: ConfigurationScope | null): string[] {
56-
return Object.entries(Workspace.getConfiguration('files', scope)?.get('exclude') ?? [])
57-
.filter(([, value]) => value === true)
58-
.map(([key]) => key)
59-
.filter(Boolean)
60-
}
61-
62-
function getExcludePatterns(scope: ConfigurationScope | null): string[] {
63-
return [
64-
...getGlobalExcludePatterns(scope),
65-
...(<string[]>Workspace.getConfiguration('tailwindCSS', scope).get('files.exclude')).filter(
66-
Boolean,
67-
),
68-
]
69-
}
70-
71-
function isExcluded(file: string, folder: WorkspaceFolder): boolean {
72-
for (let pattern of getExcludePatterns(folder)) {
73-
let matcher = picomatch(path.join(folder.uri.fsPath, pattern))
74-
75-
if (matcher(file)) {
76-
return true
77-
}
78-
}
79-
80-
return false
81-
}
82-
83-
function mergeExcludes(settings: WorkspaceConfiguration, scope: ConfigurationScope | null): any {
84-
return {
85-
...settings,
86-
files: {
87-
...settings.files,
88-
exclude: getExcludePatterns(scope),
89-
},
90-
}
91-
}
92-
93-
async function fileMayBeTailwindRelated(uri: Uri) {
94-
let contents = (await Workspace.fs.readFile(uri)).toString()
95-
96-
let HAS_CONFIG = /@config\s*['"]/
97-
let HAS_IMPORT = /@import\s*['"]/
98-
let HAS_TAILWIND = /@tailwind\s*[^;]+;/
99-
let HAS_THEME = /@theme\s*\{/
100-
101-
return (
102-
HAS_CONFIG.test(contents) ||
103-
HAS_IMPORT.test(contents) ||
104-
HAS_TAILWIND.test(contents) ||
105-
HAS_THEME.test(contents)
106-
)
107-
}
108-
10951
function selectionsAreEqual(
11052
aSelections: readonly Selection[],
11153
bSelections: readonly Selection[],
@@ -177,6 +119,12 @@ function resetActiveTextEditorContext(): void {
177119

178120
export async function activate(context: ExtensionContext) {
179121
let outputChannel = Window.createOutputChannel(CLIENT_NAME)
122+
123+
let api = await createApi({
124+
context,
125+
outputChannel,
126+
})
127+
180128
context.subscriptions.push(outputChannel)
181129
context.subscriptions.push(
182130
commands.registerCommand('tailwindCSS.showOutput', () => {
@@ -282,7 +230,7 @@ export async function activate(context: ExtensionContext) {
282230
if (!folder || isExcluded(uri.fsPath, folder)) {
283231
return
284232
}
285-
if (await fileMayBeTailwindRelated(uri)) {
233+
if (await api.stylesheeNeedsLanguageServer(uri)) {
286234
await bootWorkspaceClient()
287235
}
288236
}
@@ -579,87 +527,11 @@ export async function activate(context: ExtensionContext) {
579527
}
580528

581529
async function bootClientIfNeeded(): Promise<void> {
582-
if (currentClient) {
583-
return
584-
}
585-
586-
let source: CancellationTokenSource | null = new CancellationTokenSource()
587-
source.token.onCancellationRequested(() => {
588-
source?.dispose()
589-
source = null
590-
outputChannel.appendLine(
591-
'Server was not started. Search for Tailwind CSS-related files was taking too long.',
592-
)
593-
})
594-
595-
// Cancel the search after roughly 15 seconds
596-
setTimeout(() => source?.cancel(), 15_000)
530+
if (currentClient) return
597531

598-
if (!(await anyFolderNeedsLanguageServer(Workspace.workspaceFolders ?? [], source!.token))) {
599-
source?.dispose()
600-
return
601-
}
602-
603-
source?.dispose()
604-
605-
await bootWorkspaceClient()
606-
}
607-
608-
async function anyFolderNeedsLanguageServer(
609-
folders: readonly WorkspaceFolder[],
610-
token: CancellationToken,
611-
): Promise<boolean> {
612-
for (let folder of folders) {
613-
if (await folderNeedsLanguageServer(folder, token)) {
614-
return true
615-
}
616-
}
617-
618-
return false
619-
}
620-
621-
async function folderNeedsLanguageServer(
622-
folder: WorkspaceFolder,
623-
token: CancellationToken,
624-
): Promise<boolean> {
625-
let settings = Workspace.getConfiguration('tailwindCSS', folder)
626-
if (settings.get('experimental.configFile') !== null) {
627-
return true
628-
}
629-
630-
let exclude = `{${getExcludePatterns(folder)
631-
.flatMap((pattern) => braces.expand(pattern))
632-
.join(',')
633-
.replace(/{/g, '%7B')
634-
.replace(/}/g, '%7D')}}`
635-
636-
let configFiles = await Workspace.findFiles(
637-
new RelativePattern(folder, `**/${CONFIG_GLOB}`),
638-
exclude,
639-
1,
640-
token,
641-
)
642-
643-
for (let file of configFiles) {
644-
return true
645-
}
646-
647-
let cssFiles = await Workspace.findFiles(
648-
new RelativePattern(folder, `**/${CSS_GLOB}`),
649-
exclude,
650-
undefined,
651-
token,
652-
)
653-
654-
for (let file of cssFiles) {
655-
outputChannel.appendLine(`Checking if ${file.fsPath} may be Tailwind-related…`)
656-
657-
if (await fileMayBeTailwindRelated(file)) {
658-
return true
659-
}
532+
if (await api.workspaceNeedsLanguageServer()) {
533+
await bootWorkspaceClient()
660534
}
661-
662-
return false
663535
}
664536

665537
async function didOpenTextDocument(document: TextDocument): Promise<void> {

0 commit comments

Comments
 (0)