From cda8551cb058f16cb459fe3a12f4806cd6282666 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 3 Jan 2023 08:22:03 +0800 Subject: [PATCH] refactor: abstract `semanticService` and `syntacticService` (#2273) --- examples/angular-language-server/src/index.ts | 35 ++-- .../src/index.ts | 76 +++------ .../src/common.ts | 2 +- .../src/common/features/documentFeatures.ts | 69 -------- .../src/common/features/languageFeatures.ts | 46 +++++ .../language-server/src/common/project.ts | 23 ++- packages/language-server/src/common/server.ts | 46 +---- .../src/common/syntaxServicesHost.ts | 65 ------- .../src/common/utils/registerFeatures.ts | 63 ++++--- .../language-server/src/common/workspaces.ts | 14 +- packages/language-server/src/types.ts | 57 +++---- .../src/baseDocumentService.ts | 97 ----------- .../src/baseLanguageService.ts | 17 ++ .../src/documentFeatures/autoInsert.ts | 49 ------ .../documentFeatures/colorPresentations.ts | 20 ++- .../src/documentFeatures/documentColors.ts | 11 +- .../src/documentFeatures/documentSymbols.ts | 11 +- .../src/documentFeatures/foldingRanges.ts | 9 +- .../src/documentFeatures/format.ts | 142 +++++++++------ .../documentFeatures/linkedEditingRanges.ts | 14 +- .../src/documentFeatures/selectionRanges.ts | 28 +-- packages/language-service/src/documents.ts | 4 + packages/language-service/src/index.ts | 1 - packages/language-service/src/types.ts | 10 -- .../src/utils/featureWorkers.ts | 107 ++---------- .../src/utils/singleFileTypeScriptService.ts | 34 ---- plugins/typescript/src/index.ts | 2 +- plugins/typescript/src/services/formatting.ts | 2 +- .../src/generators/template.ts | 2 +- .../src/languageServerPlugin.ts | 161 ++++++++---------- .../src/documentService.ts | 76 --------- .../vue-language-service/src/index.ts | 1 - 32 files changed, 402 insertions(+), 892 deletions(-) delete mode 100644 packages/language-server/src/common/features/documentFeatures.ts delete mode 100644 packages/language-server/src/common/syntaxServicesHost.ts delete mode 100644 packages/language-service/src/baseDocumentService.ts delete mode 100644 packages/language-service/src/documentFeatures/autoInsert.ts delete mode 100644 packages/language-service/src/utils/singleFileTypeScriptService.ts delete mode 100644 vue-language-tools/vue-language-service/src/documentService.ts diff --git a/examples/angular-language-server/src/index.ts b/examples/angular-language-server/src/index.ts index 56c2495ac..8f808b825 100644 --- a/examples/angular-language-server/src/index.ts +++ b/examples/angular-language-server/src/index.ts @@ -5,32 +5,17 @@ import type { LanguageServicePlugin, DocumentsAndSourceMaps, Diagnostic } from ' const plugin: LanguageServerPlugin = () => ({ extraFileExtensions: [{ extension: 'html', isMixedContent: true, scriptKind: 7 }], - semanticService: { - getLanguageModules(host) { - return [ - createTsLanguageModule(host.getTypeScriptModule()), - createHtmlLanguageModule(host.getTypeScriptModule()), - ]; - }, - getServicePlugins(_host, service) { - return [ - createTsPlugin(), - createNgTemplateLsPlugin(service.context.documents), - ]; - }, + getLanguageModules(host) { + return [ + createTsLanguageModule(host.getTypeScriptModule()), + createHtmlLanguageModule(host.getTypeScriptModule()), + ]; }, - syntacticService: { - getLanguageModules(ts) { - return [ - createTsLanguageModule(ts), - createHtmlLanguageModule(ts), - ]; - }, - getServicePlugins() { - return [ - createTsPlugin(), - ]; - } + getServicePlugins(_host, service) { + return [ + createTsPlugin(), + createNgTemplateLsPlugin(service.context.documents), + ]; }, }); diff --git a/examples/vue-and-svelte-language-server/src/index.ts b/examples/vue-and-svelte-language-server/src/index.ts index d0013d4ce..fbf9bbddb 100644 --- a/examples/vue-and-svelte-language-server/src/index.ts +++ b/examples/vue-and-svelte-language-server/src/index.ts @@ -1,6 +1,6 @@ import { languageModule as svelteLanguageModule } from '@volar-examples/svelte-language-core'; import useTsPlugin from '@volar-plugins/typescript'; -import { createLanguageServer, LanguageModule, LanguageServerInitializationOptions, LanguageServerPlugin } from '@volar/language-server/node'; +import { createLanguageServer, LanguageServerInitializationOptions, LanguageServerPlugin } from '@volar/language-server/node'; import * as vue from '@volar/vue-language-core'; const plugin: LanguageServerPlugin = () => { @@ -9,57 +9,31 @@ const plugin: LanguageServerPlugin vueOptions, - }; - }, - getLanguageModules(host) { - const vueLanguageModules = vue.createLanguageModules( - host.getTypeScriptModule(), - host.getCompilationSettings(), - host.getVueCompilationSettings(), - ); - return [ - ...vueLanguageModules, - svelteLanguageModule, - ]; - }, - getServicePlugins() { - return [ - useTsPlugin(), - ]; - }, - }, - syntacticService: { - getLanguageModules(ts) { - const vueLanguagePlugins = vue.getDefaultVueLanguagePlugins(ts, {}, {}); - const vueLanguageModule: LanguageModule = { - createFile(fileName, snapshot) { - if (fileName.endsWith('.vue')) { - return new vue.VueFile(fileName, snapshot, ts, vueLanguagePlugins); - } - }, - updateFile(sourceFile: vue.VueFile, snapshot) { - sourceFile.update(snapshot); - }, - }; - return [ - vueLanguageModule, - svelteLanguageModule, - ]; - }, - getServicePlugins() { - return [ - useTsPlugin(), - ]; + resolveLanguageServiceHost(ts, sys, tsConfig, host) { + let vueOptions: vue.VueCompilerOptions = {}; + if (typeof tsConfig === 'string') { + vueOptions = vue.createParsedCommandLine(ts, sys, tsConfig, []).vueOptions; } + return { + ...host, + getVueCompilationSettings: () => vueOptions, + }; + }, + getLanguageModules(host) { + const vueLanguageModules = vue.createLanguageModules( + host.getTypeScriptModule(), + host.getCompilationSettings(), + host.getVueCompilationSettings(), + ); + return [ + ...vueLanguageModules, + svelteLanguageModule, + ]; + }, + getServicePlugins() { + return [ + useTsPlugin(), + ]; }, }; }; diff --git a/extensions/vscode-vue-language-features/src/common.ts b/extensions/vscode-vue-language-features/src/common.ts index d311147d6..f43e099b8 100644 --- a/extensions/vscode-vue-language-features/src/common.ts +++ b/extensions/vscode-vue-language-features/src/common.ts @@ -306,7 +306,7 @@ function getInitializationOptions( configFilePath: vscode.workspace.getConfiguration('volar').get('vueserver.configFilePath'), respectClientCapabilities: true, serverMode, - diagnosticModel: diagnosticModel() === 'pull' ? DiagnosticModel.Pull : DiagnosticModel.Push, + diagnosticModel: serverMode === ServerMode.Syntactic ? DiagnosticModel.None : diagnosticModel() === 'pull' ? DiagnosticModel.Pull : DiagnosticModel.Push, textDocumentSync: textDocumentSync ? { incremental: lsp.TextDocumentSyncKind.Incremental, full: lsp.TextDocumentSyncKind.Full, diff --git a/packages/language-server/src/common/features/documentFeatures.ts b/packages/language-server/src/common/features/documentFeatures.ts deleted file mode 100644 index 9fa8c3457..000000000 --- a/packages/language-server/src/common/features/documentFeatures.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type * as vscode from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { createDocuments } from '../documents'; -import { AutoInsertRequest } from '../../protocol'; -import { createSyntaxServicesHost } from '../syntaxServicesHost'; - -export function register( - connection: vscode.Connection, - documents: ReturnType, - documentServiceHost: ReturnType, -) { - connection.onDocumentFormatting(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).format(document, params.options); - }); - }); - connection.onDocumentRangeFormatting(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).format(document, params.options, params.range); - }); - }); - connection.onDocumentOnTypeFormatting(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).format(document, params.options, undefined, params); - }); - }); - connection.onSelectionRanges(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).getSelectionRanges(document, params.positions); - }); - }); - connection.onFoldingRanges(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).getFoldingRanges(document); - }); - }); - connection.languages.onLinkedEditingRange(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).findLinkedEditingRanges(document, params.position); - }); - }); - connection.onDocumentSymbol(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).findDocumentSymbols(document); - }); - }); - connection.onDocumentColor(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).findDocumentColors(document); - }); - }); - connection.onColorPresentation(params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).getColorPresentations(document, params.color, params.range); - }); - }); - connection.onRequest(AutoInsertRequest.type, async params => { - return worker(params.textDocument.uri, document => { - return documentServiceHost.get(document.uri).doAutoInsert(document, params.position, params.options); - }); - }); - - function worker(uri: string, cb: (document: TextDocument) => T) { - const document = documents.data.uriGet(uri)?.getDocument(); - if (document) { - return cb(document); - } - } -} diff --git a/packages/language-server/src/common/features/languageFeatures.ts b/packages/language-server/src/common/features/languageFeatures.ts index d62ce2e6c..d88ee9e0c 100644 --- a/packages/language-server/src/common/features/languageFeatures.ts +++ b/packages/language-server/src/common/features/languageFeatures.ts @@ -19,6 +19,52 @@ export function register( let lastCodeActionLs: embedded.LanguageService; let lastCallHierarchyLs: embedded.LanguageService; + connection.onDocumentFormatting(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.format(params.textDocument.uri, params.options); + }); + }); + connection.onDocumentRangeFormatting(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.format(params.textDocument.uri, params.options, params.range); + }); + }); + connection.onDocumentOnTypeFormatting(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.format(params.textDocument.uri, params.options, undefined, params); + }); + }); + connection.onSelectionRanges(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.getSelectionRanges(params.textDocument.uri, params.positions); + }); + }); + connection.onFoldingRanges(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.getFoldingRanges(params.textDocument.uri); + }); + }); + connection.languages.onLinkedEditingRange(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.findLinkedEditingRanges(params.textDocument.uri, params.position); + }); + }); + connection.onDocumentSymbol(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.findDocumentSymbols(params.textDocument.uri); + }); + }); + connection.onDocumentColor(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.findDocumentColors(params.textDocument.uri); + }); + }); + connection.onColorPresentation(async params => { + return worker(params.textDocument.uri, vueLs => { + return vueLs.getColorPresentations(params.textDocument.uri, params.color, params.range); + }); + }); + connection.onCompletion(async (params) => { return worker(params.textDocument.uri, async vueLs => { lastCompleteUri = params.textDocument.uri; diff --git a/packages/language-server/src/common/project.ts b/packages/language-server/src/common/project.ts index f720e549d..65455e0aa 100644 --- a/packages/language-server/src/common/project.ts +++ b/packages/language-server/src/common/project.ts @@ -6,7 +6,7 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import * as html from 'vscode-html-languageservice'; import * as vscode from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -import { FileSystem, LanguageServerPlugin } from '../types'; +import { FileSystem, LanguageServerPlugin, ServerMode } from '../types'; import { createUriMap } from './utils/uriMap'; import { WorkspaceContext } from './workspace'; import { ServerConfig } from './utils/serverConfig'; @@ -23,7 +23,18 @@ export type Project = ReturnType; export async function createProject(context: ProjectContext) { - const sys = context.workspace.workspaces.fileSystemHost.getWorkspaceFileSystem(context.rootUri); + const sys: FileSystem = context.workspace.workspaces.initOptions.serverMode === ServerMode.Syntactic + ? { + newLine: '\n', + useCaseSensitiveFileNames: false, + fileExists: () => false, + readFile: () => undefined, + readDirectory: () => [], + getCurrentDirectory: () => '', + realpath: () => '', + resolvePath: () => '', + } + : context.workspace.workspaces.fileSystemHost.getWorkspaceFileSystem(context.rootUri); let typeRootVersion = 0; let projectVersion = 0; @@ -66,7 +77,7 @@ export async function createProject(context: ProjectContext) { function getLanguageService() { if (!languageService) { - const languageModules = context.workspace.workspaces.plugins.map(plugin => plugin.semanticService?.getLanguageModules?.(languageServiceHost) ?? []).flat(); + const languageModules = context.workspace.workspaces.plugins.map(plugin => plugin.getLanguageModules?.(languageServiceHost) ?? []).flat(); const languageContext = embedded.createLanguageContext(languageServiceHost, languageModules); const languageServiceContext = embeddedLS.createLanguageServiceContext({ host: languageServiceHost, @@ -74,7 +85,7 @@ export async function createProject(context: ProjectContext) { getPlugins() { return [ ...context.serverConfig?.plugins ?? [], - ...context.workspace.workspaces.plugins.map(plugin => plugin.semanticService?.getServicePlugins?.(languageServiceHost, languageService!) ?? []).flat(), + ...context.workspace.workspaces.plugins.map(plugin => plugin.getServicePlugins?.(languageServiceHost, languageService!) ?? []).flat(), ]; }, env: { @@ -189,8 +200,8 @@ export async function createProject(context: ProjectContext) { } for (const plugin of context.workspace.workspaces.plugins) { - if (plugin.semanticService?.resolveLanguageServiceHost) { - host = plugin.semanticService.resolveLanguageServiceHost(context.workspace.workspaces.ts, sys, context.tsConfig, host); + if (plugin.resolveLanguageServiceHost) { + host = plugin.resolveLanguageServiceHost(context.workspace.workspaces.ts, sys, context.tsConfig, host); } } diff --git a/packages/language-server/src/common/server.ts b/packages/language-server/src/common/server.ts index e51759582..e7e4a404a 100644 --- a/packages/language-server/src/common/server.ts +++ b/packages/language-server/src/common/server.ts @@ -4,8 +4,7 @@ import { FileSystemHost, LanguageServerInitializationOptions, LanguageServerPlug import { createCancellationTokenHost } from './cancellationPipe'; import { createConfigurationHost } from './configurationHost'; import { createDocuments } from './documents'; -import { createSyntaxServicesHost } from './syntaxServicesHost'; -import { setupSemanticCapabilities, setupSyntacticCapabilities } from './utils/registerFeatures'; +import { setupCapabilities } from './utils/registerFeatures'; import { createWorkspaces } from './workspaces'; export interface ServerContext { @@ -21,7 +20,6 @@ export function createCommonLanguageServer(context: ServerContext) { let roots: URI[] = []; let fsHost: FileSystemHost | undefined; let projects: ReturnType | undefined; - let documentServiceHost: ReturnType | undefined; let configurationHost: ReturnType | undefined; let plugins: ReturnType[]; @@ -51,15 +49,8 @@ export function createCommonLanguageServer(context: ServerContext) { configurationHost = initParams.capabilities.workspace?.configuration ? createConfigurationHost(initParams, context.connection) : undefined; - const serverMode = options.serverMode ?? ServerMode.Semantic; - - setupSyntacticCapabilities(initParams.capabilities, result.capabilities, options); - await _createDocumentServiceHost(); - - if (serverMode === ServerMode.Semantic) { - setupSemanticCapabilities(initParams.capabilities, result.capabilities, options, plugins, getSemanticTokensLegend()); - await createLanguageServiceHost(); - } + setupCapabilities(initParams.capabilities, result.capabilities, options, plugins, getSemanticTokensLegend()); + await createLanguageServiceHost(); try { // show version on LSP logs @@ -81,12 +72,10 @@ export function createCommonLanguageServer(context: ServerContext) { context.connection.workspace.onDidChangeWorkspaceFolders(e => { for (const folder of e.added) { - documentServiceHost?.add(URI.parse(folder.uri)); projects?.add(URI.parse(folder.uri)); } for (const folder of e.removed) { - documentServiceHost?.remove(URI.parse(folder.uri)); projects?.remove(URI.parse(folder.uri)); } }); @@ -119,33 +108,6 @@ export function createCommonLanguageServer(context: ServerContext) { }); context.connection.listen(); - async function _createDocumentServiceHost() { - - const ts = context.runtimeEnv.loadTypescript(options.typescript.tsdk); - - documentServiceHost = createSyntaxServicesHost( - context.runtimeEnv, - plugins, - ts, - configurationHost, - options, - ); - - for (const root of roots) { - documentServiceHost.add(root); - } - - (await import('./features/documentFeatures')).register( - context.connection, - documents, - documentServiceHost, - ); - - for (const plugin of plugins) { - plugin.syntacticService?.onInitialize?.(context.connection); - } - } - async function createLanguageServiceHost() { const ts = context.runtimeEnv.loadTypescript(options.typescript.tsdk); @@ -175,7 +137,7 @@ export function createCommonLanguageServer(context: ServerContext) { (await import('./features/languageFeatures')).register(context.connection, projects, initParams, cancelTokenHost, getSemanticTokensLegend()); for (const plugin of plugins) { - plugin.semanticService?.onInitialize?.(context.connection, getLanguageService as any); + plugin.onInitialize?.(context.connection, getLanguageService as any); } async function getLanguageService(uri: string) { diff --git a/packages/language-server/src/common/syntaxServicesHost.ts b/packages/language-server/src/common/syntaxServicesHost.ts deleted file mode 100644 index 64ae4116f..000000000 --- a/packages/language-server/src/common/syntaxServicesHost.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as embedded from '@volar/language-service'; -import { URI } from 'vscode-uri'; -import { LanguageServerInitializationOptions, LanguageServerPlugin, RuntimeEnvironment } from '../types'; -import { loadServerConfig } from './utils/serverConfig'; - -// fix build -import type * as _ from 'vscode-languageserver-textdocument'; - -export function createSyntaxServicesHost( - runtimeEnv: RuntimeEnvironment, - plugins: ReturnType[], - ts: typeof import('typescript/lib/tsserverlibrary'), - configHost: embedded.ConfigurationHost | undefined, - initOptions: LanguageServerInitializationOptions, -) { - - const services = new Map(); - const untitledService = create(URI.from({ scheme: 'untitled' })); - - return { - add, - remove, - get, - }; - - function add(rootUri: URI) { - services.set(rootUri.toString(), create(rootUri)); - } - - function remove(rootUri: URI) { - services.delete(rootUri.toString()); - } - - function get(documentUri: string) { - for (const [rootUri, service] of services) { - if (documentUri.startsWith(rootUri)) { - return service; - } - } - return untitledService; - } - - function create(rootUri: URI) { - const env: embedded.LanguageServicePluginContext['env'] = { - rootUri, - configurationHost: configHost, - fileSystemProvider: runtimeEnv.fileSystemProvide, - }; - const serverConfig = loadServerConfig(rootUri.fsPath, initOptions.configFilePath); - const serviceContext = embedded.createDocumentServiceContext({ - ts, - env, - getLanguageModules() { - return plugins.map(plugin => plugin.syntacticService?.getLanguageModules?.(ts, env) ?? []).flat(); - }, - getPlugins() { - return [ - ...serverConfig?.plugins ?? [], - ...plugins.map(plugin => plugin.syntacticService?.getServicePlugins?.(serviceContext) ?? []).flat(), - ]; - }, - }); - return embedded.createDocumentService(serviceContext); - } -} diff --git a/packages/language-server/src/common/utils/registerFeatures.ts b/packages/language-server/src/common/utils/registerFeatures.ts index 341f2177e..08ccc17dd 100644 --- a/packages/language-server/src/common/utils/registerFeatures.ts +++ b/packages/language-server/src/common/utils/registerFeatures.ts @@ -3,11 +3,15 @@ import { DiagnosticModel, LanguageServerPlugin, LanguageServerInitializationOpti import * as vscode from 'vscode-languageserver'; import { ClientCapabilities } from 'vscode-languageserver'; -export function setupSyntacticCapabilities( +export function setupCapabilities( params: ClientCapabilities, server: vscode.ServerCapabilities, initOptions: LanguageServerInitializationOptions, + plugins: ReturnType[], + semanticTokensLegend: vscode.SemanticTokensLegend, ) { + + // Syntactic if (!initOptions.respectClientCapabilities || params.textDocument?.selectionRange) { server.selectionRangeProvider = true; } @@ -36,15 +40,8 @@ export function setupSyntacticCapabilities( moreTriggerCharacter: ['}', '\n'], }; } -} -export function setupSemanticCapabilities( - params: ClientCapabilities, - server: vscode.ServerCapabilities, - initOptions: LanguageServerInitializationOptions, - plugins: ReturnType[], - semanticTokensLegend: vscode.SemanticTokensLegend, -) { + // Semantic if (!initOptions.respectClientCapabilities || params.textDocument?.references) { server.referencesProvider = true; } @@ -68,26 +65,6 @@ export function setupSemanticCapabilities( prepareProvider: true, }; } - if (!initOptions.respectClientCapabilities || params.workspace?.fileOperations) { - server.workspace = { - fileOperations: { - willRename: { - filters: [ - ...plugins.map(plugin => plugin.extraFileExtensions.map(ext => ({ pattern: { glob: `**/*.${ext.extension}` } }))).flat(), - { pattern: { glob: '**/*.js' } }, - { pattern: { glob: '**/*.cjs' } }, - { pattern: { glob: '**/*.mjs' } }, - { pattern: { glob: '**/*.ts' } }, - { pattern: { glob: '**/*.cts' } }, - { pattern: { glob: '**/*.mts' } }, - { pattern: { glob: '**/*.jsx' } }, - { pattern: { glob: '**/*.tsx' } }, - { pattern: { glob: '**/*.json' } }, - ] - } - } - }; - } if (!initOptions.respectClientCapabilities || params.textDocument?.signatureHelp) { server.signatureHelpProvider = { triggerCharacters: ['(', ',', '<'], @@ -123,9 +100,6 @@ export function setupSemanticCapabilities( resolveProvider: false, // TODO }; } - if (!initOptions.respectClientCapabilities || params.workspace?.symbol) { - server.workspaceSymbolProvider = true; - } if (!initOptions.respectClientCapabilities || params.textDocument?.codeLens) { server.codeLensProvider = { resolveProvider: true, @@ -169,4 +143,29 @@ export function setupSemanticCapabilities( workspaceDiagnostics: false, }; } + + // cross file features + if (!initOptions.respectClientCapabilities || params.workspace?.fileOperations) { + server.workspace = { + fileOperations: { + willRename: { + filters: [ + ...plugins.map(plugin => plugin.extraFileExtensions.map(ext => ({ pattern: { glob: `**/*.${ext.extension}` } }))).flat(), + { pattern: { glob: '**/*.js' } }, + { pattern: { glob: '**/*.cjs' } }, + { pattern: { glob: '**/*.mjs' } }, + { pattern: { glob: '**/*.ts' } }, + { pattern: { glob: '**/*.cts' } }, + { pattern: { glob: '**/*.mts' } }, + { pattern: { glob: '**/*.jsx' } }, + { pattern: { glob: '**/*.tsx' } }, + { pattern: { glob: '**/*.json' } }, + ] + } + } + }; + } + if (!initOptions.respectClientCapabilities || params.workspace?.symbol) { + server.workspaceSymbolProvider = true; + } } diff --git a/packages/language-server/src/common/workspaces.ts b/packages/language-server/src/common/workspaces.ts index 73ea27809..705f37c0a 100644 --- a/packages/language-server/src/common/workspaces.ts +++ b/packages/language-server/src/common/workspaces.ts @@ -3,7 +3,7 @@ import * as shared from '@volar/shared'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as vscode from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -import { DiagnosticModel, FileSystemHost, LanguageServerInitializationOptions, LanguageServerPlugin } from '../types'; +import { DiagnosticModel, FileSystemHost, LanguageServerInitializationOptions, LanguageServerPlugin, ServerMode } from '../types'; import { CancellationTokenHost } from './cancellationPipe'; import { createDocuments } from './documents'; import { ServerContext } from './server'; @@ -171,11 +171,13 @@ export function createWorkspaces(context: WorkspacesContext) { .filter(rootUri => shared.isFileInDir(shared.getPathOfUri(uri), shared.getPathOfUri(rootUri))) .sort((a, b) => sortTsConfigs(shared.getPathOfUri(uri), shared.getPathOfUri(a), shared.getPathOfUri(b))); - for (const rootUri of rootUris) { - const workspace = await workspaces.get(rootUri); - const projectAndTsConfig = await workspace?.getProjectAndTsConfig(uri); - if (projectAndTsConfig) { - return projectAndTsConfig; + if (context.initOptions.serverMode !== ServerMode.Syntactic) { + for (const rootUri of rootUris) { + const workspace = await workspaces.get(rootUri); + const projectAndTsConfig = await workspace?.getProjectAndTsConfig(uri); + if (projectAndTsConfig) { + return projectAndTsConfig; + } } } diff --git a/packages/language-server/src/types.ts b/packages/language-server/src/types.ts index ac52daf9e..5067f459f 100644 --- a/packages/language-server/src/types.ts +++ b/packages/language-server/src/types.ts @@ -41,45 +41,26 @@ export type LanguageServerPlugin< C = embeddedLS.LanguageService > = (initOptions: A) => { - extraFileExtensions: ts.FileExtensionInfo[], + extraFileExtensions: ts.FileExtensionInfo[]; - semanticService?: { - - resolveLanguageServiceHost?( - ts: typeof import('typescript/lib/tsserverlibrary'), - sys: FileSystem, - tsConfig: string | ts.CompilerOptions, - host: embedded.LanguageServiceHost, - ): B, - - getLanguageModules?(host: B): embedded.LanguageModule[], - - getServicePlugins?( - host: B, - service: embeddedLS.LanguageService, - ): embeddedLS.LanguageServicePlugin[], - - onInitialize?( - connection: vscode.Connection, - getLanguageService: (uri: string) => Promise, - ): void, - }, - - syntacticService?: { - - getLanguageModules?( - ts: typeof import('typescript/lib/tsserverlibrary'), - env: embeddedLS.LanguageServicePluginContext['env'], - ): embedded.LanguageModule[], - - getServicePlugins?( - context: embeddedLS.DocumentServiceRuntimeContext, - ): embeddedLS.LanguageServicePlugin[], - - onInitialize?( - connection: vscode.Connection, - ): void, - }; + resolveLanguageServiceHost?( + ts: typeof import('typescript/lib/tsserverlibrary'), + sys: FileSystem, + tsConfig: string | ts.CompilerOptions, + host: embedded.LanguageServiceHost, + ): B; + + getLanguageModules?(host: B): embedded.LanguageModule[]; + + getServicePlugins?( + host: B, + service: embeddedLS.LanguageService, + ): embeddedLS.LanguageServicePlugin[]; + + onInitialize?( + connection: vscode.Connection, + getLanguageService: (uri: string) => Promise, + ): void; }; export enum ServerMode { diff --git a/packages/language-service/src/baseDocumentService.ts b/packages/language-service/src/baseDocumentService.ts deleted file mode 100644 index 0cdfdfc11..000000000 --- a/packages/language-service/src/baseDocumentService.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { createVirtualFiles, LanguageModule } from '@volar/language-core'; -import * as shared from '@volar/shared'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import * as autoInsert from './documentFeatures/autoInsert'; -import * as colorPresentations from './documentFeatures/colorPresentations'; -import * as documentColors from './documentFeatures/documentColors'; -import * as documentSymbols from './documentFeatures/documentSymbols'; -import * as foldingRanges from './documentFeatures/foldingRanges'; -import * as format from './documentFeatures/format'; -import * as linkedEditingRanges from './documentFeatures/linkedEditingRanges'; -import * as selectionRanges from './documentFeatures/selectionRanges'; -import { createDocumentsAndSourceMaps } from './documents'; -import { DocumentServiceRuntimeContext, LanguageServicePlugin, LanguageServicePluginContext } from './types'; -import { singleFileTypeScriptServiceHost, updateSingleFileTypeScriptServiceHost } from './utils/singleFileTypeScriptService'; - -// fix build -import type * as _ from 'vscode-languageserver-protocol'; - -export type DocumentService = ReturnType; - -export function createDocumentServiceContext(options: { - ts: typeof import('typescript/lib/tsserverlibrary'), - getLanguageModules(): LanguageModule[], - getPlugins(): LanguageServicePlugin[], - env: LanguageServicePluginContext['env']; -}) { - - let plugins: LanguageServicePlugin[] | undefined; - - const ts = options.ts; - const pluginContext: LanguageServicePluginContext = { - typescript: { - module: ts, - languageServiceHost: singleFileTypeScriptServiceHost, - languageService: ts.createLanguageService(singleFileTypeScriptServiceHost), - }, - env: options.env, - }; - const languageModules = options.getLanguageModules(); - const lastUpdateVersions = new Map(); - const virtualFiles = createVirtualFiles(languageModules); - const textDocumentMapper = createDocumentsAndSourceMaps(virtualFiles); - const context: DocumentServiceRuntimeContext = { - typescript: ts, - get plugins() { - if (!plugins) { - plugins = options.getPlugins(); - for (const plugin of plugins) { - plugin.setup?.(pluginContext); - } - } - return plugins; - }, - pluginContext, - documents: textDocumentMapper, - update(document) { - let lastVersion = lastUpdateVersions.get(document.uri); - if (lastVersion === undefined || lastVersion !== document.version) { - const fileName = shared.getPathOfUri(document.uri); - virtualFiles.update(fileName, ts.ScriptSnapshot.fromString(document.getText())); - lastUpdateVersions.set(document.uri, document.version); - } - }, - updateVirtualFile(fileName, snapshot) { - virtualFiles.update(fileName, snapshot); - }, - prepareLanguageServices(document) { - if (isTsDocument(document)) { - updateSingleFileTypeScriptServiceHost(ts, document); - } - }, - }; - - return context; -} - -export function isTsDocument(document: TextDocument) { - return document.languageId === 'javascript' || - document.languageId === 'typescript' || - document.languageId === 'javascriptreact' || - document.languageId === 'typescriptreact'; -} - -export function createDocumentService(context: DocumentServiceRuntimeContext) { - - return { - format: format.register(context), - getFoldingRanges: foldingRanges.register(context), - getSelectionRanges: selectionRanges.register(context), - findLinkedEditingRanges: linkedEditingRanges.register(context), - findDocumentSymbols: documentSymbols.register(context), - findDocumentColors: documentColors.register(context), - getColorPresentations: colorPresentations.register(context), - doAutoInsert: autoInsert.register(context), - context, - }; -} diff --git a/packages/language-service/src/baseLanguageService.ts b/packages/language-service/src/baseLanguageService.ts index 375e8eff9..2117854c0 100644 --- a/packages/language-service/src/baseLanguageService.ts +++ b/packages/language-service/src/baseLanguageService.ts @@ -29,6 +29,14 @@ import * as workspaceSymbol from './languageFeatures/workspaceSymbols'; import { LanguageServicePlugin, LanguageServicePluginContext, LanguageServiceRuntimeContext } from './types'; import type * as ts from 'typescript/lib/tsserverlibrary'; +import * as colorPresentations from './documentFeatures/colorPresentations'; +import * as documentColors from './documentFeatures/documentColors'; +import * as documentSymbols from './documentFeatures/documentSymbols'; +import * as foldingRanges from './documentFeatures/foldingRanges'; +import * as format from './documentFeatures/format'; +import * as linkedEditingRanges from './documentFeatures/linkedEditingRanges'; +import * as selectionRanges from './documentFeatures/selectionRanges'; + // fix build import type * as _ from 'vscode-languageserver-protocol'; @@ -111,6 +119,15 @@ export function createLanguageServiceContext(options: { export function createLanguageService(context: LanguageServiceRuntimeContext) { return { + + format: format.register(context), + getFoldingRanges: foldingRanges.register(context), + getSelectionRanges: selectionRanges.register(context), + findLinkedEditingRanges: linkedEditingRanges.register(context), + findDocumentSymbols: documentSymbols.register(context), + findDocumentColors: documentColors.register(context), + getColorPresentations: colorPresentations.register(context), + doValidation: diagnostics.register(context), findReferences: references.register(context), findFileReferences: fileReferences.register(context), diff --git a/packages/language-service/src/documentFeatures/autoInsert.ts b/packages/language-service/src/documentFeatures/autoInsert.ts deleted file mode 100644 index f0cb8d49c..000000000 --- a/packages/language-service/src/documentFeatures/autoInsert.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { DocumentServiceRuntimeContext } from '../types'; -import { documentArgFeatureWorker } from '../utils/featureWorkers'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; -import { LanguageServicePlugin } from '@volar/language-service'; -import * as vscode from 'vscode-languageserver-protocol'; - -export function register(context: DocumentServiceRuntimeContext) { - - return (document: TextDocument, position: vscode.Position, options: Parameters>[2]) => { - - return documentArgFeatureWorker( - context, - document, - { position, options }, - () => true, - function* ({ position, options }, map) { - for (const mappedPos of map.toGeneratedPositions(position, data => !!data.completion)) { - for (const [mappedChangeOffset] of map.map.toGeneratedOffsets(options.lastChange.rangeOffset)) { - yield { - position: mappedPos, - options: { - lastChange: { - ...options.lastChange, - rangeOffset: mappedChangeOffset, - range: { - start: map.virtualFileDocument.positionAt(mappedChangeOffset), - end: map.virtualFileDocument.positionAt(mappedChangeOffset + options.lastChange.rangeLength), - }, - } - } - }; - } - } - }, - (plugin, document, { position, options }) => plugin.doAutoInsert?.(document, position, options), - (data, map) => { - - if (typeof data === 'string') - return data; - - const range = map.toSourceRange(data.range); - if (range) { - data.range = range; - return data; - } - }, - ); - }; -} diff --git a/packages/language-service/src/documentFeatures/colorPresentations.ts b/packages/language-service/src/documentFeatures/colorPresentations.ts index 00821ca6d..16e00eb54 100644 --- a/packages/language-service/src/documentFeatures/colorPresentations.ts +++ b/packages/language-service/src/documentFeatures/colorPresentations.ts @@ -1,19 +1,21 @@ -import type { DocumentServiceRuntimeContext } from '../types'; -import { documentArgFeatureWorker } from '../utils/featureWorkers'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; +import type { LanguageServiceRuntimeContext } from '../types'; +import { languageFeatureWorker } from '../utils/featureWorkers'; import * as vscode from 'vscode-languageserver-protocol'; import * as shared from '@volar/shared'; -export function register(context: DocumentServiceRuntimeContext) { +export function register(context: LanguageServiceRuntimeContext) { - return (document: TextDocument, color: vscode.Color, range: vscode.Range) => { + return (uri: string, color: vscode.Color, range: vscode.Range) => { - return documentArgFeatureWorker( + return languageFeatureWorker( context, - document, + uri, range, - (file) => !!file.capabilities.documentSymbol, // TODO: add color capabilitie setting - (range, map) => map.toGeneratedRanges(range), + (range, map, file) => { + if (file.capabilities.documentSymbol) // TODO: add color capability setting + return map.toGeneratedRanges(range); + return []; + }, (plugin, document, range) => plugin.getColorPresentations?.(document, color, range), (data, map) => data.map(cp => { diff --git a/packages/language-service/src/documentFeatures/documentColors.ts b/packages/language-service/src/documentFeatures/documentColors.ts index 6a9b78aa6..79c00cef7 100644 --- a/packages/language-service/src/documentFeatures/documentColors.ts +++ b/packages/language-service/src/documentFeatures/documentColors.ts @@ -1,17 +1,16 @@ -import type { DocumentServiceRuntimeContext } from '../types'; +import type { LanguageServiceRuntimeContext } from '../types'; import { documentFeatureWorker } from '../utils/featureWorkers'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; import * as vscode from 'vscode-languageserver-protocol'; import * as shared from '@volar/shared'; -export function register(context: DocumentServiceRuntimeContext) { +export function register(context: LanguageServiceRuntimeContext) { - return (document: TextDocument) => { + return (uri: string) => { return documentFeatureWorker( context, - document, - (file) => !!file.capabilities.documentSymbol, // TODO: add color capabilitie setting + uri, + file => !!file.capabilities.documentSymbol, // TODO: add color capability setting (plugin, document) => plugin.findDocumentColors?.(document), (data, map) => data.map(color => { const range = map.toSourceRange(color.range); diff --git a/packages/language-service/src/documentFeatures/documentSymbols.ts b/packages/language-service/src/documentFeatures/documentSymbols.ts index 4ae1c96f6..89de7ed2e 100644 --- a/packages/language-service/src/documentFeatures/documentSymbols.ts +++ b/packages/language-service/src/documentFeatures/documentSymbols.ts @@ -1,16 +1,15 @@ -import type { DocumentServiceRuntimeContext } from '../types'; +import type { LanguageServiceRuntimeContext } from '../types'; import { documentFeatureWorker } from '../utils/featureWorkers'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; import { transformSymbolInformations } from '@volar/transforms'; import * as vscode from 'vscode-languageserver-protocol'; -export function register(context: DocumentServiceRuntimeContext) { +export function register(context: LanguageServiceRuntimeContext) { - return (document: TextDocument) => { + return (uri: string) => { return documentFeatureWorker( context, - document, + uri, file => !!file.capabilities.documentSymbol, // TODO: add color capabilitie setting (plugin, document) => plugin.findDocumentSymbols?.(document), (data, map) => transformSymbolInformations( @@ -19,7 +18,7 @@ export function register(context: DocumentServiceRuntimeContext) { const range = map.toSourceRange(location.range); if (range) { // use document.uri instead of map.sourceDocument.uri to fix https://github.com/johnsoncodehk/volar/issues/1925 - return vscode.Location.create(document.uri, range); + return vscode.Location.create(uri, range); } }, ), diff --git a/packages/language-service/src/documentFeatures/foldingRanges.ts b/packages/language-service/src/documentFeatures/foldingRanges.ts index e8ff341c6..dae9a40af 100644 --- a/packages/language-service/src/documentFeatures/foldingRanges.ts +++ b/packages/language-service/src/documentFeatures/foldingRanges.ts @@ -1,16 +1,15 @@ -import type { DocumentServiceRuntimeContext } from '../types'; +import type { LanguageServiceRuntimeContext } from '../types'; import { documentFeatureWorker } from '../utils/featureWorkers'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; import { transformFoldingRanges } from '@volar/transforms'; import type * as _ from 'vscode-languageserver-protocol'; -export function register(context: DocumentServiceRuntimeContext) { +export function register(context: LanguageServiceRuntimeContext) { - return (document: TextDocument) => { + return (uri: string) => { return documentFeatureWorker( context, - document, + uri, file => !!file.capabilities.foldingRange, (plugin, document) => plugin.getFoldingRanges?.(document), (data, sourceMap) => transformFoldingRanges(data, range => sourceMap?.toSourceRange(range)), diff --git a/packages/language-service/src/documentFeatures/format.ts b/packages/language-service/src/documentFeatures/format.ts index 1f49129e7..6886addb0 100644 --- a/packages/language-service/src/documentFeatures/format.ts +++ b/packages/language-service/src/documentFeatures/format.ts @@ -1,15 +1,16 @@ import type { VirtualFile } from '@volar/language-core'; import * as vscode from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { SourceMapWithDocuments } from '../documents'; -import type { DocumentServiceRuntimeContext } from '../types'; +import type { LanguageServiceRuntimeContext } from '../types'; +import * as shared from '@volar/shared'; +import { SourceMap } from '@volar/source-map'; -export function register(context: DocumentServiceRuntimeContext) { +export function register(context: LanguageServiceRuntimeContext) { - const ts = context.typescript; + const ts = context.pluginContext.typescript.module; return async ( - document: TextDocument, + uri: string, options: vscode.FormattingOptions, range?: vscode.Range, onTypeParams?: { @@ -18,32 +19,29 @@ export function register(context: DocumentServiceRuntimeContext) { }, ) => { - if (!range) { - range = vscode.Range.create(document.positionAt(0), document.positionAt(document.getText().length)); - } - - const virtualFile = context.documents.getRootFileBySourceFileUri(document.uri); - const originalDocument = document; - const rootEdits = onTypeParams - ? await tryFormat(document, onTypeParams.position, undefined, onTypeParams.ch) - : await tryFormat(document, range, undefined); + let document = context.getTextDocument(uri); + if (!document) return; - if (!virtualFile) - return rootEdits; + range ??= vscode.Range.create(document.positionAt(0), document.positionAt(document.getText().length)); - if (rootEdits?.length) { - applyEdits(rootEdits); + const source = context.documents.getSourceByUri(document.uri); + if (!source) { + return onTypeParams + ? await tryFormat(document, onTypeParams.position, undefined, onTypeParams.ch) + : await tryFormat(document, range, undefined); } - let level = 0; - + const originalSnapshot = source[0]; + const rootVirtualFile = source[1]; + const originalDocument = document; const initialIndentLanguageId = await context.pluginContext.env.configurationHost?.getConfiguration>('volar.format.initialIndent') ?? { html: true }; - while (true) { + let level = 0; + let edited = false; - tryUpdateVueDocument(); + while (true) { - const embeddedFiles = getEmbeddedFilesByLevel(virtualFile, level++); + const embeddedFiles = getEmbeddedFilesByLevel(rootVirtualFile, level++); if (embeddedFiles.length === 0) break; @@ -58,7 +56,7 @@ export function register(context: DocumentServiceRuntimeContext) { continue; const maps = [...context.documents.getMapsByVirtualFileName(embedded.fileName)]; - const map = maps.find(map => map[1].sourceFileDocument.uri === document.uri)?.[1]; + const map = maps.find(map => map[1].sourceFileDocument.uri === document!.uri)?.[1]; if (!map) continue; @@ -129,27 +127,33 @@ export function register(context: DocumentServiceRuntimeContext) { } if (edits.length > 0) { - applyEdits(edits); + const newText = TextDocument.applyEdits(document, edits); + document = TextDocument.create(document.uri, document.languageId, document.version + 1, newText); + context.core.virtualFiles.update(shared.getPathOfUri(document.uri), ts.ScriptSnapshot.fromString(document.getText())); + edited = true; } if (toPatchIndent) { - tryUpdateVueDocument(); - - const maps = [...context.documents.getMapsByVirtualFileName(virtualFile.fileName)]; - const map = maps.find(map => map[1].sourceFileDocument.uri === toPatchIndent?.sourceMapEmbeddedDocumentUri)?.[1]; - - if (map) { + for (const [_, map] of context.documents.getMapsByVirtualFileUri(toPatchIndent?.sourceMapEmbeddedDocumentUri)) { - const indentEdits = patchInterpolationIndent(context.documents.getDocumentByFileName(virtualFile.snapshot, virtualFile.fileName), map); + const indentEdits = patchInterpolationIndent(document, map.map); if (indentEdits.length > 0) { - applyEdits(indentEdits); + const newText = TextDocument.applyEdits(document, indentEdits); + document = TextDocument.create(document.uri, document.languageId, document.version + 1, newText); + context.core.virtualFiles.update(shared.getPathOfUri(document.uri), ts.ScriptSnapshot.fromString(document.getText())); + edited = true; } } } } + if (edited) { + // recover + context.core.virtualFiles.update(shared.getPathOfUri(document.uri), originalSnapshot); + } + if (document.getText() === originalDocument.getText()) return; @@ -161,15 +165,9 @@ export function register(context: DocumentServiceRuntimeContext) { return [textEdit]; - function tryUpdateVueDocument() { - if (virtualFile && virtualFile.snapshot.getText(0, virtualFile.snapshot.getLength()) !== document.getText()) { - context.updateVirtualFile(virtualFile.fileName, ts.ScriptSnapshot.fromString(document.getText())); - } - } - function getEmbeddedFilesByLevel(rootFile: VirtualFile, level: number) { - const embeddedFilesByLevel: VirtualFile[][] = [rootFile.embeddedFiles]; + const embeddedFilesByLevel: VirtualFile[][] = [[rootFile]]; while (true) { @@ -187,7 +185,12 @@ export function register(context: DocumentServiceRuntimeContext) { } } - async function tryFormat(document: TextDocument, range: vscode.Range | vscode.Position, initialIndentBracket: [string, string] | undefined, ch?: string) { + async function tryFormat( + document: TextDocument, + range: vscode.Range | vscode.Position, + initialIndentBracket: [string, string] | undefined, + ch?: string, + ) { let formatDocument = document; let formatRange = range; @@ -222,11 +225,39 @@ export function register(context: DocumentServiceRuntimeContext) { } } - context.prepareLanguageServices(formatDocument); - for (const plugin of context.plugins) { let edits: vscode.TextEdit[] | null | undefined; + let recover: (() => void) | undefined; + + if (formatDocument !== document && isTsDocument(formatDocument)) { + const formatFileName = shared.getPathOfUri(formatDocument.uri); + const formatSnapshot = ts.ScriptSnapshot.fromString(formatDocument.getText()); + const host = context.pluginContext.typescript.languageServiceHost; + const original = { + getProjectVersion: host.getProjectVersion, + getScriptVersion: host.getScriptVersion, + getScriptSnapshot: host.getScriptSnapshot, + }; + host.getProjectVersion = () => original.getProjectVersion?.() + '-' + formatDocument.version; + host.getScriptVersion = (fileName) => { + if (fileName === formatFileName) { + return original.getScriptVersion?.(fileName) + '-' + formatDocument.version.toString(); + } + return original.getScriptVersion?.(fileName); + }; + host.getScriptSnapshot = (fileName) => { + if (fileName === formatFileName) { + return formatSnapshot; + } + return original.getScriptSnapshot?.(fileName); + }; + recover = () => { + host.getProjectVersion = original.getProjectVersion; + host.getScriptVersion = original.getScriptVersion; + host.getScriptSnapshot = original.getScriptSnapshot; + }; + } try { if (ch !== undefined && vscode.Position.is(formatRange)) { @@ -240,6 +271,8 @@ export function register(context: DocumentServiceRuntimeContext) { console.error(err); } + recover?.(); + if (!edits) continue; @@ -269,23 +302,14 @@ export function register(context: DocumentServiceRuntimeContext) { return edits; } } - - function applyEdits(textEdits: vscode.TextEdit[]) { - - const newText = TextDocument.applyEdits(document, textEdits); - - if (newText !== document.getText()) { - document = TextDocument.create(document.uri, document.languageId, document.version + 1, newText); - } - } }; } -function patchInterpolationIndent(document: TextDocument, map: SourceMapWithDocuments) { +function patchInterpolationIndent(document: TextDocument, map: SourceMap) { const indentTextEdits: vscode.TextEdit[] = []; - for (const mapped of map.map.mappings) { + for (const mapped of map.mappings) { const textRange = { start: document.positionAt(mapped.sourceRange[0]), @@ -300,6 +324,9 @@ function patchInterpolationIndent(document: TextDocument, map: SourceMapWithDocu const removeIndent = getRemoveIndent(lines); const baseIndent = getBaseIndent(mapped.sourceRange[0]); + if (removeIndent === baseIndent) + continue; + for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (line.startsWith(removeIndent)) { @@ -329,3 +356,10 @@ function patchInterpolationIndent(document: TextDocument, map: SourceMapWithDocu return startLineText.substring(0, startLineText.length - startLineText.trimStart().length); } } + +export function isTsDocument(document: TextDocument) { + return document.languageId === 'javascript' || + document.languageId === 'typescript' || + document.languageId === 'javascriptreact' || + document.languageId === 'typescriptreact'; +} diff --git a/packages/language-service/src/documentFeatures/linkedEditingRanges.ts b/packages/language-service/src/documentFeatures/linkedEditingRanges.ts index 3e48c4fd7..c3d9678f9 100644 --- a/packages/language-service/src/documentFeatures/linkedEditingRanges.ts +++ b/packages/language-service/src/documentFeatures/linkedEditingRanges.ts @@ -1,18 +1,16 @@ -import type { DocumentServiceRuntimeContext } from '../types'; -import { documentArgFeatureWorker } from '../utils/featureWorkers'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; +import type { LanguageServiceRuntimeContext } from '../types'; +import { languageFeatureWorker } from '../utils/featureWorkers'; import * as vscode from 'vscode-languageserver-protocol'; import * as shared from '@volar/shared'; -export function register(context: DocumentServiceRuntimeContext) { +export function register(context: LanguageServiceRuntimeContext) { - return (document: TextDocument, position: vscode.Position) => { + return (uri: string, position: vscode.Position) => { - return documentArgFeatureWorker( + return languageFeatureWorker( context, - document, + uri, position, - () => true, (position, map) => map.toGeneratedPositions(position, data => !!data.completion), (plugin, document, position) => plugin.findLinkedEditingRanges?.(document, position), (data, map) => ({ diff --git a/packages/language-service/src/documentFeatures/selectionRanges.ts b/packages/language-service/src/documentFeatures/selectionRanges.ts index 3c9f606be..6b2786142 100644 --- a/packages/language-service/src/documentFeatures/selectionRanges.ts +++ b/packages/language-service/src/documentFeatures/selectionRanges.ts @@ -1,25 +1,25 @@ -import type { DocumentServiceRuntimeContext } from '../types'; -import { documentArgFeatureWorker } from '../utils/featureWorkers'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; +import type { LanguageServiceRuntimeContext } from '../types'; +import { languageFeatureWorker } from '../utils/featureWorkers'; import { transformSelectionRanges } from '@volar/transforms'; import * as vscode from 'vscode-languageserver-protocol'; import * as shared from '@volar/shared'; -export function register(context: DocumentServiceRuntimeContext) { +export function register(context: LanguageServiceRuntimeContext) { - return (document: TextDocument, positions: vscode.Position[]) => { + return (uri: string, positions: vscode.Position[]) => { - return documentArgFeatureWorker( + return languageFeatureWorker( context, - document, + uri, positions, - file => !!file.capabilities.documentFormatting, - (positions, map) => { - const result = positions - .map(position => map.toGeneratedPosition(position)) - .filter(shared.notEmpty); - if (result.length) { - return [result]; + (positions, map, file) => { + if (file.capabilities.documentFormatting) { + const result = positions + .map(position => map.toGeneratedPosition(position)) + .filter(shared.notEmpty); + if (result.length) { + return [result]; + } } return []; }, diff --git a/packages/language-service/src/documents.ts b/packages/language-service/src/documents.ts index 6e43b5fa9..64c55f412 100644 --- a/packages/language-service/src/documents.ts +++ b/packages/language-service/src/documents.ts @@ -161,6 +161,10 @@ export function createDocumentsAndSourceMaps(mapper: VirtualFiles) { const _documents = new WeakMap(); return { + getSourceByUri(sourceFileUri: string) { + const fileName = shared.getPathOfUri(sourceFileUri); + return mapper.get(fileName); + }, getRootFileBySourceFileUri(sourceFileUri: string) { const fileName = shared.getPathOfUri(sourceFileUri); const rootFile = mapper.get(fileName); diff --git a/packages/language-service/src/index.ts b/packages/language-service/src/index.ts index 55a03cad5..c88eb96db 100644 --- a/packages/language-service/src/index.ts +++ b/packages/language-service/src/index.ts @@ -1,5 +1,4 @@ export * from '@volar/language-core'; -export * from './baseDocumentService'; export * from './baseLanguageService'; export * from './documents'; export { executePluginCommand, ExecutePluginCommandArgs } from './languageFeatures/executeCommand'; diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index 3e1195679..4a4bbf0e0 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -9,16 +9,6 @@ import { DocumentsAndSourceMaps } from './documents'; export * from 'vscode-languageserver-protocol'; -export interface DocumentServiceRuntimeContext { - typescript: typeof import('typescript/lib/tsserverlibrary'); - plugins: LanguageServicePlugin[]; - pluginContext: LanguageServicePluginContext; - documents: DocumentsAndSourceMaps; - update(document: TextDocument): void; - updateVirtualFile(fileName: string, snapshot: ts.IScriptSnapshot): void; - prepareLanguageServices(document: TextDocument): void; -}; - export interface LanguageServiceRuntimeContext { host: LanguageServiceHost; core: LanguageContext; diff --git a/packages/language-service/src/utils/featureWorkers.ts b/packages/language-service/src/utils/featureWorkers.ts index d5f965d94..23168e72c 100644 --- a/packages/language-service/src/utils/featureWorkers.ts +++ b/packages/language-service/src/utils/featureWorkers.ts @@ -1,111 +1,40 @@ import type { TextDocument } from 'vscode-languageserver-textdocument'; import { visitEmbedded } from './definePlugin'; -import type { DocumentServiceRuntimeContext, LanguageServiceRuntimeContext } from '../types'; +import type { LanguageServiceRuntimeContext } from '../types'; import { LanguageServicePlugin, FileRangeCapabilities, VirtualFile } from '@volar/language-service'; import { SourceMapWithDocuments } from '../documents'; export async function documentFeatureWorker( - context: DocumentServiceRuntimeContext, - document: TextDocument, + context: LanguageServiceRuntimeContext, + uri: string, isValidSourceMap: (file: VirtualFile, sourceMap: SourceMapWithDocuments) => boolean, worker: (plugin: LanguageServicePlugin, document: TextDocument) => T, - transform: (result: NonNullable>, sourceMap: SourceMapWithDocuments) => T | undefined, + transform: (result: NonNullable>, sourceMap: SourceMapWithDocuments) => Awaited | undefined, combineResult?: (results: NonNullable>[]) => NonNullable>, ) { - return documentArgFeatureWorker( + return languageFeatureWorker( context, - document, + uri, true, - isValidSourceMap, - () => [true], + (_, map, file) => { + if (isValidSourceMap(file, map)) { + return [true]; + } + return []; + }, worker, transform, combineResult, ); } -export async function documentArgFeatureWorker( - context: DocumentServiceRuntimeContext, - document: TextDocument, - arg: K, - isValidSourceMap: (file: VirtualFile, sourceMap: SourceMapWithDocuments) => boolean, - transformArg: (arg: K, sourceMap: SourceMapWithDocuments) => Generator | K[], - worker: (plugin: LanguageServicePlugin, document: TextDocument, arg: K) => T, - transform: (result: NonNullable>, sourceMap: SourceMapWithDocuments) => T | undefined, - combineResult?: (results: NonNullable>[]) => NonNullable>, -) { - - context.update(document); - const virtualFile = context.documents.getRootFileBySourceFileUri(document.uri); - - let results: NonNullable>[] = []; - - if (virtualFile) { - - await visitEmbedded(context.documents, virtualFile, async (file, map) => { - - if (!isValidSourceMap(file, map)) - return true; - - context.prepareLanguageServices(map.virtualFileDocument); - - for (const mappedArg of transformArg(arg, map)) { - - for (const plugin of context.plugins) { - - const embeddedResult = await worker(plugin, map.virtualFileDocument, mappedArg); - - if (!embeddedResult) - continue; - - const result = await transform(embeddedResult!, map); - - if (!result) - continue; - - results.push(result!); - - if (!combineResult) - return false; - } - } - - return true; - }); - } - else if (results.length === 0 || !!combineResult) { - - context.prepareLanguageServices(document); - - for (const plugin of context.plugins) { - - const result = await worker(plugin, document, arg); - - if (!result) - continue; - - results.push(result!); - - if (!combineResult) - break; - } - } - - if (combineResult && results.length > 0) { - return combineResult(results); - } - else if (results.length > 0) { - return results[0]; - } -} - export async function languageFeatureWorker( context: LanguageServiceRuntimeContext, uri: string, arg: K, transformArg: (arg: K, sourceMap: SourceMapWithDocuments, file: VirtualFile) => Generator | K[], worker: (plugin: LanguageServicePlugin, document: TextDocument, arg: K, sourceMap: SourceMapWithDocuments | undefined, file: VirtualFile | undefined) => T, - transform: (result: NonNullable>, sourceMap: SourceMapWithDocuments | undefined) => Awaited | undefined, + transform: (result: NonNullable>, sourceMap: SourceMapWithDocuments) => Awaited | undefined, combineResult?: (results: NonNullable>[]) => NonNullable>, reportProgress?: (result: NonNullable>) => void, ) { @@ -149,17 +78,11 @@ export async function languageFeatureWorker( return true; }); } - else if (document && (results.length === 0 || !!combineResult)) { + else if (document) { for (const plugin of context.plugins) { - const embeddedResult = await worker(plugin, document, arg, undefined, undefined); - - if (!embeddedResult) - continue; - - const result = transform(embeddedResult!, undefined); - + const result = await worker(plugin, document, arg, undefined, undefined); if (!result) continue; diff --git a/packages/language-service/src/utils/singleFileTypeScriptService.ts b/packages/language-service/src/utils/singleFileTypeScriptService.ts deleted file mode 100644 index da36d775e..000000000 --- a/packages/language-service/src/utils/singleFileTypeScriptService.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as shared from '@volar/shared'; -import type * as ts from 'typescript/lib/tsserverlibrary'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; - -let projectVersion = 0; -let doc: TextDocument; -let scriptFileName: string; -let scriptSnapshot: ts.IScriptSnapshot; - -export const singleFileTypeScriptServiceHost: ts.LanguageServiceHost = { - readFile: () => undefined, - fileExists: fileName => fileName === scriptFileName, - getProjectVersion: () => projectVersion.toString(), - getScriptVersion: () => projectVersion.toString(), - getCompilationSettings: () => ({ allowJs: true, jsx: 1 }), - getScriptFileNames: () => [scriptFileName], - getScriptSnapshot: fileName => { - if (fileName === scriptFileName) { - return scriptSnapshot; - } - }, - getCurrentDirectory: () => '', - getDefaultLibFileName: () => '', -}; - -export function updateSingleFileTypeScriptServiceHost( - ts: typeof import('typescript/lib/tsserverlibrary'), - _doc: TextDocument, -) { - projectVersion++; - doc = _doc; - scriptFileName = shared.getPathOfUri(_doc.uri); - scriptSnapshot = ts.ScriptSnapshot.fromString(doc.getText()); -} diff --git a/plugins/typescript/src/index.ts b/plugins/typescript/src/index.ts index 0d3118290..5ff1de5f8 100644 --- a/plugins/typescript/src/index.ts +++ b/plugins/typescript/src/index.ts @@ -287,7 +287,7 @@ export default function (): LanguageServicePlugin { return; } - return tsLs2.doFormatting.onRange(document.uri, options_2, range); + return tsLs2.doFormatting.onRange(document.uri, range, options_2); } }, diff --git a/plugins/typescript/src/services/formatting.ts b/plugins/typescript/src/services/formatting.ts index a343542af..8f222d94d 100644 --- a/plugins/typescript/src/services/formatting.ts +++ b/plugins/typescript/src/services/formatting.ts @@ -11,7 +11,7 @@ export function register( getConfiguration: GetConfiguration, ) { return { - onRange: async (uri: string, options: vscode.FormattingOptions, range?: vscode.Range): Promise => { + onRange: async (uri: string, range: vscode.Range | undefined, options: vscode.FormattingOptions): Promise => { const document = getTextDocument(uri); if (!document) return []; diff --git a/vue-language-tools/vue-language-core/src/generators/template.ts b/vue-language-tools/vue-language-core/src/generators/template.ts index 8f3028112..bcedbea8f 100644 --- a/vue-language-tools/vue-language-core/src/generators/template.ts +++ b/vue-language-tools/vue-language-core/src/generators/template.ts @@ -25,7 +25,7 @@ const formatBrackets = { empty: ['', ''] as [string, string], round: ['(', ')'] as [string, string], // fix https://github.com/johnsoncodehk/volar/issues/1210 - curly: ['({ __VLS_foo:', '})'] as [string, string], + curly: ['({', '})'] as [string, string], square: ['[', ']'] as [string, string], }; const validTsVar = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; diff --git a/vue-language-tools/vue-language-server/src/languageServerPlugin.ts b/vue-language-tools/vue-language-server/src/languageServerPlugin.ts index bb7b1c0de..976a2b3b4 100644 --- a/vue-language-tools/vue-language-server/src/languageServerPlugin.ts +++ b/vue-language-tools/vue-language-server/src/languageServerPlugin.ts @@ -29,105 +29,82 @@ const plugin: LanguageServerPlugin vueOptions, - }; - }, - getLanguageModules(host) { - const vueLanguageModules = vue2.createLanguageModules( - host.getTypeScriptModule(), - host.getCompilationSettings(), - host.getVueCompilationSettings(), - ); - return vueLanguageModules; - }, - getServicePlugins(host, service) { - const settings: vue.Settings = {}; - if (initOptions.json) { - settings.json = { schemas: [] }; - for (const blockType in initOptions.json.customBlockSchemaUrls) { - const url = initOptions.json.customBlockSchemaUrls[blockType]; - settings.json.schemas?.push({ - fileMatch: [`*.customBlock_${blockType}_*.json*`], - uri: new URL(url, service.context.pluginContext.env.rootUri.toString() + '/').toString(), - }); - } + resolveLanguageServiceHost(ts, sys, tsConfig, host) { + let vueOptions: vue.VueCompilerOptions = {}; + if (typeof tsConfig === 'string') { + vueOptions = vue2.createParsedCommandLine(ts, sys, tsConfig, []).vueOptions; + } + vueOptions.extensions = getVueExts(vueOptions.extensions ?? ['.vue']); + return { + ...host, + getVueCompilationSettings: () => vueOptions, + }; + }, + getLanguageModules(host) { + const vueLanguageModules = vue2.createLanguageModules( + host.getTypeScriptModule(), + host.getCompilationSettings(), + host.getVueCompilationSettings(), + ); + return vueLanguageModules; + }, + getServicePlugins(host, service) { + const settings: vue.Settings = {}; + if (initOptions.json) { + settings.json = { schemas: [] }; + for (const blockType in initOptions.json.customBlockSchemaUrls) { + const url = initOptions.json.customBlockSchemaUrls[blockType]; + settings.json.schemas?.push({ + fileMatch: [`*.customBlock_${blockType}_*.json*`], + uri: new URL(url, service.context.pluginContext.env.rootUri.toString() + '/').toString(), + }); } - return vue.getLanguageServicePlugins(host, service, settings); - }, - onInitialize(connection, getService) { + } + return vue.getLanguageServicePlugins(host, service, settings); + }, + onInitialize(connection, getService) { - connection.onRequest(GetVueCompilerOptionsRequest.type, async params => { - const languageService = await getService(params.uri); - const host = languageService.context.host as vue.LanguageServiceHost; - return host.getVueCompilationSettings?.(); - }); + connection.onRequest(ParseSFCRequest.type, params => { + return vue2.parse(params); + }); - connection.onRequest(DetectNameCasingRequest.type, async params => { - const languageService = await getService(params.textDocument.uri); - return nameCasing.detect(languageService.context, params.textDocument.uri); - }); + connection.onRequest(GetVueCompilerOptionsRequest.type, async params => { + const languageService = await getService(params.uri); + const host = languageService.context.host as vue.LanguageServiceHost; + return host.getVueCompilationSettings?.(); + }); - connection.onRequest(GetConvertTagCasingEditsRequest.type, async params => { - const languageService = await getService(params.textDocument.uri); - return nameCasing.convertTagName(languageService.context, params.textDocument.uri, params.casing); - }); + connection.onRequest(DetectNameCasingRequest.type, async params => { + const languageService = await getService(params.textDocument.uri); + return nameCasing.detect(languageService.context, params.textDocument.uri); + }); - connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { - const languageService = await getService(params.textDocument.uri); - return nameCasing.convertAttrName(languageService.context, params.textDocument.uri, params.casing); - }); + connection.onRequest(GetConvertTagCasingEditsRequest.type, async params => { + const languageService = await getService(params.textDocument.uri); + return nameCasing.convertTagName(languageService.context, params.textDocument.uri, params.casing); + }); - const checkers = new WeakMap(); + connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { + const languageService = await getService(params.textDocument.uri); + return nameCasing.convertAttrName(languageService.context, params.textDocument.uri, params.casing); + }); - connection.onRequest(GetComponentMeta.type, async params => { - const languageService = await getService(params.uri); - let checker = checkers.get(languageService.context.host); - if (!checker) { - checker = meta.baseCreate( - languageService.context.host as vue.LanguageServiceHost, - {}, - languageService.context.host.getCurrentDirectory() + '/tsconfig.json.global.vue', - languageService.context.pluginContext.typescript.module, - ); - checkers.set(languageService.context.host, checker); - } - return checker.getComponentMeta(shared.getPathOfUri(params.uri)); - }); - }, - }, - syntacticService: { - getLanguageModules(ts) { - const vueOptions: vue.VueCompilerOptions = { extensions: getVueExts(['.vue']) }; - const vueLanguagePlugins = vue2.getDefaultVueLanguagePlugins(ts, {}, vueOptions); - const vueLanguageModule: embedded.LanguageModule = { - createFile(fileName, snapshot) { - if (vueOptions.extensions?.some(ext => fileName.endsWith(ext))) { - return new vue2.VueFile(fileName, snapshot, ts, vueLanguagePlugins); - } - }, - updateFile(sourceFile: vue2.VueFile, snapshot) { - sourceFile.update(snapshot); - }, - }; - return [vueLanguageModule]; - }, - getServicePlugins(context) { - return vue.getDocumentServicePlugins(context); - }, - onInitialize(connection) { - connection.onRequest(ParseSFCRequest.type, params => { - return vue2.parse(params); - }); - }, + const checkers = new WeakMap(); + + connection.onRequest(GetComponentMeta.type, async params => { + const languageService = await getService(params.uri); + let checker = checkers.get(languageService.context.host); + if (!checker) { + checker = meta.baseCreate( + languageService.context.host as vue.LanguageServiceHost, + {}, + languageService.context.host.getCurrentDirectory() + '/tsconfig.json.global.vue', + languageService.context.pluginContext.typescript.module, + ); + checkers.set(languageService.context.host, checker); + } + return checker.getComponentMeta(shared.getPathOfUri(params.uri)); + }); }, }; diff --git a/vue-language-tools/vue-language-service/src/documentService.ts b/vue-language-tools/vue-language-service/src/documentService.ts deleted file mode 100644 index e37eaf931..000000000 --- a/vue-language-tools/vue-language-service/src/documentService.ts +++ /dev/null @@ -1,76 +0,0 @@ -import useCssPlugin from '@volar-plugins/css'; -import useHtmlPlugin from '@volar-plugins/html'; -import useJsonPlugin from '@volar-plugins/json'; -import usePugPlugin from '@volar-plugins/pug'; -import usePugFormatPlugin from '@volar-plugins/pug-beautify'; -import useTsPlugin from '@volar-plugins/typescript'; -import { DocumentServiceRuntimeContext } from '@volar/language-service'; -import useVuePlugin from './plugins/vue'; -import useAutoWrapParenthesesPlugin from './plugins/vue-autoinsert-parentheses'; -import useAutoAddSpacePlugin from './plugins/vue-autoinsert-space'; -import * as embeddedLS from '@volar/language-service'; -import * as vue from '@volar/vue-language-core'; - -import type * as _1 from 'vscode-languageserver-protocol'; -import type * as _2 from 'vscode-languageserver-textdocument'; -import { VueFile } from '@volar/vue-language-core'; - -export function getDocumentServicePlugins( - context: DocumentServiceRuntimeContext -) { - - const getVueFile = (document: _2.TextDocument) => { - context.update(document); - const virtualFile = context.documents.getVirtualFileByUri(document.uri); - if (virtualFile instanceof VueFile) { - return virtualFile; - } - }; - const vuePlugin = useVuePlugin({ getVueFile }); - const htmlPlugin = useHtmlPlugin(); - const pugPlugin = usePugPlugin(); - const cssPlugin = useCssPlugin(); - const jsonPlugin = useJsonPlugin(); - const tsPlugin = useTsPlugin(); - const autoWrapParenthesesPlugin = useAutoWrapParenthesesPlugin({ getVueFile }); - const autoAddSpacePlugin = useAutoAddSpacePlugin(); - const pugFormatPlugin = usePugFormatPlugin(); - - return [ - vuePlugin, - htmlPlugin, - pugPlugin, - pugFormatPlugin, - cssPlugin, - jsonPlugin, - tsPlugin, - autoWrapParenthesesPlugin, - autoAddSpacePlugin, - ]; -} - -export function createDocumentService( - ts: typeof import('typescript/lib/tsserverlibrary'), - env: embeddedLS.LanguageServicePluginContext['env'], -) { - - const vueLanguageModules = vue.createLanguageModules( - ts, - {}, - {}, - ); - const languageServiceContext = embeddedLS.createDocumentServiceContext({ - ts, - env, - getLanguageModules() { - return vueLanguageModules; - }, - getPlugins() { - return plugins; - }, - }); - const plugins = getDocumentServicePlugins(languageServiceContext); - const languageService = embeddedLS.createDocumentService(languageServiceContext); - - return languageService; -} diff --git a/vue-language-tools/vue-language-service/src/index.ts b/vue-language-tools/vue-language-service/src/index.ts index e903541ad..75cdfda8a 100644 --- a/vue-language-tools/vue-language-service/src/index.ts +++ b/vue-language-tools/vue-language-service/src/index.ts @@ -1,4 +1,3 @@ -export * from './documentService'; export * from './languageService'; export { ConfigurationHost, LanguageServicePlugin as EmbeddedLanguageServicePlugin, executePluginCommand, ExecutePluginCommandArgs, SemanticToken } from '@volar/language-service'; export * from './ideFeatures/nameCasing';