From f432e120f4a27185936ea7d737d8febd08727158 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 8 Nov 2024 18:13:11 -0500 Subject: [PATCH] Show expansion of `@source` globs on hover --- .../tests/hover/hover.test.js | 54 ++++++++++--- .../tailwindcss-language-service/package.json | 1 + .../src/hoverProvider.ts | 76 ++++++++++++++++++- 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index ce271963..878fed21 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -157,7 +157,10 @@ withFixture('basic', (c) => { }) withFixture('v4/basic', (c) => { - async function testHover(name, { text, lang, position, expected, expectedRange, settings }) { + async function testHover( + name, + { text, exact = false, lang, position, expected, expectedRange, settings }, + ) { test.concurrent(name, async ({ expect }) => { let textDocument = await c.openDocument({ text, lang, settings }) let res = await c.sendRequest('textDocument/hover', { @@ -165,17 +168,17 @@ withFixture('v4/basic', (c) => { position, }) - expect(res).toEqual( - expected - ? { - contents: { - language: 'css', - value: expected, - }, - range: expectedRange, - } - : expected, - ) + if (!exact && expected) { + expected = { + contents: { + language: 'css', + value: expected, + }, + range: expectedRange, + } + } + + expect(res).toEqual(expected) }) } @@ -242,6 +245,33 @@ withFixture('v4/basic', (c) => { end: { line: 2, character: 18 }, }, }) + + testHover('css @source glob expansion', { + exact: true, + lang: 'css', + text: `@source "../{app,components}/**/*.jsx"`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- ../app/**/*.jsx', + '- ../components/**/*.jsx', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 8 }, + end: { line: 0, character: 38 }, + }, + }, + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 18 }, + }, + }) }) withFixture('v4/css-loading-js', (c) => { diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 3c66323d..29034fb0 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -19,6 +19,7 @@ "@types/culori": "^2.1.0", "@types/moo": "0.5.3", "@types/semver": "7.3.10", + "braces": "3.0.3", "color-name": "1.1.4", "css.escape": "1.5.1", "culori": "^4.0.1", diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 1ef981a0..5798b66f 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -1,9 +1,14 @@ import type { State } from './util/state' -import type { Hover, Position } from 'vscode-languageserver' +import type { Hover, MarkupContent, Position, Range } from 'vscode-languageserver' import { stringifyCss, stringifyConfigValue } from './util/stringify' import dlv from 'dlv' import { isCssContext } from './util/css' -import { findClassNameAtPosition, findHelperFunctionsInRange } from './util/find' +import { + findAll, + findClassNameAtPosition, + findHelperFunctionsInRange, + indexToPosition, +} from './util/find' import { validateApply } from './util/validateApply' import { getClassNameParts } from './util/getClassNameAtPosition' import * as jit from './util/jit' @@ -11,6 +16,9 @@ import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostic import { isWithinRange } from './util/isWithinRange' import type { TextDocument } from 'vscode-languageserver-textdocument' import { addPixelEquivalentsToValue } from './util/pixelEquivalents' +import { getTextWithoutComments } from './util/doc' +import braces from 'braces' +import { absoluteRange } from './util/absoluteRange' export async function doHover( state: State, @@ -19,7 +27,8 @@ export async function doHover( ): Promise { return ( (await provideClassNameHover(state, document, position)) || - (await provideCssHelperHover(state, document, position)) + (await provideCssHelperHover(state, document, position)) || + (await provideSourceGlobHover(state, document, position)) ) } @@ -133,3 +142,64 @@ async function provideClassNameHover( range: className.range, } } + +function markdown(lines: string[]): MarkupContent { + return { + kind: 'markdown', + value: lines.join('\n'), + } +} + +async function provideSourceGlobHover( + state: State, + document: TextDocument, + position: Position, +): Promise { + if (!isCssContext(state, document, position)) { + return null + } + + let range = { + start: { line: position.line, character: 0 }, + end: { line: position.line + 1, character: 0 }, + } + + let text = getTextWithoutComments(document, 'css', range) + + let pattern = /@source\s*(?'[^']+'|"[^"]+")/dg + + for (let match of findAll(pattern, text)) { + let path = match.groups.path.slice(1, -1) + + // Ignore paths that don't need brace expansion + if (!path.includes('{') || !path.includes('}')) continue + + // Ignore paths that don't contain the current position + let slice: Range = absoluteRange( + { + start: indexToPosition(text, match.indices.groups.path[0]), + end: indexToPosition(text, match.indices.groups.path[1]), + }, + range, + ) + + if (!isWithinRange(position, slice)) continue + + // Perform brace expansion + let paths = new Set(braces.expand(path)) + if (paths.size < 2) continue + + return { + range: slice, + contents: markdown([ + // + '**Expansion**', + '```plaintext', + ...Array.from(paths, (path) => `- ${path}`), + '```', + ]), + } + } + + return null +}