mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
HTML implementation for #88424
This commit is contained in:
parent
f76ca9f6cc
commit
01e01b13f8
|
@ -21,13 +21,12 @@ import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
|
||||||
import { activateTagClosing } from './tagClosing';
|
import { activateTagClosing } from './tagClosing';
|
||||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||||
import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData';
|
import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData';
|
||||||
import { activateMirrorCursor } from './mirrorCursor';
|
|
||||||
|
|
||||||
namespace TagCloseRequest {
|
namespace TagCloseRequest {
|
||||||
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
|
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
|
||||||
}
|
}
|
||||||
namespace MatchingTagPositionRequest {
|
namespace OnTypeRenameRequest {
|
||||||
export const type: RequestType<TextDocumentPositionParams, Position | null, any, any> = new RequestType('html/matchingTagPosition');
|
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
|
||||||
}
|
}
|
||||||
|
|
||||||
// experimental: semantic tokens
|
// experimental: semantic tokens
|
||||||
|
@ -131,14 +130,6 @@ export function activate(context: ExtensionContext) {
|
||||||
disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true }, 'html.autoClosingTags');
|
disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true }, 'html.autoClosingTags');
|
||||||
toDispose.push(disposable);
|
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 => {
|
disposable = client.onTelemetry(e => {
|
||||||
if (telemetryReporter) {
|
if (telemetryReporter) {
|
||||||
telemetryReporter.sendTelemetryEvent(e.key, e.data);
|
telemetryReporter.sendTelemetryEvent(e.key, e.data);
|
||||||
|
@ -289,6 +280,15 @@ export function activate(context: ExtensionContext) {
|
||||||
return results;
|
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 {
|
function getPackageInfo(context: ExtensionContext): IPackageInfo | 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<Position | null>,
|
|
||||||
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<boolean>(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 <div |></div |>
|
|
||||||
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
|
|
||||||
* |<div>
|
|
||||||
* |</div>
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
charBeforePrimarySelection === ' ' &&
|
|
||||||
charBeforeSecondarySelection === ' ' &&
|
|
||||||
charAfterPrimarySelection === '<' &&
|
|
||||||
charAfterSecondarySelection === '<'
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Special case for exiting
|
|
||||||
* | <div>
|
|
||||||
* | </div>
|
|
||||||
*/
|
|
||||||
if (charBeforePrimarySelection === '\n' && charBeforeSecondarySelection === '\n') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Special case for exiting
|
|
||||||
* <div>|
|
|
||||||
* </div>|
|
|
||||||
*/
|
|
||||||
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 <div |></div |>
|
|
||||||
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
|
|
||||||
* <div |></div >
|
|
||||||
* <div | id="a"></div >
|
|
||||||
* Before 1st cursor: ` `
|
|
||||||
* After 1st cursor: `>` or ` `
|
|
||||||
* Before 2nd cursor: ` `
|
|
||||||
* After 2nd cursor: `>`
|
|
||||||
*/
|
|
||||||
return (
|
|
||||||
primaryBeforeSecondary &&
|
|
||||||
charBeforePrimarySelection === ' ' &&
|
|
||||||
(charAfterPrimarySelection === '>' || charAfterPrimarySelection === ' ') &&
|
|
||||||
charBeforeSecondarySelection === ' ' &&
|
|
||||||
charAfterSecondarySelection === '>'
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -165,7 +165,8 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"scope": "resource",
|
"scope": "resource",
|
||||||
"default": false,
|
"default": false,
|
||||||
"description": "%html.mirrorCursorOnMatchingTag%"
|
"description": "%html.mirrorCursorOnMatchingTag%",
|
||||||
|
"deprecationMessage": "%html.mirrorCursorOnMatchingTagDeprecationMessage%"
|
||||||
},
|
},
|
||||||
"html.trace.server": {
|
"html.trace.server": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -25,5 +25,6 @@
|
||||||
"html.validate.scripts": "Controls whether the built-in HTML language support validates embedded scripts.",
|
"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.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.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`"
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,8 @@ import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanti
|
||||||
namespace TagCloseRequest {
|
namespace TagCloseRequest {
|
||||||
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
|
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
|
||||||
}
|
}
|
||||||
namespace MatchingTagPositionRequest {
|
namespace OnTypeRenameRequest {
|
||||||
export const type: RequestType<TextDocumentPositionParams, Position | null, any, any> = new RequestType('html/matchingTagPosition');
|
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
|
||||||
}
|
}
|
||||||
|
|
||||||
// experimental: semantic tokens
|
// experimental: semantic tokens
|
||||||
|
@ -499,20 +499,20 @@ connection.onRenameRequest((params, token) => {
|
||||||
}, null, `Error while computing rename for ${params.textDocument.uri}`, 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(() => {
|
return runSafe(() => {
|
||||||
const document = documents.get(params.textDocument.uri);
|
const document = documents.get(params.textDocument.uri);
|
||||||
if (document) {
|
if (document) {
|
||||||
const pos = params.position;
|
const pos = params.position;
|
||||||
if (pos.character > 0) {
|
if (pos.character > 0) {
|
||||||
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
||||||
if (mode && mode.findMatchingTagPosition) {
|
if (mode && mode.doOnTypeRename) {
|
||||||
return mode.findMatchingTagPosition(document, pos);
|
return mode.doOnTypeRename(document, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
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;
|
let semanticTokensProvider: SemanticTokenProvider | undefined;
|
||||||
|
|
|
@ -85,6 +85,10 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace:
|
||||||
const htmlDocument = htmlDocuments.get(document);
|
const htmlDocument = htmlDocuments.get(document);
|
||||||
return htmlLanguageService.findMatchingTagPosition(document, position, htmlDocument);
|
return htmlLanguageService.findMatchingTagPosition(document, position, htmlDocument);
|
||||||
},
|
},
|
||||||
|
doOnTypeRename(document: TextDocument, position: Position) {
|
||||||
|
const htmlDocument = htmlDocuments.get(document);
|
||||||
|
return htmlLanguageService.findSyncedRegions(document, position, htmlDocument);
|
||||||
|
},
|
||||||
dispose() {
|
dispose() {
|
||||||
htmlDocuments.dispose();
|
htmlDocuments.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ export interface LanguageMode {
|
||||||
doHover?: (document: TextDocument, position: Position) => Hover | null;
|
doHover?: (document: TextDocument, position: Position) => Hover | null;
|
||||||
doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null;
|
doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null;
|
||||||
doRename?: (document: TextDocument, position: Position, newName: string) => WorkspaceEdit | null;
|
doRename?: (document: TextDocument, position: Position, newName: string) => WorkspaceEdit | null;
|
||||||
|
doOnTypeRename?: (document: TextDocument, position: Position) => Range[] | null;
|
||||||
findDocumentHighlight?: (document: TextDocument, position: Position) => DocumentHighlight[];
|
findDocumentHighlight?: (document: TextDocument, position: Position) => DocumentHighlight[];
|
||||||
findDocumentSymbols?: (document: TextDocument) => SymbolInformation[];
|
findDocumentSymbols?: (document: TextDocument) => SymbolInformation[];
|
||||||
findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => DocumentLink[];
|
findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => DocumentLink[];
|
||||||
|
|
Loading…
Reference in a new issue