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+ }
0 commit comments