@jupyterlab/codemirror 4.0.0-alpha.8 → 4.0.0-beta.0
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.
- package/lib/codemirror-ipythongfm.d.ts +9 -3
- package/lib/codemirror-ipythongfm.js +52 -30
- package/lib/codemirror-ipythongfm.js.map +1 -1
- package/lib/commands.d.ts +13 -0
- package/lib/commands.js +32 -0
- package/lib/commands.js.map +1 -0
- package/lib/editor.d.ts +48 -273
- package/lib/editor.js +220 -819
- package/lib/editor.js.map +1 -1
- package/lib/extension.d.ts +236 -0
- package/lib/extension.js +696 -0
- package/lib/extension.js.map +1 -0
- package/lib/extensions/customStyle.d.ts +25 -0
- package/lib/extensions/customStyle.js +51 -0
- package/lib/extensions/customStyle.js.map +1 -0
- package/lib/extensions/index.d.ts +3 -0
- package/lib/extensions/index.js +8 -0
- package/lib/extensions/index.js.map +1 -0
- package/lib/extensions/rulers.d.ts +8 -0
- package/lib/extensions/rulers.js +85 -0
- package/lib/extensions/rulers.js.map +1 -0
- package/lib/extensions/ybinding.d.ts +129 -0
- package/lib/extensions/ybinding.js +229 -0
- package/lib/extensions/ybinding.js.map +1 -0
- package/lib/factory.d.ts +15 -5
- package/lib/factory.js +53 -24
- package/lib/factory.js.map +1 -1
- package/lib/index.d.ts +7 -19
- package/lib/index.js +7 -23
- package/lib/index.js.map +1 -1
- package/lib/language.d.ts +96 -0
- package/lib/language.js +1687 -0
- package/lib/language.js.map +1 -0
- package/lib/mimetype.d.ts +3 -0
- package/lib/mimetype.js +16 -5
- package/lib/mimetype.js.map +1 -1
- package/lib/searchprovider.d.ts +219 -0
- package/lib/searchprovider.js +640 -0
- package/lib/searchprovider.js.map +1 -0
- package/lib/theme.d.ts +57 -0
- package/lib/theme.js +194 -0
- package/lib/theme.js.map +1 -0
- package/lib/token.d.ts +375 -0
- package/lib/token.js +18 -0
- package/lib/token.js.map +1 -0
- package/package.json +45 -41
- package/src/codemirror-ipythongfm.ts +109 -0
- package/src/commands.ts +34 -0
- package/src/editor.ts +783 -0
- package/src/extension.ts +890 -0
- package/src/extensions/customStyle.ts +79 -0
- package/src/extensions/index.ts +8 -0
- package/src/extensions/rulers.ts +112 -0
- package/src/extensions/ybinding.ts +295 -0
- package/src/factory.ts +100 -0
- package/src/index.ts +17 -0
- package/src/language.ts +1726 -0
- package/src/mimetype.ts +54 -0
- package/src/searchprovider.ts +813 -0
- package/src/theme.ts +219 -0
- package/src/token.ts +436 -0
- package/src/typings.d.ts +13 -0
- package/style/base.css +33 -187
- package/style/index.css +1 -7
- package/style/index.js +1 -7
- package/lib/codemirror-ipython.d.ts +0 -2
- package/lib/codemirror-ipython.js +0 -30
- package/lib/codemirror-ipython.js.map +0 -1
- package/lib/mode.d.ts +0 -82
- package/lib/mode.js +0 -150
- package/lib/mode.js.map +0 -1
- package/lib/syntaxstatus.d.ts +0 -67
- package/lib/syntaxstatus.js +0 -140
- package/lib/syntaxstatus.js.map +0 -1
- package/lib/tokens.d.ts +0 -18
- package/lib/tokens.js +0 -8
- package/lib/tokens.js.map +0 -1
- package/typings/codemirror/codemirror.d.ts +0 -227
package/src/editor.ts
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
2
|
+
// Copyright (c) Jupyter Development Team.
|
|
3
|
+
// Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|
|
5
|
+
import { insertNewlineAndIndent } from '@codemirror/commands';
|
|
6
|
+
import { ensureSyntaxTree } from '@codemirror/language';
|
|
7
|
+
import {
|
|
8
|
+
Compartment,
|
|
9
|
+
EditorSelection,
|
|
10
|
+
EditorState,
|
|
11
|
+
Extension,
|
|
12
|
+
Prec,
|
|
13
|
+
StateCommand,
|
|
14
|
+
Text
|
|
15
|
+
} from '@codemirror/state';
|
|
16
|
+
import { Command, EditorView, ViewUpdate } from '@codemirror/view';
|
|
17
|
+
import { CodeEditor } from '@jupyterlab/codeeditor';
|
|
18
|
+
import { SyntaxNodeRef } from '@lezer/common';
|
|
19
|
+
import { UUID } from '@lumino/coreutils';
|
|
20
|
+
import { Signal } from '@lumino/signaling';
|
|
21
|
+
import './codemirror-ipythongfm';
|
|
22
|
+
import { ExtensionsHandler } from './extension';
|
|
23
|
+
import { EditorLanguageRegistry } from './language';
|
|
24
|
+
import {
|
|
25
|
+
IEditorExtensionRegistry,
|
|
26
|
+
IEditorLanguageRegistry,
|
|
27
|
+
IExtensionsHandler
|
|
28
|
+
} from './token';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The class name added to CodeMirrorWidget instances.
|
|
32
|
+
*/
|
|
33
|
+
const EDITOR_CLASS = 'jp-CodeMirrorEditor';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The key code for the up arrow key.
|
|
37
|
+
*/
|
|
38
|
+
const UP_ARROW = 38;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The key code for the down arrow key.
|
|
42
|
+
*/
|
|
43
|
+
const DOWN_ARROW = 40;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* CodeMirror editor.
|
|
47
|
+
*/
|
|
48
|
+
export class CodeMirrorEditor implements CodeEditor.IEditor {
|
|
49
|
+
/**
|
|
50
|
+
* Construct a CodeMirror editor.
|
|
51
|
+
*/
|
|
52
|
+
constructor(options: CodeMirrorEditor.IOptions) {
|
|
53
|
+
this._languages = options.languages ?? new EditorLanguageRegistry();
|
|
54
|
+
this._configurator =
|
|
55
|
+
options.extensionsRegistry?.createNew({
|
|
56
|
+
...options,
|
|
57
|
+
inline: options.inline ?? false
|
|
58
|
+
}) ?? new ExtensionsHandler();
|
|
59
|
+
const host = (this.host = options.host);
|
|
60
|
+
|
|
61
|
+
host.classList.add(EDITOR_CLASS);
|
|
62
|
+
host.classList.add('jp-Editor');
|
|
63
|
+
host.addEventListener('focus', this, true);
|
|
64
|
+
host.addEventListener('blur', this, true);
|
|
65
|
+
host.addEventListener('scroll', this, true);
|
|
66
|
+
|
|
67
|
+
this._uuid = options.uuid ?? UUID.uuid4();
|
|
68
|
+
|
|
69
|
+
const model = (this._model = options.model);
|
|
70
|
+
|
|
71
|
+
// Default keydown handler - it will have low priority
|
|
72
|
+
const onKeyDown = EditorView.domEventHandlers({
|
|
73
|
+
keydown: (event: KeyboardEvent, view: EditorView) => {
|
|
74
|
+
return this.onKeydown(event);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const updateListener = EditorView.updateListener.of(
|
|
79
|
+
(update: ViewUpdate) => {
|
|
80
|
+
this._onDocChanged(update);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
this._editor = Private.createEditor(
|
|
85
|
+
host,
|
|
86
|
+
this._configurator,
|
|
87
|
+
[
|
|
88
|
+
// We need to set the order to high, otherwise the keybinding for ArrowUp/ArrowDown
|
|
89
|
+
// will process the event shunting our edge detection code.
|
|
90
|
+
Prec.high(onKeyDown),
|
|
91
|
+
updateListener,
|
|
92
|
+
// Initialize with empty extension
|
|
93
|
+
this._language.of([]),
|
|
94
|
+
...(options.extensions ?? [])
|
|
95
|
+
],
|
|
96
|
+
model.sharedModel.source
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
this._onMimeTypeChanged();
|
|
100
|
+
this._onCursorActivity();
|
|
101
|
+
|
|
102
|
+
this._configurator.configChanged.connect(this.onConfigChanged, this);
|
|
103
|
+
model.mimeTypeChanged.connect(this._onMimeTypeChanged, this);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A signal emitted when either the top or bottom edge is requested.
|
|
108
|
+
*/
|
|
109
|
+
readonly edgeRequested = new Signal<this, CodeEditor.EdgeLocation>(this);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The DOM node that hosts the editor.
|
|
113
|
+
*/
|
|
114
|
+
readonly host: HTMLElement;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The uuid of this editor;
|
|
118
|
+
*/
|
|
119
|
+
get uuid(): string {
|
|
120
|
+
return this._uuid;
|
|
121
|
+
}
|
|
122
|
+
set uuid(value: string) {
|
|
123
|
+
this._uuid = value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the codemirror editor wrapped by the editor.
|
|
128
|
+
*/
|
|
129
|
+
get editor(): EditorView {
|
|
130
|
+
return this._editor;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the codemirror doc wrapped by the widget.
|
|
135
|
+
*/
|
|
136
|
+
get doc(): Text {
|
|
137
|
+
return this._editor.state.doc;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the number of lines in the editor.
|
|
142
|
+
*/
|
|
143
|
+
get lineCount(): number {
|
|
144
|
+
return this.doc.lines;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns a model for this editor.
|
|
149
|
+
*/
|
|
150
|
+
get model(): CodeEditor.IModel {
|
|
151
|
+
return this._model;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The height of a line in the editor in pixels.
|
|
156
|
+
*/
|
|
157
|
+
get lineHeight(): number {
|
|
158
|
+
return this._editor.defaultLineHeight;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The widget of a character in the editor in pixels.
|
|
163
|
+
*/
|
|
164
|
+
get charWidth(): number {
|
|
165
|
+
return this._editor.defaultCharacterWidth;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Tests whether the editor is disposed.
|
|
170
|
+
*/
|
|
171
|
+
get isDisposed(): boolean {
|
|
172
|
+
return this._isDisposed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Dispose of the resources held by the widget.
|
|
177
|
+
*/
|
|
178
|
+
dispose(): void {
|
|
179
|
+
if (this.isDisposed) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this._isDisposed = true;
|
|
183
|
+
this.host.removeEventListener('focus', this, true);
|
|
184
|
+
this.host.removeEventListener('blur', this, true);
|
|
185
|
+
this.host.removeEventListener('scroll', this, true);
|
|
186
|
+
this._configurator.dispose();
|
|
187
|
+
Signal.clearData(this);
|
|
188
|
+
this.editor.destroy();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get a config option for the editor.
|
|
193
|
+
*/
|
|
194
|
+
getOption(option: string): unknown {
|
|
195
|
+
return this._configurator.getOption(option);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether the option exists or not.
|
|
200
|
+
*/
|
|
201
|
+
hasOption(option: string): boolean {
|
|
202
|
+
return this._configurator.hasOption(option);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Set a config option for the editor.
|
|
207
|
+
*/
|
|
208
|
+
setOption(option: string, value: unknown): void {
|
|
209
|
+
this._configurator.setOption(option, value);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Set config options for the editor.
|
|
214
|
+
*
|
|
215
|
+
* This method is preferred when setting several options. The
|
|
216
|
+
* options are set within an operation, which only performs
|
|
217
|
+
* the costly update at the end, and not after every option
|
|
218
|
+
* is set.
|
|
219
|
+
*/
|
|
220
|
+
setOptions(options: Record<string, any>): void {
|
|
221
|
+
this._configurator.setOptions(options);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Inject an extension into the editor
|
|
226
|
+
*
|
|
227
|
+
* @alpha
|
|
228
|
+
* @experimental
|
|
229
|
+
* @param ext CodeMirror 6 extension
|
|
230
|
+
*/
|
|
231
|
+
injectExtension(ext: Extension): void {
|
|
232
|
+
this._configurator.injectExtension(this._editor, ext);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns the content for the given line number.
|
|
237
|
+
*/
|
|
238
|
+
getLine(line: number): string | undefined {
|
|
239
|
+
// TODO: CM6 remove +1 when CM6 first line number has propagated
|
|
240
|
+
line = line + 1;
|
|
241
|
+
return line <= this.doc.lines ? this.doc.line(line).text : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Find an offset for the given position.
|
|
246
|
+
*/
|
|
247
|
+
getOffsetAt(position: CodeEditor.IPosition): number {
|
|
248
|
+
// TODO: CM6 remove +1 when CM6 first line number has propagated
|
|
249
|
+
return this.doc.line(position.line + 1).from + position.column;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Find a position for the given offset.
|
|
254
|
+
*/
|
|
255
|
+
getPositionAt(offset: number): CodeEditor.IPosition {
|
|
256
|
+
// TODO: CM6 remove -1 when CM6 first line number has propagated
|
|
257
|
+
const line = this.doc.lineAt(offset);
|
|
258
|
+
return { line: line.number - 1, column: offset - line.from };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Undo one edit (if any undo events are stored).
|
|
263
|
+
*/
|
|
264
|
+
undo(): void {
|
|
265
|
+
this.model.sharedModel.undo();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Redo one undone edit.
|
|
270
|
+
*/
|
|
271
|
+
redo(): void {
|
|
272
|
+
this.model.sharedModel.redo();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Clear the undo history.
|
|
277
|
+
*/
|
|
278
|
+
clearHistory(): void {
|
|
279
|
+
this.model.sharedModel.clearUndoHistory();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Brings browser focus to this editor text.
|
|
284
|
+
*/
|
|
285
|
+
focus(): void {
|
|
286
|
+
this._editor.focus();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Test whether the editor has keyboard focus.
|
|
291
|
+
*/
|
|
292
|
+
hasFocus(): boolean {
|
|
293
|
+
return this._editor.hasFocus;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Explicitly blur the editor.
|
|
298
|
+
*/
|
|
299
|
+
blur(): void {
|
|
300
|
+
this._editor.contentDOM.blur();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
get state(): EditorState {
|
|
304
|
+
return this._editor.state;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
firstLine(): number {
|
|
308
|
+
// TODO: return 1 when CM6 first line number has propagated
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
lastLine(): number {
|
|
313
|
+
return this.doc.lines - 1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
cursorCoords(
|
|
317
|
+
where: boolean,
|
|
318
|
+
mode?: 'window' | 'page' | 'local'
|
|
319
|
+
): { left: number; top: number; bottom: number } {
|
|
320
|
+
const selection = this.state.selection.main;
|
|
321
|
+
const pos = where ? selection.from : selection.to;
|
|
322
|
+
const rect = this.editor.coordsAtPos(pos);
|
|
323
|
+
return rect as { left: number; top: number; bottom: number };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
getRange(
|
|
327
|
+
from: { line: number; ch: number },
|
|
328
|
+
to: { line: number; ch: number },
|
|
329
|
+
separator?: string
|
|
330
|
+
): string {
|
|
331
|
+
const fromOffset = this.getOffsetAt(this._toPosition(from));
|
|
332
|
+
const toOffset = this.getOffsetAt(this._toPosition(to));
|
|
333
|
+
return this.state.sliceDoc(fromOffset, toOffset);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Reveal the given position in the editor.
|
|
338
|
+
*/
|
|
339
|
+
revealPosition(position: CodeEditor.IPosition): void {
|
|
340
|
+
const offset = this.getOffsetAt(position);
|
|
341
|
+
this._editor.dispatch({
|
|
342
|
+
effects: EditorView.scrollIntoView(offset)
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Reveal the given selection in the editor.
|
|
348
|
+
*/
|
|
349
|
+
revealSelection(selection: CodeEditor.IRange): void {
|
|
350
|
+
const start = this.getOffsetAt(selection.start);
|
|
351
|
+
const end = this.getOffsetAt(selection.end);
|
|
352
|
+
this._editor.dispatch({
|
|
353
|
+
effects: EditorView.scrollIntoView(EditorSelection.range(start, end))
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get the window coordinates given a cursor position.
|
|
359
|
+
*/
|
|
360
|
+
getCoordinateForPosition(
|
|
361
|
+
position: CodeEditor.IPosition
|
|
362
|
+
): CodeEditor.ICoordinate {
|
|
363
|
+
const offset = this.getOffsetAt(position);
|
|
364
|
+
const rect = this.editor.coordsAtPos(offset);
|
|
365
|
+
return rect as CodeEditor.ICoordinate;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get the cursor position given window coordinates.
|
|
370
|
+
*
|
|
371
|
+
* @param coordinate - The desired coordinate.
|
|
372
|
+
*
|
|
373
|
+
* @returns The position of the coordinates, or null if not
|
|
374
|
+
* contained in the editor.
|
|
375
|
+
*/
|
|
376
|
+
getPositionForCoordinate(
|
|
377
|
+
coordinate: CodeEditor.ICoordinate
|
|
378
|
+
): CodeEditor.IPosition | null {
|
|
379
|
+
const offset = this.editor.posAtCoords({
|
|
380
|
+
x: coordinate.left,
|
|
381
|
+
y: coordinate.top
|
|
382
|
+
});
|
|
383
|
+
return this.getPositionAt(offset!) || null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Returns the primary position of the cursor, never `null`.
|
|
388
|
+
*/
|
|
389
|
+
getCursorPosition(): CodeEditor.IPosition {
|
|
390
|
+
const offset = this.state.selection.main.head;
|
|
391
|
+
return this.getPositionAt(offset);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Set the primary position of the cursor.
|
|
396
|
+
*
|
|
397
|
+
* #### Notes
|
|
398
|
+
* This will remove any secondary cursors.
|
|
399
|
+
*/
|
|
400
|
+
setCursorPosition(
|
|
401
|
+
position: CodeEditor.IPosition,
|
|
402
|
+
options?: { bias?: number; origin?: string; scroll?: boolean }
|
|
403
|
+
): void {
|
|
404
|
+
const offset = this.getOffsetAt(position);
|
|
405
|
+
this.editor.dispatch({
|
|
406
|
+
selection: { anchor: offset },
|
|
407
|
+
scrollIntoView: true
|
|
408
|
+
});
|
|
409
|
+
// If the editor does not have focus, this cursor change
|
|
410
|
+
// will get screened out in _onCursorsChanged(). Make an
|
|
411
|
+
// exception for this method.
|
|
412
|
+
if (!this.editor.hasFocus) {
|
|
413
|
+
this.model.selections.set(this.uuid, this.getSelections());
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Returns the primary selection, never `null`.
|
|
419
|
+
*/
|
|
420
|
+
getSelection(): CodeEditor.ITextSelection {
|
|
421
|
+
return this.getSelections()[0];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Set the primary selection. This will remove any secondary cursors.
|
|
426
|
+
*/
|
|
427
|
+
setSelection(selection: CodeEditor.IRange): void {
|
|
428
|
+
this.setSelections([selection]);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Gets the selections for all the cursors, never `null` or empty.
|
|
433
|
+
*/
|
|
434
|
+
getSelections(): CodeEditor.ITextSelection[] {
|
|
435
|
+
const selections = this.state.selection.ranges; //= [{anchor: number, head: number}]
|
|
436
|
+
if (selections.length > 0) {
|
|
437
|
+
const sel = selections.map(r => ({
|
|
438
|
+
anchor: this._toCodeMirrorPosition(this.getPositionAt(r.from)),
|
|
439
|
+
head: this._toCodeMirrorPosition(this.getPositionAt(r.to))
|
|
440
|
+
}));
|
|
441
|
+
return sel.map(selection => this._toSelection(selection));
|
|
442
|
+
}
|
|
443
|
+
const cursor = this._toCodeMirrorPosition(
|
|
444
|
+
this.getPositionAt(this.state.selection.main.head)
|
|
445
|
+
);
|
|
446
|
+
const selection = this._toSelection({ anchor: cursor, head: cursor });
|
|
447
|
+
return [selection];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Sets the selections for all the cursors, should not be empty.
|
|
452
|
+
* Cursors will be removed or added, as necessary.
|
|
453
|
+
* Passing an empty array resets a cursor position to the start of a document.
|
|
454
|
+
*/
|
|
455
|
+
setSelections(selections: CodeEditor.IRange[]): void {
|
|
456
|
+
const sel = selections.length
|
|
457
|
+
? selections.map(r =>
|
|
458
|
+
EditorSelection.range(
|
|
459
|
+
this.getOffsetAt(r.start),
|
|
460
|
+
this.getOffsetAt(r.end)
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
: [EditorSelection.range(0, 0)];
|
|
464
|
+
this.editor.dispatch({ selection: EditorSelection.create(sel) });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Replaces the current selection with the given text.
|
|
469
|
+
*
|
|
470
|
+
* Behaviour for multiple selections is undefined.
|
|
471
|
+
*
|
|
472
|
+
* @param text The text to be inserted.
|
|
473
|
+
*/
|
|
474
|
+
replaceSelection(text: string): void {
|
|
475
|
+
const firstSelection = this.getSelections()[0];
|
|
476
|
+
this.model.sharedModel.updateSource(
|
|
477
|
+
this.getOffsetAt(firstSelection.start),
|
|
478
|
+
this.getOffsetAt(firstSelection.end),
|
|
479
|
+
text
|
|
480
|
+
);
|
|
481
|
+
const newPosition = this.getPositionAt(
|
|
482
|
+
this.getOffsetAt(firstSelection.start) + text.length
|
|
483
|
+
);
|
|
484
|
+
this.setSelection({ start: newPosition, end: newPosition });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get a list of tokens for the current editor text content.
|
|
489
|
+
*/
|
|
490
|
+
getTokens(): CodeEditor.IToken[] {
|
|
491
|
+
const tokens: CodeEditor.IToken[] = [];
|
|
492
|
+
const tree = ensureSyntaxTree(this.state, this.doc.length);
|
|
493
|
+
if (tree) {
|
|
494
|
+
tree.iterate({
|
|
495
|
+
enter: (node: SyntaxNodeRef) => {
|
|
496
|
+
// If it has a child, it is not a leaf, but we still want to enter
|
|
497
|
+
if (node.node.firstChild !== null) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
tokens.push({
|
|
501
|
+
value: this.state.sliceDoc(node.from, node.to),
|
|
502
|
+
offset: node.from,
|
|
503
|
+
type: node.name
|
|
504
|
+
});
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return tokens;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Get the token at a given editor position.
|
|
514
|
+
*/
|
|
515
|
+
getTokenAt(offset: number): CodeEditor.IToken {
|
|
516
|
+
const tree = ensureSyntaxTree(this.state, offset);
|
|
517
|
+
let token: CodeEditor.IToken | null = null;
|
|
518
|
+
if (tree) {
|
|
519
|
+
tree.iterate({
|
|
520
|
+
enter: (node: SyntaxNodeRef) => {
|
|
521
|
+
// If it has a child, it is not a leaf, but we still want to enter
|
|
522
|
+
if (node.node.firstChild !== null) {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (offset >= node.from && offset <= node.to) {
|
|
526
|
+
token = {
|
|
527
|
+
value: this.state.sliceDoc(node.from, node.to),
|
|
528
|
+
offset: node.from,
|
|
529
|
+
type: node.name
|
|
530
|
+
};
|
|
531
|
+
// We have just found the relevant leaf token, no need to iterate further
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (token !== null) {
|
|
539
|
+
return token;
|
|
540
|
+
} else {
|
|
541
|
+
return {
|
|
542
|
+
value: '',
|
|
543
|
+
offset: offset
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get the token a the cursor position.
|
|
550
|
+
*/
|
|
551
|
+
getTokenAtCursor(): CodeEditor.IToken {
|
|
552
|
+
return this.getTokenAt(this.state.selection.main.head);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Insert a new indented line at the current cursor position.
|
|
557
|
+
*/
|
|
558
|
+
newIndentedLine(): void {
|
|
559
|
+
insertNewlineAndIndent({
|
|
560
|
+
state: this.state,
|
|
561
|
+
dispatch: this.editor.dispatch
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Execute a codemirror command on the editor.
|
|
567
|
+
*
|
|
568
|
+
* @param command - The name of the command to execute.
|
|
569
|
+
*/
|
|
570
|
+
execCommand(command: Command | StateCommand): void {
|
|
571
|
+
command(this.editor);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
protected onConfigChanged(
|
|
575
|
+
configurator: IExtensionsHandler,
|
|
576
|
+
changes: Record<string, any>
|
|
577
|
+
): void {
|
|
578
|
+
configurator.reconfigureExtensions(this._editor, changes);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Handle keydown events from the editor.
|
|
583
|
+
*/
|
|
584
|
+
protected onKeydown(event: KeyboardEvent): boolean {
|
|
585
|
+
const position = this.state.selection.main.head;
|
|
586
|
+
|
|
587
|
+
if (position === 0 && event.keyCode === UP_ARROW) {
|
|
588
|
+
if (!event.shiftKey) {
|
|
589
|
+
this.edgeRequested.emit('top');
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const line = this.doc.lineAt(position).number;
|
|
595
|
+
if (line === 1 && event.keyCode === UP_ARROW) {
|
|
596
|
+
if (!event.shiftKey) {
|
|
597
|
+
this.edgeRequested.emit('topLine');
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const length = this.doc.length;
|
|
603
|
+
if (position === length && event.keyCode === DOWN_ARROW) {
|
|
604
|
+
if (!event.shiftKey) {
|
|
605
|
+
this.edgeRequested.emit('bottom');
|
|
606
|
+
}
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Handles a mime type change.
|
|
615
|
+
*/
|
|
616
|
+
private _onMimeTypeChanged(): void {
|
|
617
|
+
// TODO: should we provide a hook for when the mode is done being set?
|
|
618
|
+
this._languages
|
|
619
|
+
.getLanguage(this._model.mimeType)
|
|
620
|
+
.then(language => {
|
|
621
|
+
this._editor.dispatch({
|
|
622
|
+
effects: this._language.reconfigure(language?.support ?? [])
|
|
623
|
+
});
|
|
624
|
+
})
|
|
625
|
+
.catch(reason => {
|
|
626
|
+
console.log(
|
|
627
|
+
`Failed to load language for '${this._model.mimeType}'.`,
|
|
628
|
+
reason
|
|
629
|
+
);
|
|
630
|
+
this._editor.dispatch({
|
|
631
|
+
effects: this._language.reconfigure([])
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Handles a cursor activity event.
|
|
638
|
+
*/
|
|
639
|
+
private _onCursorActivity(): void {
|
|
640
|
+
// Only add selections if the editor has focus. This avoids unwanted
|
|
641
|
+
// triggering of cursor activity due to collaborator actions.
|
|
642
|
+
if (this._editor.hasFocus) {
|
|
643
|
+
const selections = this.getSelections();
|
|
644
|
+
this.model.selections.set(this.uuid, selections);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Converts a code mirror selection to an editor selection.
|
|
650
|
+
*/
|
|
651
|
+
private _toSelection(selection: {
|
|
652
|
+
anchor: { line: number; ch: number };
|
|
653
|
+
head: { line: number; ch: number };
|
|
654
|
+
}): CodeEditor.ITextSelection {
|
|
655
|
+
return {
|
|
656
|
+
uuid: this.uuid,
|
|
657
|
+
start: this._toPosition(selection.anchor),
|
|
658
|
+
end: this._toPosition(selection.head)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Convert a code mirror position to an editor position.
|
|
664
|
+
*/
|
|
665
|
+
private _toPosition(position: { line: number; ch: number }) {
|
|
666
|
+
return {
|
|
667
|
+
line: position.line,
|
|
668
|
+
column: position.ch
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Convert an editor position to a code mirror position.
|
|
674
|
+
*/
|
|
675
|
+
private _toCodeMirrorPosition(position: CodeEditor.IPosition) {
|
|
676
|
+
return {
|
|
677
|
+
line: position.line,
|
|
678
|
+
ch: position.column
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Handles document changes.
|
|
684
|
+
*/
|
|
685
|
+
private _onDocChanged(update: ViewUpdate) {
|
|
686
|
+
if (update.transactions.length && update.transactions[0].selection) {
|
|
687
|
+
this._onCursorActivity();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Handle the DOM events for the editor.
|
|
693
|
+
*
|
|
694
|
+
* @param event - The DOM event sent to the editor.
|
|
695
|
+
*
|
|
696
|
+
* #### Notes
|
|
697
|
+
* This method implements the DOM `EventListener` interface and is
|
|
698
|
+
* called in response to events on the editor's DOM node. It should
|
|
699
|
+
* not be called directly by user code.
|
|
700
|
+
*/
|
|
701
|
+
handleEvent(event: Event): void {
|
|
702
|
+
switch (event.type) {
|
|
703
|
+
case 'focus':
|
|
704
|
+
this._evtFocus(event as FocusEvent);
|
|
705
|
+
break;
|
|
706
|
+
case 'blur':
|
|
707
|
+
this._evtBlur(event as FocusEvent);
|
|
708
|
+
break;
|
|
709
|
+
default:
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Handle `focus` events for the editor.
|
|
716
|
+
*/
|
|
717
|
+
private _evtFocus(event: FocusEvent): void {
|
|
718
|
+
this.host.classList.add('jp-mod-focused');
|
|
719
|
+
|
|
720
|
+
// Update the selections on editor gaining focus because
|
|
721
|
+
// the onCursorActivity function filters usual cursor events
|
|
722
|
+
// based on the editor's focus.
|
|
723
|
+
this._onCursorActivity();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Handle `blur` events for the editor.
|
|
728
|
+
*/
|
|
729
|
+
private _evtBlur(event: FocusEvent): void {
|
|
730
|
+
this.host.classList.remove('jp-mod-focused');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private _configurator: IExtensionsHandler;
|
|
734
|
+
private _editor: EditorView;
|
|
735
|
+
private _isDisposed = false;
|
|
736
|
+
private _language = new Compartment();
|
|
737
|
+
private _languages: IEditorLanguageRegistry;
|
|
738
|
+
private _model: CodeEditor.IModel;
|
|
739
|
+
private _uuid = '';
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* The namespace for `CodeMirrorEditor` statics.
|
|
744
|
+
*/
|
|
745
|
+
export namespace CodeMirrorEditor {
|
|
746
|
+
/**
|
|
747
|
+
* The options used to initialize a code mirror editor.
|
|
748
|
+
*/
|
|
749
|
+
export interface IOptions extends CodeEditor.IOptions {
|
|
750
|
+
/**
|
|
751
|
+
* CodeMirror extensions registry
|
|
752
|
+
*/
|
|
753
|
+
extensionsRegistry?: IEditorExtensionRegistry;
|
|
754
|
+
/**
|
|
755
|
+
* CodeMirror languages registry
|
|
756
|
+
*/
|
|
757
|
+
languages?: IEditorLanguageRegistry;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* The namespace for module private data.
|
|
763
|
+
*/
|
|
764
|
+
namespace Private {
|
|
765
|
+
export function createEditor(
|
|
766
|
+
host: HTMLElement,
|
|
767
|
+
editorConfig: IExtensionsHandler,
|
|
768
|
+
additionalExtensions: Extension[],
|
|
769
|
+
doc?: string
|
|
770
|
+
): EditorView {
|
|
771
|
+
const extensions = editorConfig.getInitialExtensions();
|
|
772
|
+
extensions.push(...additionalExtensions);
|
|
773
|
+
const view = new EditorView({
|
|
774
|
+
state: EditorState.create({
|
|
775
|
+
doc,
|
|
776
|
+
extensions
|
|
777
|
+
}),
|
|
778
|
+
parent: host
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
return view;
|
|
782
|
+
}
|
|
783
|
+
}
|