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