From de0fa5c6a20f7f7473068a1a4fffbc1ca48fa5eb Mon Sep 17 00:00:00 2001 From: Qiming Zhao Date: Tue, 8 Dec 2020 18:50:01 +0800 Subject: [PATCH] support languages from plugins Closes #235 --- package.json | 2 + src/index.ts | 49 +++-- src/server/commands.ts | 49 ++--- src/server/features/moduleInstall.ts | 6 +- src/server/features/watchBuild.ts | 9 +- src/server/index.ts | 18 ++ src/server/languageProvider.ts | 224 ++++++---------------- src/server/organizeImports.ts | 19 +- src/server/typescriptServiceClientHost.ts | 67 +++++-- src/server/utils/modules.ts | 19 +- 10 files changed, 207 insertions(+), 255 deletions(-) diff --git a/package.json b/package.json index b7d0978..55c30d0 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "onLanguage:typescript.jsx", "onLanguage:typescriptreact", "onLanguage:jsx-tags", + "onLanguage:jsonc", + "onCommand:_typescript.configurePlugin", "onCommand:typescript.reloadProjects", "onCommand:javascript.reloadProjects", "onCommand:javascript.goToProjectConfig", diff --git a/src/index.ts b/src/index.ts index f08cac3..54db754 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,40 +9,39 @@ interface API { } export async function activate(context: ExtensionContext): Promise { - let { subscriptions } = context + let { subscriptions, logger } = context const config = workspace.getConfiguration().get('tsserver', {}) if (!config.enable) return const pluginManager = new PluginManager() const service = new TsserverService(pluginManager) - - subscriptions.push( - (services as any).regist(service) - ) - - await service.start() - function registCommand(cmd: Command): void { let { id, execute } = cmd subscriptions.push(commands.registerCommand(id as string, execute, cmd)) } - - registCommand(new AutoFixCommand(service.clientHost)) - registCommand(new ReloadProjectsCommand(service.clientHost)) - registCommand(new OpenTsServerLogCommand(service.clientHost)) - registCommand(new TypeScriptGoToProjectConfigCommand(service.clientHost)) - registCommand(new OrganizeImportsCommand(service.clientHost.serviceClient)) registCommand(new ConfigurePluginCommand(pluginManager)) - registCommand(commands.register({ - id: 'tsserver.restart', - execute: (): void => { - // tslint:disable-next-line:no-floating-promises - service.stop().then(() => { - setTimeout(() => { - service.restart() - }, 100) - }) - } - })) + registCommand(new AutoFixCommand(service)) + registCommand(new ReloadProjectsCommand(service)) + registCommand(new OpenTsServerLogCommand(service)) + registCommand(new TypeScriptGoToProjectConfigCommand(service)) + registCommand(new OrganizeImportsCommand(service)) + + service.start().then(() => { + subscriptions.push(services.regist(service)) + registCommand(commands.register({ + id: 'tsserver.restart', + execute: (): void => { + // tslint:disable-next-line:no-floating-promises + service.stop().then(() => { + setTimeout(() => { + service.restart() + }, 100) + }) + } + })) + }, e => { + logger.error(`Error on service start:`, e) + }) + return { configurePlugin: (pluginId: string, configuration: {}): void => { pluginManager.setConfiguration(pluginId, configuration) diff --git a/src/server/commands.ts b/src/server/commands.ts index fe55060..362f1e2 100644 --- a/src/server/commands.ts +++ b/src/server/commands.ts @@ -1,4 +1,4 @@ -import { Uri as URI, diagnosticManager, workspace, commands } from 'coc.nvim' +import { Uri as URI, diagnosticManager, workspace, commands, ServiceStat } from 'coc.nvim' import { CancellationToken, Diagnostic } from 'vscode-languageserver-protocol' import * as Proto from './protocol' import TypeScriptServiceClientHost from './typescriptServiceClientHost' @@ -7,6 +7,7 @@ import { TextEdit, Range } from 'vscode-languageserver-types' import { installModules } from './utils/modules' import { nodeModules } from './utils/helper' import { PluginManager } from '../utils/plugins' +import TsserverService from '../server' export interface Command { readonly id: string | string[] @@ -17,11 +18,12 @@ export class ReloadProjectsCommand implements Command { public readonly id = 'tsserver.reloadProjects' public constructor( - private readonly client: TypeScriptServiceClientHost + private readonly service: TsserverService ) { } - public execute(): void { - this.client.reloadProjects() + public async execute(): Promise { + let client = await this.service.getClientHost() + client.reloadProjects() workspace.showMessage('projects reloaded') } } @@ -30,11 +32,12 @@ export class OpenTsServerLogCommand implements Command { public readonly id = 'tsserver.openTsServerLog' public constructor( - private readonly client: TypeScriptServiceClientHost + private readonly service: TsserverService ) { } - public execute(): void { - this.client.serviceClient.openTsServerLogFile() // tslint:disable-line + public async execute(): Promise { + let client = await this.service.getClientHost() + client.serviceClient.openTsServerLogFile() // tslint:disable-line } } @@ -42,17 +45,18 @@ export class TypeScriptGoToProjectConfigCommand implements Command { public readonly id = 'tsserver.goToProjectConfig' public constructor( - private readonly client: TypeScriptServiceClientHost + private readonly service: TsserverService ) { } public async execute(): Promise { + let client = await this.service.getClientHost() let doc = await workspace.document let { languageId } = doc.textDocument - if (this.client.serviceClient.modeIds.indexOf(languageId) == -1) { - workspace.showMessage(`Could not determine TypeScript or JavaScript project. Unsupported file type: ${languageId}`, 'warning') + if (client.serviceClient.modeIds.indexOf(languageId) == -1) { + throw new Error(`Could not determine TypeScript or JavaScript project. Unsupported file type: ${languageId}`) return } - await goToProjectConfig(this.client, doc.uri) + await goToProjectConfig(client, doc.uri) } } @@ -69,13 +73,11 @@ async function goToProjectConfig(clientHost: TypeScriptServiceClientHost, uri: s workspace.showMessage('Could not determine TypeScript or JavaScript project.', 'warning') return } - const { configFileName } = res.body if (configFileName && !isImplicitProjectConfigFile(configFileName)) { await workspace.openResource(URI.file(configFileName).toString()) return } - workspace.showMessage('Config file not found', 'warning') } @@ -92,18 +94,22 @@ const autoFixableDiagnosticCodes = new Set([ export class AutoFixCommand implements Command { public readonly id = 'tsserver.executeAutofix' - constructor(private client: TypeScriptServiceClientHost) { + constructor(private service: TsserverService) { } public async execute(): Promise { - let document = await workspace.document - let { uri } = document - let handles = await this.client.handles(uri) - if (!handles) { - workspace.showMessage(`Document ${uri} is not handled by tsserver.`, 'warning') + if (this.service.state != ServiceStat.Running) { + throw new Error('service not running') return } - let file = this.client.serviceClient.toPath(document.uri) + let client = await this.service.getClientHost() + let document = await workspace.document + let handles = await client.handles(document.textDocument) + if (!handles) { + throw new Error(`Document ${document.uri} is not handled by tsserver.`) + return + } + let file = client.serviceClient.toPath(document.uri) let diagnostics = diagnosticManager.getDiagnostics(document.uri) let missingDiagnostics = diagnostics.filter(o => o.code == 2307) if (missingDiagnostics.length) { @@ -125,7 +131,6 @@ export class AutoFixCommand implements Command { arr.push(curr) return arr }, [] as Diagnostic[]) - let client = this.client.serviceClient let edits: TextEdit[] = [] let command: string let names: string[] = [] @@ -134,7 +139,7 @@ export class AutoFixCommand implements Command { ...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range), errorCodes: [+(diagnostic.code!)] } - const response = await client.execute('getCodeFixes', args, CancellationToken.None) + const response = await client.serviceClient.execute('getCodeFixes', args, CancellationToken.None) if (response.type !== 'response' || !response.body || response.body.length < 1) { if (diagnostic.code == 2304) { let { range } = diagnostic diff --git a/src/server/features/moduleInstall.ts b/src/server/features/moduleInstall.ts index ec3a8dc..7e4fc85 100644 --- a/src/server/features/moduleInstall.ts +++ b/src/server/features/moduleInstall.ts @@ -1,7 +1,7 @@ import { Uri, commands } from 'coc.nvim' import { Command } from 'coc.nvim/lib/commands' import { CodeActionProvider } from 'coc.nvim/lib/provider' -import { CancellationToken, CodeAction, CodeActionContext, Range } from 'vscode-languageserver-protocol' +import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Range } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { ITypeScriptServiceClient } from '../typescriptService' import { installModules } from '../utils/modules' @@ -35,7 +35,7 @@ export default class InstallModuleProvider implements CodeActionProvider { let { diagnostics } = context let diags = diagnostics.filter(s => s.code == 2307) let names = diags.map(o => { - let ms = o.message.match(/module\s'(.+)'\./) + let ms = o.message.match(/module\s'(.+)'/) return ms ? ms[1] : null }) names = names.filter(s => s != null) @@ -48,7 +48,7 @@ export default class InstallModuleProvider implements CodeActionProvider { command: InstallModuleCommand.ID, arguments: [document.uri, name] } - let codeAction = CodeAction.create(title, command) + let codeAction = CodeAction.create(title, command, CodeActionKind.QuickFix) actions.push(codeAction) } return actions diff --git a/src/server/features/watchBuild.ts b/src/server/features/watchBuild.ts index f6d33e5..d25e72f 100644 --- a/src/server/features/watchBuild.ts +++ b/src/server/features/watchBuild.ts @@ -1,6 +1,4 @@ -import { disposeAll, StatusBarItem, TaskOptions, Uri, workspace } from 'coc.nvim' -import { CommandManager } from 'coc.nvim/lib/commands' -import Task from 'coc.nvim/lib/model/task' +import { disposeAll, commands, StatusBarItem, TaskOptions, Uri, workspace } from 'coc.nvim' import path from 'path' import { Disposable, Location } from 'vscode-languageserver-protocol' import TypeScriptServiceClient from '../typescriptServiceClient' @@ -26,16 +24,15 @@ export default class WatchProject implements Disposable { public static readonly id: string = 'tsserver.watchBuild' public static readonly startTexts: string[] = ['Starting compilation in watch mode', 'Starting incremental compilation'] private statusItem: StatusBarItem - private task: Task + private task: any private options: TaskOptions public constructor( - commandManager: CommandManager, private client: TypeScriptServiceClient ) { this.statusItem = workspace.createStatusBarItem(1, { progress: true }) let task = this.task = workspace.createTask('TSC') - this.disposables.push(commandManager.registerCommand(WatchProject.id, async () => { + this.disposables.push(commands.registerCommand(WatchProject.id, async () => { let opts = this.options = await this.getOptions() await this.start(opts) })) diff --git a/src/server/index.ts b/src/server/index.ts index 143336b..92a0b1a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -33,6 +33,24 @@ export default class TsserverService implements IServiceProvider { return workspace.getConfiguration('tsserver') } + /** + * Get running client host. + */ + public getClientHost(): Promise { + if (this.state == ServiceStat.Running) return Promise.resolve(this.clientHost) + this.start() + return new Promise((resolve, reject) => { + let timer = setTimeout(() => { + reject(new Error(`Server not started after 5s`)) + }, 5000) + let disposable = this.onServiceReady(() => { + clearTimeout(timer) + disposable.dispose() + resolve(this.clientHost) + }) + }) + } + public start(): Promise { if (this.clientHost) return this.state = ServiceStat.Starting diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index 5f6bde9..39b8780 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { commands, DiagnosticKind, disposeAll, languages, Uri, workspace } from 'coc.nvim' +import { DiagnosticKind, disposeAll, languages, Uri, workspace } from 'coc.nvim' import path from 'path' import { CodeActionKind, Diagnostic, DiagnosticSeverity, Disposable, TextDocument } from 'vscode-languageserver-protocol' import { CachedNavTreeResponse } from './features/baseCodeLensProvider' @@ -27,8 +27,6 @@ import RenameProvider from './features/rename' import SignatureHelpProvider from './features/signatureHelp' import SmartSelection from './features/smartSelect' import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename' -import WatchBuild from './features/watchBuild' -import WorkspaceSymbolProvider from './features/workspaceSymbols' import { OrganizeImportsCodeActionProvider } from './organizeImports' import TypeScriptServiceClient from './typescriptServiceClient' import API from './utils/api' @@ -48,16 +46,14 @@ export default class LanguageProvider { ) { workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) this.configurationChanged() - let initialized = false + let initialized = false client.onTsServerStarted(async () => { // tslint:disable-line if (!initialized) { initialized = true this.registerProviders(client, typingsStatus) - } else { - this.client.diagnosticsManager.reInitialize() } - }) + }, null, this.disposables) } private configurationChanged(): void { @@ -69,201 +65,89 @@ export default class LanguageProvider { disposeAll(this.disposables) } + private _register(disposable: Disposable): void { + this.disposables.push(disposable) + } + private registerProviders( client: TypeScriptServiceClient, typingsStatus: TypingsStatus ): void { let languageIds = this.description.modeIds - - this.disposables.push( - languages.registerCompletionItemProvider( - `tsserver-${this.description.id}`, - 'TSC', - languageIds, - new CompletionItemProvider( - client, - typingsStatus, - this.fileConfigurationManager, - this.description.id - ), + let clientId = `tsserver-${this.description.id}` + this._register( + languages.registerCompletionItemProvider(clientId, 'TSC', languageIds, + new CompletionItemProvider(client, typingsStatus, this.fileConfigurationManager, this.description.id), CompletionItemProvider.triggerCharacters ) ) - if (this.client.apiVersion.gte(API.v230)) { - this.disposables.push( - languages.registerCompletionItemProvider( - `${this.description.id}-directive`, - 'TSC', - languageIds, - new DirectiveCommentCompletionProvider( - client, - ), - ['@'] - ) - ) + this._register(languages.registerCompletionItemProvider( + `${this.description.id}-directive`, + 'TSC', languageIds, new DirectiveCommentCompletionProvider(client,), ['@'] + )) } + let definitionProvider = new DefinitionProvider(client) - - this.disposables.push( - languages.registerDefinitionProvider( - languageIds, - definitionProvider - ) - ) - - this.disposables.push( - languages.registerTypeDefinitionProvider( - languageIds, - definitionProvider - ) - ) - - this.disposables.push( - languages.registerImplementationProvider( - languageIds, - definitionProvider - ) - ) - - this.disposables.push( - languages.registerReferencesProvider( - languageIds, - new ReferenceProvider(client) - ) - ) - - this.disposables.push( - languages.registerHoverProvider( - languageIds, - new HoverProvider(client)) - ) - - this.disposables.push( - languages.registerDocumentHighlightProvider(languageIds, new DocumentHighlight(this.client)) - ) - - this.disposables.push( - languages.registerSignatureHelpProvider( - languageIds, - new SignatureHelpProvider(client), - ['(', ',', '<', ')']) - ) - - this.disposables.push( - languages.registerDocumentSymbolProvider( - languageIds, - new DocumentSymbolProvider(client)) - ) - - if (this.description.id == 'typescript') { - this.disposables.push( - languages.registerWorkspaceSymbolProvider( - new WorkspaceSymbolProvider(client, languageIds)) - ) - } - - this.disposables.push( - languages.registerRenameProvider( - languageIds, - new RenameProvider(client, this.fileConfigurationManager)) - ) + this._register(languages.registerDefinitionProvider(languageIds, definitionProvider)) + this._register(languages.registerTypeDefinitionProvider(languageIds, definitionProvider)) + this._register(languages.registerImplementationProvider(languageIds, definitionProvider)) + this._register(languages.registerReferencesProvider(languageIds, new ReferenceProvider(client))) + this._register(languages.registerHoverProvider(languageIds, new HoverProvider(client))) + this._register(languages.registerDocumentHighlightProvider(languageIds, new DocumentHighlight(this.client))) + this._register(languages.registerSignatureHelpProvider(languageIds, new SignatureHelpProvider(client), ['(', ',', '<', ')'])) + this._register(languages.registerDocumentSymbolProvider(languageIds, new DocumentSymbolProvider(client))) + this._register(languages.registerRenameProvider(languageIds, new RenameProvider(client, this.fileConfigurationManager))) let formatProvider = new FormattingProvider(client, this.fileConfigurationManager) - this.disposables.push( - languages.registerDocumentFormatProvider(languageIds, formatProvider) - ) - this.disposables.push( - languages.registerDocumentRangeFormatProvider(languageIds, formatProvider) - ) - this.disposables.push( - languages.registerOnTypeFormattingEditProvider(languageIds, formatProvider, [';', '}', '\n', String.fromCharCode(27)]) - ) - - // this.disposables.push( - // new ProjectError(client, commandManager) - // ) - - if (this.client.apiVersion.gte(API.v280)) { - this.disposables.push( - languages.registerFoldingRangeProvider(languageIds, new Folding(this.client)) - ) - this.disposables.push( - languages.registerCodeActionProvider(languageIds, - new OrganizeImportsCodeActionProvider(this.client, this.fileConfigurationManager), - `tsserver-${this.description.id}`, [CodeActionKind.SourceOrganizeImports]) - ) - } + this._register(languages.registerDocumentFormatProvider(languageIds, formatProvider)) + this._register(languages.registerDocumentRangeFormatProvider(languageIds, formatProvider)) + this._register(languages.registerOnTypeFormattingEditProvider(languageIds, formatProvider, [';', '}', '\n', String.fromCharCode(27)])) + this._register(languages.registerCodeActionProvider(languageIds, new InstallModuleProvider(client), 'tsserver')) let { fileConfigurationManager } = this let conf = fileConfigurationManager.getLanguageConfiguration(this.id) - - if (this.client.apiVersion.gte(API.v290) - && conf.get('updateImportsOnFileMove.enable')) { - this.disposables.push( - new UpdateImportsOnFileRenameHandler(client, this.fileConfigurationManager, this.id) - ) + if (['javascript', 'typescript'].includes(this.id)) { + if (this.client.apiVersion.gte(API.v290) && conf.get('updateImportsOnFileMove.enable')) { + this._register(new UpdateImportsOnFileRenameHandler(client, this.fileConfigurationManager, this.id)) + } } + if (this.client.apiVersion.gte(API.v280)) { + this._register(languages.registerFoldingRangeProvider(languageIds, new Folding(this.client))) + this._register( + languages.registerCodeActionProvider(languageIds, + new OrganizeImportsCodeActionProvider(this.client, this.fileConfigurationManager), + 'tsserver', [CodeActionKind.SourceOrganizeImports]) + ) + } if (this.client.apiVersion.gte(API.v240)) { - this.disposables.push( + this._register( languages.registerCodeActionProvider( languageIds, new RefactorProvider(client, this.fileConfigurationManager), 'tsserver', [CodeActionKind.Refactor])) } - - this.disposables.push( + this._register( languages.registerCodeActionProvider( - languageIds, - new InstallModuleProvider(client), - 'tsserver') - ) - - this.disposables.push( + languageIds, new QuickfixProvider(client, this.fileConfigurationManager), + 'tsserver', [CodeActionKind.QuickFix])) + this._register( languages.registerCodeActionProvider( - languageIds, - new QuickfixProvider(client, this.fileConfigurationManager), - 'tsserver', - [CodeActionKind.QuickFix])) - - this.disposables.push( - languages.registerCodeActionProvider( - languageIds, - new ImportfixProvider(this.client.bufferSyncSupport), - 'tsserver', - [CodeActionKind.QuickFix])) + languageIds, new ImportfixProvider(this.client.bufferSyncSupport), + 'tsserver', [CodeActionKind.QuickFix])) let cachedResponse = new CachedNavTreeResponse() - if (this.client.apiVersion.gte(API.v206) - && conf.get('referencesCodeLens.enable')) { - this.disposables.push( - languages.registerCodeLensProvider( - languageIds, - new ReferencesCodeLensProvider(client, cachedResponse))) + if (this.client.apiVersion.gte(API.v206) && conf.get('referencesCodeLens.enable')) { + this._register(languages.registerCodeLensProvider(languageIds, new ReferencesCodeLensProvider(client, cachedResponse))) } - - if (this.client.apiVersion.gte(API.v220) - && conf.get('implementationsCodeLens.enable')) { - this.disposables.push( - languages.registerCodeLensProvider( - languageIds, - new ImplementationsCodeLensProvider(client, cachedResponse))) + if (this.client.apiVersion.gte(API.v220) && conf.get('implementationsCodeLens.enable')) { + this._register(languages.registerCodeLensProvider(languageIds, new ImplementationsCodeLensProvider(client, cachedResponse))) } if (this.client.apiVersion.gte(API.v350)) { - this.disposables.push( - languages.registerSelectionRangeProvider(languageIds, new SmartSelection(this.client)) - ) + this._register(languages.registerSelectionRangeProvider(languageIds, new SmartSelection(this.client))) } - - if (this.description.id == 'typescript') { - // this.client.apiVersion - this.disposables.push( - new WatchBuild(commands, this.client) - ) - } - // if (this.client.apiVersion.gte(API.v300)) { - // this.disposables.push( + // this._register( // languages.registerCompletionItemProvider( // `tsserver-${this.description.id}-tag`, // 'TSC', diff --git a/src/server/organizeImports.ts b/src/server/organizeImports.ts index 68b418a..2308932 100644 --- a/src/server/organizeImports.ts +++ b/src/server/organizeImports.ts @@ -11,17 +11,17 @@ import { standardLanguageDescriptions } from './utils/languageDescription' import * as typeconverts from './utils/typeConverters' import FileConfigurationManager from './features/fileConfigurationManager' import TypeScriptServiceClient from './typescriptServiceClient' +import TsserverService from '../server' export class OrganizeImportsCommand implements Command { public readonly id: string = 'tsserver.organizeImports' constructor( - private readonly client: TypeScriptServiceClient + private readonly service: TsserverService ) { } - private async getTextEdits(document: TextDocument): Promise { - let client = this.client + private async getTextEdits(client: TypeScriptServiceClient, document: TextDocument): Promise { let file = client.toPath(document.uri) const args: Proto.OrganizeImportsRequestArgs = { scope: { @@ -31,7 +31,7 @@ export class OrganizeImportsCommand implements Command { } } } - const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, CancellationToken.None)) + const response = await client.interruptGetErr(() => client.execute('organizeImports', args, CancellationToken.None)) if (!response || response.type != 'response' || !response.success) { return } @@ -46,13 +46,18 @@ export class OrganizeImportsCommand implements Command { } public async execute(document?: TextDocument): Promise { + let client = await this.service.getClientHost() if (!document) { let doc = await workspace.document - if (!doc.attached) return - if (this.client.modeIds.indexOf(doc.textDocument.languageId) == -1) return + if (!doc.attached) { + throw new Error(`Document not attached.`) + } + if (client.serviceClient.modeIds.indexOf(doc.filetype) == -1) { + throw new Error(`filetype "${doc.filetype}" not supported by tsserver.`) + } document = doc.textDocument } - let edit = await this.getTextEdits(document) + let edit = await this.getTextEdits(client.serviceClient, document) if (edit) await workspace.applyEdit(edit) return } diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts index 0e5cbf0..99e4a1a 100644 --- a/src/server/typescriptServiceClientHost.ts +++ b/src/server/typescriptServiceClientHost.ts @@ -2,18 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, DiagnosticKind, disposeAll, workspace } from 'coc.nvim' +import { Uri, DiagnosticKind, disposeAll, workspace, languages } from 'coc.nvim' import { Range, Diagnostic, DiagnosticSeverity, Disposable, Position, CancellationToken, DiagnosticRelatedInformation } from 'vscode-languageserver-protocol' import LanguageProvider from './languageProvider' import * as Proto from './protocol' import * as PConst from './protocol.const' import FileConfigurationManager from './features/fileConfigurationManager' import TypeScriptServiceClient from './typescriptServiceClient' -import { LanguageDescription } from './utils/languageDescription' +import { DiagnosticLanguage, LanguageDescription } from './utils/languageDescription' import * as typeConverters from './utils/typeConverters' import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus' import { PluginManager } from '../utils/plugins' import { flatten } from '../utils/arrays' +import WatchBuild from './features/watchBuild' +import WorkspaceSymbolProvider from './features/workspaceSymbols' +import { TextDocument } from 'vscode-languageserver-textdocument' // Style check diagnostics that can be reported as warnings const styleCheckDiagnostics = [ @@ -29,7 +32,6 @@ export default class TypeScriptServiceClientHost implements Disposable { private readonly ataProgressReporter: AtaProgressReporter private readonly typingsStatus: TypingsStatus private readonly client: TypeScriptServiceClient - private readonly languages: LanguageProvider[] = [] private readonly languagePerId = new Map() private readonly disposables: Disposable[] = [] private readonly fileConfigurationManager: FileConfigurationManager @@ -62,6 +64,9 @@ export default class TypeScriptServiceClientHost implements Disposable { }) }, null, this.disposables) + // features + this.disposables.push(new WatchBuild(this.client)) + this.disposables.push(languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.client, allModeIds))) this.client.onConfigDiagnosticsReceived(diag => { let { body } = diag if (body) { @@ -95,10 +100,36 @@ export default class TypeScriptServiceClientHost implements Disposable { description, this.typingsStatus ) - this.languages.push(manager) - this.disposables.push(manager) this.languagePerId.set(description.id, manager) } + const languageIds = new Set() + for (const plugin of pluginManager.plugins) { + if (plugin.configNamespace && plugin.languages.length) { + this.registerExtensionLanguageProvider({ + id: plugin.configNamespace, + modeIds: Array.from(plugin.languages), + diagnosticSource: 'ts-plugin', + diagnosticLanguage: DiagnosticLanguage.TypeScript, + diagnosticOwner: 'typescript', + isExternal: true + }) + } else { + for (const language of plugin.languages) { + languageIds.add(language) + } + } + } + + if (languageIds.size) { + this.registerExtensionLanguageProvider({ + id: 'typescript-plugins', + modeIds: Array.from(languageIds.values()), + diagnosticSource: 'ts-plugin', + diagnosticLanguage: DiagnosticLanguage.TypeScript, + diagnosticOwner: 'typescript', + isExternal: true + }) + } this.client.ensureServiceStarted() this.client.onTsServerStarted(() => { @@ -108,8 +139,17 @@ export default class TypeScriptServiceClientHost implements Disposable { this.configurationChanged() } + private registerExtensionLanguageProvider(description: LanguageDescription) { + const manager = new LanguageProvider(this.client, this.fileConfigurationManager, description, this.typingsStatus) + this.languagePerId.set(description.id, manager) + } + public dispose(): void { disposeAll(this.disposables) + for (let language of this.languagePerId.values()) { + language.dispose() + } + this.languagePerId.clear() this.fileConfigurationManager.dispose() this.typingsStatus.dispose() this.ataProgressReporter.dispose() @@ -124,6 +164,7 @@ export default class TypeScriptServiceClientHost implements Disposable { } public reloadProjects(): void { + this.client.diagnosticsManager.reInitialize() this.client.execute('reloadProjects', null, CancellationToken.None) this.triggerAllDiagnostics() } @@ -142,18 +183,18 @@ export default class TypeScriptServiceClientHost implements Disposable { try { let doc = await workspace.loadFile(uri) if (!doc) return undefined - return this.languages.find(language => language.handles(uri, doc.textDocument)) + let languages = Array.from(this.languagePerId.values()) + return languages.find(language => language.handles(uri, doc.textDocument)) } catch { return undefined } } - public async handles(uri: string): Promise { - const provider = await this.findLanguage(uri) - if (provider) { - return true - } - return this.client.bufferSyncSupport.handles(uri) + public async handles(doc: TextDocument): Promise { + let languages = Array.from(this.languagePerId.values()) + let idx = languages.findIndex(language => language.handles(doc.uri, doc)) + if (idx != -1) return true + return this.client.bufferSyncSupport.handles(doc.uri) } private triggerAllDiagnostics(): void { @@ -235,7 +276,7 @@ export default class TypeScriptServiceClientHost implements Disposable { return code ? styleCheckDiagnostics.indexOf(code) !== -1 : false } - private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager) { + private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager): string[] { const allModeIds = flatten([ ...descriptions.map(x => x.modeIds), ...pluginManager.plugins.map(x => x.languages) diff --git a/src/server/utils/modules.ts b/src/server/utils/modules.ts index 6d137a6..38ddba5 100644 --- a/src/server/utils/modules.ts +++ b/src/server/utils/modules.ts @@ -1,6 +1,7 @@ import { exec } from 'child_process' +import { workspace, Uri } from 'coc.nvim' +import fs from 'fs' import path from 'path' -import { Uri, workspace } from 'coc.nvim' export function runCommand(cmd: string, cwd: string, timeout?: number): Promise { return new Promise((resolve, reject) => { @@ -72,9 +73,10 @@ export function distinct(array: T[], keyFn?: (t: T) => string): T[] { export async function installModules(uri: string, names: string[]): Promise { names = distinct(names) - let root = await getRoot() - if (!root) { - workspace.showMessage(`package.json not found from cwd: ${workspace.cwd}`, 'error') + let workspaceFolder = workspace.getWorkspaceFolder(uri) + let root = workspaceFolder ? Uri.parse(workspaceFolder.uri).fsPath : undefined + if (!root || !fs.existsSync(path.join(root, 'package.json'))) { + workspace.showMessage(`package.json not found from workspaceFolder: ${root}`, 'error') return } let arr = names.concat(names.map(s => `@types/${s}`)) @@ -93,11 +95,10 @@ export async function installModules(uri: string, names: string[]): Promise devs.indexOf(s) == -1) statusItem.text = `Installing ${exists.join(' ')}` try { - await Promise.all([deps, devs].map((names, i) => { - let cmd = manager == 'npm' ? `npm i ${names.join(' ')}` : `yarn add ${names.join(' ')} --ignore-scripts --no-default-rc` - if (i == 1) cmd = cmd + ' --dev' - return runCommand(cmd, root) - })) + let cmd = manager == 'npm' ? `npm i ${deps.join(' ')}` : `yarn add ${deps.join(' ')}` + await runCommand(cmd, root) + cmd = manager == 'npm' ? `npm i ${deps.join(' ')} --save-dev` : `yarn add ${deps.join(' ')} --save-dev` + await runCommand(cmd, root) } catch (e) { statusItem.dispose() workspace.showMessage(`Install error ${e.message}`, 'error')