diff --git a/extensions/typescript-language-features/src/features/jsDocCompletions.ts b/extensions/typescript-language-features/src/features/jsDocCompletions.ts index 02eb7439d95d27ad94f1f534186a936473644e74..e251e37542ea68acc9a7d95b7dfb77ba95c02eb6 100644 --- a/extensions/typescript-language-features/src/features/jsDocCompletions.ts +++ b/extensions/typescript-language-features/src/features/jsDocCompletions.ts @@ -7,21 +7,21 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; -import { Command, CommandManager } from '../utils/commandManager'; import { ConfigurationDependentRegistration } from '../utils/dependentRegistration'; import * as typeConverters from '../utils/typeConverters'; const localize = nls.loadMessageBundle(); +const defaultJsDoc = new vscode.SnippetString(`/**\n * $0\n */`); + class JsDocCompletionItem extends vscode.CompletionItem { constructor( - document: vscode.TextDocument, - position: vscode.Position + public readonly document: vscode.TextDocument, + public readonly position: vscode.Position ) { super('/** */', vscode.CompletionItemKind.Snippet); this.detail = localize('typescript.jsDocCompletionItem.documentation', 'JSDoc comment'); - this.insertText = ''; this.sortText = '\0'; const line = document.lineAt(position.line).text; @@ -31,12 +31,6 @@ class JsDocCompletionItem extends vscode.CompletionItem { this.range = new vscode.Range( start, position.translate(0, suffix ? suffix[0].length : 0)); - - this.command = { - title: 'Try Complete JSDoc', - command: TryCompleteJsDocCommand.COMMAND_NAME, - arguments: [document.uri, start] - }; } } @@ -44,154 +38,66 @@ class JsDocCompletionProvider implements vscode.CompletionItemProvider { constructor( private readonly client: ITypeScriptServiceClient, - commandManager: CommandManager - ) { - commandManager.register(new TryCompleteJsDocCommand(client)); - } + ) { } public async provideCompletionItems( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken - ): Promise { + ): Promise { const file = this.client.toPath(document.uri); if (!file) { - return []; + return undefined; } - if (!this.isValidCursorPosition(document, position)) { - return []; + if (!this.isPotentiallyValidDocCompletionPosition(document, position)) { + return undefined; } - if (!await this.isCommentableLocation(file, position, token)) { - return []; + const args = typeConverters.Position.toFileLocationRequestArgs(file, position); + let res: Proto.DocCommandTemplateResponse | undefined; + try { + res = await this.client.execute('docCommentTemplate', args, token); + } catch { + return undefined; } - return [new JsDocCompletionItem(document, position)]; - } - - private async isCommentableLocation( - file: string, - position: vscode.Position, - token: vscode.CancellationToken - ): Promise { - const args: Proto.FileRequestArgs = { - file - }; - const response = await Promise.race([ - this.client.execute('navtree', args, token), - new Promise((resolve) => setTimeout(resolve, 250)) - ]); - - if (!response || !response.body) { - return false; + if (!res.body) { + return undefined; } - const body = response.body; - - function matchesPosition(tree: Proto.NavigationTree): boolean { - if (!tree.spans.length) { - return false; - } - const span = typeConverters.Range.fromTextSpan(tree.spans[0]); - if (position.line === span.start.line - 1 || position.line === span.start.line) { - return true; - } + const item = new JsDocCompletionItem(document, position); - return tree.childItems ? tree.childItems.some(matchesPosition) : false; + // Workaround for #43619 + // docCommentTemplate previously returned undefined for empty jsdoc templates. + // TS 2.7 now returns a single line doc comment, which breaks indentation. + if (res.body.newText === '/** */') { + item.insertText = defaultJsDoc; + } else { + item.insertText = templateToSnippet(res.body.newText); } - return matchesPosition(body); + return [item]; } - private isValidCursorPosition(document: vscode.TextDocument, position: vscode.Position): boolean { + private isPotentiallyValidDocCompletionPosition( + document: vscode.TextDocument, + position: vscode.Position + ): boolean { // Only show the JSdoc completion when the everything before the cursor is whitespace // or could be the opening of a comment const line = document.lineAt(position.line).text; const prefix = line.slice(0, position.character); - return prefix.match(/^\s*$|\/\*\*\s*$|^\s*\/\*\*+\s*$/) !== null; - } - - public resolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken) { - return item; - } -} - -class TryCompleteJsDocCommand implements Command { - public static readonly COMMAND_NAME = '_typeScript.tryCompleteJsDoc'; - public readonly id = TryCompleteJsDocCommand.COMMAND_NAME; - - constructor( - private readonly client: ITypeScriptServiceClient - ) { } - - /** - * Try to insert a jsdoc comment, using a template provide by typescript - * if possible, otherwise falling back to a default comment format. - */ - public async execute(resource: vscode.Uri, start: vscode.Position): Promise { - const file = this.client.toPath(resource); - if (!file) { - return false; - } - - const editor = vscode.window.activeTextEditor; - if (!editor || editor.document.uri.fsPath !== resource.fsPath) { - return false; - } - - const didInsertFromTemplate = await this.tryInsertJsDocFromTemplate(editor, file, start); - if (didInsertFromTemplate) { - return true; - } - - return this.tryInsertDefaultDoc(editor, start); - } - - private async tryInsertJsDocFromTemplate(editor: vscode.TextEditor, file: string, position: vscode.Position): Promise { - const snippet = await TryCompleteJsDocCommand.getSnippetTemplate(this.client, file, position); - if (!snippet) { + if (prefix.match(/^\s*$|\/\*\*\s*$|^\s*\/\*\*+\s*$/) === null) { return false; } - return editor.insertSnippet( - snippet, - position, - { undoStopBefore: false, undoStopAfter: true }); - } - public static getSnippetTemplate(client: ITypeScriptServiceClient, file: string, position: vscode.Position): Promise { - const args = typeConverters.Position.toFileLocationRequestArgs(file, position); - const tokenSource = new vscode.CancellationTokenSource(); - return Promise.race([ - client.execute('docCommentTemplate', args, tokenSource.token), - new Promise((_, reject) => setTimeout(() => { - tokenSource.cancel(); - reject(); - }, 250)) - ]).then((res: Proto.DocCommandTemplateResponse) => { - if (!res || !res.body) { - return undefined; - } - // Workaround for #43619 - // docCommentTemplate previously returned undefined for empty jsdoc templates. - // TS 2.7 now returns a single line doc comment, which breaks indentation. - if (res.body.newText === '/** */') { - return undefined; - } - return templateToSnippet(res.body.newText); - }, () => undefined); - } - - /** - * Insert the default JSDoc - */ - private tryInsertDefaultDoc(editor: vscode.TextEditor, position: vscode.Position): Thenable { - const snippet = new vscode.SnippetString(`/**\n * $0\n */`); - return editor.insertSnippet(snippet, position, { undoStopBefore: false, undoStopAfter: true }); + // And everything after is possibly a closing comment or more whitespace + const suffix = line.slice(position.character); + return suffix.match(/^\s*\*+\//) !== null; } } - export function templateToSnippet(template: string): vscode.SnippetString { // TODO: use append placeholder let snippetIndex = 1; @@ -214,11 +120,10 @@ export function templateToSnippet(template: string): vscode.SnippetString { export function register( selector: vscode.DocumentSelector, client: ITypeScriptServiceClient, - commandManager: CommandManager ): vscode.Disposable { return new ConfigurationDependentRegistration('jsDocCompletion', 'enabled', () => { return vscode.languages.registerCompletionItemProvider(selector, - new JsDocCompletionProvider(client, commandManager), + new JsDocCompletionProvider(client), '*'); }); }