Skip to content

Commit 0d25d0a

Browse files
committed
Fix #47069
1 parent 6b78be0 commit 0d25d0a

File tree

9 files changed

+196
-6
lines changed

9 files changed

+196
-6
lines changed

extensions/html-language-features/client/src/htmlMain.ts

+12
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
1414
import { activateTagClosing } from './tagClosing';
1515
import TelemetryReporter from 'vscode-extension-telemetry';
1616
import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData';
17+
import { activateMatchingTagPosition as activateMatchingTagSelection } from './matchingTag';
1718

1819
namespace TagCloseRequest {
1920
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
2021
}
22+
namespace MatchingTagPositionRequest {
23+
export const type: RequestType<TextDocumentPositionParams, Position | null, any, any> = new RequestType('html/matchingTagPosition');
24+
}
2125

2226
interface IPackageInfo {
2327
name: string;
@@ -84,6 +88,14 @@ export function activate(context: ExtensionContext) {
8488
disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true }, 'html.autoClosingTags');
8589
toDispose.push(disposable);
8690

91+
const matchingTagPositionRequestor = (document: TextDocument, position: Position) => {
92+
let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
93+
return client.sendRequest(MatchingTagPositionRequest.type, param);
94+
};
95+
96+
disposable = activateMatchingTagSelection(matchingTagPositionRequestor, { html: true, handlebars: true }, 'html.autoSelectingMatchingTags');
97+
toDispose.push(disposable);
98+
8799
disposable = client.onTelemetry(e => {
88100
if (telemetryReporter) {
89101
telemetryReporter.sendTelemetryEvent(e.key, e.data);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import {
7+
window,
8+
workspace,
9+
Disposable,
10+
TextDocument,
11+
Position,
12+
TextEditorSelectionChangeEvent,
13+
Selection,
14+
Range,
15+
WorkspaceEdit
16+
} from 'vscode';
17+
18+
export function activateMatchingTagPosition(
19+
matchingTagPositionProvider: (document: TextDocument, position: Position) => Thenable<Position | null>,
20+
supportedLanguages: { [id: string]: boolean },
21+
configName: string
22+
): Disposable {
23+
let disposables: Disposable[] = [];
24+
25+
window.onDidChangeTextEditorSelection(event => onDidChangeTextEditorSelection(event), null, disposables);
26+
27+
let isEnabled = false;
28+
updateEnabledState();
29+
30+
window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
31+
32+
function updateEnabledState() {
33+
isEnabled = false;
34+
let editor = window.activeTextEditor;
35+
if (!editor) {
36+
return;
37+
}
38+
let document = editor.document;
39+
if (!supportedLanguages[document.languageId]) {
40+
return;
41+
}
42+
if (!workspace.getConfiguration(undefined, document.uri).get<boolean>(configName)) {
43+
return;
44+
}
45+
isEnabled = true;
46+
}
47+
48+
// let prevCursorCount = 0;
49+
let cursorCount = 0;
50+
let inMirrorMode = false;
51+
52+
function onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent) {
53+
if (!isEnabled) {
54+
return;
55+
}
56+
57+
// prevCursorCount = cursorCount;
58+
cursorCount = event.selections.length;
59+
60+
if (cursorCount === 1) {
61+
if (event.selections[0].isEmpty) {
62+
matchingTagPositionProvider(event.textEditor.document, event.selections[0].active).then(position => {
63+
if (position && window.activeTextEditor) {
64+
inMirrorMode = true;
65+
const newCursor = new Selection(position.line, position.character, position.line, position.character);
66+
window.activeTextEditor.selections = [...window.activeTextEditor.selections, newCursor];
67+
}
68+
});
69+
}
70+
}
71+
72+
if (cursorCount === 2 && inMirrorMode) {
73+
// Check two cases
74+
if (event.selections[0].isEmpty && event.selections[1].isEmpty) {
75+
const charBeforePrimarySelection = getCharBefore(event.textEditor.document, event.selections[0].anchor);
76+
const charAfterPrimarySelection = getCharAfter(event.textEditor.document, event.selections[0].anchor);
77+
const charBeforeSecondarySelection = getCharBefore(event.textEditor.document, event.selections[1].anchor);
78+
const charAfterSecondarySelection = getCharAfter(event.textEditor.document, event.selections[1].anchor);
79+
80+
// Exit mirror mode when cursor position no longer mirror
81+
// Unless it's in the case of `<|></|>`
82+
const charBeforeBothPositionRoughlyEqual =
83+
charBeforePrimarySelection === charBeforeSecondarySelection ||
84+
(charBeforePrimarySelection === '/' && charBeforeSecondarySelection === '<') ||
85+
(charBeforeSecondarySelection === '/' && charBeforePrimarySelection === '<');
86+
const charAfterBothPositionRoughlyEqual =
87+
charAfterPrimarySelection === charAfterSecondarySelection ||
88+
(charAfterPrimarySelection === ' ' && charAfterSecondarySelection === '>') ||
89+
(charAfterSecondarySelection === ' ' && charAfterPrimarySelection === '>');
90+
91+
if (!charBeforeBothPositionRoughlyEqual || !charAfterBothPositionRoughlyEqual) {
92+
inMirrorMode = false;
93+
window.activeTextEditor!.selections = [window.activeTextEditor!.selections[0]];
94+
return;
95+
} else {
96+
// Need to cleanup in the case of <div |></div |>
97+
if (
98+
charBeforePrimarySelection === ' ' &&
99+
charAfterPrimarySelection === '>' &&
100+
charBeforeSecondarySelection === ' ' &&
101+
charAfterSecondarySelection === '>'
102+
) {
103+
inMirrorMode = false;
104+
const cleanupEdit = new WorkspaceEdit();
105+
106+
const primaryBeforeSecondary =
107+
event.textEditor.document.offsetAt(event.selections[0].anchor) <
108+
event.textEditor.document.offsetAt(event.selections[1].anchor);
109+
const cleanupRange = primaryBeforeSecondary
110+
? new Range(event.selections[1].anchor.translate(0, -1), event.selections[1].anchor)
111+
: new Range(event.selections[0].anchor.translate(0, -1), event.selections[0].anchor);
112+
113+
cleanupEdit.replace(event.textEditor.document.uri, cleanupRange, '');
114+
window.activeTextEditor!.selections = primaryBeforeSecondary
115+
? [window.activeTextEditor!.selections[0]]
116+
: [window.activeTextEditor!.selections[1]];
117+
workspace.applyEdit(cleanupEdit);
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
return Disposable.from(...disposables);
125+
}
126+
127+
function getCharBefore(document: TextDocument, position: Position) {
128+
const offset = document.offsetAt(position);
129+
if (offset === 0) {
130+
return '';
131+
}
132+
133+
return document.getText(
134+
new Range(document.positionAt(offset - 1), position)
135+
);
136+
}
137+
138+
function getCharAfter(document: TextDocument, position: Position) {
139+
const offset = document.offsetAt(position);
140+
if (offset === document.getText().length) {
141+
return '';
142+
}
143+
144+
return document.getText(
145+
new Range(position, document.positionAt(offset + 1))
146+
);
147+
}

extensions/html-language-features/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@
161161
"default": true,
162162
"description": "%html.autoClosingTags%"
163163
},
164+
"html.autoSelectingMatchingTags": {
165+
"type": "boolean",
166+
"scope": "resource",
167+
"default": true,
168+
"description": "%html.autoSelectingMatchingTags%"
169+
},
164170
"html.trace.server": {
165171
"type": "string",
166172
"scope": "window",

extensions/html-language-features/package.nls.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
"html.trace.server.desc": "Traces the communication between VS Code and the HTML language server.",
2525
"html.validate.scripts": "Controls whether the built-in HTML language support validates embedded scripts.",
2626
"html.validate.styles": "Controls whether the built-in HTML language support validates embedded styles.",
27-
"html.autoClosingTags": "Enable/disable autoclosing of HTML tags."
27+
"html.autoClosingTags": "Enable/disable autoclosing of HTML tags.",
28+
"html.autoSelectingMatchingTags": "Enable/disable auto selecting matching HTML tags."
2829
}

extensions/html-language-features/server/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"main": "./out/htmlServerMain",
1111
"dependencies": {
1212
"vscode-css-languageservice": "^4.0.3-next.20",
13-
"vscode-html-languageservice": "^3.0.4-next.9",
13+
"vscode-html-languageservice": "^3.0.4-next.10",
1414
"vscode-languageserver": "^6.0.0-next.3",
1515
"vscode-nls": "^4.1.1",
1616
"vscode-uri": "^2.0.3"

extensions/html-language-features/server/src/htmlServerMain.ts

+19
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import { getDataProviders } from './customData';
2424
namespace TagCloseRequest {
2525
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
2626
}
27+
namespace MatchingTagPositionRequest {
28+
export const type: RequestType<TextDocumentPositionParams, Position | null, any, any> = new RequestType('html/matchingTagPosition');
29+
}
2730

2831
// Create a connection for the server
2932
const connection: IConnection = createConnection();
@@ -485,5 +488,21 @@ connection.onRenameRequest((params, token) => {
485488
}, null, `Error while computing rename for ${params.textDocument.uri}`, token);
486489
});
487490

491+
connection.onRequest(MatchingTagPositionRequest.type, (params, token) => {
492+
return runSafe(() => {
493+
const document = documents.get(params.textDocument.uri);
494+
if (document) {
495+
const pos = params.position;
496+
if (pos.character > 0) {
497+
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
498+
if (mode && mode.findMatchingTagPosition) {
499+
return mode.findMatchingTagPosition(document, pos);
500+
}
501+
}
502+
}
503+
return null;
504+
}, null, `Error while computing matching tag position for ${params.textDocument.uri}`, token);
505+
});
506+
488507
// Listen on the connection
489508
connection.listen();

extensions/html-language-features/server/src/modes/htmlMode.ts

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace:
8181
onDocumentRemoved(document: TextDocument) {
8282
htmlDocuments.onDocumentRemoved(document);
8383
},
84+
findMatchingTagPosition(document: TextDocument, position: Position) {
85+
const htmlDocument = htmlDocuments.get(document);
86+
return htmlLanguageService.findMatchingTagPosition(document, position, htmlDocument);
87+
},
8488
dispose() {
8589
htmlDocuments.dispose();
8690
}

extensions/html-language-features/server/src/modes/languageModes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface LanguageMode {
4848
findDocumentColors?: (document: TextDocument) => ColorInformation[];
4949
getColorPresentations?: (document: TextDocument, color: Color, range: Range) => ColorPresentation[];
5050
doAutoClose?: (document: TextDocument, position: Position) => string | null;
51+
findMatchingTagPosition?: (document: TextDocument, position: Position) => Position | null;
5152
getFoldingRanges?: (document: TextDocument) => FoldingRange[];
5253
onDocumentRemoved(document: TextDocument): void;
5354
dispose(): void;

extensions/html-language-features/server/yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -621,10 +621,10 @@ vscode-css-languageservice@^4.0.3-next.20:
621621
vscode-nls "^4.1.1"
622622
vscode-uri "^2.1.1"
623623

624-
vscode-html-languageservice@^3.0.4-next.9:
625-
version "3.0.4-next.9"
626-
resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.9.tgz#b27f26c29f3af64fa32eabb7425749f95f64036a"
627-
integrity sha512-9V9G7508ybFcn9gQpuucEZIGv8kKlBMEVD8lFFWwWS1yEonKchsxIGJZFbmSGr/n//2anfya8F8yL5ybKuWIRA==
624+
vscode-html-languageservice@^3.0.4-next.10:
625+
version "3.0.4-next.10"
626+
resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.10.tgz#da426326833770c51712abb2c7473b9b30bf1cbc"
627+
integrity sha512-8P0QBtMPJ9nDMhW8MF/z+5JGg6rK6UOa9po18KIleNuV0rDHU9CAqDyUjxW0CEfLrHYz6dQdkW12ZTClvQnNHw==
628628
dependencies:
629629
vscode-languageserver-textdocument "^1.0.0-next.4"
630630
vscode-languageserver-types "^3.15.0-next.6"

0 commit comments

Comments
 (0)