diff --git a/packages/language-server/lib/project/typescriptProject.ts b/packages/language-server/lib/project/typescriptProject.ts index a3bd0390..e21ab969 100644 --- a/packages/language-server/lib/project/typescriptProject.ts +++ b/packages/language-server/lib/project/typescriptProject.ts @@ -28,7 +28,7 @@ export function createTypeScriptProject( const inferredProjects = createUriMap>(); const rootTsConfigs = new Set(); const searchedDirs = new Set(); - const projects: LanguageServerProject = { + const project: LanguageServerProject = { setup(_server) { uriConverter = createUriConverter([..._server.workspaceFolders.keys()]); server = _server; @@ -51,10 +51,7 @@ export function createTypeScriptProject( } } - if (tsConfigChanges.length) { - server.clearPushDiagnostics(); - } - server.refresh(projects); + server.diagnosticsSupport?.refresh(project, !!tsConfigChanges.length); }); }, async getLanguageService(uri) { @@ -85,7 +82,7 @@ export function createTypeScriptProject( inferredProjects.clear(); }, }; - return projects; + return project; async function findMatchTSConfig(server: LanguageServer, uri: URI) { diff --git a/packages/language-server/lib/register/registerLanguageFeatures.ts b/packages/language-server/lib/register/registerLanguageFeatures.ts index 4e5c22cf..8b2f5a73 100644 --- a/packages/language-server/lib/register/registerLanguageFeatures.ts +++ b/packages/language-server/lib/register/registerLanguageFeatures.ts @@ -5,36 +5,6 @@ import { AutoInsertRequest, FindFileReferenceRequest } from '../../protocol'; import type { LanguageServer } from '../types'; export function registerLanguageFeatures(server: LanguageServer) { - const { - documentFormattingProvider, - documentRangeFormattingProvider, - documentOnTypeFormattingProvider, - selectionRangeProvider, - foldingRangeProvider, - linkedEditingRangeProvider, - documentSymbolProvider, - colorProvider, - completionProvider, - hoverProvider, - signatureHelpProvider, - renameProvider, - codeLensProvider, - codeActionProvider, - referencesProvider, - implementationProvider, - definitionProvider, - typeDefinitionProvider, - documentHighlightProvider, - documentLinkProvider, - workspaceSymbolProvider, - callHierarchyProvider, - semanticTokensProvider, - diagnosticProvider, - inlayHintProvider, - executeCommandProvider, - experimental, - } = server.initializeResult.capabilities; - let lastCompleteUri: string; let lastCompleteLs: LanguageService | undefined; let lastCodeLensLs: LanguageService | undefined; @@ -45,412 +15,336 @@ export function registerLanguageFeatures(server: LanguageServer) { let languageServiceToId = new WeakMap(); let currentLanguageServiceId = 0; - const idToLanguageService = new Map>(); + const languageServiceById = new Map>(); - if (documentFormattingProvider) { - server.connection.onDocumentFormatting(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getDocumentFormattingEdits(uri, params.options, undefined, undefined, token); - }); - }); - } - if (documentRangeFormattingProvider) { - server.connection.onDocumentRangeFormatting(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getDocumentFormattingEdits(uri, params.options, params.range, undefined, token); - }); - }); - } - if (documentOnTypeFormattingProvider) { - server.connection.onDocumentOnTypeFormatting(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getDocumentFormattingEdits(uri, params.options, undefined, params, token); - }); - }); - } - if (selectionRangeProvider) { - server.connection.onSelectionRanges(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getSelectionRanges(uri, params.positions, token); - }); - }); - } - if (foldingRangeProvider) { - server.connection.onFoldingRanges(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getFoldingRanges(uri, token); - }); - }); - } - if (linkedEditingRangeProvider) { - server.connection.languages.onLinkedEditingRange(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getLinkedEditingRanges(uri, params.position, token); - }); - }); - } - if (documentSymbolProvider) { - server.connection.onDocumentSymbol(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getDocumentSymbols(uri, token); - }); - }); - } - if (colorProvider) { - server.connection.onDocumentColor(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getDocumentColors(uri, token); - }); - }); - server.connection.onColorPresentation(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getColorPresentations(uri, params.color, params.range, token); - }); - }); - } - if (completionProvider) { - server.connection.onCompletion(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, async languageService => { - lastCompleteUri = params.textDocument.uri; - lastCompleteLs = languageService; - const list = await languageService.getCompletionItems( - uri, - params.position, - params.context, - token - ); - for (const item of list.items) { - fixTextEdit(item); - } - return list; - }); - }); - } - if (completionProvider?.resolveProvider) { - server.connection.onCompletionResolve(async (item, token) => { - if (lastCompleteUri && lastCompleteLs) { - item = await lastCompleteLs.resolveCompletionItem(item, token); + server.connection.onDocumentFormatting(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getDocumentFormattingEdits(uri, params.options, undefined, undefined, token); + }); + }); + server.connection.onDocumentRangeFormatting(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getDocumentFormattingEdits(uri, params.options, params.range, undefined, token); + }); + }); + server.connection.onDocumentOnTypeFormatting(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getDocumentFormattingEdits(uri, params.options, undefined, params, token); + }); + }); + server.connection.onSelectionRanges(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getSelectionRanges(uri, params.positions, token); + }); + }); + server.connection.onFoldingRanges(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getFoldingRanges(uri, token); + }); + }); + server.connection.languages.onLinkedEditingRange(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getLinkedEditingRanges(uri, params.position, token); + }); + }); + server.connection.onDocumentSymbol(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getDocumentSymbols(uri, token); + }); + }); + server.connection.onDocumentColor(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getDocumentColors(uri, token); + }); + }); + server.connection.onColorPresentation(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getColorPresentations(uri, params.color, params.range, token); + }); + }); + server.connection.onCompletion(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, async languageService => { + lastCompleteUri = params.textDocument.uri; + lastCompleteLs = languageService; + const list = await languageService.getCompletionItems( + uri, + params.position, + params.context, + token + ); + for (const item of list.items) { fixTextEdit(item); } - return item; - }); - } - if (hoverProvider) { - server.connection.onHover(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getHover(uri, params.position, token); - }); - }); - } - if (signatureHelpProvider) { - server.connection.onSignatureHelp(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getSignatureHelp(uri, params.position, params.context, token); - }); - }); - } - if (renameProvider) { - server.connection.onRenameRequest(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getRenameEdits(uri, params.position, params.newName, token); - }); - }); - } - if (typeof renameProvider === 'object' && renameProvider.prepareProvider) { - server.connection.onPrepareRename(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, async languageService => { - const result = await languageService.getRenameRange(uri, params.position, token); - if (result && 'message' in result) { - return new vscode.ResponseError(0, result.message); - } - return result; - }); - }); - } - if (codeLensProvider) { - server.connection.onCodeLens(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - lastCodeLensLs = languageService; - return languageService.getCodeLenses(uri, token); - }); - }); - } - if (codeLensProvider?.resolveProvider) { - server.connection.onCodeLensResolve(async (codeLens, token) => { - return await lastCodeLensLs?.resolveCodeLens(codeLens, token) ?? codeLens; - }); - } - if (codeActionProvider) { - server.connection.onCodeAction(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, async languageService => { - lastCodeActionLs = languageService; - let codeActions = await languageService.getCodeActions(uri, params.range, params.context, token) ?? []; - for (const codeAction of codeActions) { - if (codeAction.data && typeof codeAction.data === 'object') { - (codeAction.data as any).uri = params.textDocument.uri; - } - else { - codeAction.data = { uri: params.textDocument.uri }; - } - } - if (!server.initializeParams?.capabilities.textDocument?.codeAction?.disabledSupport) { - codeActions = codeActions.filter(codeAction => !codeAction.disabled); - } - return codeActions; - }); - }); - } - if (typeof codeActionProvider === 'object' && codeActionProvider.resolveProvider) { - server.connection.onCodeActionResolve(async (codeAction, token) => { - return await lastCodeActionLs?.resolveCodeAction(codeAction, token) ?? codeAction; - }); - } - if (referencesProvider) { - server.connection.onReferences(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getReferences(uri, params.position, { includeDeclaration: true }, token); - }); - }); - } - if (implementationProvider) { - server.connection.onImplementation(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getImplementations(uri, params.position, token); - }); - }); - } - if (definitionProvider) { - server.connection.onDefinition(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getDefinition(uri, params.position, token); - }); - }); - } - if (typeDefinitionProvider) { - server.connection.onTypeDefinition(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getTypeDefinition(uri, params.position, token); - }); - }); - } - if (documentHighlightProvider) { - server.connection.onDocumentHighlight(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getDocumentHighlights(uri, params.position, token); - }); + return list; }); - } - if (documentLinkProvider) { - server.connection.onDocumentLinks(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - lastDocumentLinkLs = languageService; - return languageService.getDocumentLinks(uri, token); - }); - }); - } - if (documentLinkProvider?.resolveProvider) { - server.connection.onDocumentLinkResolve(async (link, token) => { - return await lastDocumentLinkLs?.resolveDocumentLink(link, token); - }); - } - if (workspaceSymbolProvider) { - server.connection.onWorkspaceSymbol(async (params, token) => { - const symbols: vscode.WorkspaceSymbol[] = []; - for (const languageService of await server.project.getExistingLanguageServices()) { - if (token.isCancellationRequested) { - return; - } - let languageServiceId = languageServiceToId.get(languageService); - if (languageServiceId === undefined) { - languageServiceId = currentLanguageServiceId; - languageServiceToId.set(languageService, languageServiceId); - idToLanguageService.set(languageServiceId, new WeakRef(languageService)); + }); + server.connection.onCompletionResolve(async (item, token) => { + if (lastCompleteUri && lastCompleteLs) { + item = await lastCompleteLs.resolveCompletionItem(item, token); + fixTextEdit(item); + } + return item; + }); + server.connection.onHover(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getHover(uri, params.position, token); + }); + }); + server.connection.onSignatureHelp(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getSignatureHelp(uri, params.position, params.context, token); + }); + }); + server.connection.onRenameRequest(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getRenameEdits(uri, params.position, params.newName, token); + }); + }); + server.connection.onPrepareRename(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, async languageService => { + const result = await languageService.getRenameRange(uri, params.position, token); + if (result && 'message' in result) { + return new vscode.ResponseError(0, result.message); + } + return result; + }); + }); + server.connection.onCodeLens(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + lastCodeLensLs = languageService; + return languageService.getCodeLenses(uri, token); + }); + }); + server.connection.onCodeLensResolve(async (codeLens, token) => { + return await lastCodeLensLs?.resolveCodeLens(codeLens, token) ?? codeLens; + }); + server.connection.onCodeAction(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, async languageService => { + lastCodeActionLs = languageService; + let codeActions = await languageService.getCodeActions(uri, params.range, params.context, token) ?? []; + for (const codeAction of codeActions) { + if (codeAction.data && typeof codeAction.data === 'object') { + (codeAction.data as any).uri = params.textDocument.uri; } - const languageServiceResult = await languageService.getWorkspaceSymbols(params.query, token); - for (const symbol of languageServiceResult) { - symbol.data = { - languageServiceId, - originalData: symbol.data, - }; + else { + codeAction.data = { uri: params.textDocument.uri }; } - symbols.push(...await languageService.getWorkspaceSymbols(params.query, token)); } - return symbols; - }); - } - if (typeof workspaceSymbolProvider === 'object' && workspaceSymbolProvider.resolveProvider) { - server.connection.onWorkspaceSymbolResolve(async (symbol, token) => { - const languageServiceId = (symbol.data as any)?.languageServiceId; - const languageService = idToLanguageService.get(languageServiceId)?.deref(); - if (!languageService) { - return symbol; + if (!server.initializeParams?.capabilities.textDocument?.codeAction?.disabledSupport) { + codeActions = codeActions.filter(codeAction => !codeAction.disabled); } - symbol.data = (symbol.data as any)?.originalData; - return await languageService.resolveWorkspaceSymbol?.(symbol, token); - }); - } - if (callHierarchyProvider) { - server.connection.languages.callHierarchy.onPrepare(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - lastCallHierarchyLs = languageService; - return languageService.getCallHierarchyItems(uri, params.position, token); - }) ?? []; - }); - server.connection.languages.callHierarchy.onIncomingCalls(async (params, token) => { - return await lastCallHierarchyLs?.getCallHierarchyIncomingCalls(params.item, token) ?? []; - }); - server.connection.languages.callHierarchy.onOutgoingCalls(async (params, token) => { - return await lastCallHierarchyLs?.getCallHierarchyOutgoingCalls(params.item, token) ?? []; - }); - } - if (semanticTokensProvider?.full) { - server.connection.languages.semanticTokens.on(async (params, token, _, resultProgress) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, async languageService => { - return await languageService?.getSemanticTokens( - uri, - undefined, - server.initializeResult.capabilities.semanticTokensProvider!.legend, - tokens => resultProgress?.report(tokens), - token - ); - }) ?? { data: [] }; - }); - } - if (semanticTokensProvider?.range) { - server.connection.languages.semanticTokens.onRange(async (params, token, _, resultProgress) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, async languageService => { - return await languageService?.getSemanticTokens( - uri, - params.range, - server.initializeResult.capabilities.semanticTokensProvider!.legend, - tokens => resultProgress?.report(tokens), - token - ); - }) ?? { data: [] }; - }); - } - if (diagnosticProvider) { - server.connection.languages.diagnostics.on(async (params, token, _workDoneProgressReporter, resultProgressReporter) => { - const uri = URI.parse(params.textDocument.uri); - const result = await worker(uri, token, languageService => { - return languageService.getDiagnostics( - uri, - errors => { - // resultProgressReporter is undefined in vscode - resultProgressReporter?.report({ - relatedDocuments: { - [params.textDocument.uri]: { - kind: vscode.DocumentDiagnosticReportKind.Full, - items: errors, - }, - }, - }); - }, - token - ); - }); - return { - kind: vscode.DocumentDiagnosticReportKind.Full, - items: result ?? [], - }; - }); - } - if (diagnosticProvider?.workspaceDiagnostics) { - server.connection.languages.diagnostics.onWorkspace(async (_params, token) => { - const items: vscode.WorkspaceDocumentDiagnosticReport[] = []; - for (const languageService of await server.project.getExistingLanguageServices()) { - if (token.isCancellationRequested) { - break; - } - const result = await languageService.getWorkspaceDiagnostics(token); - items.push(...result); + return codeActions; + }); + }); + server.connection.onCodeActionResolve(async (codeAction, token) => { + return await lastCodeActionLs?.resolveCodeAction(codeAction, token) ?? codeAction; + }); + server.connection.onReferences(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getReferences(uri, params.position, { includeDeclaration: true }, token); + }); + }); + server.connection.onImplementation(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getImplementations(uri, params.position, token); + }); + }); + server.connection.onDefinition(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getDefinition(uri, params.position, token); + }); + }); + server.connection.onTypeDefinition(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getTypeDefinition(uri, params.position, token); + }); + }); + server.connection.onDocumentHighlight(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getDocumentHighlights(uri, params.position, token); + }); + }); + server.connection.onDocumentLinks(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + lastDocumentLinkLs = languageService; + return languageService.getDocumentLinks(uri, token); + }); + }); + server.connection.onDocumentLinkResolve(async (link, token) => { + return await lastDocumentLinkLs?.resolveDocumentLink(link, token); + }); + server.connection.onWorkspaceSymbol(async (params, token) => { + const symbols: vscode.WorkspaceSymbol[] = []; + for (const languageService of await server.project.getExistingLanguageServices()) { + if (token.isCancellationRequested) { + return; } - return { items }; - }); - } - if (inlayHintProvider) { - server.connection.languages.inlayHint.on(async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - lastInlayHintLs = languageService; - return languageService.getInlayHints(uri, params.range, token); - }); - }); - } - if (executeCommandProvider) { - server.connection.onExecuteCommand(async (params, token) => { - for (const languageService of await server.project.getExistingLanguageServices()) { - if (languageService.executeCommand && languageService.commands.includes(params.command)) { - try { - return await languageService.executeCommand(params.command, params.arguments ?? [], token); - } catch { } - } + let languageServiceId = languageServiceToId.get(languageService); + if (languageServiceId === undefined) { + languageServiceId = currentLanguageServiceId; + languageServiceToId.set(languageService, languageServiceId); + languageServiceById.set(languageServiceId, new WeakRef(languageService)); } - }); - } - if (typeof inlayHintProvider === 'object' && inlayHintProvider.resolveProvider) { - server.connection.languages.inlayHint.resolve(async (hint, token) => { - return await lastInlayHintLs?.resolveInlayHint(hint, token) ?? hint; - }); - } - if (experimental?.fileRenameProvider) { - server.connection.workspace.onWillRenameFiles(async (params, token) => { - const _edits = await Promise.all(params.files.map(async file => { - const oldUri = URI.parse(file.oldUri); - const newUri = URI.parse(file.newUri); - return await worker(oldUri, token, languageService => { - return languageService.getFileRenameEdits(oldUri, newUri, token) ?? null; - }) ?? null; - })); - const edits = _edits.filter((edit): edit is NonNullable => !!edit); - if (edits.length) { - mergeWorkspaceEdits(edits[0], ...edits.slice(1)); - return edits[0]; + const languageServiceResult = await languageService.getWorkspaceSymbols(params.query, token); + for (const symbol of languageServiceResult) { + symbol.data = { + languageServiceId, + originalData: symbol.data, + }; } - return null; - }); - } - if (experimental?.autoInsertionProvider) { - server.connection.onRequest(AutoInsertRequest.type, async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getAutoInsertSnippet(uri, params.selection, params.change, token); - }); - }); - } - if (experimental?.fileReferencesProvider) { - server.connection.onRequest(FindFileReferenceRequest.type, async (params, token) => { - const uri = URI.parse(params.textDocument.uri); - return await worker(uri, token, languageService => { - return languageService.getFileReferences(uri, token); - }); - }); - } + symbols.push(...await languageService.getWorkspaceSymbols(params.query, token)); + } + return symbols; + }); + server.connection.onWorkspaceSymbolResolve(async (symbol, token) => { + const languageServiceId = (symbol.data as any)?.languageServiceId; + const languageService = languageServiceById.get(languageServiceId)?.deref(); + if (!languageService) { + return symbol; + } + symbol.data = (symbol.data as any)?.originalData; + return await languageService.resolveWorkspaceSymbol?.(symbol, token); + }); + server.connection.languages.callHierarchy.onPrepare(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + lastCallHierarchyLs = languageService; + return languageService.getCallHierarchyItems(uri, params.position, token); + }) ?? []; + }); + server.connection.languages.callHierarchy.onIncomingCalls(async (params, token) => { + return await lastCallHierarchyLs?.getCallHierarchyIncomingCalls(params.item, token) ?? []; + }); + server.connection.languages.callHierarchy.onOutgoingCalls(async (params, token) => { + return await lastCallHierarchyLs?.getCallHierarchyOutgoingCalls(params.item, token) ?? []; + }); + server.connection.languages.semanticTokens.on(async (params, token, _, resultProgress) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, async languageService => { + return await languageService?.getSemanticTokens( + uri, + undefined, + server.initializeResult.capabilities.semanticTokensProvider!.legend, + tokens => resultProgress?.report(tokens), + token + ); + }) ?? { data: [] }; + }); + server.connection.languages.semanticTokens.onRange(async (params, token, _, resultProgress) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, async languageService => { + return await languageService?.getSemanticTokens( + uri, + params.range, + server.initializeResult.capabilities.semanticTokensProvider!.legend, + tokens => resultProgress?.report(tokens), + token + ); + }) ?? { data: [] }; + }); + server.connection.languages.diagnostics.on(async (params, token, _workDoneProgressReporter, resultProgressReporter) => { + const uri = URI.parse(params.textDocument.uri); + const result = await worker(uri, token, languageService => { + return languageService.getDiagnostics( + uri, + errors => { + // resultProgressReporter is undefined in vscode + resultProgressReporter?.report({ + relatedDocuments: { + [params.textDocument.uri]: { + kind: vscode.DocumentDiagnosticReportKind.Full, + items: errors, + }, + }, + }); + }, + token + ); + }); + return { + kind: vscode.DocumentDiagnosticReportKind.Full, + items: result ?? [], + }; + }); + server.connection.languages.diagnostics.onWorkspace(async (_params, token) => { + const items: vscode.WorkspaceDocumentDiagnosticReport[] = []; + for (const languageService of await server.project.getExistingLanguageServices()) { + if (token.isCancellationRequested) { + break; + } + const result = await languageService.getWorkspaceDiagnostics(token); + items.push(...result); + } + return { items }; + }); + server.connection.languages.inlayHint.on(async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + lastInlayHintLs = languageService; + return languageService.getInlayHints(uri, params.range, token); + }); + }); + server.connection.onExecuteCommand(async (params, token) => { + for (const languageService of await server.project.getExistingLanguageServices()) { + if (languageService.executeCommand && languageService.commands.includes(params.command)) { + try { + return await languageService.executeCommand(params.command, params.arguments ?? [], token); + } catch { } + } + } + }); + server.connection.languages.inlayHint.resolve(async (hint, token) => { + return await lastInlayHintLs?.resolveInlayHint(hint, token) ?? hint; + }); + server.connection.workspace.onWillRenameFiles(async (params, token) => { + const _edits = await Promise.all(params.files.map(async file => { + const oldUri = URI.parse(file.oldUri); + const newUri = URI.parse(file.newUri); + return await worker(oldUri, token, languageService => { + return languageService.getFileRenameEdits(oldUri, newUri, token) ?? null; + }) ?? null; + })); + const edits = _edits.filter((edit): edit is NonNullable => !!edit); + if (edits.length) { + mergeWorkspaceEdits(edits[0], ...edits.slice(1)); + return edits[0]; + } + return null; + }); + server.connection.onRequest(AutoInsertRequest.type, async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getAutoInsertSnippet(uri, params.selection, params.change, token); + }); + }); + server.connection.onRequest(FindFileReferenceRequest.type, async (params, token) => { + const uri = URI.parse(params.textDocument.uri); + return await worker(uri, token, languageService => { + return languageService.getFileReferences(uri, token); + }); + }); function worker(uri: URI, token: vscode.CancellationToken, cb: (languageService: LanguageService) => T) { return new Promise(resolve => { diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index f2ff3868..ffe5f286 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -13,8 +13,6 @@ export function createServerBase( connection: vscode.Connection, fs: FileSystem ) { - let semanticTokensReq = 0; - let documentUpdatedReq = 0; let watchFilesDisposableCounter = 0; let watchFilesDisposable: Disposable | undefined; @@ -48,14 +46,14 @@ export function createServerBase( syncedDocumentParsedUriToUri.delete(URI.parse(e.document.uri).toString()); }); - const status = { + const state = { connection, - fs: createFsWithCache(fs), + fs: createCachedFileSystem(fs), initializeParams: undefined! as vscode.InitializeParams, initializeResult: undefined! as VolarInitializeResult, languageServicePlugins: [] as LanguageServicePlugin[], project: undefined! as LanguageServerProject, - pullModelDiagnostics: false, + diagnosticsSupport: undefined as ReturnType | undefined, documents, workspaceFolders: createUriMap(), getSyncedDocumentKey, @@ -66,10 +64,8 @@ export function createServerBase( getConfiguration, onDidChangeConfiguration, onDidChangeWatchedFiles, - clearPushDiagnostics, - refresh, }; - return status; + return state; function getSyncedDocumentKey(uri: URI) { const originalUri = syncedDocumentParsedUriToUri.get(uri.toString()); @@ -79,34 +75,28 @@ export function createServerBase( } function initialize( - initializeParams: vscode.InitializeParams, + params: vscode.InitializeParams, project: LanguageServerProject, - languageServicePlugins: LanguageServicePlugin[], - options?: { - pullModelDiagnostics?: boolean; - } + languageServicePlugins: LanguageServicePlugin[] ) { - status.initializeParams = initializeParams; - status.project = project; - status.languageServicePlugins = languageServicePlugins; - status.pullModelDiagnostics = options?.pullModelDiagnostics ?? false; - - if (initializeParams.workspaceFolders?.length) { - for (const folder of initializeParams.workspaceFolders) { - status.workspaceFolders.set(URI.parse(folder.uri), true); + state.initializeParams = params; + state.project = project; + state.languageServicePlugins = languageServicePlugins; + + if (params.workspaceFolders?.length) { + for (const folder of params.workspaceFolders) { + state.workspaceFolders.set(URI.parse(folder.uri), true); } } - else if (initializeParams.rootUri) { - status.workspaceFolders.set(URI.parse(initializeParams.rootUri), true); + else if (params.rootUri) { + state.workspaceFolders.set(URI.parse(params.rootUri), true); } - else if (initializeParams.rootPath) { - status.workspaceFolders.set(URI.file(initializeParams.rootPath), true); + else if (params.rootPath) { + state.workspaceFolders.set(URI.file(params.rootPath), true); } - const capabilitiesArr = status.languageServicePlugins.map(plugin => plugin.capabilities); - - status.initializeResult = { capabilities: {} }; - status.initializeResult.capabilities = { + state.initializeResult = { capabilities: {} }; + state.initializeResult.capabilities = { textDocumentSync: vscode.TextDocumentSyncKind.Incremental, workspace: { // #18 @@ -115,90 +105,95 @@ export function createServerBase( changeNotifications: true, }, }, - selectionRangeProvider: capabilitiesArr.some(data => data.selectionRangeProvider) || undefined, - foldingRangeProvider: capabilitiesArr.some(data => data.foldingRangeProvider) || undefined, - linkedEditingRangeProvider: capabilitiesArr.some(data => data.linkedEditingRangeProvider) || undefined, - colorProvider: capabilitiesArr.some(data => data.colorProvider) || undefined, - documentSymbolProvider: capabilitiesArr.some(data => data.documentSymbolProvider) || undefined, - documentFormattingProvider: capabilitiesArr.some(data => data.documentFormattingProvider) || undefined, - documentRangeFormattingProvider: capabilitiesArr.some(data => data.documentFormattingProvider) || undefined, - referencesProvider: capabilitiesArr.some(data => data.referencesProvider) || undefined, - implementationProvider: capabilitiesArr.some(data => data.implementationProvider) || undefined, - definitionProvider: capabilitiesArr.some(data => data.definitionProvider) || undefined, - typeDefinitionProvider: capabilitiesArr.some(data => data.typeDefinitionProvider) || undefined, - callHierarchyProvider: capabilitiesArr.some(data => data.callHierarchyProvider) || undefined, - hoverProvider: capabilitiesArr.some(data => data.hoverProvider) || undefined, - documentHighlightProvider: capabilitiesArr.some(data => data.documentHighlightProvider) || undefined, - workspaceSymbolProvider: capabilitiesArr.some(data => data.workspaceSymbolProvider) - ? { resolveProvider: capabilitiesArr.some(data => data.workspaceSymbolProvider?.resolveProvider) || undefined } + selectionRangeProvider: languageServicePlugins.some(({ capabilities }) => capabilities.selectionRangeProvider) || undefined, + foldingRangeProvider: languageServicePlugins.some(({ capabilities }) => capabilities.foldingRangeProvider) || undefined, + linkedEditingRangeProvider: languageServicePlugins.some(({ capabilities }) => capabilities.linkedEditingRangeProvider) || undefined, + colorProvider: languageServicePlugins.some(({ capabilities }) => capabilities.colorProvider) || undefined, + documentSymbolProvider: languageServicePlugins.some(({ capabilities }) => capabilities.documentSymbolProvider) || undefined, + documentFormattingProvider: languageServicePlugins.some(({ capabilities }) => capabilities.documentFormattingProvider) || undefined, + documentRangeFormattingProvider: languageServicePlugins.some(({ capabilities }) => capabilities.documentFormattingProvider) || undefined, + referencesProvider: languageServicePlugins.some(({ capabilities }) => capabilities.referencesProvider) || undefined, + implementationProvider: languageServicePlugins.some(({ capabilities }) => capabilities.implementationProvider) || undefined, + definitionProvider: languageServicePlugins.some(({ capabilities }) => capabilities.definitionProvider) || undefined, + typeDefinitionProvider: languageServicePlugins.some(({ capabilities }) => capabilities.typeDefinitionProvider) || undefined, + callHierarchyProvider: languageServicePlugins.some(({ capabilities }) => capabilities.callHierarchyProvider) || undefined, + hoverProvider: languageServicePlugins.some(({ capabilities }) => capabilities.hoverProvider) || undefined, + documentHighlightProvider: languageServicePlugins.some(({ capabilities }) => capabilities.documentHighlightProvider) || undefined, + workspaceSymbolProvider: languageServicePlugins.some(({ capabilities }) => capabilities.workspaceSymbolProvider) + ? { resolveProvider: languageServicePlugins.some(({ capabilities }) => capabilities.workspaceSymbolProvider?.resolveProvider) || undefined } : undefined, - renameProvider: capabilitiesArr.some(data => data.renameProvider) - ? { prepareProvider: capabilitiesArr.some(data => data.renameProvider?.prepareProvider) || undefined } + renameProvider: languageServicePlugins.some(({ capabilities }) => capabilities.renameProvider) + ? { prepareProvider: languageServicePlugins.some(({ capabilities }) => capabilities.renameProvider?.prepareProvider) || undefined } : undefined, - documentLinkProvider: capabilitiesArr.some(data => data.documentLinkProvider) - ? { resolveProvider: capabilitiesArr.some(data => data.documentLinkProvider?.resolveProvider) || undefined } + documentLinkProvider: languageServicePlugins.some(({ capabilities }) => capabilities.documentLinkProvider) + ? { resolveProvider: languageServicePlugins.some(({ capabilities }) => capabilities.documentLinkProvider?.resolveProvider) || undefined } : undefined, - codeLensProvider: capabilitiesArr.some(data => data.codeLensProvider) - ? { resolveProvider: capabilitiesArr.some(data => data.codeLensProvider?.resolveProvider) || undefined } + codeLensProvider: languageServicePlugins.some(({ capabilities }) => capabilities.codeLensProvider) + ? { resolveProvider: languageServicePlugins.some(({ capabilities }) => capabilities.codeLensProvider?.resolveProvider) || undefined } : undefined, - inlayHintProvider: capabilitiesArr.some(data => data.inlayHintProvider) - ? { resolveProvider: capabilitiesArr.some(data => data.inlayHintProvider?.resolveProvider) || undefined } + inlayHintProvider: languageServicePlugins.some(({ capabilities }) => capabilities.inlayHintProvider) + ? { resolveProvider: languageServicePlugins.some(({ capabilities }) => capabilities.inlayHintProvider?.resolveProvider) || undefined } : undefined, - signatureHelpProvider: capabilitiesArr.some(data => data.signatureHelpProvider) + signatureHelpProvider: languageServicePlugins.some(({ capabilities }) => capabilities.signatureHelpProvider) ? { - triggerCharacters: [...new Set(capabilitiesArr.map(data => data.signatureHelpProvider?.triggerCharacters ?? []).flat())], - retriggerCharacters: [...new Set(capabilitiesArr.map(data => data.signatureHelpProvider?.retriggerCharacters ?? []).flat())], + triggerCharacters: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.signatureHelpProvider?.triggerCharacters ?? []).flat())], + retriggerCharacters: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.signatureHelpProvider?.retriggerCharacters ?? []).flat())], } : undefined, - completionProvider: capabilitiesArr.some(data => data.completionProvider) + completionProvider: languageServicePlugins.some(({ capabilities }) => capabilities.completionProvider) ? { - resolveProvider: capabilitiesArr.some(data => data.completionProvider?.resolveProvider) || undefined, - triggerCharacters: [...new Set(capabilitiesArr.map(data => data.completionProvider?.triggerCharacters ?? []).flat())], + resolveProvider: languageServicePlugins.some(({ capabilities }) => capabilities.completionProvider?.resolveProvider) || undefined, + triggerCharacters: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.completionProvider?.triggerCharacters ?? []).flat())], } : undefined, - semanticTokensProvider: capabilitiesArr.some(data => data.semanticTokensProvider) + semanticTokensProvider: languageServicePlugins.some(({ capabilities }) => capabilities.semanticTokensProvider) ? { range: true, full: false, legend: { - tokenTypes: [...new Set(capabilitiesArr.map(data => data.semanticTokensProvider?.legend?.tokenTypes ?? []).flat())], - tokenModifiers: [...new Set(capabilitiesArr.map(data => data.semanticTokensProvider?.legend?.tokenModifiers ?? []).flat())], + tokenTypes: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.semanticTokensProvider?.legend?.tokenTypes ?? []).flat())], + tokenModifiers: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.semanticTokensProvider?.legend?.tokenModifiers ?? []).flat())], }, } : undefined, - codeActionProvider: capabilitiesArr.some(data => data.codeActionProvider) + codeActionProvider: languageServicePlugins.some(({ capabilities }) => capabilities.codeActionProvider) ? { - resolveProvider: capabilitiesArr.some(data => data.codeActionProvider?.resolveProvider) || undefined, - codeActionKinds: capabilitiesArr.some(data => data.codeActionProvider?.codeActionKinds) - ? [...new Set(capabilitiesArr.map(data => data.codeActionProvider?.codeActionKinds ?? []).flat())] + resolveProvider: languageServicePlugins.some(({ capabilities }) => capabilities.codeActionProvider?.resolveProvider) || undefined, + codeActionKinds: languageServicePlugins.some(({ capabilities }) => capabilities.codeActionProvider?.codeActionKinds) + ? [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.codeActionProvider?.codeActionKinds ?? []).flat())] : undefined, } : undefined, - diagnosticProvider: capabilitiesArr.some(data => data.diagnosticProvider) + documentOnTypeFormattingProvider: languageServicePlugins.some(({ capabilities }) => capabilities.documentOnTypeFormattingProvider) ? { - interFileDependencies: true, - workspaceDiagnostics: capabilitiesArr.some(data => data.diagnosticProvider?.workspaceDiagnostics), + firstTriggerCharacter: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.documentOnTypeFormattingProvider?.triggerCharacters ?? []).flat())][0], + moreTriggerCharacter: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.documentOnTypeFormattingProvider?.triggerCharacters ?? []).flat())].slice(1), } : undefined, - documentOnTypeFormattingProvider: capabilitiesArr.some(data => data.documentOnTypeFormattingProvider) + executeCommandProvider: languageServicePlugins.some(({ capabilities }) => capabilities.executeCommandProvider) ? { - firstTriggerCharacter: [...new Set(capabilitiesArr.map(data => data.documentOnTypeFormattingProvider?.triggerCharacters ?? []).flat())][0], - moreTriggerCharacter: [...new Set(capabilitiesArr.map(data => data.documentOnTypeFormattingProvider?.triggerCharacters ?? []).flat())].slice(1), - } - : undefined, - executeCommandProvider: capabilitiesArr.some(data => data.executeCommandProvider) - ? { - commands: [...new Set(capabilitiesArr.map(data => data.executeCommandProvider?.commands ?? []).flat())], + commands: [...new Set(languageServicePlugins.map(({ capabilities }) => capabilities.executeCommandProvider?.commands ?? []).flat())], } : undefined, }; - if (!status.pullModelDiagnostics && status.initializeResult.capabilities.diagnosticProvider) { - status.initializeResult.capabilities.diagnosticProvider = undefined; - activateServerPushDiagnostics(project); + if (languageServicePlugins.some(({ capabilities }) => capabilities.diagnosticProvider)) { + const supportsDiagnosticPull = !!params.capabilities.workspace?.diagnostics; + const interFileDependencies = state.languageServicePlugins.some(({ capabilities }) => capabilities.diagnosticProvider?.interFileDependencies); + if (supportsDiagnosticPull && !interFileDependencies) { + state.initializeResult.capabilities.diagnosticProvider = { + // Unreliable, see https://github.com/microsoft/vscode-languageserver-node/issues/848#issuecomment-2189521060 + interFileDependencies: false, + workspaceDiagnostics: languageServicePlugins.some(({ capabilities }) => capabilities.diagnosticProvider?.workspaceDiagnostics), + }; + registerDiagnosticsSupport(project, 'pull', false); + } + else { + registerDiagnosticsSupport(project, 'push', interFileDependencies); + } } - if (capabilitiesArr.some(data => data.autoInsertionProvider)) { + if (languageServicePlugins.some(({ capabilities }) => capabilities.autoInsertionProvider)) { const triggerCharacterToConfigurationSections = new Map>(); const tryAdd = (char: string, section?: string) => { let sectionSet = triggerCharacterToConfigurationSections.get(char); @@ -209,9 +204,9 @@ export function createServerBase( sectionSet.add(section); } }; - for (const data of capabilitiesArr) { - if (data.autoInsertionProvider) { - const { triggerCharacters, configurationSections } = data.autoInsertionProvider; + for (const { capabilities } of languageServicePlugins) { + if (capabilities.autoInsertionProvider) { + const { triggerCharacters, configurationSections } = capabilities.autoInsertionProvider; if (configurationSections) { if (configurationSections.length !== triggerCharacters.length) { throw new Error('configurationSections.length !== triggerCharacters.length'); @@ -227,40 +222,40 @@ export function createServerBase( } } } - status.initializeResult.capabilities.experimental ??= {}; - status.initializeResult.capabilities.experimental.autoInsertionProvider = { + state.initializeResult.capabilities.experimental ??= {}; + state.initializeResult.capabilities.experimental.autoInsertionProvider = { triggerCharacters: [], configurationSections: [], }; for (const [char, sections] of triggerCharacterToConfigurationSections) { if (sections.size) { - status.initializeResult.capabilities.experimental.autoInsertionProvider.triggerCharacters.push(char); - status.initializeResult.capabilities.experimental.autoInsertionProvider.configurationSections!.push([...sections]); + state.initializeResult.capabilities.experimental.autoInsertionProvider.triggerCharacters.push(char); + state.initializeResult.capabilities.experimental.autoInsertionProvider.configurationSections!.push([...sections]); } else { - status.initializeResult.autoInsertionProvider.triggerCharacters.push(char); - status.initializeResult.autoInsertionProvider.configurationSections.push(null); + state.initializeResult.autoInsertionProvider.triggerCharacters.push(char); + state.initializeResult.autoInsertionProvider.configurationSections.push(null); } } } - if (capabilitiesArr.some(data => data.fileRenameProvider)) { - status.initializeResult.capabilities.experimental ??= {}; - status.initializeResult.capabilities.experimental.fileRenameProvider = true; + if (languageServicePlugins.some(({ capabilities }) => capabilities.fileRenameProvider)) { + state.initializeResult.capabilities.experimental ??= {}; + state.initializeResult.capabilities.experimental.fileRenameProvider = true; } - if (capabilitiesArr.some(data => data.fileReferencesProvider)) { - status.initializeResult.capabilities.experimental ??= {}; - status.initializeResult.capabilities.experimental.fileReferencesProvider = true; + if (languageServicePlugins.some(({ capabilities }) => capabilities.fileReferencesProvider)) { + state.initializeResult.capabilities.experimental ??= {}; + state.initializeResult.capabilities.experimental.fileReferencesProvider = true; } - return status.initializeResult; + return state.initializeResult; } function initialized() { - status.project.setup(status); - registerEditorFeatures(status); - registerLanguageFeatures(status); + state.project.setup(state); + registerEditorFeatures(state); + registerLanguageFeatures(state); registerWorkspaceFoldersWatcher(); registerConfigurationWatcher(); updateHttpSettings(); @@ -268,7 +263,7 @@ export function createServerBase( } function shutdown() { - status.project.reload(); + state.project.reload(); } async function updateHttpSettings() { @@ -277,10 +272,10 @@ export function createServerBase( } function getConfiguration(section: string, scopeUri?: string): Promise { - if (!status.initializeParams?.capabilities.workspace?.configuration) { + if (!state.initializeParams?.capabilities.workspace?.configuration) { return Promise.resolve(undefined); } - if (!scopeUri && status.initializeParams.capabilities.workspace?.didChangeConfiguration) { + if (!scopeUri && state.initializeParams.capabilities.workspace?.didChangeConfiguration) { if (!configurations.has(section)) { configurations.set(section, getConfigurationWorker(section, scopeUri)); } @@ -311,7 +306,7 @@ export function createServerBase( }; } - function createFsWithCache(fs: FileSystem): FileSystem { + function createCachedFileSystem(fs: FileSystem): FileSystem { const readFileCache = createUriMap>(); const statCache = createUriMap>(); @@ -367,7 +362,7 @@ export function createServerBase( } function registerConfigurationWatcher() { - const didChangeConfiguration = status.initializeParams?.capabilities.workspace?.didChangeConfiguration; + const didChangeConfiguration = state.initializeParams?.capabilities.workspace?.didChangeConfiguration; if (didChangeConfiguration) { connection.onDidChangeConfiguration(params => { configurations.clear(); @@ -383,8 +378,8 @@ export function createServerBase( async function watchFiles(patterns: string[]): Promise { const disposables: Disposable[] = []; - const didChangeWatchedFiles = status.initializeParams?.capabilities.workspace?.didChangeWatchedFiles; - const fileOperations = status.initializeParams?.capabilities.workspace?.fileOperations; + const didChangeWatchedFiles = state.initializeParams?.capabilities.workspace?.didChangeWatchedFiles; + const fileOperations = state.initializeParams?.capabilities.workspace?.fileOperations; if (didChangeWatchedFiles) { if (watchFilesDisposableCounter === 0) { watchFilesDisposable = connection.onDidChangeWatchedFiles(e => { @@ -423,99 +418,117 @@ export function createServerBase( } function registerWorkspaceFoldersWatcher() { - if (status.initializeParams?.capabilities.workspace?.workspaceFolders) { + if (state.initializeParams?.capabilities.workspace?.workspaceFolders) { connection.workspace.onDidChangeWorkspaceFolders(e => { for (const folder of e.added) { - status.workspaceFolders.set(URI.parse(folder.uri), true); + state.workspaceFolders.set(URI.parse(folder.uri), true); } for (const folder of e.removed) { - status.workspaceFolders.delete(URI.parse(folder.uri)); + state.workspaceFolders.delete(URI.parse(folder.uri)); } - status.project.reload(); + state.project.reload(); }); } } - function activateServerPushDiagnostics(project: LanguageServerProject) { - documents.onDidChangeContent(({ document }) => { - pushAllDiagnostics(project, document.uri); - }); - documents.onDidClose(({ document }) => { - connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); - }); - onDidChangeConfiguration(() => refresh(project)); - } + function registerDiagnosticsSupport(project: LanguageServerProject, mode: 'pull' | 'push', interFileDependencies: boolean) { + let refreshReq = 0; + let updateDiagnosticsBatchReq = 0; - function clearPushDiagnostics() { - if (!status.pullModelDiagnostics) { - for (const document of documents.all()) { + if (mode === 'push') { + documents.onDidChangeContent(({ document }) => { + const changedDocument = documents.get(document.uri); + if (!changedDocument) { + return; + } + if (interFileDependencies) { + const remainingDocuments = [...documents.all()].filter(doc => doc !== changedDocument); + updateDiagnosticsBatch(project, [changedDocument, ...remainingDocuments]); + } + else { + updateDiagnosticsBatch(project, [changedDocument]); + } + }); + documents.onDidClose(({ document }) => { connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); - } + }); + onDidChangeConfiguration(() => refresh(project, false)); } - } - - async function refresh(project: LanguageServerProject) { - const req = ++semanticTokensReq; + return { refresh }; - if (!status.pullModelDiagnostics) { - await pushAllDiagnostics(project); - } - - const delay = 250; - await sleep(delay); + async function refresh(project: LanguageServerProject, clearDiagnostics: boolean) { + const req = ++refreshReq; + const delay = 250; + await sleep(delay); + if (req !== refreshReq) { + return; + } - if (req === semanticTokensReq) { - if (status.initializeParams?.capabilities.workspace?.semanticTokens?.refreshSupport) { - connection.languages.semanticTokens.refresh(); + if (state.initializeResult.capabilities.semanticTokensProvider) { + if (state.initializeParams?.capabilities.workspace?.semanticTokens?.refreshSupport) { + connection.languages.semanticTokens.refresh(); + } + else { + console.warn('Semantic tokens refresh is not supported by the client.'); + } } - if (status.initializeParams?.capabilities.workspace?.inlayHint?.refreshSupport) { - connection.languages.inlayHint.refresh(); + if (state.initializeResult.capabilities.inlayHintProvider) { + if (state.initializeParams?.capabilities.workspace?.inlayHint?.refreshSupport) { + connection.languages.inlayHint.refresh(); + } + else { + console.warn('Inlay hint refresh is not supported by the client.'); + } } - if (status.pullModelDiagnostics && status.initializeParams?.capabilities.workspace?.diagnostics?.refreshSupport) { - connection.languages.diagnostics.refresh(); + if (mode === 'push') { + if (clearDiagnostics) { + for (const document of documents.all()) { + connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); + } + } + await updateDiagnosticsBatch(project, [...documents.all()]); + } + else { + if (state.initializeParams?.capabilities.workspace?.diagnostics?.refreshSupport) { + connection.languages.diagnostics.refresh(); + } + else { + console.warn('Diagnostics refresh is not supported by the client.'); + } } } - } - async function pushAllDiagnostics(project: LanguageServerProject, docUri?: string) { - const req = ++documentUpdatedReq; - const delay = 250; - const token: vscode.CancellationToken = { - get isCancellationRequested() { - return req !== documentUpdatedReq; - }, - onCancellationRequested: vscode.Event.None, - }; - const changeDoc = docUri ? documents.get(docUri) : undefined; - const otherDocs = [...documents.all()].filter(doc => doc !== changeDoc); - - if (changeDoc) { - await sleep(delay); - if (token.isCancellationRequested) { - return; + async function updateDiagnosticsBatch(project: LanguageServerProject, documents: SnapshotDocument[]) { + const req = ++updateDiagnosticsBatchReq; + const delay = 250; + const token: vscode.CancellationToken = { + get isCancellationRequested() { + return req !== updateDiagnosticsBatchReq; + }, + onCancellationRequested: vscode.Event.None, + }; + for (const doc of documents) { + await sleep(delay); + if (token.isCancellationRequested) { + break; + } + await updateDiagnostics(project, URI.parse(doc.uri), doc.version, token); } - await pushDiagnostics(project, changeDoc.uri, changeDoc.version, token); } - for (const doc of otherDocs) { - await sleep(delay); - if (token.isCancellationRequested) { - break; + async function updateDiagnostics(project: LanguageServerProject, uri: URI, version: number, token: vscode.CancellationToken) { + const languageService = await project.getLanguageService(uri); + const diagnostics = await languageService.getDiagnostics( + uri, + diagnostics => connection.sendDiagnostics({ uri: uri.toString(), diagnostics, version }), + token + ); + if (!token.isCancellationRequested) { + connection.sendDiagnostics({ uri: uri.toString(), diagnostics, version }); } - await pushDiagnostics(project, doc.uri, doc.version, token); } } - - async function pushDiagnostics(project: LanguageServerProject, uriStr: string, version: number, cancel: vscode.CancellationToken) { - const uri = URI.parse(uriStr); - const languageService = await project.getLanguageService(uri); - const errors = await languageService.getDiagnostics(uri, result => { - connection.sendDiagnostics({ uri: uriStr, diagnostics: result, version }); - }, cancel); - - connection.sendDiagnostics({ uri: uriStr, diagnostics: errors, version }); - } } function sleep(ms: number) { diff --git a/packages/language-service/lib/features/provideDiagnostics.ts b/packages/language-service/lib/features/provideDiagnostics.ts index ab85a101..c793ef6a 100644 --- a/packages/language-service/lib/features/provideDiagnostics.ts +++ b/packages/language-service/lib/features/provideDiagnostics.ts @@ -10,92 +10,6 @@ import * as dedupe from '../utils/dedupe'; import { documentFeatureWorker, DocumentsAndMap, getSourceRange } from '../utils/featureWorkers'; import { createUriMap } from '../utils/uriMap'; -export function updateRange( - range: vscode.Range, - change: { - range: vscode.Range, - newEnd: vscode.Position; - } -) { - if (!updatePosition(range.start, change, false)) { - return; - } - if (!updatePosition(range.end, change, true)) { - return; - } - if (range.end.line === range.start.line && range.end.character <= range.start.character) { - range.end.character++; - } - return range; -} - -function updatePosition( - position: vscode.Position, - change: { - range: vscode.Range, - newEnd: vscode.Position; - }, - isEnd: boolean -) { - if (change.range.end.line > position.line) { - if (change.newEnd.line > position.line) { - // No change - return true; - } - else if (change.newEnd.line === position.line) { - position.character = Math.min(position.character, change.newEnd.character); - return true; - } - else if (change.newEnd.line < position.line) { - position.line = change.newEnd.line; - position.character = change.newEnd.character; - return true; - } - } - else if (change.range.end.line === position.line) { - const characterDiff = change.newEnd.character - change.range.end.character; - if (position.character >= change.range.end.character) { - if (change.newEnd.line !== change.range.end.line) { - position.line = change.newEnd.line; - position.character = change.newEnd.character + position.character - change.range.end.character; - } - else { - if (isEnd ? change.range.end.character < position.character : change.range.end.character <= position.character) { - position.character += characterDiff; - } - else { - const offset = change.range.end.character - position.character; - if (-characterDiff > offset) { - position.character += characterDiff + offset; - } - } - } - return true; - } - else { - if (change.newEnd.line === change.range.end.line) { - const offset = change.range.end.character - position.character; - if (-characterDiff > offset) { - position.character += characterDiff + offset; - } - } - else if (change.newEnd.line < change.range.end.line) { - position.line = change.newEnd.line; - position.character = change.newEnd.character; - } - else { - // No change - } - return true; - } - } - else if (change.range.end.line < position.line) { - position.line += change.newEnd.line - change.range.end.line; - return true; - } - return false; -} - export interface ServiceDiagnosticData { uri: string; version: number; @@ -201,13 +115,13 @@ export function register(context: LanguageServiceContext) { } } - await worker('provideDiagnostics', cacheMaps.syntactic, lastResponse.syntactic); - await doResponse(); - await worker('provideSemanticDiagnostics', cacheMaps.semantic, lastResponse.semantic); + await worker('syntactic', cacheMaps.syntactic, lastResponse.syntactic); + processResponse(); + await worker('semantic', cacheMaps.semantic, lastResponse.semantic); return collectErrors(); - function doResponse() { + function processResponse() { if (errorsUpdated && !updateCacheRangeFailed) { response?.(collectErrors()); errorsUpdated = false; @@ -219,7 +133,7 @@ export function register(context: LanguageServiceContext) { } async function worker( - api: 'provideDiagnostics' | 'provideSemanticDiagnostics', + kind: 'syntactic' | 'semantic', cacheMap: CacheMap, cache: Cache ) { @@ -228,6 +142,11 @@ export function register(context: LanguageServiceContext) { uri, docs => docs[2].mappings.some(mapping => isDiagnosticsEnabled(mapping.data)), async (plugin, document) => { + const interFileDependencies = plugin[0].capabilities.diagnosticProvider?.interFileDependencies; + if (kind === 'semantic' !== interFileDependencies) { + return; + } + if (Date.now() - lastCheckCancelAt >= 10) { await sleep(10); // waiting LSP event polling lastCheckCancelAt = Date.now(); @@ -240,11 +159,11 @@ export function register(context: LanguageServiceContext) { const pluginCache = cacheMap.get(pluginIndex) ?? cacheMap.set(pluginIndex, new Map()).get(pluginIndex)!; const cache = pluginCache.get(document.uri); - if (api !== 'provideSemanticDiagnostics' && cache && cache.documentVersion === document.version) { + if (!interFileDependencies && cache && cache.documentVersion === document.version) { return cache.errors; } - const errors = await plugin[1][api]?.(document, token) || []; + const errors = await plugin[1].provideDiagnostics?.(document, token) || []; errors.forEach(error => { error.data = { @@ -341,3 +260,89 @@ export function transformDiagnostic( return _error; } + +export function updateRange( + range: vscode.Range, + change: { + range: vscode.Range, + newEnd: vscode.Position; + } +) { + if (!updatePosition(range.start, change, false)) { + return; + } + if (!updatePosition(range.end, change, true)) { + return; + } + if (range.end.line === range.start.line && range.end.character <= range.start.character) { + range.end.character++; + } + return range; +} + +function updatePosition( + position: vscode.Position, + change: { + range: vscode.Range, + newEnd: vscode.Position; + }, + isEnd: boolean +) { + if (change.range.end.line > position.line) { + if (change.newEnd.line > position.line) { + // No change + return true; + } + else if (change.newEnd.line === position.line) { + position.character = Math.min(position.character, change.newEnd.character); + return true; + } + else if (change.newEnd.line < position.line) { + position.line = change.newEnd.line; + position.character = change.newEnd.character; + return true; + } + } + else if (change.range.end.line === position.line) { + const characterDiff = change.newEnd.character - change.range.end.character; + if (position.character >= change.range.end.character) { + if (change.newEnd.line !== change.range.end.line) { + position.line = change.newEnd.line; + position.character = change.newEnd.character + position.character - change.range.end.character; + } + else { + if (isEnd ? change.range.end.character < position.character : change.range.end.character <= position.character) { + position.character += characterDiff; + } + else { + const offset = change.range.end.character - position.character; + if (-characterDiff > offset) { + position.character += characterDiff + offset; + } + } + } + return true; + } + else { + if (change.newEnd.line === change.range.end.line) { + const offset = change.range.end.character - position.character; + if (-characterDiff > offset) { + position.character += characterDiff + offset; + } + } + else if (change.newEnd.line < change.range.end.line) { + position.line = change.newEnd.line; + position.character = change.newEnd.character; + } + else { + // No change + } + return true; + } + } + else if (change.range.end.line < position.line) { + position.line += change.newEnd.line - change.range.end.line; + return true; + } + return false; +} diff --git a/packages/language-service/lib/types.ts b/packages/language-service/lib/types.ts index bd55a286..8cc0d1c5 100644 --- a/packages/language-service/lib/types.ts +++ b/packages/language-service/lib/types.ts @@ -140,8 +140,8 @@ export interface LanguageServicePlugin

{ resolveProvider?: boolean; }; diagnosticProvider?: { - // TODO: interFileDependencies - workspaceDiagnostics?: boolean; + interFileDependencies: boolean; + workspaceDiagnostics: boolean; }; fileReferencesProvider?: boolean; fileRenameProvider?: boolean; @@ -186,7 +186,6 @@ export interface LanguageServicePluginInstance

{ provideDocumentSemanticTokens?(document: TextDocument, range: vscode.Range, legend: vscode.SemanticTokensLegend, token: vscode.CancellationToken): NullableProviderResult; provideWorkspaceSymbols?(query: string, token: vscode.CancellationToken): NullableProviderResult; provideDiagnostics?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; - provideSemanticDiagnostics?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; provideWorkspaceDiagnostics?(token: vscode.CancellationToken): NullableProviderResult; provideFileReferences?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; // volar specific provideReferencesCodeLensRanges?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; // volar specific