Skip to content

Commit f8313ab

Browse files
Fix problem with too many ripgrep processes being spawned by VSCode (#1287)
We run a file search on a per-workspace basis but we wait until a file has been opened. However, there are two big problems here: - A file being "opened" does **not** mean it is visible. Just that an extension has, effectively, taken an interest in it, can read its contents, etc. This happens for things like tsconfig files, some files inside a `.git` folder, etc… - We're running the search any time we see an opened document. What should happen is that we run the search when a document is opened _and visible_, the language server has not started, and we need to checking a workspace folder that has not been searched yet. This code here needs to be restructured to ensure that these searches only run when they are needed. If the searches don't return anything or time out then they should not be run again. Notifications from file watching should take care of the rest in case the initial search turned up nothing and the user adds a file that should cause the server to start. Fixes #986 (for real maybe this time??)
1 parent 6c573a8 commit f8313ab

File tree

5 files changed

+222
-161
lines changed

5 files changed

+222
-161
lines changed

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Prerelease
44

5-
- Nothing yet!
5+
- Only scan the file system once when needed ([#1287](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1287))
66

77
# 0.14.13
88

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
let configFilePath = settings.get('experimental.configFile')
16+
17+
// No setting provided
18+
if (!configFilePath) continue
19+
20+
// Ths config file may be a string:
21+
// A path pointing to a CSS or JS config file
22+
if (typeof configFilePath === 'string') return true
23+
24+
// Ths config file may be an object:
25+
// A map of config files to one or more globs
26+
//
27+
// If we get an empty object the language server will do a search anyway so
28+
// we'll act as if no option was passed to be consistent
29+
if (typeof configFilePath === 'object' && Object.values(configFilePath).length > 0) return true
30+
}
31+
32+
let configs: Array<() => Thenable<Uri[]>> = []
33+
let stylesheets: Array<() => Thenable<Uri[]>> = []
34+
35+
for (let folder of folders) {
36+
let exclusions = getExcludePatterns(folder).flatMap((pattern) => braces.expand(pattern))
37+
let exclude = `{${exclusions.join(',').replace(/{/g, '%7B').replace(/}/g, '%7D')}}`
38+
39+
configs.push(() =>
40+
workspace.findFiles(
41+
new RelativePattern(folder, `**/${CONFIG_GLOB}`),
42+
exclude,
43+
undefined,
44+
token,
45+
),
46+
)
47+
48+
stylesheets.push(() =>
49+
workspace.findFiles(new RelativePattern(folder, `**/${CSS_GLOB}`), exclude, undefined, token),
50+
)
51+
}
52+
53+
// If we find a config file then we need the server
54+
let configUrls = await Promise.all(configs.map((fn) => fn()))
55+
for (let group of configUrls) {
56+
if (group.length > 0) {
57+
return true
58+
}
59+
}
60+
61+
// If we find a possibly-related stylesheet then we need the server
62+
// The step is done last because it requires reading individual files
63+
// to determine if the server should be started.
64+
//
65+
// This is also, unfortunately, prone to starting the server unncessarily
66+
// in projects that don't use TailwindCSS so we do this one-by-one instead
67+
// of all at once to keep disk I/O low.
68+
let stylesheetUrls = await Promise.all(stylesheets.map((fn) => fn()))
69+
for (let group of stylesheetUrls) {
70+
for (let file of group) {
71+
if (await fileMayBeTailwindRelated(file)) {
72+
return true
73+
}
74+
}
75+
}
76+
}
77+
78+
let HAS_CONFIG = /@config\s*['"]/
79+
let HAS_IMPORT = /@import\s*['"]/
80+
let HAS_TAILWIND = /@tailwind\s*[^;]+;/
81+
let HAS_THEME = /@theme\s*\{/
82+
83+
export async function fileMayBeTailwindRelated(uri: Uri) {
84+
let buffer = await workspace.fs.readFile(uri)
85+
let contents = buffer.toString()
86+
87+
return (
88+
HAS_CONFIG.test(contents) ||
89+
HAS_IMPORT.test(contents) ||
90+
HAS_TAILWIND.test(contents) ||
91+
HAS_THEME.test(contents)
92+
)
93+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
let folderAnalysis: Promise<boolean> | null = null
11+
12+
async function workspaceNeedsLanguageServer() {
13+
if (folderAnalysis) return folderAnalysis
14+
15+
let source: CancellationTokenSource | null = new CancellationTokenSource()
16+
source.token.onCancellationRequested(() => {
17+
source?.dispose()
18+
source = null
19+
20+
outputChannel.appendLine(
21+
'Server was not started. Search for Tailwind CSS-related files was taking too long.',
22+
)
23+
})
24+
25+
// Cancel the search after roughly 15 seconds
26+
setTimeout(() => source?.cancel(), 15_000)
27+
context.subscriptions.push(source)
28+
29+
folderAnalysis ??= anyWorkspaceFoldersNeedServer({
30+
token: source.token,
31+
folders: workspace.workspaceFolders ?? [],
32+
})
33+
34+
let result = await folderAnalysis
35+
source?.dispose()
36+
return result
37+
}
38+
39+
async function stylesheetNeedsLanguageServer(uri: Uri) {
40+
outputChannel.appendLine(`Checking if ${uri.fsPath} may be Tailwind-related…`)
41+
42+
return fileMayBeTailwindRelated(uri)
43+
}
44+
45+
return {
46+
workspaceNeedsLanguageServer,
47+
stylesheetNeedsLanguageServer,
48+
}
49+
}
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+
}

0 commit comments

Comments
 (0)