From 01e01b13f88064374ebc5aa789cb0ee76d03a17c Mon Sep 17 00:00:00 2001 From: Pine Wu Date: Wed, 18 Mar 2020 12:42:48 -0700 Subject: [PATCH] HTML implementation for #88424 --- .../client/src/htmlMain.ts | 22 +- .../client/src/mirrorCursor.ts | 259 ------------------ .../html-language-features/package.json | 3 +- .../html-language-features/package.nls.json | 3 +- .../server/src/htmlServerMain.ts | 12 +- .../server/src/modes/htmlMode.ts | 4 + .../server/src/modes/languageModes.ts | 1 + 7 files changed, 26 insertions(+), 278 deletions(-) delete mode 100644 extensions/html-language-features/client/src/mirrorCursor.ts diff --git a/extensions/html-language-features/client/src/htmlMain.ts b/extensions/html-language-features/client/src/htmlMain.ts index c54a97f9b07..567e073f838 100644 --- a/extensions/html-language-features/client/src/htmlMain.ts +++ b/extensions/html-language-features/client/src/htmlMain.ts @@ -21,13 +21,12 @@ import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; import { activateTagClosing } from './tagClosing'; import TelemetryReporter from 'vscode-extension-telemetry'; import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData'; -import { activateMirrorCursor } from './mirrorCursor'; namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); } -namespace MatchingTagPositionRequest { - export const type: RequestType = new RequestType('html/matchingTagPosition'); +namespace OnTypeRenameRequest { + export const type: RequestType = new RequestType('html/onTypeRename'); } // experimental: semantic tokens @@ -131,14 +130,6 @@ export function activate(context: ExtensionContext) { disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true }, 'html.autoClosingTags'); toDispose.push(disposable); - const matchingTagPositionRequestor = (document: TextDocument, position: Position) => { - let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position); - return client.sendRequest(MatchingTagPositionRequest.type, param); - }; - - disposable = activateMirrorCursor(matchingTagPositionRequestor, { html: true, handlebars: true }, 'html.mirrorCursorOnMatchingTag'); - toDispose.push(disposable); - disposable = client.onTelemetry(e => { if (telemetryReporter) { telemetryReporter.sendTelemetryEvent(e.key, e.data); @@ -289,6 +280,15 @@ export function activate(context: ExtensionContext) { return results; } }); + + languages.registerOnTypeRenameProvider(documentSelector, { + async provideOnTypeRenameRanges(document, position) { + const param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position); + const response = await client.sendRequest(OnTypeRenameRequest.type, param); + + return response || []; + } + }); } function getPackageInfo(context: ExtensionContext): IPackageInfo | null { diff --git a/extensions/html-language-features/client/src/mirrorCursor.ts b/extensions/html-language-features/client/src/mirrorCursor.ts deleted file mode 100644 index 5c9ae446087..00000000000 --- a/extensions/html-language-features/client/src/mirrorCursor.ts +++ /dev/null @@ -1,259 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { - window, - workspace, - Disposable, - TextDocument, - Position, - TextEditorSelectionChangeEvent, - Selection, - Range, - WorkspaceEdit -} from 'vscode'; - -export function activateMirrorCursor( - matchingTagPositionProvider: (document: TextDocument, position: Position) => Thenable, - supportedLanguages: { [id: string]: boolean }, - configName: string -): Disposable { - let disposables: Disposable[] = []; - - window.onDidChangeTextEditorSelection(event => onDidChangeTextEditorSelection(event), null, disposables); - - let isEnabled = false; - updateEnabledState(); - - window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables); - - function updateEnabledState() { - isEnabled = false; - let editor = window.activeTextEditor; - if (!editor) { - return; - } - let document = editor.document; - if (!supportedLanguages[document.languageId]) { - return; - } - if (!workspace.getConfiguration(undefined, document.uri).get(configName)) { - return; - } - isEnabled = true; - } - - let prevCursors: readonly Selection[] = []; - let cursors: readonly Selection[] = []; - let inMirrorMode = false; - - function onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent) { - if (!isEnabled) { - return; - } - - if (event.textEditor.document?.languageId !== 'html' && event.textEditor.document?.languageId !== 'handlebars') { - return; - } - - prevCursors = cursors; - cursors = event.selections; - - if (cursors.length === 1) { - if (inMirrorMode && prevCursors.length === 2) { - if (cursors[0].isEqual(prevCursors[0]) || cursors[0].isEqual(prevCursors[1])) { - return; - } - } - if (event.selections[0].isEmpty) { - matchingTagPositionProvider(event.textEditor.document, event.selections[0].active).then(matchingTagPosition => { - if (matchingTagPosition && window.activeTextEditor) { - const charBeforeAndAfterPositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( - event.textEditor.document, - event.selections[0].anchor, - new Position(matchingTagPosition.line, matchingTagPosition.character) - ); - - if (charBeforeAndAfterPositionsRoughlyEqual) { - inMirrorMode = true; - const newCursor = new Selection( - matchingTagPosition.line, - matchingTagPosition.character, - matchingTagPosition.line, - matchingTagPosition.character - ); - window.activeTextEditor.selections = [...window.activeTextEditor.selections, newCursor]; - } - } - }); - } - } - - const exitMirrorMode = () => { - inMirrorMode = false; - window.activeTextEditor!.selections = [window.activeTextEditor!.selections[0]]; - }; - - if (cursors.length === 2 && inMirrorMode) { - /** - * Both cursors are positions - */ - if (event.selections[0].isEmpty && event.selections[1].isEmpty) { - if ( - prevCursors.length === 2 && - event.selections[0].anchor.line !== prevCursors[0].anchor.line && - event.selections[1].anchor.line !== prevCursors[0].anchor.line - ) { - exitMirrorMode(); - return; - } - - const charBeforeAndAfterPositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( - event.textEditor.document, - event.selections[0].anchor, - event.selections[1].anchor - ); - - if (!charBeforeAndAfterPositionsRoughlyEqual) { - exitMirrorMode(); - return; - } else { - // Need to cleanup in the case of
- if ( - shouldDoCleanupForHtmlAttributeInput( - event.textEditor.document, - event.selections[0].anchor, - event.selections[1].anchor - ) - ) { - const cleanupEdit = new WorkspaceEdit(); - const cleanupRange = new Range(event.selections[1].anchor.translate(0, -1), event.selections[1].anchor); - cleanupEdit.replace(event.textEditor.document.uri, cleanupRange, ''); - exitMirrorMode(); - workspace.applyEdit(cleanupEdit); - } - } - } else { - /** - * Both cursors are selections - */ - const charBeforeAndAfterAnchorPositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( - event.textEditor.document, - event.selections[0].anchor, - event.selections[1].anchor - ); - - const charBeforeAndAfterActivePositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( - event.textEditor.document, - event.selections[0].active, - event.selections[1].active - ); - - if (!charBeforeAndAfterAnchorPositionsRoughlyEqual || !charBeforeAndAfterActivePositionsRoughlyEqual) { - exitMirrorMode(); - } - } - } - } - - return Disposable.from(...disposables); -} - -function getCharBefore(document: TextDocument, position: Position) { - const offset = document.offsetAt(position); - if (offset === 0) { - return ''; - } - - return document.getText(new Range(document.positionAt(offset - 1), position)); -} - -function getCharAfter(document: TextDocument, position: Position) { - const offset = document.offsetAt(position); - if (offset === document.getText().length) { - return ''; - } - - return document.getText(new Range(position, document.positionAt(offset + 1))); -} - -// Check if chars before and after the two positions are equal -// For the chars before, `<` and `/` are considered equal to handle the case of `<|>` -function isCharBeforeAndAfterPositionsRoughlyEqual(document: TextDocument, firstPos: Position, secondPos: Position) { - const charBeforePrimarySelection = getCharBefore(document, firstPos); - const charAfterPrimarySelection = getCharAfter(document, firstPos); - const charBeforeSecondarySelection = getCharBefore(document, secondPos); - const charAfterSecondarySelection = getCharAfter(document, secondPos); - - /** - * Special case for exiting - * |
- * |
- */ - if ( - charBeforePrimarySelection === ' ' && - charBeforeSecondarySelection === ' ' && - charAfterPrimarySelection === '<' && - charAfterSecondarySelection === '<' - ) { - return false; - } - /** - * Special case for exiting - * |
- * |
- */ - if (charBeforePrimarySelection === '\n' && charBeforeSecondarySelection === '\n') { - return false; - } - /** - * Special case for exiting - *
| - *
| - */ - if (charAfterPrimarySelection === '\n' && charAfterSecondarySelection === '\n') { - return false; - } - - // Exit mirror mode when cursor position no longer mirror - // Unless it's in the case of `<|>` - const charBeforeBothPositionRoughlyEqual = - charBeforePrimarySelection === charBeforeSecondarySelection || - (charBeforePrimarySelection === '/' && charBeforeSecondarySelection === '<') || - (charBeforeSecondarySelection === '/' && charBeforePrimarySelection === '<'); - const charAfterBothPositionRoughlyEqual = - charAfterPrimarySelection === charAfterSecondarySelection || - (charAfterPrimarySelection === ' ' && charAfterSecondarySelection === '>') || - (charAfterSecondarySelection === ' ' && charAfterPrimarySelection === '>'); - - return charBeforeBothPositionRoughlyEqual && charAfterBothPositionRoughlyEqual; -} - -function shouldDoCleanupForHtmlAttributeInput(document: TextDocument, firstPos: Position, secondPos: Position) { - // Need to cleanup in the case of
- const charBeforePrimarySelection = getCharBefore(document, firstPos); - const charAfterPrimarySelection = getCharAfter(document, firstPos); - const charBeforeSecondarySelection = getCharBefore(document, secondPos); - const charAfterSecondarySelection = getCharAfter(document, secondPos); - - const primaryBeforeSecondary = document.offsetAt(firstPos) < document.offsetAt(secondPos); - - /** - * Check two cases - *
- *
- * Before 1st cursor: ` ` - * After 1st cursor: `>` or ` ` - * Before 2nd cursor: ` ` - * After 2nd cursor: `>` - */ - return ( - primaryBeforeSecondary && - charBeforePrimarySelection === ' ' && - (charAfterPrimarySelection === '>' || charAfterPrimarySelection === ' ') && - charBeforeSecondarySelection === ' ' && - charAfterSecondarySelection === '>' - ); -} diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 00289a314a1..b92820fade8 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -165,7 +165,8 @@ "type": "boolean", "scope": "resource", "default": false, - "description": "%html.mirrorCursorOnMatchingTag%" + "description": "%html.mirrorCursorOnMatchingTag%", + "deprecationMessage": "%html.mirrorCursorOnMatchingTagDeprecationMessage%" }, "html.trace.server": { "type": "string", diff --git a/extensions/html-language-features/package.nls.json b/extensions/html-language-features/package.nls.json index 8fa34d25270..90e4e73f568 100644 --- a/extensions/html-language-features/package.nls.json +++ b/extensions/html-language-features/package.nls.json @@ -25,5 +25,6 @@ "html.validate.scripts": "Controls whether the built-in HTML language support validates embedded scripts.", "html.validate.styles": "Controls whether the built-in HTML language support validates embedded styles.", "html.autoClosingTags": "Enable/disable autoclosing of HTML tags.", - "html.mirrorCursorOnMatchingTag": "Enable/disable mirroring cursor on matching HTML tag." + "html.mirrorCursorOnMatchingTag": "Enable/disable mirroring cursor on matching HTML tag.", + "html.mirrorCursorOnMatchingTagDeprecationMessage": "Deprecated in favor of `editor.renameOnType`" } diff --git a/extensions/html-language-features/server/src/htmlServerMain.ts b/extensions/html-language-features/server/src/htmlServerMain.ts index 9ed3ce0f0a0..78385d54719 100644 --- a/extensions/html-language-features/server/src/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/htmlServerMain.ts @@ -28,8 +28,8 @@ import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanti namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); } -namespace MatchingTagPositionRequest { - export const type: RequestType = new RequestType('html/matchingTagPosition'); +namespace OnTypeRenameRequest { + export const type: RequestType = new RequestType('html/onTypeRename'); } // experimental: semantic tokens @@ -499,20 +499,20 @@ connection.onRenameRequest((params, token) => { }, null, `Error while computing rename for ${params.textDocument.uri}`, token); }); -connection.onRequest(MatchingTagPositionRequest.type, (params, token) => { +connection.onRequest(OnTypeRenameRequest.type, (params, token) => { return runSafe(() => { const document = documents.get(params.textDocument.uri); if (document) { const pos = params.position; if (pos.character > 0) { const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); - if (mode && mode.findMatchingTagPosition) { - return mode.findMatchingTagPosition(document, pos); + if (mode && mode.doOnTypeRename) { + return mode.doOnTypeRename(document, pos); } } } return null; - }, null, `Error while computing matching tag position for ${params.textDocument.uri}`, token); + }, null, `Error while computing synced regions for ${params.textDocument.uri}`, token); }); let semanticTokensProvider: SemanticTokenProvider | undefined; diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts index 251821d1272..b4b72fe7c96 100644 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ b/extensions/html-language-features/server/src/modes/htmlMode.ts @@ -85,6 +85,10 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: const htmlDocument = htmlDocuments.get(document); return htmlLanguageService.findMatchingTagPosition(document, position, htmlDocument); }, + doOnTypeRename(document: TextDocument, position: Position) { + const htmlDocument = htmlDocuments.get(document); + return htmlLanguageService.findSyncedRegions(document, position, htmlDocument); + }, dispose() { htmlDocuments.dispose(); } diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts index 43bbc1f3d6c..624e8de448f 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -47,6 +47,7 @@ export interface LanguageMode { doHover?: (document: TextDocument, position: Position) => Hover | null; doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null; doRename?: (document: TextDocument, position: Position, newName: string) => WorkspaceEdit | null; + doOnTypeRename?: (document: TextDocument, position: Position) => Range[] | null; findDocumentHighlight?: (document: TextDocument, position: Position) => DocumentHighlight[]; findDocumentSymbols?: (document: TextDocument) => SymbolInformation[]; findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => DocumentLink[];