Skip to content

Commit 6a50851

Browse files
committed
Refactor language service usage
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 1b26cf3 commit 6a50851

File tree

8 files changed

+367
-164
lines changed

8 files changed

+367
-164
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 { File, FileType } from '@tailwindcss/language-service/src/fs'
9078

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

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

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

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

267290
if (projectConfig.configPath && projectConfig.config.source === 'js') {
268291
let deps = []
@@ -1171,139 +1194,79 @@ export async function createProjectService(
11711194
},
11721195
onFileEvents,
11731196
async onHover(params: TextDocumentPositionParams): Promise<Hover> {
1174-
return withFallback(async () => {
1175-
if (!state.enabled) return null
1176-
let document = documentService.getDocument(params.textDocument.uri)
1177-
if (!document) return null
1178-
let settings = await state.editor.getConfiguration(document.uri)
1179-
if (!settings.tailwindCSS.hovers) return null
1180-
if (await isExcluded(state, document)) return null
1181-
return doHover(state, document, params.position)
1182-
}, null)
1197+
try {
1198+
let doc = await service.open(params.textDocument.uri)
1199+
if (!doc) return null
1200+
return doc.hover(params.position)
1201+
} catch {
1202+
return null
1203+
}
11831204
},
11841205
async onCodeLens(params: CodeLensParams): Promise<CodeLens[]> {
1185-
return withFallback(async () => {
1186-
if (!state.enabled) return null
1187-
let document = documentService.getDocument(params.textDocument.uri)
1188-
if (!document) return null
1189-
let settings = await state.editor.getConfiguration(document.uri)
1190-
if (!settings.tailwindCSS.codeLens) return null
1191-
if (await isExcluded(state, document)) return null
1192-
return getCodeLens(state, document)
1193-
}, null)
1206+
try {
1207+
let doc = await service.open(params.textDocument.uri)
1208+
if (!doc) return null
1209+
return doc.codeLenses()
1210+
} catch {
1211+
return []
1212+
}
11941213
},
11951214
async onCompletion(params: CompletionParams): Promise<CompletionList> {
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.suggestions) return null
1202-
if (await isExcluded(state, document)) return null
1203-
return doComplete(state, document, params.position, params.context)
1204-
}, null)
1215+
try {
1216+
let doc = await service.open(params.textDocument.uri)
1217+
if (!doc) return null
1218+
return doc.completions(params.position)
1219+
} catch {
1220+
return null
1221+
}
12051222
},
1206-
onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
1207-
return withFallback(() => {
1208-
if (!state.enabled) return null
1209-
return resolveCompletionItem(state, item)
1210-
}, null)
1223+
async onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
1224+
try {
1225+
return await service.resolveCompletion(item)
1226+
} catch {
1227+
return null
1228+
}
12111229
},
12121230
async onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {
1213-
return withFallback(async () => {
1214-
if (!state.enabled) return null
1215-
let document = documentService.getDocument(params.textDocument.uri)
1216-
if (!document) return null
1217-
let settings = await state.editor.getConfiguration(document.uri)
1218-
if (!settings.tailwindCSS.codeActions) return null
1219-
return doCodeActions(state, params, document)
1220-
}, null)
1231+
try {
1232+
let doc = await service.open(params.textDocument.uri)
1233+
if (!doc) return null
1234+
return doc.codeActions(params.range, params.context)
1235+
} catch {
1236+
return []
1237+
}
12211238
},
1222-
onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
1223-
if (!state.enabled) return null
1224-
let document = documentService.getDocument(params.textDocument.uri)
1225-
if (!document) return null
1226-
1227-
let documentPath = URI.parse(document.uri).fsPath
1228-
let baseDir = path.dirname(documentPath)
1229-
1230-
async function resolveTarget(linkPath: string) {
1231-
linkPath = (await resolver.substituteId(linkPath, baseDir)) ?? linkPath
1232-
1233-
return URI.file(path.resolve(baseDir, linkPath)).toString()
1239+
async onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
1240+
try {
1241+
let doc = await service.open(params.textDocument.uri)
1242+
if (!doc) return null
1243+
return doc.documentLinks()
1244+
} catch {
1245+
return []
12341246
}
1235-
1236-
return getDocumentLinks(state, document, resolveTarget)
12371247
},
12381248
provideDiagnostics: debounce(
1239-
(document: TextDocument) => {
1240-
if (!state.enabled) return
1241-
provideDiagnostics(state, document)
1242-
},
1249+
(document) => provideDiagnostics(service, state, document),
12431250
params.initializationOptions?.testMode ? 0 : 500,
12441251
),
1245-
provideDiagnosticsForce: (document: TextDocument) => {
1246-
if (!state.enabled) return
1247-
provideDiagnostics(state, document)
1248-
},
1252+
provideDiagnosticsForce: (document) => provideDiagnostics(service, state, document),
12491253
async onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]> {
1250-
return withFallback(async () => {
1251-
if (!state.enabled) return []
1252-
let document = documentService.getDocument(params.textDocument.uri)
1253-
if (!document) return []
1254-
if (await isExcluded(state, document)) return null
1255-
return getDocumentColors(state, document)
1256-
}, null)
1254+
try {
1255+
let doc = await service.open(params.textDocument.uri)
1256+
if (!doc) return null
1257+
return doc.documentColors()
1258+
} catch {
1259+
return []
1260+
}
12571261
},
12581262
async onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]> {
1259-
let document = documentService.getDocument(params.textDocument.uri)
1260-
if (!document) return []
1261-
let className = document.getText(params.range)
1262-
let match = className.match(
1263-
new RegExp(`-\\[(${colorNames.join('|')}|(?:(?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$`, 'i'),
1264-
)
1265-
// let match = className.match(/-\[((?:#|rgba?\(|hsla?\()[^\]]+)\]$/i)
1266-
if (match === null) return []
1267-
1268-
let currentColor = match[1]
1269-
1270-
let isNamedColor = colorNames.includes(currentColor)
1271-
1272-
let color: culori.Color = {
1273-
mode: 'rgb',
1274-
r: params.color.red,
1275-
g: params.color.green,
1276-
b: params.color.blue,
1277-
alpha: params.color.alpha,
1278-
}
1279-
1280-
let hexValue = culori.formatHex8(color)
1281-
1282-
if (!isNamedColor && (currentColor.length === 4 || currentColor.length === 5)) {
1283-
let [, ...chars] =
1284-
hexValue.match(/^#([a-f\d])\1([a-f\d])\2([a-f\d])\3(?:([a-f\d])\4)?$/i) ?? []
1285-
if (chars.length) {
1286-
hexValue = `#${chars.filter(Boolean).join('')}`
1287-
}
1288-
}
1289-
1290-
if (hexValue.length === 5) {
1291-
hexValue = hexValue.replace(/f$/, '')
1292-
} else if (hexValue.length === 9) {
1293-
hexValue = hexValue.replace(/ff$/, '')
1263+
try {
1264+
let doc = await service.open(params.textDocument.uri)
1265+
if (!doc) return null
1266+
return doc.colorPresentation(params.color, params.range)
1267+
} catch {
1268+
return []
12941269
}
1295-
1296-
let prefix = className.substr(0, match.index)
1297-
1298-
return [
1299-
hexValue,
1300-
culori.formatRgb(color).replace(/ /g, ''),
1301-
culori
1302-
.formatHsl(color)
1303-
.replace(/ /g, '')
1304-
// round numbers
1305-
.replace(/\d+\.\d+(%?)/g, (value, suffix) => `${Math.round(parseFloat(value))}${suffix}`),
1306-
].map((value) => ({ label: `${prefix}-[${value}]` }))
13071270
},
13081271
sortClassLists(classLists: string[]): string[] {
13091272
if (!state.jit) {

packages/tailwindcss-language-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@
4444
"@types/css.escape": "^1.5.2",
4545
"@types/line-column": "^1.0.2",
4646
"@types/node": "^18.19.33",
47+
"@types/picomatch": "^2.3.3",
4748
"@types/stringify-object": "^4.0.5",
4849
"esbuild": "^0.25.0",
4950
"esbuild-node-externals": "^1.9.0",
5051
"minimist": "^1.2.8",
52+
"picomatch": "^4.0.1",
5153
"tslib": "2.2.0",
5254
"typescript": "^5.3.3",
5355
"vitest": "^1.6.1"

0 commit comments

Comments
 (0)