Skip to content

Commit ab0bef5

Browse files
Compute correct document selectors when a project is initialized (#1335)
Fixes #1302 Possibly helps #1322 Possibly helps #1323 This PR fixes a problem where files may fail to match against the appropriate project in a given workspace — and in some cases this behavior could be "fixed" by opening multiple files until all projects in a workspace had their selectors recomputed. (A selector is a file pattern/path paired with a "priority" that tells us how files match different projects in a workspace) The problem here is caused by a few things: - We fixed a bug where auto source detection in v4 silently failed in IntelliSense. After fixing this a file could get matched against one of the globs or file paths detected by Oxide. - A workspace with lots of CSS files may end up creating more than one "project" - Upon project initialization we would recompute these selectors **based on the resolved JS config** (necessary for v3 projects because we compile ESM or TS configs during intiialization and not discovery). Obviously, v4 projects do not have JS configs (even if you're using `@config` or `@plugin` it's not actually the config file we care about. It's the design system created from the CSS file that matters.) so we were then throwing away these document selectors. In a workspace with multiple detected projects (could be because of multiple CSS "roots", some v3 and some v4 files, etc…) we would check the file against the selectors of each project, pick out the most specific match, then initialize the project. The problem is that, when we re-computed these selectors during initialization they changed. This has the side effect of dropping the patterns that we picked up from Oxide for a v4 project. This would then cause any subsequent requests for a file to match a *different* project. So for example, a request to compute the document colors would cause a project to be matched then initialized. Then a hover in the same file would end up matching a completely different project. This PR addresses this by doing two things: 1. Using the same codepath for computing a projects document selectors during discovery and initalization 2. Normalize Windows drive letters in source paths picked up by Oxide. This would have the effect of some content paths not matching a project when it otherwise should on Windows. In the future it'd probably be a good idea to make documents "sticky" while they are open such that an open document picks a project and "sticks" to it. We'd still want to recompute this stickiness if the config a file is attached to changed but this is a future task as there might be side effects from doing this.
1 parent d907701 commit ab0bef5

File tree

4 files changed

+175
-110
lines changed

4 files changed

+175
-110
lines changed

packages/tailwindcss-language-server/src/project-locator.ts

+86-62
Original file line numberDiff line numberDiff line change
@@ -206,62 +206,7 @@ export class ProjectLocator {
206206
// Look for the package root for the config
207207
config.packageRoot = await getPackageRoot(path.dirname(config.path), this.base)
208208

209-
let selectors: DocumentSelector[] = []
210-
211-
// selectors:
212-
// - CSS files
213-
for (let entry of config.entries) {
214-
if (entry.type !== 'css') continue
215-
selectors.push({
216-
pattern: entry.path,
217-
priority: DocumentSelectorPriority.CSS_FILE,
218-
})
219-
}
220-
221-
// - Config File
222-
selectors.push({
223-
pattern: config.path,
224-
priority: DocumentSelectorPriority.CONFIG_FILE,
225-
})
226-
227-
// - Content patterns from config
228-
for await (let selector of contentSelectorsFromConfig(
229-
config,
230-
tailwind.features,
231-
this.resolver,
232-
)) {
233-
selectors.push(selector)
234-
}
235-
236-
// - Directories containing the CSS files
237-
for (let entry of config.entries) {
238-
if (entry.type !== 'css') continue
239-
selectors.push({
240-
pattern: normalizePath(path.join(path.dirname(entry.path), '**')),
241-
priority: DocumentSelectorPriority.CSS_DIRECTORY,
242-
})
243-
}
244-
245-
// - Directory containing the config
246-
selectors.push({
247-
pattern: normalizePath(path.join(path.dirname(config.path), '**')),
248-
priority: DocumentSelectorPriority.CONFIG_DIRECTORY,
249-
})
250-
251-
// - Root of package that contains the config
252-
selectors.push({
253-
pattern: normalizePath(path.join(config.packageRoot, '**')),
254-
priority: DocumentSelectorPriority.PACKAGE_DIRECTORY,
255-
})
256-
257-
// Reorder selectors from most specific to least specific
258-
selectors.sort((a, z) => a.priority - z.priority)
259-
260-
// Eliminate duplicate selector patterns
261-
selectors = selectors.filter(
262-
({ pattern }, index, documentSelectors) =>
263-
documentSelectors.findIndex(({ pattern: p }) => p === pattern) === index,
264-
)
209+
let selectors = await calculateDocumentSelectors(config, tailwind.features, this.resolver)
265210

266211
return {
267212
config,
@@ -545,13 +490,14 @@ function contentSelectorsFromConfig(
545490
entry: ConfigEntry,
546491
features: Feature[],
547492
resolver: Resolver,
493+
actualConfig?: any,
548494
): AsyncIterable<DocumentSelector> {
549495
if (entry.type === 'css') {
550496
return contentSelectorsFromCssConfig(entry, resolver)
551497
}
552498

553499
if (entry.type === 'js') {
554-
return contentSelectorsFromJsConfig(entry, features)
500+
return contentSelectorsFromJsConfig(entry, features, actualConfig)
555501
}
556502
}
557503

@@ -586,11 +532,18 @@ async function* contentSelectorsFromJsConfig(
586532
if (typeof item !== 'string') continue
587533

588534
let filepath = item.startsWith('!')
589-
? `!${path.resolve(contentBase, item.slice(1))}`
535+
? path.resolve(contentBase, item.slice(1))
590536
: path.resolve(contentBase, item)
591537

538+
filepath = normalizePath(filepath)
539+
filepath = normalizeDriveLetter(filepath)
540+
541+
if (item.startsWith('!')) {
542+
filepath = `!${filepath}`
543+
}
544+
592545
yield {
593-
pattern: normalizePath(filepath),
546+
pattern: filepath,
594547
priority: DocumentSelectorPriority.CONTENT_FILE,
595548
}
596549
}
@@ -603,8 +556,11 @@ async function* contentSelectorsFromCssConfig(
603556
let auto = false
604557
for (let item of entry.content) {
605558
if (item.kind === 'file') {
559+
let filepath = item.file
560+
filepath = normalizePath(filepath)
561+
filepath = normalizeDriveLetter(filepath)
606562
yield {
607-
pattern: normalizePath(item.file),
563+
pattern: filepath,
608564
priority: DocumentSelectorPriority.CONTENT_FILE,
609565
}
610566
} else if (item.kind === 'auto' && !auto) {
@@ -657,12 +613,16 @@ async function* detectContentFiles(
657613
if (!result) return
658614

659615
for (let file of result.files) {
660-
yield normalizePath(file)
616+
file = normalizePath(file)
617+
file = normalizeDriveLetter(file)
618+
yield file
661619
}
662620

663621
for (let { base, pattern } of result.globs) {
664622
// Do not normalize the glob itself as it may contain escape sequences
665-
yield normalizePath(base) + '/' + pattern
623+
base = normalizePath(base)
624+
base = normalizeDriveLetter(base)
625+
yield `${base}/${pattern}`
666626
}
667627
} catch {
668628
//
@@ -793,3 +753,67 @@ function requiresPreprocessor(filepath: string) {
793753

794754
return ext === '.scss' || ext === '.sass' || ext === '.less' || ext === '.styl' || ext === '.pcss'
795755
}
756+
757+
export async function calculateDocumentSelectors(
758+
config: ConfigEntry,
759+
features: Feature[],
760+
resolver: Resolver,
761+
actualConfig?: any,
762+
) {
763+
let selectors: DocumentSelector[] = []
764+
765+
// selectors:
766+
// - CSS files
767+
for (let entry of config.entries) {
768+
if (entry.type !== 'css') continue
769+
770+
selectors.push({
771+
pattern: normalizeDriveLetter(normalizePath(entry.path)),
772+
priority: DocumentSelectorPriority.CSS_FILE,
773+
})
774+
}
775+
776+
// - Config File
777+
selectors.push({
778+
pattern: normalizeDriveLetter(normalizePath(config.path)),
779+
priority: DocumentSelectorPriority.CONFIG_FILE,
780+
})
781+
782+
// - Content patterns from config
783+
for await (let selector of contentSelectorsFromConfig(config, features, resolver, actualConfig)) {
784+
selectors.push(selector)
785+
}
786+
787+
// - Directories containing the CSS files
788+
for (let entry of config.entries) {
789+
if (entry.type !== 'css') continue
790+
791+
selectors.push({
792+
pattern: normalizeDriveLetter(normalizePath(path.join(path.dirname(entry.path), '**'))),
793+
priority: DocumentSelectorPriority.CSS_DIRECTORY,
794+
})
795+
}
796+
797+
// - Directory containing the config
798+
selectors.push({
799+
pattern: normalizeDriveLetter(normalizePath(path.join(path.dirname(config.path), '**'))),
800+
priority: DocumentSelectorPriority.CONFIG_DIRECTORY,
801+
})
802+
803+
// - Root of package that contains the config
804+
selectors.push({
805+
pattern: normalizeDriveLetter(normalizePath(path.join(config.packageRoot, '**'))),
806+
priority: DocumentSelectorPriority.PACKAGE_DIRECTORY,
807+
})
808+
809+
// Reorder selectors from most specific to least specific
810+
selectors.sort((a, z) => a.priority - z.priority)
811+
812+
// Eliminate duplicate selector patterns
813+
selectors = selectors.filter(
814+
({ pattern }, index, documentSelectors) =>
815+
documentSelectors.findIndex(({ pattern: p }) => p === pattern) === index,
816+
)
817+
818+
return selectors
819+
}

packages/tailwindcss-language-server/src/projects.ts

+15-23
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ import {
8080
normalizeDriveLetter,
8181
} from './utils'
8282
import type { DocumentService } from './documents'
83-
import type { ProjectConfig } from './project-locator'
83+
import { calculateDocumentSelectors, type ProjectConfig } from './project-locator'
8484
import { supportedFeatures } from '@tailwindcss/language-service/src/features'
8585
import { loadDesignSystem } from './util/v4'
8686
import { readCssFile } from './util/css'
@@ -286,7 +286,9 @@ export async function createProjectService(
286286
)
287287
}
288288

289-
function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void {
289+
async function onFileEvents(
290+
changes: Array<{ file: string; type: FileChangeType }>,
291+
): Promise<void> {
290292
let needsInit = false
291293
let needsRebuild = false
292294

@@ -307,16 +309,11 @@ export async function createProjectService(
307309
projectConfig.configPath &&
308310
(isConfigFile || isDependency)
309311
) {
310-
documentSelector = [
311-
...documentSelector.filter(
312-
({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE,
313-
),
314-
...getContentDocumentSelectorFromConfigFile(
315-
projectConfig.configPath,
316-
initialTailwindVersion,
317-
projectConfig.folder,
318-
),
319-
]
312+
documentSelector = await calculateDocumentSelectors(
313+
projectConfig.config,
314+
state.features,
315+
resolver,
316+
)
320317

321318
checkOpenDocuments()
322319
}
@@ -963,17 +960,12 @@ export async function createProjectService(
963960

964961
/////////////////////
965962
if (!projectConfig.isUserConfigured) {
966-
documentSelector = [
967-
...documentSelector.filter(
968-
({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE,
969-
),
970-
...getContentDocumentSelectorFromConfigFile(
971-
state.configPath,
972-
tailwindcss.version,
973-
projectConfig.folder,
974-
originalConfig,
975-
),
976-
]
963+
documentSelector = await calculateDocumentSelectors(
964+
projectConfig.config,
965+
state.features,
966+
resolver,
967+
originalConfig,
968+
)
977969
}
978970
//////////////////////
979971

Original file line numberDiff line numberDiff line change
@@ -1,38 +1,85 @@
1-
import { test } from 'vitest'
2-
import { withFixture } from '../common'
1+
import { expect } from 'vitest'
2+
import { css, defineTest, html, js, json, symlinkTo } from '../../src/testing'
3+
import dedent from 'dedent'
4+
import { createClient } from '../utils/client'
35

4-
withFixture('multi-config-content', (c) => {
5-
test.concurrent('multi-config with content config - 1', async ({ expect }) => {
6-
let textDocument = await c.openDocument({ text: '<div class="bg-foo">', dir: 'one' })
7-
let res = await c.sendRequest('textDocument/hover', {
8-
textDocument,
9-
position: { line: 0, character: 13 },
6+
defineTest({
7+
name: 'multi-config with content config',
8+
fs: {
9+
'tailwind.config.one.js': js`
10+
module.exports = {
11+
content: ['./one/**/*'],
12+
theme: {
13+
extend: {
14+
colors: {
15+
foo: 'red',
16+
},
17+
},
18+
},
19+
}
20+
`,
21+
'tailwind.config.two.js': js`
22+
module.exports = {
23+
content: ['./two/**/*'],
24+
theme: {
25+
extend: {
26+
colors: {
27+
foo: 'blue',
28+
},
29+
},
30+
},
31+
}
32+
`,
33+
},
34+
prepare: async ({ root }) => ({ client: await createClient({ root }) }),
35+
handle: async ({ client }) => {
36+
let one = await client.open({
37+
lang: 'html',
38+
name: 'one/index.html',
39+
text: '<div class="bg-foo">',
1040
})
1141

12-
expect(res).toEqual({
42+
let two = await client.open({
43+
lang: 'html',
44+
name: 'two/index.html',
45+
text: '<div class="bg-foo">',
46+
})
47+
48+
// <div class="bg-foo">
49+
// ^
50+
let hoverOne = await one.hover({ line: 0, character: 13 })
51+
let hoverTwo = await two.hover({ line: 0, character: 13 })
52+
53+
expect(hoverOne).toEqual({
1354
contents: {
1455
language: 'css',
15-
value:
16-
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity, 1)) /* #ff0000 */;\n}',
56+
value: dedent`
57+
.bg-foo {
58+
--tw-bg-opacity: 1;
59+
background-color: rgb(255 0 0 / var(--tw-bg-opacity, 1)) /* #ff0000 */;
60+
}
61+
`,
62+
},
63+
range: {
64+
start: { line: 0, character: 12 },
65+
end: { line: 0, character: 18 },
1766
},
18-
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
19-
})
20-
})
21-
22-
test.concurrent('multi-config with content config - 2', async ({ expect }) => {
23-
let textDocument = await c.openDocument({ text: '<div class="bg-foo">', dir: 'two' })
24-
let res = await c.sendRequest('textDocument/hover', {
25-
textDocument,
26-
position: { line: 0, character: 13 },
2767
})
2868

29-
expect(res).toEqual({
69+
expect(hoverTwo).toEqual({
3070
contents: {
3171
language: 'css',
32-
value:
33-
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity, 1)) /* #0000ff */;\n}',
72+
value: dedent`
73+
.bg-foo {
74+
--tw-bg-opacity: 1;
75+
background-color: rgb(0 0 255 / var(--tw-bg-opacity, 1)) /* #0000ff */;
76+
}
77+
`,
78+
},
79+
range: {
80+
start: { line: 0, character: 12 },
81+
end: { line: 0, character: 18 },
3482
},
35-
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
3683
})
37-
})
84+
},
3885
})

packages/vscode-tailwindcss/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
- Handle helper function lookups in nested parens ([#1354](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1354))
1414
- Hide `@property` declarations from completion details ([#1356](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1356))
1515
- Hide variant-provided declarations from completion details for a utility ([#1356](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1356))
16+
- Compute correct document selectors when a project is initialized ([#1335](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1335))
17+
- Fix matching of some content file paths on Windows ([#1335](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1335))
1618

1719
# 0.14.16
1820

0 commit comments

Comments
 (0)