From 0d25d0a79823802d0c5df42db4d4d40b099c14d5 Mon Sep 17 00:00:00 2001 From: Pine Wu Date: Fri, 22 Nov 2019 10:16:38 -0800 Subject: [PATCH] Fix #47069 --- .../client/src/htmlMain.ts | 12 ++ .../client/src/matchingTag.ts | 147 ++++++++++++++++++ .../html-language-features/package.json | 6 + .../html-language-features/package.nls.json | 3 +- .../server/package.json | 2 +- .../server/src/htmlServerMain.ts | 19 +++ .../server/src/modes/htmlMode.ts | 4 + .../server/src/modes/languageModes.ts | 1 + .../html-language-features/server/yarn.lock | 8 +- 9 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 extensions/html-language-features/client/src/matchingTag.ts diff --git a/extensions/html-language-features/client/src/htmlMain.ts b/extensions/html-language-features/client/src/htmlMain.ts index 89bdc325721b6..194935b25fc90 100644 --- a/extensions/html-language-features/client/src/htmlMain.ts +++ b/extensions/html-language-features/client/src/htmlMain.ts @@ -14,10 +14,14 @@ import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; import { activateTagClosing } from './tagClosing'; import TelemetryReporter from 'vscode-extension-telemetry'; import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData'; +import { activateMatchingTagPosition as activateMatchingTagSelection } from './matchingTag'; namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); } +namespace MatchingTagPositionRequest { + export const type: RequestType = new RequestType('html/matchingTagPosition'); +} interface IPackageInfo { name: string; @@ -84,6 +88,14 @@ 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 = activateMatchingTagSelection(matchingTagPositionRequestor, { html: true, handlebars: true }, 'html.autoSelectingMatchingTags'); + toDispose.push(disposable); + disposable = client.onTelemetry(e => { if (telemetryReporter) { telemetryReporter.sendTelemetryEvent(e.key, e.data); diff --git a/extensions/html-language-features/client/src/matchingTag.ts b/extensions/html-language-features/client/src/matchingTag.ts new file mode 100644 index 0000000000000..589504ce1bb0b --- /dev/null +++ b/extensions/html-language-features/client/src/matchingTag.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * 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 activateMatchingTagPosition( + 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 prevCursorCount = 0; + let cursorCount = 0; + let inMirrorMode = false; + + function onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent) { + if (!isEnabled) { + return; + } + + // prevCursorCount = cursorCount; + cursorCount = event.selections.length; + + if (cursorCount === 1) { + if (event.selections[0].isEmpty) { + matchingTagPositionProvider(event.textEditor.document, event.selections[0].active).then(position => { + if (position && window.activeTextEditor) { + inMirrorMode = true; + const newCursor = new Selection(position.line, position.character, position.line, position.character); + window.activeTextEditor.selections = [...window.activeTextEditor.selections, newCursor]; + } + }); + } + } + + if (cursorCount === 2 && inMirrorMode) { + // Check two cases + if (event.selections[0].isEmpty && event.selections[1].isEmpty) { + const charBeforePrimarySelection = getCharBefore(event.textEditor.document, event.selections[0].anchor); + const charAfterPrimarySelection = getCharAfter(event.textEditor.document, event.selections[0].anchor); + const charBeforeSecondarySelection = getCharBefore(event.textEditor.document, event.selections[1].anchor); + const charAfterSecondarySelection = getCharAfter(event.textEditor.document, event.selections[1].anchor); + + // 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 === '>'); + + if (!charBeforeBothPositionRoughlyEqual || !charAfterBothPositionRoughlyEqual) { + inMirrorMode = false; + window.activeTextEditor!.selections = [window.activeTextEditor!.selections[0]]; + return; + } else { + // Need to cleanup in the case of
+ if ( + charBeforePrimarySelection === ' ' && + charAfterPrimarySelection === '>' && + charBeforeSecondarySelection === ' ' && + charAfterSecondarySelection === '>' + ) { + inMirrorMode = false; + const cleanupEdit = new WorkspaceEdit(); + + const primaryBeforeSecondary = + event.textEditor.document.offsetAt(event.selections[0].anchor) < + event.textEditor.document.offsetAt(event.selections[1].anchor); + const cleanupRange = primaryBeforeSecondary + ? new Range(event.selections[1].anchor.translate(0, -1), event.selections[1].anchor) + : new Range(event.selections[0].anchor.translate(0, -1), event.selections[0].anchor); + + cleanupEdit.replace(event.textEditor.document.uri, cleanupRange, ''); + window.activeTextEditor!.selections = primaryBeforeSecondary + ? [window.activeTextEditor!.selections[0]] + : [window.activeTextEditor!.selections[1]]; + workspace.applyEdit(cleanupEdit); + } + } + } + } + } + + 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)) + ); +} \ No newline at end of file diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 7c4f83d6b5b56..a641eb4ca4616 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -161,6 +161,12 @@ "default": true, "description": "%html.autoClosingTags%" }, + "html.autoSelectingMatchingTags": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%html.autoSelectingMatchingTags%" + }, "html.trace.server": { "type": "string", "scope": "window", diff --git a/extensions/html-language-features/package.nls.json b/extensions/html-language-features/package.nls.json index 2cb7e3d16973a..4e4d666b9f7f5 100644 --- a/extensions/html-language-features/package.nls.json +++ b/extensions/html-language-features/package.nls.json @@ -24,5 +24,6 @@ "html.trace.server.desc": "Traces the communication between VS Code and the HTML language server.", "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.autoClosingTags": "Enable/disable autoclosing of HTML tags.", + "html.autoSelectingMatchingTags": "Enable/disable auto selecting matching HTML tags." } diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 0f3ff9f949733..be9e3fd2a2175 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -10,7 +10,7 @@ "main": "./out/htmlServerMain", "dependencies": { "vscode-css-languageservice": "^4.0.3-next.20", - "vscode-html-languageservice": "^3.0.4-next.9", + "vscode-html-languageservice": "^3.0.4-next.10", "vscode-languageserver": "^6.0.0-next.3", "vscode-nls": "^4.1.1", "vscode-uri": "^2.0.3" diff --git a/extensions/html-language-features/server/src/htmlServerMain.ts b/extensions/html-language-features/server/src/htmlServerMain.ts index dcbbdca483b3f..8964b5ee035bc 100644 --- a/extensions/html-language-features/server/src/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/htmlServerMain.ts @@ -24,6 +24,9 @@ import { getDataProviders } from './customData'; namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); } +namespace MatchingTagPositionRequest { + export const type: RequestType = new RequestType('html/matchingTagPosition'); +} // Create a connection for the server const connection: IConnection = createConnection(); @@ -485,5 +488,21 @@ connection.onRenameRequest((params, token) => { }, null, `Error while computing rename for ${params.textDocument.uri}`, token); }); +connection.onRequest(MatchingTagPositionRequest.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); + } + } + } + return null; + }, null, `Error while computing matching tag position for ${params.textDocument.uri}`, token); +}); + // Listen on the connection connection.listen(); diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts index 9c64fb3a9d7d1..23304a28ec71c 100644 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ b/extensions/html-language-features/server/src/modes/htmlMode.ts @@ -81,6 +81,10 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: onDocumentRemoved(document: TextDocument) { htmlDocuments.onDocumentRemoved(document); }, + findMatchingTagPosition(document: TextDocument, position: Position) { + const htmlDocument = htmlDocuments.get(document); + return htmlLanguageService.findMatchingTagPosition(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 f50287330eaa3..a1fc67b27e1a5 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -48,6 +48,7 @@ export interface LanguageMode { findDocumentColors?: (document: TextDocument) => ColorInformation[]; getColorPresentations?: (document: TextDocument, color: Color, range: Range) => ColorPresentation[]; doAutoClose?: (document: TextDocument, position: Position) => string | null; + findMatchingTagPosition?: (document: TextDocument, position: Position) => Position | null; getFoldingRanges?: (document: TextDocument) => FoldingRange[]; onDocumentRemoved(document: TextDocument): void; dispose(): void; diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index 439f360ddb555..07ad507e2ffbf 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -621,10 +621,10 @@ vscode-css-languageservice@^4.0.3-next.20: vscode-nls "^4.1.1" vscode-uri "^2.1.1" -vscode-html-languageservice@^3.0.4-next.9: - version "3.0.4-next.9" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.9.tgz#b27f26c29f3af64fa32eabb7425749f95f64036a" - integrity sha512-9V9G7508ybFcn9gQpuucEZIGv8kKlBMEVD8lFFWwWS1yEonKchsxIGJZFbmSGr/n//2anfya8F8yL5ybKuWIRA== +vscode-html-languageservice@^3.0.4-next.10: + version "3.0.4-next.10" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.10.tgz#da426326833770c51712abb2c7473b9b30bf1cbc" + integrity sha512-8P0QBtMPJ9nDMhW8MF/z+5JGg6rK6UOa9po18KIleNuV0rDHU9CAqDyUjxW0CEfLrHYz6dQdkW12ZTClvQnNHw== dependencies: vscode-languageserver-textdocument "^1.0.0-next.4" vscode-languageserver-types "^3.15.0-next.6"