@jupyterlab/codemirror 4.0.0-alpha.19 → 4.0.0-alpha.20

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.
Files changed (78) hide show
  1. package/lib/codemirror-ipythongfm.d.ts +0 -1
  2. package/lib/codemirror-ipythongfm.js +0 -8
  3. package/lib/codemirror-ipythongfm.js.map +1 -1
  4. package/lib/commands.d.ts +13 -0
  5. package/lib/commands.js +32 -0
  6. package/lib/commands.js.map +1 -0
  7. package/lib/editor.d.ts +29 -68
  8. package/lib/editor.js +66 -212
  9. package/lib/editor.js.map +1 -1
  10. package/lib/extension.d.ts +236 -0
  11. package/lib/extension.js +687 -0
  12. package/lib/extension.js.map +1 -0
  13. package/lib/extensions/customStyle.d.ts +25 -0
  14. package/lib/extensions/customStyle.js +51 -0
  15. package/lib/extensions/customStyle.js.map +1 -0
  16. package/lib/extensions/index.d.ts +3 -0
  17. package/lib/extensions/index.js +8 -0
  18. package/lib/extensions/index.js.map +1 -0
  19. package/lib/extensions/rulers.d.ts +8 -0
  20. package/lib/extensions/rulers.js +85 -0
  21. package/lib/extensions/rulers.js.map +1 -0
  22. package/lib/extensions/ybinding.d.ts +125 -0
  23. package/lib/extensions/ybinding.js +213 -0
  24. package/lib/extensions/ybinding.js.map +1 -0
  25. package/lib/factory.d.ts +15 -5
  26. package/lib/factory.js +45 -37
  27. package/lib/factory.js.map +1 -1
  28. package/lib/index.d.ts +7 -20
  29. package/lib/index.js +7 -24
  30. package/lib/index.js.map +1 -1
  31. package/lib/language.d.ts +96 -0
  32. package/lib/language.js +1690 -0
  33. package/lib/language.js.map +1 -0
  34. package/lib/mimetype.d.ts +3 -0
  35. package/lib/mimetype.js +9 -4
  36. package/lib/mimetype.js.map +1 -1
  37. package/lib/searchprovider.d.ts +90 -80
  38. package/lib/searchprovider.js +221 -352
  39. package/lib/searchprovider.js.map +1 -1
  40. package/lib/theme.d.ts +57 -0
  41. package/lib/{editortheme.js → theme.js} +57 -30
  42. package/lib/theme.js.map +1 -0
  43. package/lib/token.d.ts +375 -0
  44. package/lib/token.js +18 -0
  45. package/lib/token.js.map +1 -0
  46. package/package.json +28 -36
  47. package/src/codemirror-ipythongfm.ts +109 -0
  48. package/src/commands.ts +34 -0
  49. package/src/editor.ts +783 -0
  50. package/src/extension.ts +878 -0
  51. package/src/extensions/customStyle.ts +79 -0
  52. package/src/extensions/index.ts +8 -0
  53. package/src/extensions/rulers.ts +112 -0
  54. package/src/extensions/ybinding.ts +267 -0
  55. package/src/factory.ts +100 -0
  56. package/src/index.ts +17 -0
  57. package/src/language.ts +1729 -0
  58. package/src/mimetype.ts +54 -0
  59. package/src/searchprovider.ts +700 -0
  60. package/src/theme.ts +219 -0
  61. package/src/token.ts +436 -0
  62. package/src/typings.d.ts +13 -0
  63. package/style/index.css +0 -3
  64. package/style/index.js +0 -3
  65. package/lib/codemirror-ipython.d.ts +0 -1
  66. package/lib/codemirror-ipython.js +0 -12
  67. package/lib/codemirror-ipython.js.map +0 -1
  68. package/lib/editorconfiguration.d.ts +0 -149
  69. package/lib/editorconfiguration.js +0 -289
  70. package/lib/editorconfiguration.js.map +0 -1
  71. package/lib/editortheme.d.ts +0 -41
  72. package/lib/editortheme.js.map +0 -1
  73. package/lib/mode.d.ts +0 -88
  74. package/lib/mode.js +0 -1518
  75. package/lib/mode.js.map +0 -1
  76. package/lib/syntaxstatus.d.ts +0 -67
  77. package/lib/syntaxstatus.js +0 -146
  78. package/lib/syntaxstatus.js.map +0 -1
@@ -1,426 +1,293 @@
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
- */
31
- import { GenericSearchProvider, TextSearchEngine } from '@jupyterlab/documentsearch';
32
3
  import { JSONExt } from '@lumino/coreutils';
33
- import { Signal } from '@lumino/signaling';
34
- import { RegExpCursor } from '@codemirror/search';
35
- import { Compartment, StateEffect, StateField } from '@codemirror/state';
4
+ import { StateEffect, StateField } from '@codemirror/state';
36
5
  import { Decoration, EditorView } from '@codemirror/view';
6
+ import { GenericSearchProvider, TextSearchEngine } from '@jupyterlab/documentsearch';
7
+ import { Signal } from '@lumino/signaling';
37
8
  /**
38
- * CodeMirror search provider for file editor
9
+ * Search provider for editors.
39
10
  */
40
- export class CodeMirrorSearchProvider {
41
- constructor(editor) {
42
- /**
43
- * Set to true if the widget under search is read-only, false
44
- * if it is editable. Will be used to determine whether to show
45
- * the replace option.
46
- */
47
- this.isReadOnly = false;
48
- this._matchState = {};
49
- this._changed = new Signal(this);
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([]));
86
- }
11
+ export class EditorSearchProvider {
87
12
  /**
88
- * Whether the search provider is disposed or not.
13
+ * Constructor
89
14
  */
90
- get isDisposed() {
91
- return this._disposed;
15
+ constructor() {
16
+ /**
17
+ * Current match index
18
+ */
19
+ this.currentIndex = null;
20
+ /**
21
+ * Current search query
22
+ */
23
+ this.query = null;
24
+ this._isActive = true;
25
+ this._isDisposed = false;
26
+ this._cmHandler = null;
27
+ this.currentIndex = null;
28
+ this._stateChanged = new Signal(this);
92
29
  }
93
30
  /**
94
- * Dispose the search provider
31
+ * CodeMirror search highlighter
95
32
  */
96
- dispose() {
97
- if (this._disposed) {
98
- return;
33
+ get cmHandler() {
34
+ if (!this._cmHandler) {
35
+ this._cmHandler = new CodeMirrorSearchHighlighter(this.editor);
99
36
  }
100
- this._disposed = true;
101
- Signal.clearData(this);
37
+ return this._cmHandler;
102
38
  }
103
39
  /**
104
- * Initialize the search using a CodeMirrorEditor object.
105
- *
106
- * @param query the search regular expression
40
+ * Changed signal to be emitted when search matches change.
107
41
  */
108
- async startQuery(query) {
109
- if (!this.editor) {
110
- return Promise.resolve();
111
- }
112
- return this._startQuery(query);
42
+ get stateChanged() {
43
+ return this._stateChanged;
113
44
  }
114
45
  /**
115
- * Refresh the search highlight overlay
46
+ * Current match index
116
47
  */
117
- refreshOverlay() {
118
- this._refreshOverlay();
119
- }
120
- async _startQuery(query, refreshOverlay = true) {
121
- // no point in removing overlay in the middle of the search
122
- await this.endQuery(false);
123
- this._query = query;
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);
132
- if (refreshOverlay) {
133
- this._refreshOverlay();
134
- }
135
- const matches = this._parseMatchesFromState();
136
- if (matches.length === 0) {
137
- return Promise.resolve();
138
- }
139
- const cursorMatch = this._findNext(false);
140
- const match = cursorMatch &&
141
- this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
142
- this._currentMatch = match;
143
- await this._highlightCurrentMatch(cursorMatch);
144
- return Promise.resolve();
48
+ get currentMatchIndex() {
49
+ return this.isActive ? this.currentIndex : null;
145
50
  }
146
51
  /**
147
- * Clears state of a search provider to prepare for startQuery to be called
148
- * in order to start a new query or refresh an existing one.
52
+ * Whether the cell search is active.
149
53
  *
150
- * @param removeOverlay Whether to remove the search highlight overlay or not.
54
+ * This is used when applying search only on selected cells.
151
55
  */
152
- async endQuery(removeOverlay = true) {
153
- this._matchState = {};
154
- this._currentMatch = null;
155
- const from = this.editor.getPositionAt(this.editor.state.selection.main.from);
156
- const to = this.editor.getPositionAt(this.editor.state.selection.main.to);
157
- // Setting a reverse selection to allow search-as-you-type to maintain the
158
- // current selected match. See comment in _findNext for more details.
159
- if (from !== to) {
160
- this.editor.setSelection({
161
- start: to,
162
- end: from
163
- });
164
- }
165
- this.editor.editor.dispatch({
166
- effects: this._listener.reconfigure([])
167
- });
56
+ get isActive() {
57
+ return this._isActive;
168
58
  }
169
59
  /**
170
- * Clear currently highlighted match.
60
+ * Whether the search provider is disposed or not.
171
61
  */
172
- clearHighlight() {
173
- this._currentMatch = null;
174
- const cursor = this.editor.getCursorPosition();
175
- // Reset cursor position to remove any selection
176
- this.editor.setCursorPosition(cursor);
177
- return Promise.resolve();
62
+ get isDisposed() {
63
+ return this._isDisposed;
178
64
  }
179
65
  /**
180
- * Move the current match indicator to the next match.
181
- *
182
- * @param loop Whether to loop within the matches list.
183
- *
184
- * @returns A promise that resolves once the action has completed.
66
+ * Number of matches in the cell.
185
67
  */
186
- async highlightNext(loop) {
187
- return this._highlightCurrentMatch(this._findNext(false));
68
+ get matchesCount() {
69
+ return this.isActive ? this.cmHandler.matches.length : 0;
188
70
  }
189
71
  /**
190
- * Move the current match indicator to the previous match.
191
- *
192
- * @param loop Whether to loop within the matches list.
193
- *
194
- * @returns A promise that resolves once the action has completed.
72
+ * Clear currently highlighted match
195
73
  */
196
- async highlightPrevious(loop) {
197
- return this._highlightCurrentMatch(this._findNext(true));
74
+ clearHighlight() {
75
+ this.currentIndex = null;
76
+ this.cmHandler.clearHighlight();
77
+ return Promise.resolve();
198
78
  }
199
- async _highlightCurrentMatch(cursorMatch) {
200
- // Highlight the current index
201
- if (!cursorMatch) {
202
- // Set cursor to remove any selection
203
- this.editor.editor.dispatch({ selection: { anchor: 0 } });
79
+ /**
80
+ * Dispose the search provider
81
+ */
82
+ dispose() {
83
+ if (this._isDisposed) {
204
84
  return;
205
85
  }
206
- const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
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
- });
216
- return match;
86
+ this._isDisposed = true;
87
+ Signal.clearData(this);
88
+ if (this.isActive) {
89
+ this.endQuery().catch(reason => {
90
+ console.error(`Failed to end search query on cells.`, reason);
91
+ });
92
+ }
217
93
  }
218
94
  /**
219
- * Replace the currently selected match with the provided text
95
+ * Set `isActive` status.
220
96
  *
221
- * @param newText The replacement text
222
- * @param loop Whether to loop within the matches list.
97
+ * #### Notes
98
+ * It will start or end the search
223
99
  *
224
- * @returns A promise that resolves with a boolean indicating whether a replace occurred.
100
+ * @param v New value
225
101
  */
226
- async replaceCurrentMatch(newText, loop, options) {
227
- // If the current selection exactly matches the current match,
228
- // replace it. Otherwise, just select the next match after the cursor.
229
- let replaceOccurred = false;
230
- if (this._currentMatchIsSelected()) {
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) {
233
- return replaceOccurred;
102
+ async setIsActive(v) {
103
+ if (this._isActive !== v) {
104
+ this._isActive = v;
105
+ }
106
+ if (this._isActive) {
107
+ if (this.query !== null) {
108
+ await this.startQuery(this.query, this.filters);
234
109
  }
235
- const value = cursor.value;
236
- replaceOccurred = true;
237
- const insertText = (options === null || options === void 0 ? void 0 : options.preserveCase)
238
- ? GenericSearchProvider.preserveCase(this.editor.doc.sliceString(cursor.value.from, cursor.value.to), newText)
239
- : newText;
240
- this.editor.editor.dispatch({
241
- changes: { from: value.from, to: value.to, insert: insertText }
242
- });
243
110
  }
244
- await this.highlightNext();
245
- return replaceOccurred;
111
+ else {
112
+ await this.endQuery();
113
+ }
246
114
  }
247
115
  /**
248
- * Replace all matches in the notebook with the provided text
249
- *
250
- * @param newText The replacement text
116
+ * Initialize the search using the provided options. Should update the UI
117
+ * to highlight all matches and "select" the first match.
251
118
  *
252
- * @returns A promise that resolves with a boolean indicating whether a replace occurred.
119
+ * @param query A RegExp to be use to perform the search
120
+ * @param filters Filter parameters to pass to provider
253
121
  */
254
- async replaceAllMatches(newText, options) {
255
- let replaceOccurred = false;
256
- return new Promise((resolve, _) => {
257
- let cursor = new RegExpCursor(this.editor.doc, this._query.source, {
258
- ignoreCase: !this._query.ignoreCase
259
- }).next();
260
- let changeSpec = [];
261
- while (!cursor.done) {
262
- replaceOccurred = true;
263
- const insertText = (options === null || options === void 0 ? void 0 : options.preserveCase)
264
- ? GenericSearchProvider.preserveCase(this.editor.doc.sliceString(cursor.value.from, cursor.value.to), newText)
265
- : newText;
266
- changeSpec.push({
267
- from: cursor.value.from,
268
- to: cursor.value.to,
269
- insert: insertText
270
- });
271
- cursor = cursor.next();
272
- }
273
- this.editor.editor.dispatch({ changes: changeSpec });
274
- this._matchState = {};
275
- this._currentMatch = null;
276
- resolve(replaceOccurred);
277
- });
122
+ async startQuery(query, filters) {
123
+ this.query = query;
124
+ this.filters = filters;
125
+ // Search input
126
+ const content = this.model.sharedModel.getSource();
127
+ await this.updateCodeMirror(content);
128
+ this.model.sharedModel.changed.connect(this.onSharedModelChanged, this);
278
129
  }
279
130
  /**
280
- * The list of matches
131
+ * Stop the search and clean any UI elements.
281
132
  */
282
- get matches() {
283
- return this._parseMatchesFromState();
133
+ async endQuery() {
134
+ await this.cmHandler.endQuery();
135
+ this.currentIndex = null;
284
136
  }
285
137
  /**
286
- * The number of matches.
138
+ * Highlight the next match.
139
+ *
140
+ * @returns The next match if there is one.
287
141
  */
288
- get matchesCount() {
289
- let size = 0;
290
- for (const line in this._matchState) {
291
- size += Object.keys(this._matchState[line]).length;
142
+ async highlightNext(loop = true) {
143
+ if (this.matchesCount === 0 || !this.isActive) {
144
+ this.currentIndex = null;
145
+ }
146
+ else {
147
+ // This starts from the cursor position
148
+ let match = await this.cmHandler.highlightNext();
149
+ if (match) {
150
+ this.currentIndex = this.cmHandler.currentIndex;
151
+ }
152
+ else {
153
+ this.currentIndex = loop ? 0 : null;
154
+ }
155
+ return match;
292
156
  }
293
- return size;
157
+ return Promise.resolve(this.getCurrentMatch());
294
158
  }
295
159
  /**
296
- * The current match
160
+ * Highlight the previous match.
161
+ *
162
+ * @returns The previous match if there is one.
297
163
  */
298
- get currentMatch() {
299
- return this._currentMatch;
164
+ async highlightPrevious(loop = true) {
165
+ if (this.matchesCount === 0 || !this.isActive) {
166
+ this.currentIndex = null;
167
+ }
168
+ else {
169
+ // This starts from the cursor position
170
+ let match = await this.cmHandler.highlightPrevious();
171
+ if (match) {
172
+ this.currentIndex = this.cmHandler.currentIndex;
173
+ }
174
+ else {
175
+ this.currentIndex = loop ? this.matchesCount - 1 : null;
176
+ }
177
+ return match;
178
+ }
179
+ return Promise.resolve(this.getCurrentMatch());
300
180
  }
301
181
  /**
302
- * Signal indicating that something in the search has changed, so the UI should update
182
+ * Replace the currently selected match with the provided text.
183
+ *
184
+ * If no match is selected, it won't do anything.
185
+ *
186
+ * @param newText The replacement text.
187
+ * @returns Whether a replace occurred.
303
188
  */
304
- get stateChanged() {
305
- return this._changed;
189
+ replaceCurrentMatch(newText, loop, options) {
190
+ if (!this.isActive) {
191
+ return Promise.resolve(false);
192
+ }
193
+ let occurred = false;
194
+ if (this.currentIndex !== null &&
195
+ this.currentIndex < this.cmHandler.matches.length) {
196
+ const editor = this.editor;
197
+ const selection = editor.state.sliceDoc(editor.state.selection.main.from, editor.state.selection.main.to);
198
+ const match = this.getCurrentMatch();
199
+ // If cursor is not on a selection, highlight the next match
200
+ if (selection !== (match === null || match === void 0 ? void 0 : match.text)) {
201
+ this.currentIndex = null;
202
+ // The next will be highlighted as a consequence of this returning false
203
+ }
204
+ else {
205
+ this.cmHandler.matches.splice(this.currentIndex, 1);
206
+ this.currentIndex = null;
207
+ const substitutedText = (options === null || options === void 0 ? void 0 : options.regularExpression)
208
+ ? match.text.replace(this.query, newText)
209
+ : newText;
210
+ const insertText = (options === null || options === void 0 ? void 0 : options.preserveCase)
211
+ ? GenericSearchProvider.preserveCase(match.text, substitutedText)
212
+ : substitutedText;
213
+ this.model.sharedModel.updateSource(match.position, match.position + match.text.length, insertText);
214
+ occurred = true;
215
+ }
216
+ }
217
+ return Promise.resolve(occurred);
306
218
  }
307
219
  /**
308
- * The current index of the selected match.
220
+ * Replace all matches in the cell source with the provided text
221
+ *
222
+ * @param newText The replacement text.
223
+ * @returns Whether a replace occurred.
309
224
  */
310
- get currentMatchIndex() {
311
- if (!this._currentMatch) {
312
- return null;
313
- }
314
- // TODO make it more efficient
315
- return this.matches.indexOf(this._currentMatch);
316
- }
317
- _onDocChanged(update) {
318
- // TODO: CM6 migration
319
- if (update.changes.length) {
320
- this._setInitialMatches(this._query)
321
- .then(() => {
322
- this._changed.emit(undefined);
323
- })
324
- .catch(reason => {
325
- console.error(`Fail to reapply search on CodeMirror document change:\n${reason}`);
326
- });
225
+ replaceAllMatches(newText, options) {
226
+ if (!this.isActive) {
227
+ return Promise.resolve(false);
327
228
  }
328
- }
329
- _refreshOverlay() {
330
- let effects = [
331
- this._highlightEffect.of({ matches: this.matches })
332
- ];
333
- if (!this.editor.state.field(this._highlightField, false)) {
334
- effects.push(StateEffect.appendConfig.of([this._highlightField]));
229
+ let occurred = this.cmHandler.matches.length > 0;
230
+ let src = this.model.sharedModel.getSource();
231
+ let lastEnd = 0;
232
+ const finalSrc = this.cmHandler.matches.reduce((agg, match) => {
233
+ const start = match.position;
234
+ const end = start + match.text.length;
235
+ const substitutedText = (options === null || options === void 0 ? void 0 : options.regularExpression)
236
+ ? match.text.replace(this.query, newText)
237
+ : newText;
238
+ const insertText = (options === null || options === void 0 ? void 0 : options.preserveCase)
239
+ ? GenericSearchProvider.preserveCase(match.text, substitutedText)
240
+ : substitutedText;
241
+ const newStep = `${agg}${src.slice(lastEnd, start)}${insertText}`;
242
+ lastEnd = end;
243
+ return newStep;
244
+ }, '');
245
+ if (occurred) {
246
+ this.cmHandler.matches = [];
247
+ this.currentIndex = null;
248
+ this.model.sharedModel.setSource(`${finalSrc}${src.slice(lastEnd)}`);
335
249
  }
336
- this.editor.editor.dispatch({ effects });
337
- this._changed.emit(undefined);
250
+ return Promise.resolve(occurred);
338
251
  }
339
252
  /**
340
- * Do a full search on the entire document.
341
- *
342
- * This manually constructs the initial match state across the whole
343
- * document. This must be done manually because the codemirror overlay
344
- * is lazy-loaded, so it will only tokenize lines that are in or near
345
- * the viewport. This is sufficient for efficiently maintaining the
346
- * state when changes are made to the document, as changes occur in or
347
- * near the viewport, but to scan the whole document, a manual search
348
- * across the entire content is required.
253
+ * Get the current match if it exists.
349
254
  *
350
- * @param query The search term
255
+ * @returns The current match
351
256
  */
352
- async _setInitialMatches(query) {
353
- this._matchState = {};
354
- const content = this.editor.doc.toString();
355
- const matches = await TextSearchEngine.search(query, content);
356
- matches.forEach(match => {
357
- const { line, ch } = this._getPosition(match.position);
358
- if (!this._matchState[line]) {
359
- this._matchState[line] = {};
360
- }
361
- this._matchState[line][ch] = match;
362
- });
363
- }
364
- _findNext(reverse) {
365
- const caseSensitive = this._query.ignoreCase;
366
- const state = this.editor.state;
367
- // In order to support search-as-you-type, we needed a way to allow the first
368
- // match to be selected when a search is started, but prevent the selected
369
- // search to move for each new keypress. To do this, when a search is ended,
370
- // the cursor is reversed, putting the head at the 'from' position. When a new
371
- // search is started, the cursor we want is at the 'from' position, so that the same
372
- // match is selected when the next key is entered (if it is still a match).
373
- //
374
- // When toggling through a search normally, the cursor is always set in the forward
375
- // direction, so head is always at the 'to' position. That way, if reverse = false,
376
- // the search proceeds from the 'to' position during normal toggling. If reverse = true,
377
- // the search always proceeds from the 'anchor' position, which is at the 'from'.
378
- const lastPosition = reverse
379
- ? state.selection.main.anchor
380
- : state.selection.main.head;
381
- let cursor = new RegExpCursor(this.editor.doc, this._query.source, { ignoreCase: !caseSensitive }, lastPosition).next();
382
- if (cursor.done) {
383
- const startOrEnd = reverse ? this.editor.doc.length : 0;
384
- cursor = new RegExpCursor(this.editor.doc, this._query.source, { ignoreCase: !caseSensitive }, startOrEnd).next();
385
- if (cursor.done) {
386
- return null;
387
- }
257
+ getCurrentMatch() {
258
+ if (this.currentIndex === null) {
259
+ return undefined;
388
260
  }
389
- return {
390
- from: this._getPosition(cursor.value.from),
391
- to: this._getPosition(cursor.value.to)
392
- };
393
- }
394
- // TODO: remove this when Position API are removed and we work with
395
- // offsets only. Contrary to the functions in CodeMirrorEditor,
396
- // these functions work with CM6 position (i.e. line number starting at 1).
397
- _getPosition(offset) {
398
- const line = this.editor.doc.lineAt(offset);
399
- return { line: line.number, ch: offset - line.from };
400
- }
401
- _parseMatchesFromState() {
402
- // Flatten state map
403
- const matches = new Array();
404
- for (const lineKey in this._matchState) {
405
- const lineMatches = this._matchState[lineKey];
406
- for (const posKey in lineMatches) {
407
- matches.push(lineMatches[posKey]);
261
+ else {
262
+ let match = undefined;
263
+ if (this.currentIndex < this.cmHandler.matches.length) {
264
+ match = this.cmHandler.matches[this.currentIndex];
408
265
  }
266
+ return match;
267
+ }
268
+ }
269
+ /**
270
+ * Callback on source change
271
+ *
272
+ * @param emitter Source of the change
273
+ * @param changes Source change
274
+ */
275
+ async onSharedModelChanged(emitter, changes) {
276
+ if (changes.sourceChange) {
277
+ await this.updateCodeMirror(emitter.getSource());
278
+ this._stateChanged.emit();
409
279
  }
410
- return matches;
411
280
  }
412
- _currentMatchIsSelected() {
413
- if (!this._currentMatch) {
414
- return false;
281
+ /**
282
+ * Update matches
283
+ */
284
+ async updateCodeMirror(content) {
285
+ if (this.query !== null && this.isActive) {
286
+ this.cmHandler.matches = await TextSearchEngine.search(this.query, content);
287
+ }
288
+ else {
289
+ this.cmHandler.matches = [];
415
290
  }
416
- const currentSelection = this.editor.getSelection();
417
- const currentSelectionLength = currentSelection.end.column - currentSelection.start.column;
418
- const selectionIsOneLine = currentSelection.start.line === currentSelection.end.line;
419
- return (selectionIsOneLine &&
420
- this._currentMatch.text.length === currentSelectionLength &&
421
- this._currentMatch.position ===
422
- this.editor.doc.line(currentSelection.start.line).from +
423
- currentSelection.start.column);
424
291
  }
425
292
  }
426
293
  /**
@@ -459,7 +326,9 @@ export class CodeMirrorSearchHighlighter {
459
326
  const e = ef;
460
327
  if (e.value.matches.length) {
461
328
  highlights = highlights.update({
462
- add: e.value.matches.map(m => this._highlightMark.range(m.position, m.position + m.text.length))
329
+ add: e.value.matches.map(m => this._highlightMark.range(m.position, m.position + m.text.length)),
330
+ // filter out old marks
331
+ filter: () => false
463
332
  });
464
333
  }
465
334
  else {