From 5b1a6f4155ab734d082d679f42e482c55123c602 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 21 Feb 2017 16:02:53 -0800 Subject: [PATCH] Use CompletionItem for JsDoc Comment auto fill (#21025) Fixes #20990 Switches back to using a completion item provider for jsdoc auto complete. The completion list will be automatically shown when you are in a potentially valid jsdoc completion context --- extensions/typescript/package.json | 23 --- .../typescript/snippets/typescript.json | 9 - .../typescript/snippets/typescriptreact.json | 9 - .../src/features/jsDocCompletionProvider.ts | 184 ++++++++++++++++++ extensions/typescript/src/typescriptMain.ts | 18 +- .../src/utils/JsDocCompletionHelper.ts | 97 --------- 6 files changed, 188 insertions(+), 152 deletions(-) create mode 100644 extensions/typescript/src/features/jsDocCompletionProvider.ts delete mode 100644 extensions/typescript/src/utils/JsDocCompletionHelper.ts diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index 4fffc101944..9140b12d890 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -36,7 +36,6 @@ "onCommand:typescript.selectTypeScriptVersion", "onCommand:javascript.goToProjectConfig", "onCommand:typescript.goToProjectConfig", - "onCommand:_typescript.tryCompleteJsDoc", "workspaceContains:jsconfig.json", "workspaceContains:tsconfig.json" ], @@ -84,28 +83,6 @@ } } ], - "keybindings": [ - { - "key": "enter", - "command": "_typescript.tryCompleteJsDoc", - "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == 'typescript'" - }, - { - "key": "enter", - "command": "_typescript.tryCompleteJsDoc", - "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == 'typescriptreact'" - }, - { - "key": "enter", - "command": "_typescript.tryCompleteJsDoc", - "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == 'javascript'" - }, - { - "key": "enter", - "command": "_typescript.tryCompleteJsDoc", - "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == 'javascriptreact'" - } - ], "configuration": { "type": "object", "title": "%configuration.typescript%", diff --git a/extensions/typescript/snippets/typescript.json b/extensions/typescript/snippets/typescript.json index c0414e57eb9..40ff628a00b 100644 --- a/extensions/typescript/snippets/typescript.json +++ b/extensions/typescript/snippets/typescript.json @@ -1,13 +1,4 @@ { - "jsdoc snippet": { - "prefix": "jsdoc comment", - "body": [ - "/**", - " * $0", - " */" - ], - "description": "jsdoc snippet" - }, "Constructor": { "prefix": "ctor", "body": [ diff --git a/extensions/typescript/snippets/typescriptreact.json b/extensions/typescript/snippets/typescriptreact.json index 2c57867f023..c0d38f23c18 100644 --- a/extensions/typescript/snippets/typescriptreact.json +++ b/extensions/typescript/snippets/typescriptreact.json @@ -1,13 +1,4 @@ { - "jsdoc snippet": { - "prefix": "jsdoc comment", - "body": [ - "/**", - " * $0", - " */" - ], - "description": "jsdoc snippet" - }, "Constructor": { "prefix": "ctor", "body": [ diff --git a/extensions/typescript/src/features/jsDocCompletionProvider.ts b/extensions/typescript/src/features/jsDocCompletionProvider.ts new file mode 100644 index 00000000000..55e8662e28e --- /dev/null +++ b/extensions/typescript/src/features/jsDocCompletionProvider.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Position, Selection, Range, CompletionItemProvider, CompletionItemKind, TextDocument, CancellationToken, CompletionItem, window, commands, Uri, ProviderResult, TextEditor } from 'vscode'; + +import { ITypescriptServiceClient } from '../typescriptService'; +import { FileLocationRequestArgs, DocCommandTemplateResponse } from '../protocol'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +const tryCompleteJsDocCommand = '_typeScript.tryCompleteJsDoc'; + + +class JsDocCompletionItem extends CompletionItem { + constructor(file: Uri, position: Position) { + super('/** @param */', CompletionItemKind.Snippet); + this.detail = localize('typescript.jsDocCompletionItem.detail', 'Complete JSDoc comment'); + this.insertText = ''; + this.command = { + title: 'Try Complete Js Doc', + command: tryCompleteJsDocCommand, + arguments: [file, position] + }; + } +} + +export default class JsDocCompletionHelper implements CompletionItemProvider { + + constructor( + private client: ITypescriptServiceClient, + ) { + window.onDidChangeTextEditorSelection(e => { + if (e.textEditor.document.languageId !== 'typescript' + && e.textEditor.document.languageId !== 'typescriptreact' + && e.textEditor.document.languageId !== 'javascript' + && e.textEditor.document.languageId !== 'javascriptreact' + ) { + return; + } + + const selection = e.selections[0]; + if (!selection.start.isEqual(selection.end)) { + return; + } + if (this.shouldAutoShowJsDocSuggestion(e.textEditor.document, selection.start)) { + return commands.executeCommand('editor.action.triggerSuggest'); + } + return; + }); + + commands.registerCommand( + tryCompleteJsDocCommand, + (file: Uri, position: Position) => this.tryCompleteJsDoc(file, position)); + } + + public provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken): ProviderResult { + const file = this.client.normalizePath(document.uri); + if (file) { + return [new JsDocCompletionItem(document.uri, position)]; + } + return []; + } + + public resolveCompletionItem(item: CompletionItem, _token: CancellationToken) { + return item; + } + + private shouldAutoShowJsDocSuggestion(document: TextDocument, position: Position): boolean { + const line = document.lineAt(position.line).text; + + // Ensure line starts with '/**' then cursor + const prefix = line.slice(0, position.character).match(/^\s*(\/\*\*+)\s*$/); + if (prefix === null) { + return false; + } + + // Ensure there is no content after the cursor besides possibly the end of the comment + const suffix = line.slice(position.character).match(/^\s*\**\/?$/); + return suffix !== null; + } + + /** + * Try to insert a jsdoc comment, using a template provide by typescript + * if possible, otherwise falling back to a default comment format. + */ + private tryCompleteJsDoc(resource: Uri, position: Position): Thenable { + const file = this.client.normalizePath(resource); + if (!file) { + return Promise.resolve(false); + } + + const editor = window.activeTextEditor; + if (!editor || editor.document.uri.fsPath !== resource.fsPath) { + return Promise.resolve(false); + } + + return this.prepForDocCompletion(editor, position) + .then((start: Position) => { + return this.tryInsertJsDocFromTemplate(editor, file, start); + }) + .then((didInsertFromTemplate: boolean) => { + if (didInsertFromTemplate) { + return true; + } + return this.tryInsertDefaultDoc(editor, position); + }); + } + + /** + * Prepare the area around the position for insertion of the jsdoc. + * + * Removes any the prefix and suffix of a possible jsdoc + */ + private prepForDocCompletion(editor: TextEditor, position: Position): Thenable { + const line = editor.document.lineAt(position.line).text; + const prefix = line.slice(0, position.character).match(/\/\**\s*$/); + const suffix = line.slice(position.character).match(/^\s*\**\//); + if (!prefix && !suffix) { + // Nothing to remove + return Promise.resolve(position); + } + + const start = position.translate(0, prefix ? -prefix[0].length : 0); + return editor.edit( + edits => { + edits.delete(new Range(start, position.translate(0, suffix ? suffix[0].length : 0))); + }, { + undoStopBefore: true, + undoStopAfter: false + }).then(() => start); + } + + private tryInsertJsDocFromTemplate(editor: TextEditor, file: string, position: Position): Promise { + const args: FileLocationRequestArgs = { + file: file, + line: position.line + 1, + offset: position.character + 1 + }; + return this.client.execute('docCommentTemplate', args) + .then((res: DocCommandTemplateResponse) => { + if (!res || !res.body) { + return false; + } + const commentText = res.body.newText; + return editor.edit( + edits => edits.insert(position, commentText), + { undoStopBefore: false, undoStopAfter: true }); + }, () => false) + .then((didInsertComment: boolean) => { + if (didInsertComment) { + const newCursorPosition = new Position(position.line + 1, editor.document.lineAt(position.line + 1).text.length); + editor.selection = new Selection(newCursorPosition, newCursorPosition); + } + return didInsertComment; + }); + } + + /** + * Insert the default JSDoc + */ + private tryInsertDefaultDoc(editor: TextEditor, position: Position): Thenable { + const line = editor.document.lineAt(position.line).text; + const spaceBefore = line.slice(0, position.character).match(/^\s*$/); + + const indent = spaceBefore ? spaceBefore[0] : ''; + return editor.edit( + edits => edits.insert(position, `/**\n${indent} * \n${indent} */`), + { undoStopBefore: false, undoStopAfter: true }) + .then((didInsert: boolean) => { + if (didInsert) { + const newCursorPosition = new Position(position.line + 1, editor.document.lineAt(position.line + 1).text.length); + editor.selection = new Selection(newCursorPosition, newCursorPosition); + } + return didInsert; + }); + } + + +} \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 46d910a978c..35e87ad82d5 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -39,8 +39,8 @@ import CompletionItemProvider from './features/completionItemProvider'; import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; +import JsDocCompletionHelper from './features/jsDocCompletionProvider'; -import JsDocCompletionHelper from './utils/JsDocCompletionHelper'; import * as BuildStatus from './utils/buildStatus'; import * as ProjectStatus from './utils/projectStatus'; import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; @@ -69,6 +69,7 @@ export function activate(context: ExtensionContext): void { const MODE_ID_TSX = 'typescriptreact'; const MODE_ID_JS = 'javascript'; const MODE_ID_JSX = 'javascriptreact'; + const selector = [MODE_ID_TS, MODE_ID_TSX, MODE_ID_JS, MODE_ID_JSX]; const clientHost = new TypeScriptServiceClientHost([ { @@ -102,19 +103,8 @@ export function activate(context: ExtensionContext): void { client.onVersionStatusClicked(); })); - const jsDocCompletionHelper = new JsDocCompletionHelper(client); - context.subscriptions.push(commands.registerCommand('_typescript.tryCompleteJsDoc', () => { - const editor = window.activeTextEditor; - if (!editor || !editor.selection.isEmpty) { - return commands.executeCommand('type', { text: '\n' }); - } - return jsDocCompletionHelper.tryCompleteJsDoc(editor, editor.selection.active).then(didCompleteComment => { - if (didCompleteComment) { - return; - } - return commands.executeCommand('type', { text: '\n' }); - }); - })); + context.subscriptions.push( + languages.registerCompletionItemProvider(selector, new JsDocCompletionHelper(client))); const goToProjectConfig = (isTypeScript: boolean) => { const editor = window.activeTextEditor; diff --git a/extensions/typescript/src/utils/JsDocCompletionHelper.ts b/extensions/typescript/src/utils/JsDocCompletionHelper.ts deleted file mode 100644 index 4b175aa4df9..00000000000 --- a/extensions/typescript/src/utils/JsDocCompletionHelper.ts +++ /dev/null @@ -1,97 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TextEditor, Position, Range, Selection } from 'vscode'; - -import { ITypescriptServiceClient } from '../typescriptService'; - -import { FileLocationRequestArgs, DocCommandTemplateResponse } from '../protocol'; - -export default class JsDocCompletionHelper { - - constructor( - private client: ITypescriptServiceClient, - ) { } - - public tryCompleteJsDoc(editor: TextEditor, position: Position): Thenable { - const file = this.client.normalizePath(editor.document.uri); - if (!file) { - return Promise.resolve(false); - } - - const line = editor.document.lineAt(position.line).text; - - // Ensure line starts with '/**' then cursor - const prefix = line.slice(0, position.character).match(/^\s*(\/\*\*+)$/); - if (!prefix) { - return Promise.resolve(false); - } - - // Ensure there is no content after the cursor besides possibly the end of the comment - const suffix = line.slice(position.character).match(/^\s*\**\/?$/); - if (!suffix) { - return Promise.resolve(false); - } - - const start = position.translate(0, -prefix[1].length); - return editor.edit( - edits => { - edits.delete(new Range(start, new Position(start.line, line.length))); - }, { - undoStopBefore: true, - undoStopAfter: false - } - ).then(removedComment => { - if (!removedComment) { - // Edit failed, nothing to revert. - return false; - } - - const args: FileLocationRequestArgs = { - file: file, - line: start.line + 1, - offset: start.character + 1 - }; - - return Promise.race([ - this.client.execute('docCommentTemplate', args), - new Promise((_, reject) => { - setTimeout(reject, 250); - }) - ]).then((res: DocCommandTemplateResponse) => { - if (!res || !res.body) { - return false; - } - const commentText = res.body.newText; - return editor.edit( - edits => edits.insert(start, commentText), - { undoStopBefore: false, undoStopAfter: true }); - }, () => { - return false; - }).then(didInsertComment => { - if (didInsertComment) { - const newCursorPosition = new Position(start.line + 1, editor.document.lineAt(start.line + 1).text.length); - editor.selection = new Selection(newCursorPosition, newCursorPosition); - return true; - } - - // Revert to the original line content and restore position - return editor.edit( - edits => { - edits.insert(start, prefix[1] + suffix[0]); - }, { - undoStopBefore: false, - undoStopAfter: true - } - ).then(() => { - editor.selection = new Selection(position, position); - return false; - }); - }); - }); - } -} \ No newline at end of file