Skip to content

Commit 4a3e09d

Browse files
committed
Refactor language service
This will set us up for more direct, language-service specific testing. This is very much a work in progress but the ultimate goal is for the majority of language server tests to be able to run against both the language service _and_ language server
1 parent 7f16067 commit 4a3e09d

File tree

8 files changed

+401
-170
lines changed

8 files changed

+401
-170
lines changed
Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import type { TextDocument } from 'vscode-languageserver-textdocument'
22
import type { State } from '@tailwindcss/language-service/src/util/state'
3-
import { doValidate } from '@tailwindcss/language-service/src/diagnostics/diagnosticsProvider'
4-
import isExcluded from '../util/isExcluded'
3+
import type { LanguageService } from '@tailwindcss/language-service/src/service'
54

6-
export async function provideDiagnostics(state: State, document: TextDocument) {
7-
if (await isExcluded(state, document)) {
8-
clearDiagnostics(state, document)
9-
} else {
10-
state.editor?.connection.sendDiagnostics({
11-
uri: document.uri,
12-
diagnostics: await doValidate(state, document),
13-
})
14-
}
15-
}
5+
export async function provideDiagnostics(
6+
service: LanguageService,
7+
state: State,
8+
document: TextDocument,
9+
) {
10+
if (!state.enabled) return
11+
let doc = await service.open(document.uri)
12+
let diagnostics = await doc?.diagnostics()
1613

17-
export function clearDiagnostics(state: State, document: TextDocument): void {
1814
state.editor?.connection.sendDiagnostics({
1915
uri: document.uri,
20-
diagnostics: [],
16+
diagnostics: diagnostics ?? [],
2117
})
2218
}

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

Lines changed: 113 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,8 @@ import pkgUp from 'pkg-up'
3636
import stackTrace from 'stack-trace'
3737
import extractClassNames from './lib/extractClassNames'
3838
import { klona } from 'klona/full'
39-
import { doHover } from '@tailwindcss/language-service/src/hoverProvider'
40-
import { getCodeLens } from '@tailwindcss/language-service/src/codeLensProvider'
39+
import { createLanguageService } from '@tailwindcss/language-service/src/service'
4140
import { Resolver } from './resolver'
42-
import {
43-
doComplete,
44-
resolveCompletionItem,
45-
} from '@tailwindcss/language-service/src/completionProvider'
4641
import type {
4742
State,
4843
FeatureFlags,
@@ -52,17 +47,12 @@ import type {
5247
ClassEntry,
5348
} from '@tailwindcss/language-service/src/util/state'
5449
import { provideDiagnostics } from './lsp/diagnosticsProvider'
55-
import { doCodeActions } from '@tailwindcss/language-service/src/codeActions/codeActionProvider'
56-
import { getDocumentColors } from '@tailwindcss/language-service/src/documentColorProvider'
57-
import { getDocumentLinks } from '@tailwindcss/language-service/src/documentLinksProvider'
5850
import { debounce } from 'debounce'
5951
import { getModuleDependencies } from './util/getModuleDependencies'
6052
import assert from 'node:assert'
6153
// import postcssLoadConfig from 'postcss-load-config'
6254
import { bigSign } from '@tailwindcss/language-service/src/util/jit'
6355
import { getColor } from '@tailwindcss/language-service/src/util/color'
64-
import * as culori from 'culori'
65-
import namedColors from 'color-name'
6656
import tailwindPlugins from './lib/plugins'
6757
import isExcluded from './util/isExcluded'
6858
import { getFileFsPath } from './util/uri'
@@ -72,7 +62,6 @@ import {
7262
firstOptional,
7363
withoutLogs,
7464
clearRequireCache,
75-
withFallback,
7665
isObject,
7766
pathToFileURL,
7867
changeAffectsFile,
@@ -85,8 +74,7 @@ import { supportedFeatures } from '@tailwindcss/language-service/src/features'
8574
import { loadDesignSystem } from './util/v4'
8675
import { readCssFile } from './util/css'
8776
import type { DesignSystem } from '@tailwindcss/language-service/src/util/v4'
88-
89-
const colorNames = Object.keys(namedColors)
77+
import type { File, FileType } from '@tailwindcss/language-service/src/fs'
9078

9179
function getConfigId(configPath: string, configDependencies: string[]): string {
9280
return JSON.stringify(
@@ -234,36 +222,71 @@ export async function createProjectService(
234222
getDocumentSymbols: (uri: string) => {
235223
return connection.sendRequest('@/tailwindCSS/getDocumentSymbols', { uri })
236224
},
237-
async readDirectory(document, directory) {
225+
async readDirectory() {
226+
// NOTE: This is overwritten in `createLanguageDocument`
227+
throw new Error('Not implemented')
228+
},
229+
},
230+
}
231+
232+
let service = createLanguageService({
233+
state: () => state,
234+
fs: {
235+
async document(uri: string) {
236+
return documentService.getDocument(uri)
237+
},
238+
async resolve(document: TextDocument, relativePath: string): Promise<string | null> {
239+
let documentPath = URI.parse(document.uri).fsPath
240+
let baseDir = path.dirname(documentPath)
241+
242+
let resolved = await resolver.substituteId(relativePath, baseDir)
243+
resolved ??= relativePath
244+
245+
return URI.file(path.resolve(baseDir, resolved)).toString()
246+
},
247+
248+
async readDirectory(document: TextDocument, filepath: string): Promise<File[]> {
238249
try {
239250
let baseDir = path.dirname(getFileFsPath(document.uri))
240-
directory = await resolver.substituteId(`${directory}/`, baseDir)
241-
directory = path.resolve(baseDir, directory)
242-
243-
let dirents = await fs.promises.readdir(directory, { withFileTypes: true })
244-
245-
let result: Array<[string, { isDirectory: boolean }] | null> = await Promise.all(
246-
dirents.map(async (dirent) => {
247-
let isDirectory = dirent.isDirectory()
248-
let shouldRemove = await isExcluded(
249-
state,
250-
document,
251-
path.join(directory, dirent.name, isDirectory ? '/' : ''),
252-
)
251+
filepath = await resolver.substituteId(`${filepath}/`, baseDir)
252+
filepath = path.resolve(baseDir, filepath)
253253

254-
if (shouldRemove) return null
254+
let dirents = await fs.promises.readdir(filepath, { withFileTypes: true })
255255

256-
return [dirent.name, { isDirectory }]
257-
}),
258-
)
256+
let results: File[] = []
257+
258+
for (let dirent of dirents) {
259+
let isDirectory = dirent.isDirectory()
260+
let shouldRemove = await isExcluded(
261+
state,
262+
document,
263+
path.join(filepath, dirent.name, isDirectory ? '/' : ''),
264+
)
265+
if (shouldRemove) continue
266+
267+
let type: FileType = 'unknown'
259268

260-
return result.filter((item) => item !== null)
269+
if (dirent.isFile()) {
270+
type = 'file'
271+
} else if (dirent.isDirectory()) {
272+
type = 'directory'
273+
} else if (dirent.isSymbolicLink()) {
274+
type = 'symbolic-link'
275+
}
276+
277+
results.push({
278+
name: dirent.name,
279+
type,
280+
})
281+
}
282+
283+
return results
261284
} catch {
262285
return []
263286
}
264287
},
265288
},
266-
}
289+
})
267290

268291
if (projectConfig.configPath && projectConfig.config.source === 'js') {
269292
let deps = []
@@ -1193,139 +1216,79 @@ export async function createProjectService(
11931216
},
11941217
onFileEvents,
11951218
async onHover(params: TextDocumentPositionParams): Promise<Hover> {
1196-
return withFallback(async () => {
1197-
if (!state.enabled) return null
1198-
let document = documentService.getDocument(params.textDocument.uri)
1199-
if (!document) return null
1200-
let settings = await state.editor.getConfiguration(document.uri)
1201-
if (!settings.tailwindCSS.hovers) return null
1202-
if (await isExcluded(state, document)) return null
1203-
return doHover(state, document, params.position)
1204-
}, null)
1219+
try {
1220+
let doc = await service.open(params.textDocument.uri)
1221+
if (!doc) return null
1222+
return doc.hover(params.position)
1223+
} catch {
1224+
return null
1225+
}
12051226
},
12061227
async onCodeLens(params: CodeLensParams): Promise<CodeLens[]> {
1207-
return withFallback(async () => {
1208-
if (!state.enabled) return null
1209-
let document = documentService.getDocument(params.textDocument.uri)
1210-
if (!document) return null
1211-
let settings = await state.editor.getConfiguration(document.uri)
1212-
if (!settings.tailwindCSS.codeLens) return null
1213-
if (await isExcluded(state, document)) return null
1214-
return getCodeLens(state, document)
1215-
}, null)
1228+
try {
1229+
let doc = await service.open(params.textDocument.uri)
1230+
if (!doc) return null
1231+
return doc.codeLenses()
1232+
} catch {
1233+
return []
1234+
}
12161235
},
12171236
async onCompletion(params: CompletionParams): Promise<CompletionList> {
1218-
return withFallback(async () => {
1219-
if (!state.enabled) return null
1220-
let document = documentService.getDocument(params.textDocument.uri)
1221-
if (!document) return null
1222-
let settings = await state.editor.getConfiguration(document.uri)
1223-
if (!settings.tailwindCSS.suggestions) return null
1224-
if (await isExcluded(state, document)) return null
1225-
return doComplete(state, document, params.position, params.context)
1226-
}, null)
1237+
try {
1238+
let doc = await service.open(params.textDocument.uri)
1239+
if (!doc) return null
1240+
return doc.completions(params.position)
1241+
} catch {
1242+
return null
1243+
}
12271244
},
1228-
onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
1229-
return withFallback(() => {
1230-
if (!state.enabled) return null
1231-
return resolveCompletionItem(state, item)
1232-
}, null)
1245+
async onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
1246+
try {
1247+
return await service.resolveCompletion(item)
1248+
} catch {
1249+
return null
1250+
}
12331251
},
12341252
async onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {
1235-
return withFallback(async () => {
1236-
if (!state.enabled) return null
1237-
let document = documentService.getDocument(params.textDocument.uri)
1238-
if (!document) return null
1239-
let settings = await state.editor.getConfiguration(document.uri)
1240-
if (!settings.tailwindCSS.codeActions) return null
1241-
return doCodeActions(state, params, document)
1242-
}, null)
1253+
try {
1254+
let doc = await service.open(params.textDocument.uri)
1255+
if (!doc) return null
1256+
return doc.codeActions(params.range, params.context)
1257+
} catch {
1258+
return []
1259+
}
12431260
},
1244-
onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
1245-
if (!state.enabled) return null
1246-
let document = documentService.getDocument(params.textDocument.uri)
1247-
if (!document) return null
1248-
1249-
let documentPath = URI.parse(document.uri).fsPath
1250-
let baseDir = path.dirname(documentPath)
1251-
1252-
async function resolveTarget(linkPath: string) {
1253-
linkPath = (await resolver.substituteId(linkPath, baseDir)) ?? linkPath
1254-
1255-
return URI.file(path.resolve(baseDir, linkPath)).toString()
1261+
async onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
1262+
try {
1263+
let doc = await service.open(params.textDocument.uri)
1264+
if (!doc) return null
1265+
return doc.documentLinks()
1266+
} catch {
1267+
return []
12561268
}
1257-
1258-
return getDocumentLinks(state, document, resolveTarget)
12591269
},
12601270
provideDiagnostics: debounce(
1261-
(document: TextDocument) => {
1262-
if (!state.enabled) return
1263-
provideDiagnostics(state, document)
1264-
},
1271+
(document) => provideDiagnostics(service, state, document),
12651272
params.initializationOptions?.testMode ? 0 : 500,
12661273
),
1267-
provideDiagnosticsForce: (document: TextDocument) => {
1268-
if (!state.enabled) return
1269-
provideDiagnostics(state, document)
1270-
},
1274+
provideDiagnosticsForce: (document) => provideDiagnostics(service, state, document),
12711275
async onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]> {
1272-
return withFallback(async () => {
1273-
if (!state.enabled) return []
1274-
let document = documentService.getDocument(params.textDocument.uri)
1275-
if (!document) return []
1276-
if (await isExcluded(state, document)) return null
1277-
return getDocumentColors(state, document)
1278-
}, null)
1276+
try {
1277+
let doc = await service.open(params.textDocument.uri)
1278+
if (!doc) return null
1279+
return doc.documentColors()
1280+
} catch {
1281+
return []
1282+
}
12791283
},
12801284
async onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]> {
1281-
let document = documentService.getDocument(params.textDocument.uri)
1282-
if (!document) return []
1283-
let className = document.getText(params.range)
1284-
let match = className.match(
1285-
new RegExp(`-\\[(${colorNames.join('|')}|(?:(?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$`, 'i'),
1286-
)
1287-
// let match = className.match(/-\[((?:#|rgba?\(|hsla?\()[^\]]+)\]$/i)
1288-
if (match === null) return []
1289-
1290-
let currentColor = match[1]
1291-
1292-
let isNamedColor = colorNames.includes(currentColor)
1293-
1294-
let color: culori.Color = {
1295-
mode: 'rgb',
1296-
r: params.color.red,
1297-
g: params.color.green,
1298-
b: params.color.blue,
1299-
alpha: params.color.alpha,
1300-
}
1301-
1302-
let hexValue = culori.formatHex8(color)
1303-
1304-
if (!isNamedColor && (currentColor.length === 4 || currentColor.length === 5)) {
1305-
let [, ...chars] =
1306-
hexValue.match(/^#([a-f\d])\1([a-f\d])\2([a-f\d])\3(?:([a-f\d])\4)?$/i) ?? []
1307-
if (chars.length) {
1308-
hexValue = `#${chars.filter(Boolean).join('')}`
1309-
}
1310-
}
1311-
1312-
if (hexValue.length === 5) {
1313-
hexValue = hexValue.replace(/f$/, '')
1314-
} else if (hexValue.length === 9) {
1315-
hexValue = hexValue.replace(/ff$/, '')
1285+
try {
1286+
let doc = await service.open(params.textDocument.uri)
1287+
if (!doc) return null
1288+
return doc.colorPresentation(params.color, params.range)
1289+
} catch {
1290+
return []
13161291
}
1317-
1318-
let prefix = className.substr(0, match.index)
1319-
1320-
return [
1321-
hexValue,
1322-
culori.formatRgb(color).replace(/ /g, ''),
1323-
culori
1324-
.formatHsl(color)
1325-
.replace(/ /g, '')
1326-
// round numbers
1327-
.replace(/\d+\.\d+(%?)/g, (value, suffix) => `${Math.round(parseFloat(value))}${suffix}`),
1328-
].map((value) => ({ label: `${prefix}-[${value}]` }))
13291292
},
13301293
sortClassLists(classLists: string[]): string[] {
13311294
if (!state.jit) {

packages/tailwindcss-language-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@
4646
"@types/dedent": "^0.7.2",
4747
"@types/line-column": "^1.0.2",
4848
"@types/node": "^18.19.33",
49+
"@types/picomatch": "^2.3.3",
4950
"@types/stringify-object": "^4.0.5",
5051
"dedent": "^1.5.3",
5152
"esbuild": "^0.25.5",
5253
"esbuild-node-externals": "^1.9.0",
5354
"minimist": "^1.2.8",
55+
"picomatch": "^4.0.1",
5456
"tslib": "2.2.0",
5557
"typescript": "^5.8.3",
5658
"vite": "^6.3.5",

0 commit comments

Comments
 (0)