-
-
Notifications
You must be signed in to change notification settings - Fork 67
/
Copy pathsearch-model.js
279 lines (185 loc) · 8.95 KB
/
search-model.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
const { getActiveEditor, getActiveEditors } = require('./utils/editor-finders');
const EarlyTerminationSignal = require('./errors/early-termination-signal');
const getNonWordCharacters = require('./utils/non-word-characters');
const isWordSelected = require('./utils/is-word-selected');
const escapeRegExp = require('./utils/escape-reg-exp');
const { config } = atom;
module.exports = class SearchModel {
static hideHighlightOnSelectedWord ( range , selections ){
if( ! config.get('highlight-selected.hideHighlightOnSelectedWord') )
return false;
let outcome = false;
for( let i = 0 ; i < selections.length ; i += 1 ){
const selection = selections[i];
const selectionRange = selection.getBufferRange();
outcome =
range.start.column === selectionRange.start.column &&
range.start.row === selectionRange.start.row &&
range.end.column === selectionRange.end.column &&
range.end.row === selectionRange.end.row;
if( outcome )
break;
}
return outcome
}
static makeClasses (){
let className = 'highlight-selected';
if( config.get('highlight-selected.lightTheme') )
className += ' light-theme';
if( config.get('highlight-selected.highlightBackground') )
className += ' background';
return className
}
// This functions replicates `\bTEXT\b` regex with allowed non-word characters to be something
// like: `(^|[ @$#\.])TEXT([ @$#\.]|$)`. This allows unicode characters to be highlighted. Also,
// it allows characters such as `@` in Ruby to selected and highlighted.
// This is the first part of highlighting the whole words. We need to run another regex later to
// ensure we only highlight the selection from the Atom Editor.
static updateRegexSearchForWholeWords (
regexSearch , editor , lastSelection ,
regexFlags , text ){
if( ! config.get('highlight-selected.onlyHighlightWholeWords') )
return regexSearch
if( ! isWordSelected(editor,lastSelection))
return regexSearch
const selectionStart = lastSelection
.getBufferRange().start;
const nonWordCharacters =
getNonWordCharacters(editor,selectionStart);
const allowedCharactersToSelect = config
.get('highlight-selected.allowedCharactersToSelect');
const nonWordCharactersToStrip = nonWordCharacters
.replace(new RegExp(`[${allowedCharactersToSelect}]`,'g'),'');
const regexForWholeWord =
new RegExp(`[ \\t${escapeRegExp(nonWordCharactersToStrip)}]`,regexFlags);
if( regexForWholeWord.test(text) )
return regexSearch
return `(?:[ \\t${
escapeRegExp(nonWordCharacters)
}]|^)(${
regexSearch
})(?=[ \\t${
escapeRegExp(nonWordCharacters)
}]|$)`;
}
constructor ( selectionManager ){
this.selectionManager = selectionManager;
}
handleSelection (){
const editor = getActiveEditor();
if( ! editor )
return
this.selectionManager.removeAllMarkers();
if( this.selectionManager.disabled )
return;
if( editor.getLastSelection().isEmpty() )
return;
this.selections = editor
.getSelections();
const lastSelection = editor
.getLastSelection();
const text = lastSelection
.getText();
if( text.length < config.get('highlight-selected.minimumLength') )
return
if( text.includes('\n') )
return
const regex = new RegExp('^\\s+$');
if( regex.test(text) )
return
let regexFlags = 'g';
if( config.get('highlight-selected.ignoreCase') )
regexFlags += 'i';
const regexSearch = SearchModel
.updateRegexSearchForWholeWords(
escapeRegExp(text) ,
editor ,
lastSelection ,
regexFlags ,
text
);
this.selectionManager.resultCount = 0;
if( config.get('highlight-selected.highlightInPanes') ){
const originalEditor = editor;
for ( const editor of getActiveEditors() )
this.highlightSelectionInEditor( editor , regexSearch , regexFlags , originalEditor );
} else {
this.highlightSelectionInEditor(editor,regexSearch,regexFlags);
}
this.selectionManager.emitter.emit('did-finish-adding-markers');
}
highlightSelectionInEditor ( editor , regexSearch , regexFlags , originalEditor ){
const maximumHighlights = atom.config
.get('highlight-selected.maximumHighlights');
if( this.selectionManager.resultCount > maximumHighlights )
return
const markerLayers = this.selectionManager
.editorToMarkerLayerMap[editor.id];
if( ! markerLayers )
return
const markerLayer = markerLayers.visibleMarkerLayer;
const markerLayerForHiddenMarkers = markerLayers.selectedMarkerLayer;
// We should have a marker layers. If not run away.
if( ! markerLayer )
return
if( ! markerLayerForHiddenMarkers )
return
// HACK: `editor.scan` is a synchronous process which iterates the entire buffer,
// executing a regex against every line and yielding each match. This can be
// costly for very large files with many matches.
//
// While we can and do limit the maximum number of highlight markers,
// `editor.scan` cannot be terminated early, meaning that we are forced to
// pay the cost of iterating every line in the file, running the regex, and
// returning matches, even if we shouldn't be creating any more markers.
//
// Instead, throw an exception. This isn't pretty, but it prevents the
// scan from running to completion unnecessarily.
try {
editor.scan(new RegExp(regexSearch,regexFlags),(result) => {
if( this.selectionManager.resultCount >= maximumHighlights )
throw new EarlyTerminationSignal
let newResult = result;
// The the following check allows the selection from the Atom Editor to have the marker on
// it. If we do not redo the regex and update the found match, we will add a marker around
// all non-word characters, rather than the allowed non-word characters.
if( config.get('highlight-selected.onlyHighlightWholeWords') )
editor.scanInBufferRange(new RegExp(escapeRegExp(result.match[1])), result.range, (e) => newResult = e);
if( ! newResult )
return
this.selectionManager.resultCount += 1;
const hideHighlight = SearchModel
.hideHighlightOnSelectedWord( newResult.range , this.selections );
// If we want to hide the highlight on the selected word, we will add it to a different
// marker layer. The hidden marker layer is used for by the `scroll-marker` API to show
// matches. We do not tell the editor to decorate this marker layer. We also use fire
// different events. This is so other packages and render them differently if they want.
if( hideHighlight && originalEditor?.id === editor.id ){
const marker = markerLayerForHiddenMarkers.markBufferRange(newResult.range);
this.selectionManager.emitter.emit('did-add-selected-marker', marker);
this.selectionManager.emitter.emit('did-add-selected-marker-for-editor',{ marker , editor });
} else {
const marker = markerLayer.markBufferRange(newResult.range);
this.selectionManager.emitter.emit('did-add-marker', marker);
this.selectionManager.emitter.emit('did-add-marker-for-editor',{ marker , editor });
}
});
} catch ( error ){
if( error instanceof EarlyTerminationSignal ){
// If this is an early termination, just continue on.
} else
if( error.message === 'regular expression is too large' ){
// User has done a huge selection which exceed regex limit.
// The user is most probably about to take action on the huge selection
// by pressing ctrl-c or backspace … the user currently does not care about highlighting.
// Therefore just continue on.
} else {
throw error;
}
}
editor.decorateMarkerLayer(markerLayer,{
class : SearchModel.makeClasses() ,
type : 'highlight'
})
}
};