Skip to content

Commit

Permalink
Allows editing of start/end tag simultaneously
Browse files Browse the repository at this point in the history
Under the preference xml.autoSelectingMatchingTags

Fixes redhat-developer#130

Signed-off-by: Nikolas Komonen <nikolaskomonen@gmail.com>
  • Loading branch information
NikolasKomonen committed Dec 9, 2019
1 parent 6a7db6e commit c19e05d
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 3 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@
"description": "Enable/disable autoclosing of XML tags. \n\nIMPORTANT: Turn off editor.autoClosingTags for this to work",
"scope": "window"
},
"xml.autoSelectingMatchingTags": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "Adds an additional cursor on the matching tag, allows for start/end tag editing."
},
"xml.codeLens.enabled": {
"type": "boolean",
"default": false,
Expand Down
22 changes: 19 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { activateTagClosing, AutoCloseResult } from './tagClosing';
import { Commands } from './commands';
import { onConfigurationChange, subscribeJDKChangeConfiguration } from './settings';
import { collectXmlJavaExtensions, onExtensionChange } from './plugin';
import { activateMatchingTagPosition } from './matchingTag';

export interface ScopeInfo {
scope : "default" | "global" | "workspace" | "folder";
Expand All @@ -30,7 +31,9 @@ namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, AutoCloseResult, any, any> = new RequestType('xml/closeTag');
}


namespace MatchingTagPositionRequest {
export const type: RequestType<TextDocumentPositionParams, Position | null, any, any> = new RequestType('xml/matchingTagPosition');
}

export function activate(context: ExtensionContext) {
let storagePath = context.storagePath;
Expand Down Expand Up @@ -114,14 +117,27 @@ export function activate(context: ExtensionContext) {
return text;
};

disposable = activateTagClosing(tagProvider, { xml: true, xsl: true }, Commands.AUTO_CLOSE_TAGS);
toDispose.push(disposable);

//Setup mirrored tag rename request
const matchingTagPositionRequestor = (document: TextDocument, position: Position) => {
let param = languageClient.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
return languageClient.sendRequest(MatchingTagPositionRequest.type, param);
};

disposable = activateMatchingTagPosition(matchingTagPositionRequestor, { xml: true}, 'xml.autoSelectingMatchingTags');
toDispose.push(disposable);

if (extensions.onDidChange) {// Theia doesn't support this API yet
extensions.onDidChange(() => {
onExtensionChange(extensions.all);
});
}

disposable = activateTagClosing(tagProvider, { xml: true, xsl: true }, Commands.AUTO_CLOSE_TAGS);
toDispose.push(disposable);



});
languages.setLanguageConfiguration('xml', getIndentationRules());
languages.setLanguageConfiguration('xsl', getIndentationRules());
Expand Down
147 changes: 147 additions & 0 deletions src/matchingTag.ts
Original file line number Diff line number Diff line change
@@ -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<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 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 <div |></div |>
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))
);
}

0 comments on commit c19e05d

Please sign in to comment.