forked from redhat-developer/vscode-xml
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allows editing of start/end tag simultaneously
Under the preference xml.autoSelectingMatchingTags Fixes redhat-developer#130 Signed-off-by: Nikolas Komonen <nikolaskomonen@gmail.com>
- Loading branch information
1 parent
6a7db6e
commit 25e4b93
Showing
3 changed files
with
300 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* 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 previousState = workspace.getConfiguration().get<boolean>(configName); | ||
let wasNotified = false; | ||
let isEnabled = false; | ||
updateEnabledState(); | ||
|
||
workspace.onDidChangeConfiguration(updateEnabledState, null, disposables); | ||
|
||
function updateEnabledState() { | ||
updateStateSetting(); | ||
promptUpdateMessage(); | ||
} | ||
|
||
function updateStateSetting() { | ||
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; | ||
} | ||
|
||
function promptUpdateMessage() { | ||
if(!wasNotified && previousState != isEnabled) { | ||
window.showInformationMessage("Toggled the `xml.autoSelectingMatchingTags` preference in the Workspace settings.") | ||
wasNotified = true; | ||
} | ||
previousState = isEnabled; | ||
} | ||
|
||
let prevCursors: readonly Selection[] = []; | ||
let cursors: readonly Selection[] = []; | ||
let inMirrorMode = false; | ||
|
||
function onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent) { | ||
if (!isEnabled) { | ||
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 charBeforeAndAfterPositionsRoughtlyEqual = isCharBeforeAndAfterPositionsRoughtlyEqual( | ||
event.textEditor.document, | ||
event.selections[0].anchor, | ||
new Position(matchingTagPosition.line, matchingTagPosition.character) | ||
); | ||
|
||
if (charBeforeAndAfterPositionsRoughtlyEqual) { | ||
inMirrorMode = true; | ||
const newCursor = new Selection( | ||
matchingTagPosition.line, | ||
matchingTagPosition.character, | ||
matchingTagPosition.line, | ||
matchingTagPosition.character | ||
); | ||
window.activeTextEditor.selections = [...window.activeTextEditor.selections, newCursor]; | ||
} | ||
} | ||
}).then(undefined, err => { | ||
const msg = err.message ; | ||
// mutes "rejected promise not handled within 1 second" | ||
if (msg && !msg.endsWith('has been cancelled')){ | ||
console.log(err); | ||
} | ||
return; | ||
}); | ||
} | ||
} | ||
|
||
const exitMirrorMode = () => { | ||
inMirrorMode = false; | ||
window.activeTextEditor!.selections = [window.activeTextEditor!.selections[0]]; | ||
}; | ||
|
||
if (cursors.length === 2 && inMirrorMode) { | ||
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 charBeforeAndAfterPositionsRoughtlyEqual = isCharBeforeAndAfterPositionsRoughtlyEqual( | ||
event.textEditor.document, | ||
event.selections[0].anchor, | ||
event.selections[1].anchor | ||
); | ||
|
||
if (!charBeforeAndAfterPositionsRoughtlyEqual) { | ||
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); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
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 consiered equal to handle the case of `<|></|>` | ||
function isCharBeforeAndAfterPositionsRoughtlyEqual(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 === '>' | ||
); | ||
} |