Skip to content

Commit a4d2549

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 e858774 commit a4d2549

File tree

8 files changed

+381
-170
lines changed

8 files changed

+381
-170
lines changed
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
}

Diff for: packages/tailwindcss-language-server/src/projects.ts

+113-150
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 = []
@@ -1187,139 +1210,79 @@ export async function createProjectService(
11871210
},
11881211
onFileEvents,
11891212
async onHover(params: TextDocumentPositionParams): Promise<Hover> {
1190-
return withFallback(async () => {
1191-
if (!state.enabled) return null
1192-
let document = documentService.getDocument(params.textDocument.uri)
1193-
if (!document) return null
1194-
let settings = await state.editor.getConfiguration(document.uri)
1195-
if (!settings.tailwindCSS.hovers) return null
1196-
if (await isExcluded(state, document)) return null
1197-
return doHover(state, document, params.position)
1198-
}, null)
1213+
try {
1214+
let doc = await service.open(params.textDocument.uri)
1215+
if (!doc) return null
1216+
return doc.hover(params.position)
1217+
} catch {
1218+
return null
1219+
}
11991220
},
12001221
async onCodeLens(params: CodeLensParams): Promise<CodeLens[]> {
1201-
return withFallback(async () => {
1202-
if (!state.enabled) return null
1203-
let document = documentService.getDocument(params.textDocument.uri)
1204-
if (!document) return null
1205-
let settings = await state.editor.getConfiguration(document.uri)
1206-
if (!settings.tailwindCSS.codeLens) return null
1207-
if (await isExcluded(state, document)) return null
1208-
return getCodeLens(state, document)
1209-
}, null)
1222+
try {
1223+
let doc = await service.open(params.textDocument.uri)
1224+
if (!doc) return null
1225+
return doc.codeLenses()
1226+
} catch {
1227+
return []
1228+
}
12101229
},
12111230
async onCompletion(params: CompletionParams): Promise<CompletionList> {
1212-
return withFallback(async () => {
1213-
if (!state.enabled) return null
1214-
let document = documentService.getDocument(params.textDocument.uri)
1215-
if (!document) return null
1216-
let settings = await state.editor.getConfiguration(document.uri)
1217-
if (!settings.tailwindCSS.suggestions) return null
1218-
if (await isExcluded(state, document)) return null
1219-
return doComplete(state, document, params.position, params.context)
1220-
}, null)
1231+
try {
1232+
let doc = await service.open(params.textDocument.uri)
1233+
if (!doc) return null
1234+
return doc.completions(params.position)
1235+
} catch {
1236+
return null
1237+
}
12211238
},
1222-
onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
1223-
return withFallback(() => {
1224-
if (!state.enabled) return null
1225-
return resolveCompletionItem(state, item)
1226-
}, null)
1239+
async onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
1240+
try {
1241+
return await service.resolveCompletion(item)
1242+
} catch {
1243+
return null
1244+
}
12271245
},
12281246
async onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {
1229-
return withFallback(async () => {
1230-
if (!state.enabled) return null
1231-
let document = documentService.getDocument(params.textDocument.uri)
1232-
if (!document) return null
1233-
let settings = await state.editor.getConfiguration(document.uri)
1234-
if (!settings.tailwindCSS.codeActions) return null
1235-
return doCodeActions(state, params, document)
1236-
}, null)
1247+
try {
1248+
let doc = await service.open(params.textDocument.uri)
1249+
if (!doc) return null
1250+
return doc.codeActions(params.range, params.context)
1251+
} catch {
1252+
return []
1253+
}
12371254
},
1238-
onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
1239-
if (!state.enabled) return null
1240-
let document = documentService.getDocument(params.textDocument.uri)
1241-
if (!document) return null
1242-
1243-
let documentPath = URI.parse(document.uri).fsPath
1244-
let baseDir = path.dirname(documentPath)
1245-
1246-
async function resolveTarget(linkPath: string) {
1247-
linkPath = (await resolver.substituteId(linkPath, baseDir)) ?? linkPath
1248-
1249-
return URI.file(path.resolve(baseDir, linkPath)).toString()
1255+
async onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
1256+
try {
1257+
let doc = await service.open(params.textDocument.uri)
1258+
if (!doc) return null
1259+
return doc.documentLinks()
1260+
} catch {
1261+
return []
12501262
}
1251-
1252-
return getDocumentLinks(state, document, resolveTarget)
12531263
},
12541264
provideDiagnostics: debounce(
1255-
(document: TextDocument) => {
1256-
if (!state.enabled) return
1257-
provideDiagnostics(state, document)
1258-
},
1265+
(document) => provideDiagnostics(service, state, document),
12591266
params.initializationOptions?.testMode ? 0 : 500,
12601267
),
1261-
provideDiagnosticsForce: (document: TextDocument) => {
1262-
if (!state.enabled) return
1263-
provideDiagnostics(state, document)
1264-
},
1268+
provideDiagnosticsForce: (document) => provideDiagnostics(service, state, document),
12651269
async onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]> {
1266-
return withFallback(async () => {
1267-
if (!state.enabled) return []
1268-
let document = documentService.getDocument(params.textDocument.uri)
1269-
if (!document) return []
1270-
if (await isExcluded(state, document)) return null
1271-
return getDocumentColors(state, document)
1272-
}, null)
1270+
try {
1271+
let doc = await service.open(params.textDocument.uri)
1272+
if (!doc) return null
1273+
return doc.documentColors()
1274+
} catch {
1275+
return []
1276+
}
12731277
},
12741278
async onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]> {
1275-
let document = documentService.getDocument(params.textDocument.uri)
1276-
if (!document) return []
1277-
let className = document.getText(params.range)
1278-
let match = className.match(
1279-
new RegExp(`-\\[(${colorNames.join('|')}|(?:(?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$`, 'i'),
1280-
)
1281-
// let match = className.match(/-\[((?:#|rgba?\(|hsla?\()[^\]]+)\]$/i)
1282-
if (match === null) return []
1283-
1284-
let currentColor = match[1]
1285-
1286-
let isNamedColor = colorNames.includes(currentColor)
1287-
1288-
let color: culori.Color = {
1289-
mode: 'rgb',
1290-
r: params.color.red,
1291-
g: params.color.green,
1292-
b: params.color.blue,
1293-
alpha: params.color.alpha,
1294-
}
1295-
1296-
let hexValue = culori.formatHex8(color)
1297-
1298-
if (!isNamedColor && (currentColor.length === 4 || currentColor.length === 5)) {
1299-
let [, ...chars] =
1300-
hexValue.match(/^#([a-f\d])\1([a-f\d])\2([a-f\d])\3(?:([a-f\d])\4)?$/i) ?? []
1301-
if (chars.length) {
1302-
hexValue = `#${chars.filter(Boolean).join('')}`
1303-
}
1304-
}
1305-
1306-
if (hexValue.length === 5) {
1307-
hexValue = hexValue.replace(/f$/, '')
1308-
} else if (hexValue.length === 9) {
1309-
hexValue = hexValue.replace(/ff$/, '')
1279+
try {
1280+
let doc = await service.open(params.textDocument.uri)
1281+
if (!doc) return null
1282+
return doc.colorPresentation(params.color, params.range)
1283+
} catch {
1284+
return []
13101285
}
1311-
1312-
let prefix = className.substr(0, match.index)
1313-
1314-
return [
1315-
hexValue,
1316-
culori.formatRgb(color).replace(/ /g, ''),
1317-
culori
1318-
.formatHsl(color)
1319-
.replace(/ /g, '')
1320-
// round numbers
1321-
.replace(/\d+\.\d+(%?)/g, (value, suffix) => `${Math.round(parseFloat(value))}${suffix}`),
1322-
].map((value) => ({ label: `${prefix}-[${value}]` }))
13231286
},
13241287
sortClassLists(classLists: string[]): string[] {
13251288
if (!state.jit) {

Diff for: packages/tailwindcss-language-service/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@
4545
"@types/dedent": "^0.7.2",
4646
"@types/line-column": "^1.0.2",
4747
"@types/node": "^18.19.33",
48+
"@types/picomatch": "^2.3.3",
4849
"@types/stringify-object": "^4.0.5",
4950
"dedent": "^1.5.3",
5051
"esbuild": "^0.25.0",
5152
"esbuild-node-externals": "^1.9.0",
5253
"minimist": "^1.2.8",
54+
"picomatch": "^4.0.1",
5355
"tslib": "2.2.0",
5456
"typescript": "^5.3.3",
5557
"vitest": "^1.6.1"

0 commit comments

Comments
 (0)