From 8ec45eb46888d21829a1a250604cac487d0a9376 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 04160569..08b26b39 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 @@ -1186,7 +1186,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 20ac0158..5a4807c7 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -643,7 +643,7 @@ export class TW { this.setupLSPHandlers() this.disposables.push( - this.connection.onDidChangeConfiguration(async ({ settings }) => { + this.connection.onDidChangeConfiguration(async () => { let previousExclude = globalSettings.tailwindCSS.files.exclude this.settingsCache.clear() @@ -656,7 +656,7 @@ export class TW { } for (let [, project] of this.projects) { - project.onUpdateSettings(settings) + project.onUpdateSettings() } }), ) From 08a200d8bcec52ac7105e3c965c9dc5264f5b778 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 10a3fe14..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 30912438588aa718e1f3b3f5249bb83aa992b765 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 839fb6d0..3befc1d4 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -29,6 +29,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 }, @@ -36,6 +37,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 }, @@ -43,6 +45,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 }, @@ -101,6 +104,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 }, @@ -108,6 +112,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 }, @@ -115,6 +120,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 }, @@ -122,6 +128,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 }, @@ -131,6 +138,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 }, @@ -138,6 +146,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 }, @@ -145,6 +154,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 }, @@ -152,6 +162,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 }, @@ -209,6 +220,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 }, @@ -218,6 +230,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 }, @@ -227,6 +240,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 }, @@ -235,6 +249,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 }, @@ -242,6 +257,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 }, @@ -249,6 +265,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 }, @@ -256,6 +273,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 }, @@ -263,6 +281,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 }, @@ -270,6 +289,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 @@ -311,6 +331,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 }, @@ -318,6 +339,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 }, @@ -376,6 +398,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 }, @@ -383,6 +406,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 }, @@ -390,6 +414,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 }, @@ -399,6 +424,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 }, @@ -406,6 +432,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 }, @@ -413,6 +440,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 }, @@ -457,6 +485,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 }, @@ -512,6 +541,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 }, @@ -519,6 +549,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 }, @@ -526,6 +557,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 }, @@ -575,6 +607,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 }, @@ -582,6 +615,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 }, @@ -589,6 +623,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 }, @@ -596,6 +631,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 }, @@ -603,6 +639,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 }, @@ -610,6 +647,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 }, @@ -654,6 +692,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 }, @@ -661,6 +700,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 }, @@ -668,6 +708,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 }, @@ -675,6 +716,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 }, @@ -714,6 +756,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 }, @@ -721,6 +764,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 }, @@ -728,6 +772,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 }, @@ -755,6 +800,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 }, @@ -784,6 +830,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 9118403d..f7f081d9 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: { @@ -409,6 +426,8 @@ export function findHelperFunctionsInRange( text, ) + let rangeStartOffset = range?.start ? doc.offsetAt(range.start) : 0 + // Eliminate matches that are on an `@import` matches = matches.filter((match) => { // Scan backwards to see if we're in an `@import` statement @@ -477,6 +496,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 42a4a495..f794d2b4 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