From f1aa9305699c16e3d755c973d7be2edf36549bff Mon Sep 17 00:00:00 2001 From: chemzqm Date: Mon, 10 Jun 2019 02:24:30 +0800 Subject: [PATCH] add new features: - Loading status - Batched buffer synchronize - Configuration for showUnused variable - Smart selection support - Support 'auto' as quoteStyle - Support validateDefaultNpmLocation --- Readme.md | 3 + package.json | 17 +- src/server/features/bufferSyncSupport.ts | 217 ++++++++++++++---- src/server/features/completionItemProvider.ts | 12 +- src/server/features/diagnostics.ts | 4 +- .../features/fileConfigurationManager.ts | 3 +- src/server/features/hover.ts | 2 +- src/server/features/quickfix.ts | 6 +- src/server/features/refactor.ts | 4 +- src/server/features/rename.ts | 4 +- src/server/features/signatureHelp.ts | 9 +- src/server/features/smartSelect.ts | 47 ++++ src/server/features/updatePathOnRename.ts | 12 +- src/server/languageProvider.ts | 107 +++------ src/server/organizeImports.ts | 2 +- src/server/typescriptService.ts | 11 +- src/server/typescriptServiceClient.ts | 41 +++- src/server/typescriptServiceClientHost.ts | 22 +- src/server/utils/api.ts | 97 ++++---- src/server/utils/languageDescription.ts | 8 + src/server/utils/typeConverters.ts | 13 ++ src/server/utils/versionStatus.ts | 4 + yarn.lock | 2 +- 23 files changed, 439 insertions(+), 208 deletions(-) create mode 100644 src/server/features/smartSelect.ts diff --git a/Readme.md b/Readme.md index b2a0bcd..8a0d6bb 100644 --- a/Readme.md +++ b/Readme.md @@ -80,6 +80,8 @@ module will be used. - `typescript.preferences.quoteStyle` default: `"single"` - `typescript.suggestionActions.enabled`:Enable/disable suggestion diagnostics for TypeScript files in the editor. Requires using TypeScript 2.8 or newer in the workspace., default: `true` - `typescript.validate.enable`:Enable/disable TypeScript validation., default: `true` +- `typescript.useBatchedBufferSync`: use batched buffer synchronize support. +- `typescript.showUnused`: show unused variable hint. - `typescript.suggest.enabled` default: `true` - `typescript.suggest.paths`:Enable/disable suggest paths in import statement and require calls, default: `true` - `typescript.suggest.autoImports`:Enable/disable auto import suggests., default: `true` @@ -99,6 +101,7 @@ module will be used. - `typescript.format.insertSpaceAfterTypeAssertion` default: `false` - `typescript.format.placeOpenBraceOnNewLineForFunctions` default: `false` - `typescript.format.placeOpenBraceOnNewLineForControlBlocks` default: `false` +- `javascript.showUnused`: show unused variable hint. - `javascript.updateImportsOnFileMove.enable` default: `true` - `javascript.implementationsCodeLens.enable` default: `true` - `javascript.referencesCodeLens.enable` default: `true` diff --git a/package.json b/package.json index 76b9216..e018c4a 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,16 @@ "default": false, "description": "Disable download of typings" }, + "typescript.useBatchedBufferSync": { + "type": "boolean", + "default": true, + "description": "Use batched buffer sync support." + }, + "typescript.showUnused": { + "type": "boolean", + "default": true, + "description": "Show unused variable hint." + }, "typescript.updateImportsOnFileMove.enable": { "type": "boolean", "default": true, @@ -311,6 +321,11 @@ "type": "boolean", "default": false }, + "javascript.showUnused": { + "type": "boolean", + "default": true, + "description": "Show unused variable hint." + }, "javascript.updateImportsOnFileMove.enable": { "type": "boolean", "default": true @@ -467,7 +482,7 @@ "semver": "^6.1.1", "tslib": "^1.9.3", "typescript": "3.5.1", - "vscode-languageserver-protocol": "^3.15.0-next.1", + "vscode-languageserver-protocol": "^3.15.0-next.5", "which": "^1.3.1" } } diff --git a/src/server/features/bufferSyncSupport.ts b/src/server/features/bufferSyncSupport.ts index 0e0db25..5bca5b3 100644 --- a/src/server/features/bufferSyncSupport.ts +++ b/src/server/features/bufferSyncSupport.ts @@ -2,12 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { disposeAll, workspace } from 'coc.nvim' -import { CancellationTokenSource, DidChangeTextDocumentParams, Disposable, TextDocument } from 'vscode-languageserver-protocol' +import { Uri, disposeAll, workspace } from 'coc.nvim' +import { CancellationTokenSource, Emitter, Event, DidChangeTextDocumentParams, Disposable, TextDocument, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol' import Proto from '../protocol' import { ITypeScriptServiceClient } from '../typescriptService' import API from '../utils/api' import { Delayer } from '../utils/async' +import * as typeConverters from '../utils/typeConverters' import * as languageModeIds from '../utils/languageModeIds' function mode2ScriptKind( @@ -30,10 +31,117 @@ function mode2ScriptKind( return undefined } +/** + * Manages synchronization of buffers with the TS server. + * + * If supported, batches together file changes. This allows the TS server to more efficiently process changes. + */ +class BufferSynchronizer { + + private _pending: Proto.UpdateOpenRequestArgs = {} + private _pendingFiles = new Set() + + constructor( + private readonly client: ITypeScriptServiceClient + ) { } + + public open(args: Proto.OpenRequestArgs): void { + if (this.supportsBatching) { + this.updatePending(args.file, pending => { + if (!pending.openFiles) { + pending.openFiles = [] + } + pending.openFiles.push(args) + }) + } else { + this.client.executeWithoutWaitingForResponse('open', args) + } + } + + public close(filepath: string): void { + if (this.supportsBatching) { + this.updatePending(filepath, pending => { + if (!pending.closedFiles) { + pending.closedFiles = [] + } + pending.closedFiles.push(filepath) + }) + } else { + const args: Proto.FileRequestArgs = { file: filepath } + this.client.executeWithoutWaitingForResponse('close', args) + } + } + + public change(filepath: string, events: TextDocumentContentChangeEvent[]): void { + if (!events.length) { + return + } + + if (this.supportsBatching) { + this.updatePending(filepath, pending => { + if (!pending.changedFiles) { + pending.changedFiles = [] + } + pending.changedFiles.push({ + fileName: filepath, + textChanges: events.map((change): Proto.CodeEdit => ({ + newText: change.text, + start: typeConverters.Position.toLocation(change.range.start), + end: typeConverters.Position.toLocation(change.range.end), + })).reverse(), // Send the edits end-of-document to start-of-document order + }) + }) + } else { + for (const { range, text } of events) { + const args: Proto.ChangeRequestArgs = { + insertString: text, + ...typeConverters.Range.toFormattingRequestArgs(filepath, range) + } + this.client.executeWithoutWaitingForResponse('change', args) + } + } + } + + public beforeCommand(command: string): void { + if (command === 'updateOpen') { + return + } + + this.flush() + } + + private flush(): void { + if (!this.supportsBatching) { + // We've already eagerly synchronized + return + } + + if (this._pending.changedFiles || this._pending.closedFiles || this._pending.openFiles) { + this.client.executeWithoutWaitingForResponse('updateOpen', this._pending) + this._pending = {} + this._pendingFiles.clear() + } + } + + private get supportsBatching(): boolean { + return this.client.apiVersion.gte(API.v340) && workspace.getConfiguration('typescript').get('useBatchedBufferSync', true) + } + + private updatePending(filepath: string, f: (pending: Proto.UpdateOpenRequestArgs) => void): void { + if (this.supportsBatching && this._pendingFiles.has(filepath)) { + this.flush() + this._pendingFiles.clear() + f(this._pending) + this._pendingFiles.add(filepath) + } else { + f(this._pending) + } + } +} + export default class BufferSyncSupport { private readonly client: ITypeScriptServiceClient - private _validate: boolean private readonly modeIds: Set private readonly uris: Set = new Set() private readonly disposables: Disposable[] = [] @@ -41,19 +149,28 @@ export default class BufferSyncSupport { private readonly pendingDiagnostics = new Map() private readonly diagnosticDelayer: Delayer private pendingGetErr: GetErrRequest | undefined + private readonly synchronizer: BufferSynchronizer + private _validateJavaScript = true + private _validateTypeScript = true + + private listening = false + private readonly _onDelete = new Emitter() + public readonly onDelete: Event = this._onDelete.event constructor( client: ITypeScriptServiceClient, - modeIds: string[], - validate: boolean ) { this.client = client - this.modeIds = new Set(modeIds) - this._validate = validate || false + this.synchronizer = new BufferSynchronizer(client) + this.modeIds = new Set(languageModeIds.languageIds) this.diagnosticDelayer = new Delayer(300) } public listen(): void { + if (this.listening) { + return + } + this.listening = true workspace.onDidOpenTextDocument( this.onDidOpenTextDocument, this, @@ -70,14 +187,8 @@ export default class BufferSyncSupport { this.disposables ) workspace.textDocuments.forEach(this.onDidOpenTextDocument, this) - } - - public reInitialize(): void { - workspace.textDocuments.forEach(this.onDidOpenTextDocument, this) - } - - public set validate(value: boolean) { - this._validate = value + this.updateConfiguration() + workspace.onDidChangeConfiguration(this.updateConfiguration, this, this.disposables) } public dispose(): void { @@ -105,8 +216,8 @@ export default class BufferSyncSupport { let root = this.client.getProjectRootPath(document.uri) if (root) args.projectRootPath = root } - - this.client.executeWithoutWaitingForResponse('open', args) // tslint:disable-line + this.synchronizer.open(args) + // this.client.executeWithoutWaitingForResponse('open', args) this.requestDiagnostic(uri) } @@ -114,10 +225,12 @@ export default class BufferSyncSupport { let { uri } = document if (!this.uris.has(uri)) return let filepath = this.client.toPath(uri) - const args: Proto.FileRequestArgs = { - file: filepath - } - this.client.executeWithoutWaitingForResponse('close', args) // tslint:disable-line + this.uris.delete(uri) + this.pendingDiagnostics.delete(uri) + this.synchronizer.close(filepath) + this._onDelete.fire(uri) + this.requestAllDiagnostics() + // this.client.executeWithoutWaitingForResponse('close', args) } private onDidChangeTextDocument(e: DidChangeTextDocumentParams): void { @@ -125,17 +238,7 @@ export default class BufferSyncSupport { let { uri } = textDocument if (!this.uris.has(uri)) return let filepath = this.client.toPath(uri) - for (const { range, text } of contentChanges) { - const args: Proto.ChangeRequestArgs = { - file: filepath, - line: range ? range.start.line + 1 : 1, - offset: range ? range.start.character + 1 : 1, - endLine: range ? range.end.line + 1 : 2 ** 24, - endOffset: range ? range.end.character + 1 : 1, - insertString: text - } - this.client.executeWithoutWaitingForResponse('change', args) // tslint:disable-line - } + this.synchronizer.change(filepath, contentChanges) const didTrigger = this.requestDiagnostic(uri) if (!didTrigger && this.pendingGetErr) { // In this case we always want to re-trigger all diagnostics @@ -145,6 +248,10 @@ export default class BufferSyncSupport { } } + public beforeCommand(command: string): void { + this.synchronizer.beforeCommand(command) + } + public interuptGetErr(f: () => R): R { if (!this.pendingGetErr) { return f() @@ -157,6 +264,19 @@ export default class BufferSyncSupport { return result } + public getErr(resources: Uri[]): any { + const handledResources = resources.filter(resource => this.uris.has(resource.toString())) + if (!handledResources.length) { + return + } + + for (const resource of handledResources) { + this.pendingDiagnostics.set(resource.toString(), Date.now()) + } + + this.triggerDiagnostics() + } + private triggerDiagnostics(delay = 200): void { this.diagnosticDelayer.trigger(() => { this.sendPendingDiagnostics() @@ -164,11 +284,11 @@ export default class BufferSyncSupport { } public requestAllDiagnostics(): void { - if (!this._validate) { - return - } for (const uri of this.uris) { - this.pendingDiagnostics.set(uri, Date.now()) + let doc = workspace.getDocument(uri) + if (doc && this.shouldValidate(doc.filetype)) { + this.pendingDiagnostics.set(uri, Date.now()) + } } this.diagnosticDelayer.trigger(() => { // tslint:disable-line this.sendPendingDiagnostics() @@ -176,11 +296,8 @@ export default class BufferSyncSupport { } public requestDiagnostic(uri: string): boolean { - if (!this._validate) { - return false - } let document = workspace.getDocument(uri) - if (!document) return false + if (!document || !this.shouldValidate(document.filetype)) return false this.pendingDiagnostics.set(uri, Date.now()) const lineCount = document.lineCount const delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800) @@ -193,9 +310,6 @@ export default class BufferSyncSupport { } private sendPendingDiagnostics(): void { - if (!this._validate) { - return - } const uris = Array.from(this.pendingDiagnostics.entries()) .sort((a, b) => a[1] - b[1]) .map(entry => entry[0]) @@ -217,6 +331,23 @@ export default class BufferSyncSupport { } this.pendingDiagnostics.clear() } + private updateConfiguration(): void { + const jsConfig = workspace.getConfiguration('javascript', null) + const tsConfig = workspace.getConfiguration('typescript', null) + + this._validateJavaScript = jsConfig.get('validate.enable', true) + this._validateTypeScript = tsConfig.get('validate.enable', true) + } + + private shouldValidate(filetype: string): boolean { + if (languageModeIds.languageIds.indexOf(filetype) == -1) { + return false + } + if (filetype.startsWith('javascript')) { + return this._validateJavaScript + } + return this._validateTypeScript + } } class GetErrRequest { diff --git a/src/server/features/completionItemProvider.ts b/src/server/features/completionItemProvider.ts index d5bc697..5bca577 100644 --- a/src/server/features/completionItemProvider.ts +++ b/src/server/features/completionItemProvider.ts @@ -16,7 +16,6 @@ import * as typeConverters from '../utils/typeConverters' import TypingsStatus from '../utils/typingsStatus' import FileConfigurationManager, { SuggestOptions } from './fileConfigurationManager' import SnippetString from '../utils/SnippetString' -import BufferSyncSupport from './bufferSyncSupport' // command center export interface CommandItem { @@ -59,7 +58,6 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP private readonly client: ITypeScriptServiceClient, private readonly typingsStatus: TypingsStatus, private readonly fileConfigurationManager: FileConfigurationManager, - private readonly bufferSyncSupport: BufferSyncSupport, languageId: string ) { @@ -128,7 +126,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP let isNewIdentifierLocation = true if (this.client.apiVersion.gte(API.v300)) { try { - const response = await this.bufferSyncSupport.interuptGetErr(() => this.client.execute('completionInfo', args, token)) + const response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token)) if (response.type !== 'response' || !response.body) { return null } @@ -141,7 +139,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP throw e } } else { - const response = await this.bufferSyncSupport.interuptGetErr(() => this.client.execute('completions', args, token)) + const response = await this.client.interruptGetErr(() => this.client.execute('completions', args, token)) if (response.type !== 'response' || !response.body) { return null } @@ -218,11 +216,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP let response: ServerResponse.Response try { - response = await this.client.execute( - 'completionEntryDetails', - args, - token - ) + response = await this.client.interruptGetErr(() => this.client.execute('completionEntryDetails', args, token)) } catch { return item } diff --git a/src/server/features/diagnostics.ts b/src/server/features/diagnostics.ts index 256beb0..33ebbb5 100644 --- a/src/server/features/diagnostics.ts +++ b/src/server/features/diagnostics.ts @@ -93,9 +93,7 @@ export class DiagnosticsManager { diagnostics: Diagnostic[] ): void { const collection = this._diagnostics.get(kind) - if (!collection) { - return - } + if (!collection) return if (diagnostics.length === 0) { const existing = collection.get(uri) diff --git a/src/server/features/fileConfigurationManager.ts b/src/server/features/fileConfigurationManager.ts index 8ea8770..5b7129d 100644 --- a/src/server/features/fileConfigurationManager.ts +++ b/src/server/features/fileConfigurationManager.ts @@ -144,10 +144,11 @@ export default class FileConfigurationManager { return {} } const config = workspace.getConfiguration(`${language}`) + const defaultQuote = this.client.apiVersion.gte(API.v333) ? 'auto' : undefined return { disableSuggestions: !config.get('suggest.enabled', true), importModuleSpecifierPreference: getImportModuleSpecifier(config) as any, - quotePreference: config.get<'single' | 'double'>('preferences.quoteStyle', 'single'), + quotePreference: config.get<'single' | 'double' | 'auto'>('preferences.quoteStyle', defaultQuote), allowRenameOfImportPath: true, allowTextChangesInNewFiles: true, } diff --git a/src/server/features/hover.ts b/src/server/features/hover.ts index 84745a8..4931b02 100644 --- a/src/server/features/hover.ts +++ b/src/server/features/hover.ts @@ -26,7 +26,7 @@ export default class TypeScriptHoverProvider implements HoverProvider { position ) try { - const response = await this.client.execute('quickinfo', args, token) + const response = await this.client.interruptGetErr(() => this.client.execute('quickinfo', args, token)) if (response && response.type == 'response' && response.body) { const data = response.body return { diff --git a/src/server/features/quickfix.ts b/src/server/features/quickfix.ts index e05bc1a..842c0b5 100644 --- a/src/server/features/quickfix.ts +++ b/src/server/features/quickfix.ts @@ -142,8 +142,6 @@ export default class TypeScriptQuickFixProvider implements CodeActionProvider { constructor( private readonly client: ITypeScriptServiceClient, - private readonly diagnosticsManager: DiagnosticsManager, - private readonly bufferSyncSupport: BufferSyncSupport, ) { commands.register( new ApplyCodeActionCommand(client) @@ -177,7 +175,7 @@ export default class TypeScriptQuickFixProvider implements CodeActionProvider { return [] } - if (this.bufferSyncSupport.hasPendingDiagnostics(document.uri)) { + if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) { return [] } @@ -273,7 +271,7 @@ export default class TypeScriptQuickFixProvider implements CodeActionProvider { } // Make sure there are multiple diagnostics of the same type in the file - if (!this.diagnosticsManager + if (!this.client.diagnosticsManager .getDiagnostics(document.uri) .some(x => x.code === diagnostic.code && x !== diagnostic)) { return diff --git a/src/server/features/refactor.ts b/src/server/features/refactor.ts index 7299542..85ea45b 100644 --- a/src/server/features/refactor.ts +++ b/src/server/features/refactor.ts @@ -127,7 +127,9 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { ) let response try { - response = await this.client.execute('getApplicableRefactors', args, token) + response = await this.client.interruptGetErr(() => { + return this.client.execute('getApplicableRefactors', args, token) + }) if (!response || !response.body) { return undefined } diff --git a/src/server/features/rename.ts b/src/server/features/rename.ts index 062071f..02c3e8f 100644 --- a/src/server/features/rename.ts +++ b/src/server/features/rename.ts @@ -81,7 +81,9 @@ export default class TypeScriptRenameProvider implements RenameProvider { findInComments: false } - return this.client.execute('rename', args, token) + return this.client.interruptGetErr(() => { + return this.client.execute('rename', args, token) + }) } private toWorkspaceEdit( diff --git a/src/server/features/signatureHelp.ts b/src/server/features/signatureHelp.ts index cf1ddee..5a3fd2c 100644 --- a/src/server/features/signatureHelp.ts +++ b/src/server/features/signatureHelp.ts @@ -28,14 +28,11 @@ export default class TypeScriptSignatureHelpProvider implements SignatureHelpPro position ) - let info: Proto.SignatureHelpItems | undefined - try { - const response = await this.client.execute('signatureHelp', args, token) - info = (response as any).body - if (!info) return undefined - } catch { + const response = await this.client.interruptGetErr(() => this.client.execute('signatureHelp', args, token)) + if (response.type !== 'response' || !response.body) { return undefined } + let info = response.body const result: SignatureHelp = { activeSignature: info.selectedItemIndex, diff --git a/src/server/features/smartSelect.ts b/src/server/features/smartSelect.ts new file mode 100644 index 0000000..8de56bb --- /dev/null +++ b/src/server/features/smartSelect.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import { TextDocument, Position, CancellationToken } from 'vscode-languageserver-protocol' +import { SelectionRange } from 'vscode-languageserver-protocol/lib/protocol.selectionRange.proposed' +import * as typeConverters from '../utils/typeConverters' +import { SelectionRangeProvider } from 'coc.nvim' + +export default class SmartSelection implements SelectionRangeProvider { + public constructor( + private readonly client: ITypeScriptServiceClient + ) { } + + public async provideSelectionRanges( + document: TextDocument, + positions: Position[], + token: CancellationToken, + ): Promise { + const file = this.client.toPath(document.uri) + if (!file) { + return undefined + } + + const args: Proto.SelectionRangeRequestArgs = { + file, + locations: positions.map(typeConverters.Position.toLocation) + } + const response = await this.client.execute('selectionRange', args, token) + if (response.type !== 'response' || !response.body) { + return undefined + } + return response.body.map(SmartSelection.convertSelectionRange) + } + + private static convertSelectionRange( + selectionRange: Proto.SelectionRange + ): SelectionRange { + return SelectionRange.create( + typeConverters.Range.fromTextSpan(selectionRange.textSpan), + selectionRange.parent ? SmartSelection.convertSelectionRange(selectionRange.parent) : undefined, + ) + } +} diff --git a/src/server/features/updatePathOnRename.ts b/src/server/features/updatePathOnRename.ts index f1b5e3e..08c804c 100644 --- a/src/server/features/updatePathOnRename.ts +++ b/src/server/features/updatePathOnRename.ts @@ -74,11 +74,13 @@ export default class UpdateImportsOnFileRenameHandler { private async getEditsForFileRename(document: TextDocument, oldFile: string, newFile: string): Promise { await this.fileConfigurationManager.ensureConfigurationForDocument(document) - const args: Proto.GetEditsForFileRenameRequestArgs = { - oldFilePath: oldFile, - newFilePath: newFile - } - const response = await this.client.execute('getEditsForFileRename', args, CancellationToken.None) + const response = await this.client.interruptGetErr(() => { + const args: Proto.GetEditsForFileRenameRequestArgs = { + oldFilePath: oldFile, + newFilePath: newFile, + } + return this.client.execute('getEditsForFileRename', args, CancellationToken.None) + }) if (!response || response.type != 'response' || !response.body) { return } diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index 45c1509..b41d5bd 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -2,13 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Diagnostic, Disposable, CodeActionKind } from 'vscode-languageserver-protocol' +import { Diagnostic, Disposable, CodeActionKind, DiagnosticSeverity } from 'vscode-languageserver-protocol' import { Uri, workspace, commands, events, languages, DiagnosticKind, ServiceStat, disposeAll } from 'coc.nvim' import { CachedNavTreeResponse } from './features/baseCodeLensProvider' -import BufferSyncSupport from './features/bufferSyncSupport' import CompletionItemProvider from './features/completionItemProvider' import DefinitionProvider from './features/definitionProvider' -import { DiagnosticsManager } from './features/diagnostics' import DirectiveCommentCompletionProvider from './features/directiveCommentCompletions' import DocumentHighlight from './features/documentHighlight' import DocumentSymbolProvider from './features/documentSymbol' @@ -28,6 +26,7 @@ import SignatureHelpProvider from './features/signatureHelp' import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename' import WatchBuild from './features/watchBuild' import WorkspaceSymbolProvider from './features/workspaceSymbols' +import SmartSelection from './features/smartSelect' import TypeScriptServiceClient from './typescriptServiceClient' import InstallModuleProvider from './features/moduleInstall' import API from './utils/api' @@ -35,15 +34,8 @@ import { LanguageDescription } from './utils/languageDescription' import TypingsStatus from './utils/typingsStatus' import { OrganizeImportsCodeActionProvider } from './organizeImports' -const validateSetting = 'validate.enable' -const suggestionSetting = 'suggestionActions.enabled' - export default class LanguageProvider { - private readonly diagnosticsManager: DiagnosticsManager - private readonly bufferSyncSupport: BufferSyncSupport public readonly fileConfigurationManager: FileConfigurationManager // tslint:disable-line - private _validate = true - private _enableSuggestionDiagnostics = true private readonly disposables: Disposable[] = [] constructor( @@ -52,54 +44,33 @@ export default class LanguageProvider { typingsStatus: TypingsStatus ) { this.fileConfigurationManager = new FileConfigurationManager(client) - this.bufferSyncSupport = new BufferSyncSupport( - client, - description.modeIds, - this._validate - ) - this.diagnosticsManager = new DiagnosticsManager() - this.disposables.push(this.diagnosticsManager) - - client.onTsServerStarted(async () => { - let document = await workspace.document - if (description.modeIds.indexOf(document.filetype) !== -1) { - this.fileConfigurationManager.ensureConfigurationForDocument(document.textDocument) // tslint:disable-line - } - }) events.on('BufEnter', bufnr => { let doc = workspace.getDocument(bufnr) - if (!doc) return + if (!doc || client.state !== ServiceStat.Running) return if (description.modeIds.indexOf(doc.filetype) == -1) return - if (client.state !== ServiceStat.Running) return this.fileConfigurationManager.ensureConfigurationForDocument(doc.textDocument) // tslint:disable-line }, this, this.disposables) - this.configurationChanged() - workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) - let initialized = false - client.onTsServerStarted(() => { // tslint:disable-line + client.onTsServerStarted(async () => { // tslint:disable-line if (!initialized) { + for (let doc of workspace.documents) { + if (description.modeIds.indexOf(doc.filetype) !== -1) { + this.fileConfigurationManager.ensureConfigurationForDocument(doc.textDocument) // tslint:disable-line + } + } initialized = true this.registerProviders(client, typingsStatus) - this.bufferSyncSupport.listen() } else { - this.reInitialize() + this.client.diagnosticsManager.reInitialize() } }) } public dispose(): void { disposeAll(this.disposables) - this.bufferSyncSupport.dispose() - } - - private configurationChanged(): void { - const config = workspace.getConfiguration(this.id) - this.updateValidate(config.get(validateSetting, true)) - this.updateSuggestionDiagnostics(config.get(suggestionSetting, true)) } private registerProviders( @@ -117,7 +88,6 @@ export default class LanguageProvider { client, typingsStatus, this.fileConfigurationManager, - this.bufferSyncSupport, this.description.id ), CompletionItemProvider.triggerCharacters @@ -256,14 +226,14 @@ export default class LanguageProvider { this.disposables.push( languages.registerCodeActionProvider( languageIds, - new QuickfixProvider(client, this.diagnosticsManager, this.bufferSyncSupport), + new QuickfixProvider(client), 'tsserver', [CodeActionKind.QuickFix])) this.disposables.push( languages.registerCodeActionProvider( languageIds, - new ImportfixProvider(this.bufferSyncSupport), + new ImportfixProvider(this.client.bufferSyncSupport), 'tsserver', [CodeActionKind.QuickFix])) let cachedResponse = new CachedNavTreeResponse() @@ -282,6 +252,11 @@ export default class LanguageProvider { languageIds, new ImplementationsCodeLensProvider(client, cachedResponse))) } + if (this.client.apiVersion.gte(API.v350)) { + this.disposables.push( + languages.registerSelectionRangeProvider(languageIds, new SmartSelection(this.client)) + ) + } if (this.description.id == 'typescript') { this.disposables.push( @@ -329,51 +304,31 @@ export default class LanguageProvider { return this.description.diagnosticSource } - private updateValidate(value: boolean): void { - if (this._validate === value) { - return - } - this._validate = value - this.bufferSyncSupport.validate = value - this.diagnosticsManager.validate = value - if (value) { - this.triggerAllDiagnostics() - } - } - - private updateSuggestionDiagnostics(value: boolean): void { - if (this._enableSuggestionDiagnostics === value) { - return - } - this._enableSuggestionDiagnostics = value - this.diagnosticsManager.enableSuggestions = value - if (value) { - this.triggerAllDiagnostics() - } - } - - public reInitialize(): void { - this.diagnosticsManager.reInitialize() - this.bufferSyncSupport.reInitialize() - } - public triggerAllDiagnostics(): void { - this.bufferSyncSupport.requestAllDiagnostics() + this.client.bufferSyncSupport.requestAllDiagnostics() } public diagnosticsReceived( diagnosticsKind: DiagnosticKind, file: Uri, - diagnostics: Diagnostic[] + diagnostics: (Diagnostic & { reportUnnecessary: any })[] ): void { - this.diagnosticsManager.diagnosticsReceived( + this.client.diagnosticsManager.diagnosticsReceived( diagnosticsKind, file.toString(), diagnostics ) - } - public configFileDiagnosticsReceived(uri: Uri, diagnostics: Diagnostic[]): void { - this.diagnosticsManager.configFileDiagnosticsReceived(uri.toString(), diagnostics) + const config = workspace.getConfiguration(this.id, file.toString()) + const reportUnnecessary = config.get('showUnused', true) + this.client.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file.toString(), diagnostics.filter(diag => { + if (!reportUnnecessary) { + diag.tags = undefined + if (diag.reportUnnecessary && diag.severity === DiagnosticSeverity.Hint) { + return false + } + } + return true + })) } } diff --git a/src/server/organizeImports.ts b/src/server/organizeImports.ts index 302eeb0..ff6aa89 100644 --- a/src/server/organizeImports.ts +++ b/src/server/organizeImports.ts @@ -31,7 +31,7 @@ export class OrganizeImportsCommand implements Command { } } } - const response = await client.execute('organizeImports', args, CancellationToken.None) + const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, CancellationToken.None)) if (!response || response.type != 'response' || !response.success) { return } diff --git a/src/server/typescriptService.ts b/src/server/typescriptService.ts index 23f2b86..b973a10 100644 --- a/src/server/typescriptService.ts +++ b/src/server/typescriptService.ts @@ -8,6 +8,8 @@ import * as Proto from './protocol' import API from './utils/api' import { TypeScriptServiceConfiguration } from './utils/configuration' import Logger from './utils/logger' +import BufferSyncSupport from './features/bufferSyncSupport' +import { DiagnosticsManager } from './features/diagnostics' export namespace ServerResponse { @@ -74,6 +76,8 @@ export interface ITypeScriptServiceClient { onDidEndInstallTypings: Event onTypesInstallerInitializationFailed: Event readonly logger: Logger + readonly bufferSyncSupport: BufferSyncSupport + readonly diagnosticsManager: DiagnosticsManager getProjectRootPath(uri: string): string | null normalizePath(resource: Uri): string | null @@ -91,10 +95,15 @@ export interface ITypeScriptServiceClient { executeWithoutWaitingForResponse(command: 'open', args: Proto.OpenRequestArgs): void executeWithoutWaitingForResponse(command: 'close', args: Proto.FileRequestArgs): void executeWithoutWaitingForResponse(command: 'change', args: Proto.ChangeRequestArgs): void - // executeWithoutWaitingForResponse(command: 'updateOpen', args: Proto.UpdateOpenRequestArgs): void + executeWithoutWaitingForResponse(command: 'updateOpen', args: Proto.UpdateOpenRequestArgs): void executeWithoutWaitingForResponse(command: 'compilerOptionsForInferredProjects', args: Proto.SetCompilerOptionsForInferredProjectsArgs): void executeWithoutWaitingForResponse(command: 'reloadProjects', args: null): void executeWithoutWaitingForResponse(command: 'configurePlugin', args: Proto.ConfigurePluginRequestArguments): void executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: CancellationToken): Promise> + + /** + * Cancel on going geterr requests and re-queue them after `f` has been evaluated. + */ + interruptGetErr(f: () => R): R } diff --git a/src/server/typescriptServiceClient.ts b/src/server/typescriptServiceClient.ts index 775041f..9e20000 100644 --- a/src/server/typescriptServiceClient.ts +++ b/src/server/typescriptServiceClient.ts @@ -8,7 +8,7 @@ import os from 'os' import path from 'path' import { CancellationToken, Disposable, Emitter, Event } from 'vscode-languageserver-protocol' import which from 'which' -import { Uri, DiagnosticKind, ServiceStat, workspace, disposeAll } from 'coc.nvim' +import { Uri, ServiceStat, workspace, disposeAll } from 'coc.nvim' import FileConfigurationManager from './features/fileConfigurationManager' import * as Proto from './protocol' import { ITypeScriptServiceClient, ServerResponse } from './typescriptService' @@ -25,6 +25,8 @@ import { PluginManager } from '../utils/plugins' import { ICallback, Reader } from './utils/wireProtocol' import { CallbackMap } from './callbackMap' import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue' +import BufferSyncSupport from './features/bufferSyncSupport' +import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics' class ForkedTsServerProcess { constructor(private childProcess: cp.ChildProcess) { } @@ -66,6 +68,9 @@ export interface TsDiagnostics { export default class TypeScriptServiceClient implements ITypeScriptServiceClient { public state = ServiceStat.Initial public readonly logger: Logger = new Logger() + public readonly bufferSyncSupport: BufferSyncSupport + public readonly diagnosticsManager: DiagnosticsManager + private fileConfigurationManager: FileConfigurationManager private pathSeparator: string private tracer: Tracer @@ -111,6 +116,16 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient pluginManager.onDidChangePlugins(() => { this.restartTsServer() }, null, this.disposables) + + this.bufferSyncSupport = new BufferSyncSupport(this) + this.onTsServerStarted(() => { + this.bufferSyncSupport.listen() + }) + + this.diagnosticsManager = new DiagnosticsManager() + this.bufferSyncSupport.onDelete(resource => { + this.diagnosticsManager.delete(resource) + }, null, this.disposables) } private _onDiagnosticsReceived = new Emitter() @@ -140,7 +155,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient }) .then(undefined, () => void 0) } - + this.bufferSyncSupport.dispose() disposeAll(this.disposables) this.logger.dispose() this._onTsServerStarted.dispose() @@ -516,6 +531,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient if (this.servicePromise == null) { return Promise.resolve(undefined) } + this.bufferSyncSupport.beforeCommand(command) const request = this._requestQueue.createRequest(command, args) const requestInfo: RequestItem = { @@ -700,7 +716,11 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient ) } break - + case 'projectsUpdatedInBackground': + const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body + const resources = body.openFiles.map(Uri.file) + this.bufferSyncSupport.getErr(resources) + break case 'typesInstallerInitializationFailed': if (event.body) { this._onTypesInstallerInitializationFailed.fire( @@ -708,6 +728,13 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient ) } break + case 'projectLoadingStart': + this.versionStatus.loading = true + break + + case 'projectLoadingFinish': + this.versionStatus.loading = false + break } } @@ -786,6 +813,10 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient if (this.apiVersion.gte(API.v291)) { args.push('--noGetErrOnBackgroundUpdate') } + + if (this.apiVersion.gte(API.v345)) { + args.push('--validateDefaultNpmLocation') + } return args } @@ -816,6 +847,10 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient }) } } + + public interruptGetErr(f: () => R): R { + return this.bufferSyncSupport.interuptGetErr(f) + } } function getDiagnosticsKind(event: Proto.Event): DiagnosticKind { diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts index 9b09a41..ec59c5e 100644 --- a/src/server/typescriptServiceClientHost.ts +++ b/src/server/typescriptServiceClientHost.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Uri, DiagnosticKind, disposeAll, workspace } from 'coc.nvim' -import { Range, Diagnostic, DiagnosticSeverity, Disposable, Position, CancellationToken } from 'vscode-languageserver-protocol' +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' @@ -64,13 +64,13 @@ export default class TypeScriptServiceClientHost implements Disposable { let language = this.findLanguage(uri) if (!language) return if (diagnostics.length == 0) { - language.configFileDiagnosticsReceived(uri, []) + this.client.diagnosticsManager.configFileDiagnosticsReceived(uri.toString(), []) } else { let range = Range.create(Position.create(0, 0), Position.create(0, 1)) let { text, code, category } = diagnostics[0] let severity = category == 'error' ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning let diagnostic = Diagnostic.create(range, text, severity, code) - language.configFileDiagnosticsReceived(uri, [diagnostic]) + this.client.diagnosticsManager.configFileDiagnosticsReceived(uri.toString(), [diagnostic]) } } }, null, this.disposables) @@ -158,22 +158,34 @@ export default class TypeScriptServiceClientHost implements Disposable { } } - private createMarkerDatas(diagnostics: Proto.Diagnostic[]): Diagnostic[] { + private createMarkerDatas(diagnostics: Proto.Diagnostic[]): (Diagnostic & { reportUnnecessary: any })[] { return diagnostics.map(tsDiag => this.tsDiagnosticToLspDiagnostic(tsDiag)) } - private tsDiagnosticToLspDiagnostic(diagnostic: Proto.Diagnostic): Diagnostic { + private tsDiagnosticToLspDiagnostic(diagnostic: Proto.Diagnostic): (Diagnostic & { reportUnnecessary: any }) { const { start, end, text } = diagnostic const range = { start: typeConverters.Position.fromLocation(start), end: typeConverters.Position.fromLocation(end) } + let relatedInformation: DiagnosticRelatedInformation[] + if (diagnostic.relatedInformation) { + relatedInformation = diagnostic.relatedInformation.map(o => { + let { span, message } = o + return { + location: typeConverters.Location.fromTextSpan(this.client.toResource(span.file), span), + message + } + }) + } return { range, message: text, code: diagnostic.code ? diagnostic.code : null, severity: this.getDiagnosticSeverity(diagnostic), + reportUnnecessary: diagnostic.reportsUnnecessary, source: diagnostic.source || 'tsserver', + relatedInformation } } diff --git a/src/server/utils/api.ts b/src/server/utils/api.ts index d9d4b93..8c097c8 100644 --- a/src/server/utils/api.ts +++ b/src/server/utils/api.ts @@ -5,56 +5,61 @@ import * as semver from 'semver' export default class API { - private static fromSimpleString(value: string): API { - return new API(value, value) - } + private static fromSimpleString(value: string): API { + return new API(value, value) + } - public static readonly defaultVersion = API.fromSimpleString('1.0.0') - public static readonly v203 = API.fromSimpleString('2.0.3') - public static readonly v206 = API.fromSimpleString('2.0.6') - public static readonly v208 = API.fromSimpleString('2.0.8') - public static readonly v213 = API.fromSimpleString('2.1.3') - public static readonly v220 = API.fromSimpleString('2.2.0') - public static readonly v222 = API.fromSimpleString('2.2.2') - public static readonly v230 = API.fromSimpleString('2.3.0') - public static readonly v234 = API.fromSimpleString('2.3.4') - public static readonly v240 = API.fromSimpleString('2.4.0') - public static readonly v250 = API.fromSimpleString('2.5.0') - public static readonly v260 = API.fromSimpleString('2.6.0') - public static readonly v270 = API.fromSimpleString('2.7.0') - public static readonly v280 = API.fromSimpleString('2.8.0') - public static readonly v290 = API.fromSimpleString('2.9.0') - public static readonly v291 = API.fromSimpleString('2.9.1') - public static readonly v292 = API.fromSimpleString('2.9.2') - public static readonly v300 = API.fromSimpleString('3.0.0') - public static readonly v310 = API.fromSimpleString('3.1.0') - public static readonly v314 = API.fromSimpleString('3.1.4') - public static readonly v320 = API.fromSimpleString('3.2.0') + public static readonly defaultVersion = API.fromSimpleString('1.0.0') + public static readonly v203 = API.fromSimpleString('2.0.3') + public static readonly v206 = API.fromSimpleString('2.0.6') + public static readonly v208 = API.fromSimpleString('2.0.8') + public static readonly v213 = API.fromSimpleString('2.1.3') + public static readonly v220 = API.fromSimpleString('2.2.0') + public static readonly v222 = API.fromSimpleString('2.2.2') + public static readonly v230 = API.fromSimpleString('2.3.0') + public static readonly v234 = API.fromSimpleString('2.3.4') + public static readonly v240 = API.fromSimpleString('2.4.0') + public static readonly v250 = API.fromSimpleString('2.5.0') + public static readonly v260 = API.fromSimpleString('2.6.0') + public static readonly v270 = API.fromSimpleString('2.7.0') + public static readonly v280 = API.fromSimpleString('2.8.0') + public static readonly v290 = API.fromSimpleString('2.9.0') + public static readonly v291 = API.fromSimpleString('2.9.1') + public static readonly v292 = API.fromSimpleString('2.9.2') + public static readonly v300 = API.fromSimpleString('3.0.0') + public static readonly v310 = API.fromSimpleString('3.1.0') + public static readonly v314 = API.fromSimpleString('3.1.4') + public static readonly v320 = API.fromSimpleString('3.2.0') + public static readonly v330 = API.fromSimpleString('3.3.0') + public static readonly v333 = API.fromSimpleString('3.3.3') + public static readonly v340 = API.fromSimpleString('3.4.0') + public static readonly v345 = API.fromSimpleString('3.4.5') + public static readonly v350 = API.fromSimpleString('3.5.0') - public static fromVersionString(versionString: string): API { - let version = semver.valid(versionString) - if (!version) { - return new API('invalid version', '1.0.0') - } + public static fromVersionString(versionString: string): API { + let version = semver.valid(versionString) + if (!version) { + return new API('invalid version', '1.0.0') + } - // Cut off any prerelease tag since we sometimes consume those on purpose. - const index = versionString.indexOf('-') - if (index >= 0) { - version = version.substr(0, index) - } - return new API(versionString, version) - } + // Cut off any prerelease tag since we sometimes consume those on purpose. + const index = versionString.indexOf('-') + if (index >= 0) { + version = version.substr(0, index) + } + return new API(versionString, version) + } - private constructor( - public readonly versionString: string, - private readonly version: string - ) { } + private constructor( + public readonly versionString: string, + private readonly version: string + ) { } - public gte(other: API): boolean { - return semver.gte(this.version, other.version) - } + public gte(other: API): boolean { + return semver.gte(this.version, other.version) + } - public lt(other: API): boolean { - return !this.gte(other) - } + public lt(other: API): boolean { + return !this.gte(other) + } } diff --git a/src/server/utils/languageDescription.ts b/src/server/utils/languageDescription.ts index e41154c..6a18820 100644 --- a/src/server/utils/languageDescription.ts +++ b/src/server/utils/languageDescription.ts @@ -7,12 +7,18 @@ import * as languageModeIds from './languageModeIds' export interface LanguageDescription { readonly id: string readonly diagnosticSource: string + readonly diagnosticLanguage: DiagnosticLanguage readonly modeIds: string[] readonly configFile?: string readonly isExternal?: boolean readonly diagnosticOwner: string } +export const enum DiagnosticLanguage { + JavaScript, + TypeScript +} + export const standardLanguageDescriptions: LanguageDescription[] = [ { id: 'typescript', @@ -20,6 +26,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ diagnosticOwner: 'typescript', modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact, languageModeIds.typescripttsx, languageModeIds.typescriptjsx], + diagnosticLanguage: DiagnosticLanguage.TypeScript, configFile: 'tsconfig.json' }, { @@ -27,6 +34,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ diagnosticSource: 'ts', diagnosticOwner: 'typescript', modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact], + diagnosticLanguage: DiagnosticLanguage.JavaScript, configFile: 'jsconfig.json' } ] diff --git a/src/server/utils/typeConverters.ts b/src/server/utils/typeConverters.ts index 0906bf5..a50f368 100644 --- a/src/server/utils/typeConverters.ts +++ b/src/server/utils/typeConverters.ts @@ -23,6 +23,14 @@ export namespace Range { } } + export const toFormattingRequestArgs = (file: string, range: language.Range): Proto.FormatRequestArgs => ({ + file, + line: range.start.line + 1, + offset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }) + export const toFileRangeRequestArgs = ( file: string, range: language.Range @@ -43,6 +51,11 @@ export namespace Position { } } + export const toLocation = (position: language.Position): Proto.Location => ({ + line: position.line + 1, + offset: position.character + 1, + }) + export const toFileLocationRequestArgs = ( file: string, position: language.Position diff --git a/src/server/utils/versionStatus.ts b/src/server/utils/versionStatus.ts index 086b4da..4ac27e2 100644 --- a/src/server/utils/versionStatus.ts +++ b/src/server/utils/versionStatus.ts @@ -27,6 +27,10 @@ export default class VersionStatus { }) } + public set loading(isLoading: boolean) { + this._versionBarEntry.isProgress = isLoading + } + private async showHideStatus(): Promise { let document = await workspace.document if (!document) { diff --git a/yarn.lock b/yarn.lock index 40fd631..fe71beb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -553,7 +553,7 @@ vscode-jsonrpc@^4.1.0-next.2: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.1.0-next.2.tgz#3bd318910a48e631742b290975386e3dae685be3" integrity sha512-GsBLjP9DxQ42yl1mW9GEIlnSc0+R8mfzhaebwmmTPEJjezD5SPoAo3DFrIAFZha9yvQ1nzZfZlhtVpGQmgxtXg== -vscode-languageserver-protocol@^3.15.0-next.1, vscode-languageserver-protocol@^3.15.0-next.5: +vscode-languageserver-protocol@^3.15.0-next.5: version "3.15.0-next.5" resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.0-next.5.tgz#23afad3d28795f2235eda7a167e2fe0825b7c151" integrity sha512-rR7Zo5WZTGSsE9lq7pPSgO+VMhVV8UVq6emrDoQ3x5dUyhLKB2/gbMkGKucQpsKGLtF/NuccCa+3jMsO788HjQ==