diff --git a/packages/tailwindcss-language-server/src/config.ts b/packages/tailwindcss-language-server/src/config.ts index d8364d06..77fedaf1 100644 --- a/packages/tailwindcss-language-server/src/config.ts +++ b/packages/tailwindcss-language-server/src/config.ts @@ -17,6 +17,7 @@ function getDefaultSettings(): Settings { classAttributes: ['class', 'className', 'ngClass', 'class:list'], codeActions: true, hovers: true, + annotations: false, suggestions: true, validate: true, colorDecorators: true, diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index b55ee078..98a0fccb 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -17,7 +17,7 @@ import type { DocumentLink, } from 'vscode-languageserver/node' import { FileChangeType } from 'vscode-languageserver/node' -import type { TextDocument } from 'vscode-languageserver-textdocument' +import type { Range, TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import { showError, showWarning, SilentError } from './util/error' import * as path from 'node:path' @@ -35,6 +35,7 @@ import stackTrace from 'stack-trace' import extractClassNames from './lib/extractClassNames' import { klona } from 'klona/full' import { doHover } from '@tailwindcss/language-service/src/hoverProvider' +import { updateAnnotation } from '@tailwindcss/language-service/src/annotation' import { Resolver } from './resolver' import { doComplete, @@ -106,6 +107,7 @@ export interface ProjectService { onCompletionResolve(item: CompletionItem): Promise provideDiagnostics(document: TextDocument): void provideDiagnosticsForce(document: TextDocument): void + provideAnnotations(document: TextDocument): Promise onDocumentColor(params: DocumentColorParams): Promise onColorPresentation(params: ColorPresentationParams): Promise onCodeAction(params: CodeActionParams): Promise @@ -480,6 +482,9 @@ export async function createProjectService( postcss: { version: null, module: null }, resolveConfig: { module: null }, loadConfig: { module: null }, + defaultExtractor: { + module: require('tailwindcss/lib/lib/defaultExtractor').defaultExtractor, + }, } return tryRebuild() @@ -693,6 +698,9 @@ export async function createProjectService( postcss: { version: null, module: null }, resolveConfig: { module: null }, loadConfig: { module: null }, + defaultExtractor: { + module: require('tailwindcss/lib/lib/defaultExtractor').defaultExtractor, + }, } return tryRebuild() @@ -733,6 +741,9 @@ export async function createProjectService( loadConfig: { module: loadConfigFn }, transformThemeValue: { module: transformThemeValueFn }, jit: jitModules, + defaultExtractor: { + module: require('tailwindcss/lib/lib/defaultExtractor').defaultExtractor, + }, } state.browserslist = browserslist state.featureFlags = featureFlags @@ -1231,6 +1242,20 @@ export async function createProjectService( if (!state.enabled) return provideDiagnostics(state, document) }, + provideAnnotations: async (params) => { + try { + if (!state.enabled) return [] + let document = documentService.getDocument(params.uri) + if (!document) return [] + let settings = await state.editor.getConfiguration(document.uri) + if (!settings.tailwindCSS.annotations) return [] + if (await isExcluded(state, document)) return [] + return updateAnnotation(state, params) + } catch (error) { + console.error(error) + return [] + } + }, async onDocumentColor(params: DocumentColorParams): Promise { return withFallback(async () => { if (!state.enabled) return [] diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index efb12a34..92f43bc0 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -624,6 +624,16 @@ export class TW { this.disposables.push( this.documentService.onDidChangeContent((change) => { this.getProject(change.document)?.provideDiagnostics(change.document) + + const { document } = change + this.getProject(document) + ?.provideAnnotations(document) + .then((annotations) => { + this.connection.sendRequest('@/tailwindCSS/annotations', { + uri: document.uri, + annotations, + }) + }) }), ) @@ -644,9 +654,30 @@ export class TW { await this.connection.sendNotification('@/tailwindCSS/documentReady', { uri: event.document.uri, }) + + const { document } = event + this.getProject(document) + ?.provideAnnotations(document) + .then((annotations) => { + this.connection.sendRequest('@/tailwindCSS/annotations', { + uri: document.uri, + annotations, + }) + }) }), ) + this.documentService.getAllDocuments().forEach((document) => { + this.getProject(document) + ?.provideAnnotations(document) + .then((annotations) => { + this.connection.sendRequest('@/tailwindCSS/annotations', { + uri: document.uri, + annotations, + }) + }) + }) + if (this.initializeParams.capabilities.workspace.workspaceFolders) { this.disposables.push( this.connection.workspace.onDidChangeWorkspaceFolders(async (evt) => { diff --git a/packages/tailwindcss-language-service/src/annotation.ts b/packages/tailwindcss-language-service/src/annotation.ts new file mode 100644 index 00000000..4017cce8 --- /dev/null +++ b/packages/tailwindcss-language-service/src/annotation.ts @@ -0,0 +1,51 @@ +import type { Range, TextDocument } from 'vscode-languageserver-textdocument' +import type { State } from './util/state' + +export async function updateAnnotation(state: State, document: TextDocument): Promise { + const text = document.getText() + + const extractorContext = { + tailwindConfig: { + separator: '-', + prefix: '', + }, + } + if (state.jitContext?.tailwindConfig?.separator) { + extractorContext.tailwindConfig.separator = state.jitContext.tailwindConfig.separator + } + if (state.jitContext?.tailwindConfig?.prefix) { + extractorContext.tailwindConfig.prefix = state.jitContext.tailwindConfig.prefix + } + + const classNames = state.modules.defaultExtractor.module(extractorContext)(text) as string[] + + const result: Range[] = [] + + if (state.v4) { + const rules = state.designSystem.compile(classNames) + + let index = 0 + classNames.forEach((className, i) => { + const start = text.indexOf(className, index) + const end = start + className.length + if (rules.at(i).nodes.length > 0 && start !== -1) { + result.push({ start: document.positionAt(start), end: document.positionAt(end) }) + } + index = end + }) + } else if (state.jit) { + const rules = state.modules.jit.generateRules.module(classNames, state.jitContext) + + let index = 0 + classNames.forEach((className) => { + const start = text.indexOf(className, index) + const end = start + className.length + if (rules?.find(([, i]) => (i.raws.tailwind as any)?.candidate === className)) { + result.push({ start: document.positionAt(start), end: document.positionAt(end) }) + } + index = end + }) + } + + return result +} diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 95afe8ec..1ecdd033 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -48,6 +48,7 @@ export type TailwindCssSettings = { classAttributes: string[] suggestions: boolean hovers: boolean + annotations: boolean codeActions: boolean validate: boolean showPixelEquivalents: boolean @@ -124,6 +125,7 @@ export interface State { expandApplyAtRules: { module: any } evaluateTailwindFunctions?: { module: any } } + defaultExtractor: { module: any } } v4?: boolean diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 08906fbd..88c16cc5 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -196,6 +196,12 @@ "markdownDescription": "Enable hovers.", "scope": "language-overridable" }, + "tailwindCSS.annotations": { + "type": "boolean", + "default": false, + "markdownDescription": "Enable annotations.", + "scope": "language-overridable" + }, "tailwindCSS.codeActions": { "type": "boolean", "default": true, diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index c0c7b9e7..f825075e 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -16,6 +16,7 @@ import { Position, Range, RelativePattern, + DecorationRangeBehavior, } from 'vscode' import type { DocumentFilter, @@ -356,6 +357,13 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(colorDecorationType) + let underlineDecorationType = Window.createTextEditorDecorationType({ + textDecoration: 'none; border-bottom: 1px dashed currentColor', + rangeBehavior: DecorationRangeBehavior.ClosedClosed, + }) + + context.subscriptions.push(underlineDecorationType) + /** * Clear all decorated colors from all visible text editors */ @@ -537,6 +545,11 @@ export async function activate(context: ExtensionContext) { client.onNotification('@/tailwindCSS/projectInitialized', updateActiveTextEditorContext) client.onNotification('@/tailwindCSS/projectReset', updateActiveTextEditorContext) client.onNotification('@/tailwindCSS/projectsDestroyed', resetActiveTextEditorContext) + client.onRequest('@/tailwindCSS/annotations', ({ uri, annotations }) => { + Window.visibleTextEditors + .find((editor) => editor.document.uri.toString() === uri) + ?.setDecorations(underlineDecorationType, annotations) + }) client.onRequest('@/tailwindCSS/getDocumentSymbols', showSymbols) interface ErrorNotification {