From 415ce3a421d84c4f7308b3066de58892669cc5e0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 31 Mar 2025 18:03:55 -0400 Subject: [PATCH 01/13] Simplify --- packages/tailwindcss-language-server/src/projects.ts | 4 ++-- packages/tailwindcss-language-server/src/tw.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 0c54942c..305a3d9c 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -102,7 +102,7 @@ export interface ProjectService { state: State tryInit: () => Promise dispose: () => Promise - onUpdateSettings: (settings: any) => void + onUpdateSettings: () => void onFileEvents: (changes: Array<{ file: string; type: FileChangeType }>) => void onHover(params: TextDocumentPositionParams): Promise onCompletion(params: CompletionParams): Promise @@ -1183,7 +1183,7 @@ export async function createProjectService( ;(await disposable).dispose() } }, - async onUpdateSettings(settings: any): Promise { + async onUpdateSettings(): Promise { if (state.enabled) { refreshDiagnostics() } diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 245fa9e5..f0f1a06f 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -655,7 +655,7 @@ export class TW { console.log(`[Global] Initialized ${enabledProjectCount} projects`) this.disposables.push( - this.connection.onDidChangeConfiguration(async ({ settings }) => { + this.connection.onDidChangeConfiguration(async () => { let previousExclude = globalSettings.tailwindCSS.files.exclude this.settingsCache.clear() @@ -668,7 +668,7 @@ export class TW { } for (let [, project] of this.projects) { - project.onUpdateSettings(settings) + project.onUpdateSettings() } }), ) From 2b9833ee16bccf89e8a2a50d30fb57b56100e33a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 31 Mar 2025 17:11:22 -0400 Subject: [PATCH 02/13] Refactor --- .../src/util/language-blocks.ts | 86 ++++++++++++------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/language-blocks.ts b/packages/tailwindcss-language-service/src/util/language-blocks.ts index 5fbc038c..1f5ed97d 100644 --- a/packages/tailwindcss-language-service/src/util/language-blocks.ts +++ b/packages/tailwindcss-language-service/src/util/language-blocks.ts @@ -1,45 +1,73 @@ import type { State } from '../util/state' -import type { Range } from 'vscode-languageserver' -import type { TextDocument } from 'vscode-languageserver-textdocument' +import type { TextDocument, Range } from 'vscode-languageserver-textdocument' import { getLanguageBoundaries } from '../util/getLanguageBoundaries' import { isCssDoc } from '../util/css' import { getTextWithoutComments } from './doc' +import { isHtmlDoc } from './html' +import { isJsDoc } from './js' export interface LanguageBlock { - document: TextDocument + context: 'html' | 'js' | 'css' | 'other' range: Range | undefined lang: string - readonly text: string + text: string } -export function* getCssBlocks( - state: State, - document: TextDocument, -): Iterable { - if (isCssDoc(state, document)) { - yield { - document, - range: undefined, - lang: document.languageId, - get text() { - return getTextWithoutComments(document, 'css') - }, - } - } else { - let boundaries = getLanguageBoundaries(state, document) - if (!boundaries) return [] +export function getDocumentBlocks(state: State, doc: TextDocument): LanguageBlock[] { + let text = doc.getText() - for (let boundary of boundaries) { - if (boundary.type !== 'css') continue + let boundaries = getLanguageBoundaries(state, doc, text) + if (boundaries && boundaries.length > 0) { + return boundaries.map((boundary) => { + let context: 'html' | 'js' | 'css' | 'other' + + if (boundary.type === 'html') { + context = 'html' + } else if (boundary.type === 'css') { + context = 'css' + } else if (boundary.type === 'js' || boundary.type === 'jsx') { + context = 'js' + } else { + context = 'other' + } - yield { - document, + let text = doc.getText(boundary.range) + + return { + context, range: boundary.range, - lang: boundary.lang ?? document.languageId, - get text() { - return getTextWithoutComments(document, 'css', boundary.range) - }, + lang: boundary.lang ?? doc.languageId, + text: context === 'other' ? text : getTextWithoutComments(text, context), } - } + }) } + + // If we get here we most likely have non-HTML document in a single language + let context: 'html' | 'js' | 'css' | 'other' + + if (isHtmlDoc(state, doc)) { + context = 'html' + } else if (isCssDoc(state, doc)) { + context = 'css' + } else if (isJsDoc(state, doc)) { + context = 'js' + } else { + context = 'other' + } + + return [ + { + context, + range: { + start: doc.positionAt(0), + end: doc.positionAt(text.length), + }, + lang: doc.languageId, + text: context === 'other' ? text : getTextWithoutComments(text, context), + }, + ] +} + +export function getCssBlocks(state: State, document: TextDocument): LanguageBlock[] { + return getDocumentBlocks(state, document).filter((block) => block.context === 'css') } From 7f160677fb77262e0f8ba7063e19b36a1de8589b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 5 Feb 2025 21:02:58 -0500 Subject: [PATCH 03/13] Collect offset-based range information --- .../tests/code-actions/conflict.json | 24 ++++++---- .../tests/diagnostics/css-conflict/css.json | 16 +++++-- .../css-conflict/jsx-concat-positive.json | 24 ++++++---- .../diagnostics/css-conflict/simple.json | 24 ++++++---- .../css-conflict/variants-positive.json | 24 ++++++---- .../css-conflict/vue-style-lang-sass.json | 16 +++++-- .../src/util/array.ts | 9 ++++ .../src/util/find.test.ts | 47 +++++++++++++++++++ .../src/util/find.ts | 43 ++++++++++++++--- .../src/util/getLanguageBoundaries.ts | 33 +++++++++++-- .../src/util/language-blocks.ts | 5 +- .../src/util/language-boundaries.test.ts | 14 ++++++ .../src/util/spans-equal.ts | 5 ++ .../src/util/state.ts | 13 +++++ 14 files changed, 244 insertions(+), 53 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/util/spans-equal.ts diff --git a/packages/tailwindcss-language-server/tests/code-actions/conflict.json b/packages/tailwindcss-language-server/tests/code-actions/conflict.json index eccb1446..55fb35a7 100644 --- a/packages/tailwindcss-language-server/tests/code-actions/conflict.json +++ b/packages/tailwindcss-language-server/tests/code-actions/conflict.json @@ -14,7 +14,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 0 }, @@ -23,7 +24,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } - } + }, + "span": [12, 21] }, "otherClassNames": [ { @@ -33,7 +35,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 10 }, @@ -42,7 +45,8 @@ "range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [22, 31] } ], "range": { @@ -92,7 +96,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 10 }, @@ -101,7 +106,8 @@ "range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [22, 31] }, "otherClassNames": [ { @@ -111,7 +117,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 0 }, @@ -120,7 +127,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } - } + }, + "span": [12, 21] } ], "range": { diff --git a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/css.json b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/css.json index da506bf1..d5706666 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/css.json +++ b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/css.json @@ -12,13 +12,15 @@ "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 34 } }, + "span": [15, 34], "important": false }, "relativeRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, - "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } } + "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } }, + "span": [15, 24] }, "otherClassNames": [ { @@ -29,6 +31,7 @@ "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 34 } }, + "span": [15, 34], "important": false }, "relativeRange": { @@ -38,7 +41,8 @@ "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 34 } - } + }, + "span": [25, 34] } ], "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } }, @@ -67,13 +71,15 @@ "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 34 } }, + "span": [15, 34], "important": false }, "relativeRange": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 19 } }, - "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 34 } } + "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 34 } }, + "span": [25, 34] }, "otherClassNames": [ { @@ -84,6 +90,7 @@ "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 34 } }, + "span": [15, 34], "important": false }, "relativeRange": { @@ -93,7 +100,8 @@ "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } - } + }, + "span": [15, 24] } ], "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 34 } }, diff --git a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/jsx-concat-positive.json b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/jsx-concat-positive.json index 39cbb515..5561d773 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/jsx-concat-positive.json +++ b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/jsx-concat-positive.json @@ -11,13 +11,15 @@ "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 36 } - } + }, + "span": [17, 36] }, "relativeRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, - "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 26 } } + "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 26 } }, + "span": [17, 26] }, "otherClassNames": [ { @@ -27,7 +29,8 @@ "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 36 } - } + }, + "span": [17, 36] }, "relativeRange": { "start": { "line": 0, "character": 10 }, @@ -36,7 +39,8 @@ "range": { "start": { "line": 0, "character": 27 }, "end": { "line": 0, "character": 36 } - } + }, + "span": [27, 36] } ], "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 26 } }, @@ -64,13 +68,15 @@ "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 36 } - } + }, + "span": [17, 36] }, "relativeRange": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 19 } }, - "range": { "start": { "line": 0, "character": 27 }, "end": { "line": 0, "character": 36 } } + "range": { "start": { "line": 0, "character": 27 }, "end": { "line": 0, "character": 36 } }, + "span": [27, 36] }, "otherClassNames": [ { @@ -80,7 +86,8 @@ "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 36 } - } + }, + "span": [17, 36] }, "relativeRange": { "start": { "line": 0, "character": 0 }, @@ -89,7 +96,8 @@ "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 26 } - } + }, + "span": [17, 26] } ], "range": { "start": { "line": 0, "character": 27 }, "end": { "line": 0, "character": 36 } }, diff --git a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/simple.json b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/simple.json index c98280a1..9f15fbcc 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/simple.json +++ b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/simple.json @@ -10,13 +10,15 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, - "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } } + "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } }, + "span": [12, 21] }, "otherClassNames": [ { @@ -26,7 +28,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 10 }, @@ -35,7 +38,8 @@ "range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [22, 31] } ], "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } }, @@ -63,13 +67,15 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 19 } }, - "range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } } + "range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } }, + "span": [22, 31] }, "otherClassNames": [ { @@ -79,7 +85,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 31 } - } + }, + "span": [12, 31] }, "relativeRange": { "start": { "line": 0, "character": 0 }, @@ -88,7 +95,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } - } + }, + "span": [12, 21] } ], "range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } }, diff --git a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/variants-positive.json b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/variants-positive.json index 15fcb457..5fbcb8ac 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/variants-positive.json +++ b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/variants-positive.json @@ -10,13 +10,15 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 37 } - } + }, + "span": [12, 37] }, "relativeRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 12 } }, - "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 24 } } + "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 24 } }, + "span": [12, 24] }, "otherClassNames": [ { @@ -26,7 +28,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 37 } - } + }, + "span": [12, 37] }, "relativeRange": { "start": { "line": 0, "character": 13 }, @@ -35,7 +38,8 @@ "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 37 } - } + }, + "span": [25, 37] } ], "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 24 } }, @@ -63,13 +67,15 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 37 } - } + }, + "span": [12, 37] }, "relativeRange": { "start": { "line": 0, "character": 13 }, "end": { "line": 0, "character": 25 } }, - "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 37 } } + "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 37 } }, + "span": [25, 37] }, "otherClassNames": [ { @@ -79,7 +85,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 37 } - } + }, + "span": [12, 37] }, "relativeRange": { "start": { "line": 0, "character": 0 }, @@ -88,7 +95,8 @@ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 24 } - } + }, + "span": [12, 24] } ], "range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 37 } }, diff --git a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/vue-style-lang-sass.json b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/vue-style-lang-sass.json index 7e9da86b..b6a3b0f5 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/vue-style-lang-sass.json +++ b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/vue-style-lang-sass.json @@ -12,13 +12,15 @@ "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 28 } }, + "span": [34, 53], "important": false }, "relativeRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, - "range": { "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 18 } } + "range": { "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 18 } }, + "span": [34, 43] }, "otherClassNames": [ { @@ -29,6 +31,7 @@ "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 28 } }, + "span": [34, 53], "important": false }, "relativeRange": { @@ -38,7 +41,8 @@ "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 28 } - } + }, + "span": [44, 53] } ], "range": { "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 18 } }, @@ -67,13 +71,15 @@ "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 28 } }, + "span": [34, 53], "important": false }, "relativeRange": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 19 } }, - "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 28 } } + "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 28 } }, + "span": [44, 53] }, "otherClassNames": [ { @@ -84,6 +90,7 @@ "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 28 } }, + "span": [34, 53], "important": false }, "relativeRange": { @@ -93,7 +100,8 @@ "range": { "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 18 } - } + }, + "span": [34, 43] } ], "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 28 } }, diff --git a/packages/tailwindcss-language-service/src/util/array.ts b/packages/tailwindcss-language-service/src/util/array.ts index 9c982640..52379e34 100644 --- a/packages/tailwindcss-language-service/src/util/array.ts +++ b/packages/tailwindcss-language-service/src/util/array.ts @@ -1,5 +1,7 @@ import type { Range } from 'vscode-languageserver' import { rangesEqual } from './rangesEqual' +import { Span } from './state' +import { spansEqual } from './spans-equal' export function dedupe(arr: Array): Array { return arr.filter((value, index, self) => self.indexOf(value) === index) @@ -16,6 +18,13 @@ export function dedupeByRange(arr: Array): Array< ) } +export function dedupeBySpan(arr: Array): Array { + return arr.filter( + (classList, classListIndex) => + classListIndex === arr.findIndex((c) => spansEqual(c.span, classList.span)), + ) +} + export function ensureArray(value: T | T[]): T[] { return Array.isArray(value) ? value : [value] } diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts index 9c65f23d..fcfddcb7 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -39,6 +39,7 @@ test('class regex works in astro', async ({ expect }) => { expect(classLists).toEqual([ { classList: 'p-4 sm:p-2 $', + span: [10, 22], range: { start: { line: 0, character: 10 }, end: { line: 0, character: 22 }, @@ -46,6 +47,7 @@ test('class regex works in astro', async ({ expect }) => { }, { classList: 'underline', + span: [33, 42], range: { start: { line: 0, character: 33 }, end: { line: 0, character: 42 }, @@ -53,6 +55,7 @@ test('class regex works in astro', async ({ expect }) => { }, { classList: 'line-through', + span: [46, 58], range: { start: { line: 0, character: 46 }, end: { line: 0, character: 58 }, @@ -111,6 +114,7 @@ test('find class lists in functions', async ({ expect }) => { // from clsx(…) { classList: 'flex p-4', + span: [45, 53], range: { start: { line: 2, character: 3 }, end: { line: 2, character: 11 }, @@ -118,6 +122,7 @@ test('find class lists in functions', async ({ expect }) => { }, { classList: 'block sm:p-0', + span: [59, 71], range: { start: { line: 3, character: 3 }, end: { line: 3, character: 15 }, @@ -125,6 +130,7 @@ test('find class lists in functions', async ({ expect }) => { }, { classList: 'text-white', + span: [96, 106], range: { start: { line: 4, character: 22 }, end: { line: 4, character: 32 }, @@ -132,6 +138,7 @@ test('find class lists in functions', async ({ expect }) => { }, { classList: 'text-black', + span: [111, 121], range: { start: { line: 4, character: 37 }, end: { line: 4, character: 47 }, @@ -141,6 +148,7 @@ test('find class lists in functions', async ({ expect }) => { // from cva(…) { classList: 'flex p-4', + span: [171, 179], range: { start: { line: 9, character: 3 }, end: { line: 9, character: 11 }, @@ -148,6 +156,7 @@ test('find class lists in functions', async ({ expect }) => { }, { classList: 'block sm:p-0', + span: [185, 197], range: { start: { line: 10, character: 3 }, end: { line: 10, character: 15 }, @@ -155,6 +164,7 @@ test('find class lists in functions', async ({ expect }) => { }, { classList: 'text-white', + span: [222, 232], range: { start: { line: 11, character: 22 }, end: { line: 11, character: 32 }, @@ -162,6 +172,7 @@ test('find class lists in functions', async ({ expect }) => { }, { classList: 'text-black', + span: [237, 247], range: { start: { line: 11, character: 37 }, end: { line: 11, character: 47 }, @@ -219,6 +230,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { expect(classLists).toMatchObject([ { classList: 'flex', + span: [193, 197], range: { start: { line: 3, character: 3 }, end: { line: 3, character: 7 }, @@ -228,6 +240,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { // TODO: This should be ignored because they're inside cn(…) { classList: 'bg-red-500', + span: [212, 222], range: { start: { line: 5, character: 5 }, end: { line: 5, character: 15 }, @@ -237,6 +250,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { // TODO: This should be ignored because they're inside cn(…) { classList: 'text-white', + span: [236, 246], range: { start: { line: 6, character: 5 }, end: { line: 6, character: 15 }, @@ -245,6 +259,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { { classList: 'fixed', + span: [286, 291], range: { start: { line: 9, character: 5 }, end: { line: 9, character: 10 }, @@ -252,6 +267,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { }, { classList: 'absolute inset-0', + span: [299, 315], range: { start: { line: 10, character: 5 }, end: { line: 10, character: 21 }, @@ -259,6 +275,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { }, { classList: 'bottom-0', + span: [335, 343], range: { start: { line: 13, character: 6 }, end: { line: 13, character: 14 }, @@ -266,6 +283,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { }, { classList: 'border', + span: [347, 353], range: { start: { line: 13, character: 18 }, end: { line: 13, character: 24 }, @@ -273,6 +291,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { }, { classList: 'bottom-0 left-0', + span: [419, 434], range: { start: { line: 17, character: 20 }, end: { line: 17, character: 35 }, @@ -280,6 +299,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { }, { classList: `inset-0\n rounded-none\n `, + span: [468, 500], range: { start: { line: 19, character: 12 }, // TODO: Fix the range calculation. Its wrong on this one @@ -321,6 +341,7 @@ test('find class lists in nested fn calls (only nested matches)', async ({ expec expect(classLists).toMatchObject([ { classList: 'fixed', + span: [228, 233], range: { start: { line: 9, character: 5 }, end: { line: 9, character: 10 }, @@ -328,6 +349,7 @@ test('find class lists in nested fn calls (only nested matches)', async ({ expec }, { classList: 'absolute inset-0', + span: [241, 257], range: { start: { line: 10, character: 5 }, end: { line: 10, character: 21 }, @@ -386,6 +408,7 @@ test('find class lists in tagged template literals', async ({ expect }) => { // from clsx`…` { classList: 'flex p-4\n block sm:p-0\n $', + span: [44, 71], range: { start: { line: 2, character: 2 }, end: { line: 4, character: 3 }, @@ -393,6 +416,7 @@ test('find class lists in tagged template literals', async ({ expect }) => { }, { classList: 'text-white', + span: [92, 102], range: { start: { line: 4, character: 24 }, end: { line: 4, character: 34 }, @@ -400,6 +424,7 @@ test('find class lists in tagged template literals', async ({ expect }) => { }, { classList: 'text-black', + span: [107, 117], range: { start: { line: 4, character: 39 }, end: { line: 4, character: 49 }, @@ -409,6 +434,7 @@ test('find class lists in tagged template literals', async ({ expect }) => { // from cva`…` { classList: 'flex p-4\n block sm:p-0\n $', + span: [166, 193], range: { start: { line: 9, character: 2 }, end: { line: 11, character: 3 }, @@ -416,6 +442,7 @@ test('find class lists in tagged template literals', async ({ expect }) => { }, { classList: 'text-white', + span: [214, 224], range: { start: { line: 11, character: 24 }, end: { line: 11, character: 34 }, @@ -423,6 +450,7 @@ test('find class lists in tagged template literals', async ({ expect }) => { }, { classList: 'text-black', + span: [229, 239], range: { start: { line: 11, character: 39 }, end: { line: 11, character: 49 }, @@ -467,6 +495,7 @@ test('classFunctions can be a regex', async ({ expect }) => { expect(classListsA).toEqual([ { classList: 'flex p-4', + span: [22, 30], range: { start: { line: 0, character: 22 }, end: { line: 0, character: 30 }, @@ -522,6 +551,7 @@ test('Finds consecutive instances of a class function', async ({ expect }) => { expect(classLists).toEqual([ { classList: 'relative flex bg-red-500', + span: [28, 52], range: { start: { line: 1, character: 6 }, end: { line: 1, character: 30 }, @@ -529,6 +559,7 @@ test('Finds consecutive instances of a class function', async ({ expect }) => { }, { classList: 'relative flex bg-red-500', + span: [62, 86], range: { start: { line: 2, character: 6 }, end: { line: 2, character: 30 }, @@ -536,6 +567,7 @@ test('Finds consecutive instances of a class function', async ({ expect }) => { }, { classList: 'relative flex bg-red-500', + span: [96, 120], range: { start: { line: 3, character: 6 }, end: { line: 3, character: 30 }, @@ -585,6 +617,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e expect(classLists).toEqual([ { classList: 'relative flex', + span: [74, 87], range: { start: { line: 3, character: 7 }, end: { line: 3, character: 20 }, @@ -592,6 +625,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e }, { classList: 'inset-0 md:h-[calc(100%-2rem)]', + span: [97, 127], range: { start: { line: 4, character: 7 }, end: { line: 4, character: 37 }, @@ -599,6 +633,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e }, { classList: 'rounded-none bg-blue-700', + span: [142, 166], range: { start: { line: 5, character: 12 }, end: { line: 5, character: 36 }, @@ -606,6 +641,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e }, { classList: 'relative flex', + span: [294, 307], range: { start: { line: 14, character: 7 }, end: { line: 14, character: 20 }, @@ -613,6 +649,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e }, { classList: 'inset-0 md:h-[calc(100%-2rem)]', + span: [317, 347], range: { start: { line: 15, character: 7 }, end: { line: 15, character: 37 }, @@ -620,6 +657,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e }, { classList: 'rounded-none bg-blue-700', + span: [362, 386], range: { start: { line: 16, character: 12 }, end: { line: 16, character: 36 }, @@ -664,6 +702,7 @@ test('classFunctions should only match in JS-like contexts', async ({ expect }) expect(classLists).toEqual([ { classList: 'relative flex', + span: [130, 143], range: { start: { line: 5, character: 16 }, end: { line: 5, character: 29 }, @@ -671,6 +710,7 @@ test('classFunctions should only match in JS-like contexts', async ({ expect }) }, { classList: 'relative flex', + span: [162, 175], range: { start: { line: 6, character: 16 }, end: { line: 6, character: 29 }, @@ -678,6 +718,7 @@ test('classFunctions should only match in JS-like contexts', async ({ expect }) }, { classList: 'relative flex', + span: [325, 338], range: { start: { line: 14, character: 16 }, end: { line: 14, character: 29 }, @@ -685,6 +726,7 @@ test('classFunctions should only match in JS-like contexts', async ({ expect }) }, { classList: 'relative flex', + span: [357, 370], range: { start: { line: 15, character: 16 }, end: { line: 15, character: 29 }, @@ -724,6 +766,7 @@ test('classAttributes find class lists inside variables in JS(X)/TS(X)', async ( expect(classLists).toEqual([ { classList: 'relative flex', + span: [24, 37], range: { start: { line: 1, character: 6 }, end: { line: 1, character: 19 }, @@ -731,6 +774,7 @@ test('classAttributes find class lists inside variables in JS(X)/TS(X)', async ( }, { classList: 'relative flex', + span: [60, 73], range: { start: { line: 3, character: 8 }, end: { line: 3, character: 21 }, @@ -738,6 +782,7 @@ test('classAttributes find class lists inside variables in JS(X)/TS(X)', async ( }, { classList: 'relative flex', + span: [102, 115], range: { start: { line: 6, character: 8 }, end: { line: 6, character: 21 }, @@ -765,6 +810,7 @@ test('classAttributes find class lists inside pug', async ({ expect }) => { expect(classLists).toEqual([ { classList: 'relative flex', + span: [15, 28], range: { start: { line: 0, character: 15 }, end: { line: 0, character: 28 }, @@ -794,6 +840,7 @@ test('classAttributes find class lists inside Vue bindings', async ({ expect }) expect(classLists).toEqual([ { classList: 'relative flex', + span: [28, 41], range: { start: { line: 1, character: 17 }, end: { line: 1, character: 30 }, diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 11f5946f..39e4b7e7 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -33,7 +33,7 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray { } export function getClassNamesInClassList( - { classList, range, important }: DocumentClassList, + { classList, span, range, important }: DocumentClassList, blocklist: State['blocklist'], ): DocumentClassName[] { const parts = classList.split(/(\s+)/) @@ -41,13 +41,16 @@ export function getClassNamesInClassList( let index = 0 for (let i = 0; i < parts.length; i++) { if (i % 2 === 0 && !blocklist.includes(parts[i])) { + const classNameSpan = [index, index + parts[i].length] const start = indexToPosition(classList, index) const end = indexToPosition(classList, index + parts[i].length) names.push({ className: parts[i], + span: [span[0] + classNameSpan[0], span[0] + classNameSpan[1]], classList: { classList, range, + span, important, }, relativeRange: { @@ -107,11 +110,19 @@ export function findClassListsInCssRange( const matches = findAll(regex, text) const globalStart: Position = range ? range.start : { line: 0, character: 0 } + const rangeStartOffset = doc.offsetAt(globalStart) + return matches.map((match) => { - const start = indexToPosition(text, match.index + match[1].length) - const end = indexToPosition(text, match.index + match[1].length + match.groups.classList.length) + let span = [ + match.index + match[1].length, + match.index + match[1].length + match.groups.classList.length, + ] as [number, number] + + const start = indexToPosition(text, span[0]) + const end = indexToPosition(text, span[1]) return { classList: match.groups.classList, + span: [rangeStartOffset + span[0], rangeStartOffset + span[1]], important: Boolean(match.groups.important), range: { start: { @@ -143,6 +154,7 @@ async function findCustomClassLists( for (let match of customClassesIn({ text, filters: regexes })) { result.push({ classList: match.classList, + span: match.range, range: { start: doc.positionAt(match.range[0]), end: doc.positionAt(match.range[1]), @@ -225,6 +237,8 @@ export async function findClassListsInHtmlRange( const existingResultSet = new Set() const results: DocumentClassList[] = [] + const rangeStartOffset = doc.offsetAt(range?.start || { line: 0, character: 0 }) + matches.forEach((match) => { const subtext = text.substr(match.index + match[0].length - 1) @@ -278,13 +292,16 @@ export async function findClassListsInHtmlRange( const after = value.match(/\s*$/) const afterOffset = after === null ? 0 : -after[0].length - const start = indexToPosition(text, match.index + match[0].length - 1 + offset + beforeOffset) - const end = indexToPosition( - text, + let span = [ + match.index + match[0].length - 1 + offset + beforeOffset, match.index + match[0].length - 1 + offset + value.length + afterOffset, - ) + ] + + const start = indexToPosition(text, span[0]) + const end = indexToPosition(text, span[1]) const result: DocumentClassList = { + span: [rangeStartOffset + span[0], rangeStartOffset + span[1]] as [number, number], classList: value.substr(beforeOffset, value.length + afterOffset), range: { start: { @@ -405,6 +422,8 @@ export function findHelperFunctionsInRange( ): DocumentHelperFunction[] { let text = getTextWithoutComments(doc, 'css', range) + let rangeStartOffset = range?.start ? doc.offsetAt(range.start) : 0 + // Find every instance of a helper function let matches = findAll(/\b(?config|theme|--theme|var)\(/g, text) @@ -573,6 +592,16 @@ export function findHelperFunctionsInRange( range, ), }, + spans: { + full: [ + rangeStartOffset + startIndex, + rangeStartOffset + startIndex + match.groups.path.length, + ], + path: [ + rangeStartOffset + startIndex + quotesBefore.length, + rangeStartOffset + startIndex + quotesBefore.length + path.length, + ], + }, }) } diff --git a/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts b/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts index 786cc2f7..cacd8eaf 100644 --- a/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts +++ b/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts @@ -4,7 +4,7 @@ import { isVueDoc, isHtmlDoc, isSvelteDoc } from './html' import type { State } from './state' import { indexToPosition } from './find' import { isJsDoc } from './js' -import moo from 'moo' +import moo, { type Rules } from 'moo' import Cache from 'tmp-cache' import { getTextWithoutComments } from './doc' import { isCssLanguage } from './css' @@ -12,6 +12,7 @@ import { isCssLanguage } from './css' export type LanguageBoundary = { type: 'html' | 'js' | 'jsx' | 'css' | (string & {}) range: Range + span: [number, number] lang?: string } @@ -29,9 +30,11 @@ let jsxScriptTypes = [ 'text/babel', ] +type States = { [x: string]: Rules } + let text = { text: { match: /[^]/, lineBreaks: true } } -let states = { +let states: States = { main: { cssBlockStart: { match: /\s])/, push: 'cssBlock' }, jsBlockStart: { match: ' { expect(boundaries).toEqual([ { type: 'html', + span: [0, 8], range: { start: { line: 0, character: 0 }, end: { line: 1, character: 2 }, @@ -55,6 +57,7 @@ test('style tags in HTML are treated as a separate boundary', ({ expect }) => { }, { type: 'css', + span: [8, 64], range: { start: { line: 1, character: 2 }, end: { line: 5, character: 2 }, @@ -62,6 +65,7 @@ test('style tags in HTML are treated as a separate boundary', ({ expect }) => { }, { type: 'html', + span: [64, 117], range: { start: { line: 5, character: 2 }, end: { line: 7, character: 6 }, @@ -91,6 +95,7 @@ test('script tags in HTML are treated as a separate boundary', ({ expect }) => { expect(boundaries).toEqual([ { type: 'html', + span: [0, 8], range: { start: { line: 0, character: 0 }, end: { line: 1, character: 2 }, @@ -98,6 +103,7 @@ test('script tags in HTML are treated as a separate boundary', ({ expect }) => { }, { type: 'js', + span: [8, 67], range: { start: { line: 1, character: 2 }, end: { line: 5, character: 2 }, @@ -105,6 +111,7 @@ test('script tags in HTML are treated as a separate boundary', ({ expect }) => { }, { type: 'html', + span: [67, 121], range: { start: { line: 5, character: 2 }, end: { line: 7, character: 6 }, @@ -140,6 +147,7 @@ test('Vue files detect