@jupyterlab/codemirror 4.0.0-alpha.11 → 4.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,44 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
+ /*
4
+ Parts of the implementation of the search in this file were derived from
5
+ CodeMirror's search at:
6
+ https://github.com/codemirror/CodeMirror/blob/c2676685866c571a1c9c82cb25018cc08b4d42b2/addon/search/search.js
7
+ which is licensed with the following license:
8
+
9
+ MIT License
10
+
11
+ Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in
21
+ all copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
+ THE SOFTWARE.
30
+ */
3
31
  import { TextSearchEngine } from '@jupyterlab/documentsearch';
4
32
  import { JSONExt } from '@lumino/coreutils';
5
33
  import { Signal } from '@lumino/signaling';
6
- import * as CodeMirror from 'codemirror';
34
+ import { RegExpCursor } from '@codemirror/search';
35
+ import { Compartment, StateEffect, StateField } from '@codemirror/state';
36
+ import { Decoration, EditorView } from '@codemirror/view';
7
37
  /**
8
38
  * CodeMirror search provider for file editor
9
39
  */
10
40
  export class CodeMirrorSearchProvider {
11
- constructor() {
41
+ constructor(editor) {
12
42
  /**
13
43
  * Set to true if the widget under search is read-only, false
14
44
  * if it is editable. Will be used to determine whether to show
@@ -18,6 +48,41 @@ export class CodeMirrorSearchProvider {
18
48
  this._matchState = {};
19
49
  this._changed = new Signal(this);
20
50
  this._disposed = false;
51
+ this.editor = editor;
52
+ this._highlightEffect = StateEffect.define({
53
+ map: (value, mapping) => ({
54
+ matches: value.matches.map(v => ({
55
+ text: v.text,
56
+ position: mapping.mapPos(v.position)
57
+ }))
58
+ })
59
+ });
60
+ this._highlightMark = Decoration.mark({ class: 'cm-searching' });
61
+ this._highlightField = StateField.define({
62
+ create: () => {
63
+ return Decoration.none;
64
+ },
65
+ update: (highlights, transaction) => {
66
+ highlights = highlights.map(transaction.changes);
67
+ for (let ef of transaction.effects) {
68
+ if (ef.is(this._highlightEffect)) {
69
+ const e = ef;
70
+ if (e.value.matches.length) {
71
+ highlights = highlights.update({
72
+ add: e.value.matches.map(m => this._highlightMark.range(m.position, m.position + m.text.length))
73
+ });
74
+ }
75
+ else {
76
+ highlights = Decoration.none;
77
+ }
78
+ }
79
+ }
80
+ return highlights;
81
+ },
82
+ provide: f => EditorView.decorations.from(f)
83
+ });
84
+ this._listener = new Compartment();
85
+ this.editor.injectExtension(this._listener.of([]));
21
86
  }
22
87
  /**
23
88
  * Whether the search provider is disposed or not.
@@ -56,11 +121,17 @@ export class CodeMirrorSearchProvider {
56
121
  // no point in removing overlay in the middle of the search
57
122
  await this.endQuery(false);
58
123
  this._query = query;
59
- CodeMirror.on(this.editor.doc, 'change', this._onDocChanged.bind(this));
124
+ this.editor.editor.dispatch({
125
+ effects: this._listener.reconfigure(EditorView.updateListener.of((v) => {
126
+ if (v.docChanged) {
127
+ this._onDocChanged(v);
128
+ }
129
+ }))
130
+ });
131
+ await this._setInitialMatches(query);
60
132
  if (refreshOverlay) {
61
133
  this._refreshOverlay();
62
134
  }
63
- await this._setInitialMatches(query);
64
135
  const matches = this._parseMatchesFromState();
65
136
  if (matches.length === 0) {
66
137
  return Promise.resolve();
@@ -69,6 +140,7 @@ export class CodeMirrorSearchProvider {
69
140
  const match = cursorMatch &&
70
141
  this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
71
142
  this._currentMatch = match;
143
+ await this._highlightCurrentMatch(cursorMatch);
72
144
  return Promise.resolve();
73
145
  }
74
146
  /**
@@ -80,20 +152,19 @@ export class CodeMirrorSearchProvider {
80
152
  async endQuery(removeOverlay = true) {
81
153
  this._matchState = {};
82
154
  this._currentMatch = null;
83
- if (removeOverlay) {
84
- this.editor.removeOverlay(this._overlay);
85
- }
86
- const from = this.editor.getCursor('from');
87
- const to = this.editor.getCursor('to');
155
+ const from = this.editor.getPositionAt(this.editor.state.selection.main.from);
156
+ const to = this.editor.getPositionAt(this.editor.state.selection.main.to);
88
157
  // Setting a reverse selection to allow search-as-you-type to maintain the
89
158
  // current selected match. See comment in _findNext for more details.
90
159
  if (from !== to) {
91
160
  this.editor.setSelection({
92
- start: this._toEditorPos(to),
93
- end: this._toEditorPos(from)
161
+ start: to,
162
+ end: from
94
163
  });
95
164
  }
96
- CodeMirror.off(this.editor.doc, 'change', this._onDocChanged.bind(this));
165
+ this.editor.editor.dispatch({
166
+ effects: this._listener.reconfigure([])
167
+ });
97
168
  }
98
169
  /**
99
170
  * Clear currently highlighted match.
@@ -113,13 +184,7 @@ export class CodeMirrorSearchProvider {
113
184
  * @returns A promise that resolves once the action has completed.
114
185
  */
115
186
  async highlightNext(loop) {
116
- const cursorMatch = this._findNext(false);
117
- if (!cursorMatch) {
118
- return;
119
- }
120
- const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
121
- this._currentMatch = match;
122
- return match;
187
+ return this._highlightCurrentMatch(this._findNext(false));
123
188
  }
124
189
  /**
125
190
  * Move the current match indicator to the previous match.
@@ -129,12 +194,25 @@ export class CodeMirrorSearchProvider {
129
194
  * @returns A promise that resolves once the action has completed.
130
195
  */
131
196
  async highlightPrevious(loop) {
132
- const cursorMatch = this._findNext(true);
197
+ return this._highlightCurrentMatch(this._findNext(true));
198
+ }
199
+ async _highlightCurrentMatch(cursorMatch) {
200
+ // Highlight the current index
133
201
  if (!cursorMatch) {
202
+ // Set cursor to remove any selection
203
+ this.editor.editor.dispatch({ selection: { anchor: 0 } });
134
204
  return;
135
205
  }
136
206
  const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
137
207
  this._currentMatch = match;
208
+ this.editor.editor.focus();
209
+ this.editor.editor.dispatch({
210
+ selection: {
211
+ anchor: match.position,
212
+ head: match.position + match.text.length
213
+ },
214
+ scrollIntoView: true
215
+ });
138
216
  return match;
139
217
  }
140
218
  /**
@@ -150,12 +228,15 @@ export class CodeMirrorSearchProvider {
150
228
  // replace it. Otherwise, just select the next match after the cursor.
151
229
  let replaceOccurred = false;
152
230
  if (this._currentMatchIsSelected()) {
153
- const cursor = this.editor.getSearchCursor(this._query, this.editor.getCursor('from'), !this._query.ignoreCase);
154
- if (!cursor.findNext()) {
231
+ const cursor = new RegExpCursor(this.editor.doc, this._query.source, { ignoreCase: !this._query.ignoreCase }, this.editor.state.selection.main.from).next();
232
+ if (cursor.done) {
155
233
  return replaceOccurred;
156
234
  }
235
+ const value = cursor.value;
157
236
  replaceOccurred = true;
158
- cursor.replace(newText);
237
+ this.editor.editor.dispatch({
238
+ changes: { from: value.from, to: value.to, insert: newText }
239
+ });
159
240
  }
160
241
  await this.highlightNext();
161
242
  return replaceOccurred;
@@ -170,16 +251,23 @@ export class CodeMirrorSearchProvider {
170
251
  async replaceAllMatches(newText) {
171
252
  let replaceOccurred = false;
172
253
  return new Promise((resolve, _) => {
173
- this.editor.operation(() => {
174
- const cursor = this.editor.getSearchCursor(this._query, undefined, !this._query.ignoreCase);
175
- while (cursor.findNext()) {
176
- replaceOccurred = true;
177
- cursor.replace(newText);
178
- }
179
- this._matchState = {};
180
- this._currentMatch = null;
181
- resolve(replaceOccurred);
182
- });
254
+ let cursor = new RegExpCursor(this.editor.doc, this._query.source, {
255
+ ignoreCase: !this._query.ignoreCase
256
+ }).next();
257
+ let changeSpec = [];
258
+ while (!cursor.done) {
259
+ replaceOccurred = true;
260
+ changeSpec.push({
261
+ from: cursor.value.from,
262
+ to: cursor.value.to,
263
+ insert: newText
264
+ });
265
+ cursor = cursor.next();
266
+ }
267
+ this.editor.editor.dispatch({ changes: changeSpec });
268
+ this._matchState = {};
269
+ this._currentMatch = null;
270
+ resolve(replaceOccurred);
183
271
  });
184
272
  }
185
273
  /**
@@ -220,11 +308,9 @@ export class CodeMirrorSearchProvider {
220
308
  // TODO make it more efficient
221
309
  return this.matches.indexOf(this._currentMatch);
222
310
  }
223
- _onDocChanged(_, changeObj) {
224
- var _a, _b;
225
- // If we get newlines added/removed, the line numbers across the
226
- // match state are all shifted, so here we need to recalculate it
227
- if (changeObj.text.length > 1 || ((_b = (_a = changeObj.removed) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 1) {
311
+ _onDocChanged(update) {
312
+ // TODO: CM6 migration
313
+ if (update.changes.length) {
228
314
  this._setInitialMatches(this._query)
229
315
  .then(() => {
230
316
  this._changed.emit(undefined);
@@ -235,13 +321,14 @@ export class CodeMirrorSearchProvider {
235
321
  }
236
322
  }
237
323
  _refreshOverlay() {
238
- this.editor.operation(() => {
239
- // clear search first
240
- this.editor.removeOverlay(this._overlay);
241
- this._overlay = this._getSearchOverlay();
242
- this.editor.addOverlay(this._overlay);
243
- this._changed.emit(undefined);
244
- });
324
+ let effects = [
325
+ this._highlightEffect.of({ matches: this.matches })
326
+ ];
327
+ if (!this.editor.state.field(this._highlightField, false)) {
328
+ effects.push(StateEffect.appendConfig.of([this._highlightField]));
329
+ }
330
+ this.editor.editor.dispatch({ effects });
331
+ this._changed.emit(undefined);
245
332
  }
246
333
  /**
247
334
  * Do a full search on the entire document.
@@ -258,125 +345,52 @@ export class CodeMirrorSearchProvider {
258
345
  */
259
346
  async _setInitialMatches(query) {
260
347
  this._matchState = {};
261
- const content = this.editor.doc.getValue();
348
+ const content = this.editor.doc.toString();
262
349
  const matches = await TextSearchEngine.search(query, content);
263
350
  matches.forEach(match => {
264
- const { line, ch } = this.editor.doc.posFromIndex(match.position);
351
+ const { line, ch } = this._getPosition(match.position);
265
352
  if (!this._matchState[line]) {
266
353
  this._matchState[line] = {};
267
354
  }
268
355
  this._matchState[line][ch] = match;
269
356
  });
270
357
  }
271
- _getSearchOverlay() {
272
- return {
273
- /**
274
- * Token function is called when a line needs to be processed -
275
- * when the overlay is initially created, it's called on all lines;
276
- * when a line is modified and needs to be re-evaluated, it's called
277
- * on just that line.
278
- *
279
- * This implementation of the token function both constructs/maintains
280
- * the overlay and keeps track of the match state as the document is
281
- * updated while a search is active.
282
- */
283
- token: (stream) => {
284
- const currentPos = stream.pos;
285
- this._query.lastIndex = currentPos;
286
- const lineText = stream.string;
287
- const match = this._query.exec(lineText);
288
- const line = stream.lineOracle.line;
289
- // If starting at position 0, the tokenization of this line has just started.
290
- // Blow away everything on this line in the state so it can be updated.
291
- if (stream.start === currentPos &&
292
- currentPos === 0 &&
293
- !!this._matchState[line]) {
294
- this._matchState[line] = {};
295
- }
296
- if (match && match.index === currentPos) {
297
- // found match, add it to state
298
- const matchLength = match[0].length;
299
- const matchObj = {
300
- text: lineText.slice(currentPos, currentPos + matchLength),
301
- position: this.editor.doc.indexFromPos({
302
- line,
303
- ch: currentPos
304
- })
305
- };
306
- if (!this._matchState[line]) {
307
- this._matchState[line] = {};
308
- }
309
- this._matchState[line][currentPos] = matchObj;
310
- // move the stream along and return searching style for the token
311
- stream.pos += matchLength || 1;
312
- // if the last thing on the line was a match, make sure we still
313
- // emit the changed signal so the display can pick up the updates
314
- if (stream.eol()) {
315
- this._changed.emit(undefined);
316
- }
317
- return 'searching';
318
- }
319
- else if (match) {
320
- // there's a match in the stream, advance the stream to its position
321
- stream.pos = match.index;
322
- }
323
- else {
324
- // no matches, consume the rest of the stream
325
- this._changed.emit(undefined);
326
- stream.skipToEnd();
327
- }
358
+ _findNext(reverse) {
359
+ const caseSensitive = this._query.ignoreCase;
360
+ const state = this.editor.state;
361
+ // In order to support search-as-you-type, we needed a way to allow the first
362
+ // match to be selected when a search is started, but prevent the selected
363
+ // search to move for each new keypress. To do this, when a search is ended,
364
+ // the cursor is reversed, putting the head at the 'from' position. When a new
365
+ // search is started, the cursor we want is at the 'from' position, so that the same
366
+ // match is selected when the next key is entered (if it is still a match).
367
+ //
368
+ // When toggling through a search normally, the cursor is always set in the forward
369
+ // direction, so head is always at the 'to' position. That way, if reverse = false,
370
+ // the search proceeds from the 'to' position during normal toggling. If reverse = true,
371
+ // the search always proceeds from the 'anchor' position, which is at the 'from'.
372
+ const lastPosition = reverse
373
+ ? state.selection.main.anchor
374
+ : state.selection.main.head;
375
+ let cursor = new RegExpCursor(this.editor.doc, this._query.source, { ignoreCase: !caseSensitive }, lastPosition).next();
376
+ if (cursor.done) {
377
+ const startOrEnd = reverse ? this.editor.doc.length : 0;
378
+ cursor = new RegExpCursor(this.editor.doc, this._query.source, { ignoreCase: !caseSensitive }, startOrEnd).next();
379
+ if (cursor.done) {
380
+ return null;
328
381
  }
382
+ }
383
+ return {
384
+ from: this._getPosition(cursor.value.from),
385
+ to: this._getPosition(cursor.value.to)
329
386
  };
330
387
  }
331
- _findNext(reverse) {
332
- return this.editor.operation(() => {
333
- const caseSensitive = this._query.ignoreCase;
334
- // In order to support search-as-you-type, we needed a way to allow the first
335
- // match to be selected when a search is started, but prevent the selected
336
- // search to move for each new keypress. To do this, when a search is ended,
337
- // the cursor is reversed, putting the head at the 'from' position. When a new
338
- // search is started, the cursor we want is at the 'from' position, so that the same
339
- // match is selected when the next key is entered (if it is still a match).
340
- //
341
- // When toggling through a search normally, the cursor is always set in the forward
342
- // direction, so head is always at the 'to' position. That way, if reverse = false,
343
- // the search proceeds from the 'to' position during normal toggling. If reverse = true,
344
- // the search always proceeds from the 'anchor' position, which is at the 'from'.
345
- const cursorToGet = reverse ? 'anchor' : 'head';
346
- const lastPosition = this.editor.getCursor(cursorToGet);
347
- let cursor = this.editor.getSearchCursor(this._query, lastPosition, !caseSensitive);
348
- if (!cursor.find(reverse)) {
349
- // if we do want to loop, try searching from the bottom/top
350
- const startOrEnd = reverse
351
- ? CodeMirror.Pos(this.editor.lastLine())
352
- : CodeMirror.Pos(this.editor.firstLine(), 0);
353
- cursor = this.editor.getSearchCursor(this._query, startOrEnd, !caseSensitive);
354
- if (!cursor.find(reverse)) {
355
- return null;
356
- }
357
- }
358
- const fromPos = cursor.from();
359
- const toPos = cursor.to();
360
- const selRange = {
361
- start: {
362
- line: fromPos.line,
363
- column: fromPos.ch
364
- },
365
- end: {
366
- line: toPos.line,
367
- column: toPos.ch
368
- }
369
- };
370
- this.editor.setSelection(selRange);
371
- this.editor.scrollIntoView({
372
- from: fromPos,
373
- to: toPos
374
- }, 100);
375
- return {
376
- from: fromPos,
377
- to: toPos
378
- };
379
- });
388
+ // TODO: remove this when Position API are removed and we work with
389
+ // offsets only. Contrary to the functions in CodeMirrorEditor,
390
+ // these functions work with CM6 position (i.e. line number starting at 1).
391
+ _getPosition(offset) {
392
+ const line = this.editor.doc.lineAt(offset);
393
+ return { line: line.number, ch: offset - line.from };
380
394
  }
381
395
  _parseMatchesFromState() {
382
396
  // Flatten state map
@@ -389,12 +403,6 @@ export class CodeMirrorSearchProvider {
389
403
  }
390
404
  return matches;
391
405
  }
392
- _toEditorPos(posIn) {
393
- return {
394
- line: posIn.line,
395
- column: posIn.ch
396
- };
397
- }
398
406
  _currentMatchIsSelected() {
399
407
  if (!this._currentMatch) {
400
408
  return false;
@@ -405,10 +413,8 @@ export class CodeMirrorSearchProvider {
405
413
  return (selectionIsOneLine &&
406
414
  this._currentMatch.text.length === currentSelectionLength &&
407
415
  this._currentMatch.position ===
408
- this.editor.doc.indexFromPos({
409
- line: currentSelection.start.line,
410
- ch: currentSelection.start.column
411
- }));
416
+ this.editor.doc.line(currentSelection.start.line).from +
417
+ currentSelection.start.column);
412
418
  }
413
419
  }
414
420
  /**
@@ -427,6 +433,38 @@ export class CodeMirrorSearchHighlighter {
427
433
  this._cm = editor;
428
434
  this._matches = new Array();
429
435
  this._currentIndex = null;
436
+ this._highlightEffect = StateEffect.define({
437
+ map: (value, mapping) => ({
438
+ matches: value.matches.map(v => ({
439
+ text: v.text,
440
+ position: mapping.mapPos(v.position)
441
+ }))
442
+ })
443
+ });
444
+ this._highlightMark = Decoration.mark({ class: 'cm-searching' });
445
+ this._highlightField = StateField.define({
446
+ create: () => {
447
+ return Decoration.none;
448
+ },
449
+ update: (highlights, transaction) => {
450
+ highlights = highlights.map(transaction.changes);
451
+ for (let ef of transaction.effects) {
452
+ if (ef.is(this._highlightEffect)) {
453
+ const e = ef;
454
+ if (e.value.matches.length) {
455
+ highlights = highlights.update({
456
+ add: e.value.matches.map(m => this._highlightMark.range(m.position, m.position + m.text.length))
457
+ });
458
+ }
459
+ else {
460
+ highlights = Decoration.none;
461
+ }
462
+ }
463
+ }
464
+ return highlights;
465
+ },
466
+ provide: f => EditorView.decorations.from(f)
467
+ });
430
468
  }
431
469
  /**
432
470
  * The current index of the selected match.
@@ -444,7 +482,7 @@ export class CodeMirrorSearchHighlighter {
444
482
  if (!JSONExt.deepEqual(this._matches, v)) {
445
483
  this._matches = v;
446
484
  }
447
- this.refresh();
485
+ this._refresh();
448
486
  }
449
487
  /**
450
488
  * Clear all highlighted matches
@@ -453,29 +491,22 @@ export class CodeMirrorSearchHighlighter {
453
491
  this._currentIndex = null;
454
492
  this._highlightCurrentMatch();
455
493
  }
456
- /**
457
- * Refresh the highlight matches overlay
458
- */
459
- refresh() {
460
- this._refreshOverlay();
461
- }
462
494
  /**
463
495
  * Clear the highlighted matches.
464
496
  */
465
497
  endQuery() {
466
498
  this._currentIndex = null;
467
499
  this._matches = [];
468
- this._cm.removeOverlay('jp-searching');
469
- this._overlay = null;
470
- const from = this._cm.getCursor('from');
471
- const to = this._cm.getCursor('to');
500
+ this._cm.editor.dispatch({
501
+ effects: this._highlightEffect.of({ matches: [] })
502
+ });
503
+ const selection = this._cm.state.selection.main;
504
+ const from = selection.from;
505
+ const to = selection.to;
472
506
  // Setting a reverse selection to allow search-as-you-type to maintain the
473
507
  // current selected match. See comment in _findNext for more details.
474
508
  if (from !== to) {
475
- this._cm.setSelection({
476
- start: this._toEditorPos(to),
477
- end: this._toEditorPos(from)
478
- });
509
+ this._cm.editor.dispatch({ selection: { anchor: to, head: from } });
479
510
  }
480
511
  return Promise.resolve();
481
512
  }
@@ -507,83 +538,28 @@ export class CodeMirrorSearchHighlighter {
507
538
  // Highlight the current index
508
539
  if (this._currentIndex !== null) {
509
540
  const match = this.matches[this._currentIndex];
510
- this._cm.operation(() => {
511
- const start = this._cm.doc.posFromIndex(match.position);
512
- const from = {
513
- line: start.line,
514
- column: start.ch
515
- };
516
- const to = {
517
- // Matches is on the same line
518
- line: start.line,
519
- column: start.ch + match.text.length
520
- };
521
- // No need to scroll into view this is the default behavior
522
- this._cm.setSelection({
523
- start: from,
524
- end: to
525
- });
541
+ this._cm.editor.focus();
542
+ this._cm.editor.dispatch({
543
+ selection: {
544
+ anchor: match.position,
545
+ head: match.position + match.text.length
546
+ },
547
+ scrollIntoView: true
526
548
  });
527
549
  }
528
550
  else {
529
551
  // Set cursor to remove any selection
530
- this._cm.setCursorPosition({ line: 0, column: 0 });
552
+ this._cm.editor.dispatch({ selection: { anchor: 0 } });
531
553
  }
532
554
  }
533
- _refreshOverlay() {
534
- // clear search first
535
- this._cm.removeOverlay('jp-searching');
536
- this._overlay = this._getSearchOverlay();
537
- this._cm.addOverlay(this._overlay);
538
- }
539
- _getSearchOverlay() {
540
- const token = (stream) => {
541
- const position = this._cm.doc.indexFromPos({
542
- line: stream.lineOracle.line,
543
- ch: stream.pos
544
- });
545
- let found = this._matches.length > 0
546
- ? Utils.findNext(this._matches, position, 0, // lastMatchIndex,
547
- this._matches.length - 1)
548
- : null;
549
- if (found !== null) {
550
- // lastMatchIndex = found;
551
- const match = this._matches[found];
552
- if (match.position > position + stream.string.length) {
553
- // next match not in this stream, consume the rest of the stream
554
- stream.skipToEnd();
555
- return null;
556
- }
557
- if (position === match.position) {
558
- // move the stream along and return searching style for the token
559
- stream.pos += match.text.length || 1;
560
- return 'searching';
561
- }
562
- else {
563
- // Move to the next match
564
- stream.pos += match.position - position;
565
- }
566
- }
567
- else {
568
- // no matches, consume the rest of the stream
569
- stream.skipToEnd();
570
- }
571
- return null;
572
- };
573
- return {
574
- name: 'jp-searching',
575
- /**
576
- * Token function is called when a line needs to be processed -
577
- * when the overlay is initially created, it's called on all lines;
578
- * when a line is modified and needs to be re-evaluated, it's called
579
- * on just that line.
580
- *
581
- * This implementation of the token function both constructs/maintains
582
- * the overlay and keeps track of the match state as the document is
583
- * updated while a search is active.
584
- */
585
- token: token.bind(this)
586
- };
555
+ _refresh() {
556
+ let effects = [
557
+ this._highlightEffect.of({ matches: this.matches })
558
+ ];
559
+ if (!this._cm.state.field(this._highlightField, false)) {
560
+ effects.push(StateEffect.appendConfig.of([this._highlightField]));
561
+ }
562
+ this._cm.editor.dispatch({ effects });
587
563
  }
588
564
  _findNext(reverse) {
589
565
  if (this._matches.length === 0) {
@@ -601,20 +577,13 @@ export class CodeMirrorSearchHighlighter {
601
577
  // direction, so head is always at the 'to' position. That way, if reverse = false,
602
578
  // the search proceeds from the 'to' position during normal toggling. If reverse = true,
603
579
  // the search always proceeds from the 'anchor' position, which is at the 'from'.
604
- const cursorToGet = reverse ? 'anchor' : 'head';
605
- let lastPosition = this._cm.getCursor(cursorToGet);
606
- if (lastPosition.line === 0 &&
607
- lastPosition.ch === 0 &&
608
- reverse &&
609
- this.currentIndex === null) {
580
+ const cursor = this._cm.state.selection.main;
581
+ let lastPosition = reverse ? cursor.anchor : cursor.head;
582
+ if (lastPosition === 0 && reverse && this.currentIndex === null) {
610
583
  // The default position is (0, 0) but we want to start from the end in that case
611
- lastPosition = {
612
- // Go to virtual next line so position got clamp to end
613
- line: this._cm.lineCount,
614
- ch: 0
615
- };
584
+ lastPosition = this._cm.doc.length;
616
585
  }
617
- const position = this._cm.doc.indexFromPos(lastPosition);
586
+ const position = lastPosition;
618
587
  let found = Utils.findNext(this._matches, position, 0, this._matches.length - 1);
619
588
  if (found === null) {
620
589
  // Don't loop
@@ -629,12 +598,6 @@ export class CodeMirrorSearchHighlighter {
629
598
  }
630
599
  return found;
631
600
  }
632
- _toEditorPos(posIn) {
633
- return {
634
- line: posIn.line,
635
- column: posIn.ch
636
- };
637
- }
638
601
  }
639
602
  /**
640
603
  * Helpers namespace