@adecore/editor 0.39.0 → 0.40.1
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/dist/controller.d.ts +1 -0
- package/dist/controller.js +17 -28
- package/dist/engine.js +2 -9
- package/dist/find.js +12 -13
- package/dist/layout.js +7 -2
- package/dist/paint.d.ts +2 -0
- package/dist/paint.js +23 -20
- package/dist/view.d.ts +3 -2
- package/dist/view.js +46 -59
- package/package.json +6 -6
- package/src/attribution.ts +14 -0
- package/src/code-block.ts +25 -0
- package/src/column-selection.ts +10 -0
- package/src/controller.ts +189 -28
- package/src/engine.ts +87 -9
- package/src/fake.ts +79 -0
- package/src/find-search.ts +20 -0
- package/src/find.ts +135 -14
- package/src/geometry-stops.ts +10 -0
- package/src/grammar-preparation.ts +23 -0
- package/src/index.ts +1 -0
- package/src/input.ts +13 -0
- package/src/keymap-table.ts +9 -0
- package/src/keymap.ts +25 -0
- package/src/layout-changes.ts +7 -0
- package/src/layout.ts +219 -3
- package/src/lens.ts +3 -0
- package/src/listeners.ts +1 -0
- package/src/metrics.ts +14 -0
- package/src/modifier-gesture.ts +17 -0
- package/src/native-input.ts +27 -0
- package/src/offsets.ts +10 -0
- package/src/outline.ts +38 -0
- package/src/overview.ts +13 -0
- package/src/paint.ts +140 -20
- package/src/php-html.ts +1 -0
- package/src/pointer.ts +50 -0
- package/src/scroll-animation.ts +10 -0
- package/src/scroll.ts +7 -0
- package/src/search-client.ts +32 -0
- package/src/search-worker.ts +38 -0
- package/src/semantic.ts +12 -0
- package/src/shiki-document.ts +19 -0
- package/src/shiki-worker.ts +68 -0
- package/src/shiki.ts +37 -0
- package/src/testing.ts +17 -0
- package/src/theme-scopes.ts +14 -0
- package/src/tokens.ts +31 -0
- package/src/touch.ts +16 -0
- package/src/tracked-range.ts +5 -0
- package/src/uniform-geometry.ts +10 -0
- package/src/view.ts +481 -75
- package/src/worker-client.ts +54 -0
package/src/controller.ts
CHANGED
|
@@ -119,6 +119,13 @@ export class InputController {
|
|
|
119
119
|
this.options = options;
|
|
120
120
|
this.gesture = new DoubleModifierGesture(options.apple ? 'Alt' : 'Control');
|
|
121
121
|
const { model, input, viewport } = view;
|
|
122
|
+
|
|
123
|
+
const lineAt = (offset: number): { from: number; to: number } => {
|
|
124
|
+
const row = view.layout.rowForLine(model.positionAt(offset).line);
|
|
125
|
+
|
|
126
|
+
return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
|
|
127
|
+
};
|
|
128
|
+
|
|
122
129
|
this.native = new NativeInput(input, model, {
|
|
123
130
|
readOnly: () => view.settings.readOnly,
|
|
124
131
|
replace: (text) => this.replaceSelections(text),
|
|
@@ -134,6 +141,7 @@ export class InputController {
|
|
|
134
141
|
view.revealCaret();
|
|
135
142
|
}
|
|
136
143
|
});
|
|
144
|
+
|
|
137
145
|
this.pointer = new PointerSelection({
|
|
138
146
|
model,
|
|
139
147
|
viewport,
|
|
@@ -141,10 +149,7 @@ export class InputController {
|
|
|
141
149
|
contentPoint: (x, y) => view.contentPoint(x, y),
|
|
142
150
|
columnSelections: (from, to) => (sameCell(view.layout, from, to) ? null : columnSelections(view.layout, from, to)),
|
|
143
151
|
visualLine: (offset) => view.layout.visualLine(offset),
|
|
144
|
-
lineAt
|
|
145
|
-
const row = view.layout.rowForLine(model.positionAt(offset).line);
|
|
146
|
-
return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
|
|
147
|
-
},
|
|
152
|
+
lineAt,
|
|
148
153
|
camelHumps: () => view.settings.smartKeys.camelHumps,
|
|
149
154
|
columnMode: () => view.columnMode,
|
|
150
155
|
selectionAt: (x, y) => (view.settings.readOnly ? null : view.selectionAtPoint(x, y)),
|
|
@@ -154,22 +159,23 @@ export class InputController {
|
|
|
154
159
|
focus: () => view.focus(),
|
|
155
160
|
scrolled: () => view.requestRender()
|
|
156
161
|
});
|
|
162
|
+
|
|
157
163
|
this.touch = new TouchSelection({
|
|
158
164
|
model,
|
|
159
165
|
offsetAt: (x, y) => view.offsetAtPoint(x, y),
|
|
160
166
|
camelHumps: () => view.settings.smartKeys.camelHumps,
|
|
161
|
-
lineAt
|
|
162
|
-
const row = view.layout.rowForLine(model.positionAt(offset).line);
|
|
163
|
-
return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
|
|
164
|
-
},
|
|
167
|
+
lineAt,
|
|
165
168
|
focus: () => view.focus()
|
|
166
169
|
});
|
|
170
|
+
|
|
167
171
|
this.subscription = model.subscribe(() => {
|
|
168
172
|
if (!this.keepDesiredXs) {
|
|
169
173
|
this.desiredXs = [];
|
|
170
174
|
}
|
|
175
|
+
|
|
171
176
|
this.native.sync();
|
|
172
177
|
});
|
|
178
|
+
|
|
173
179
|
this.native.sync();
|
|
174
180
|
this.attach();
|
|
175
181
|
}
|
|
@@ -185,55 +191,69 @@ export class InputController {
|
|
|
185
191
|
this.listen(input, 'keydown', (event) => this.keydown(event));
|
|
186
192
|
this.listen(input, 'beforeinput', (event) => this.beforeInput(event));
|
|
187
193
|
this.listen(input, 'input', () => this.native.onInput());
|
|
194
|
+
|
|
188
195
|
this.listen(input, 'compositionstart', () => {
|
|
189
196
|
this.historyGroup++;
|
|
190
197
|
this.native.startComposition();
|
|
191
198
|
});
|
|
199
|
+
|
|
192
200
|
this.listen(input, 'compositionend', (event) => {
|
|
193
201
|
this.native.endComposition(event.data);
|
|
194
202
|
this.historyGroup++;
|
|
195
203
|
this.view.render();
|
|
196
204
|
});
|
|
205
|
+
|
|
197
206
|
this.listen(input, 'select', () => this.native.readSelection());
|
|
198
207
|
this.listen(input, 'focus', () => this.view.requestRender());
|
|
208
|
+
|
|
199
209
|
this.listen(input, 'blur', () => {
|
|
200
210
|
this.gesture.reset();
|
|
201
211
|
this.view.requestRender();
|
|
202
212
|
});
|
|
213
|
+
|
|
203
214
|
this.listen(input, 'keyup', (event) => this.gesture.keyup(event));
|
|
215
|
+
|
|
204
216
|
this.listen(root, 'focusout', (event) => {
|
|
205
217
|
// A node by its type, as `instanceof Node` fails for one of another window, such as a window the opener mounted the editor in.
|
|
206
218
|
const related = event.relatedTarget as Node | null;
|
|
219
|
+
|
|
207
220
|
if (typeof related?.nodeType !== 'number' || !root.contains(related)) {
|
|
208
221
|
this.options.blur();
|
|
209
222
|
}
|
|
210
223
|
});
|
|
224
|
+
|
|
211
225
|
this.listen(input, 'paste', (event) => {
|
|
212
226
|
event.preventDefault();
|
|
213
227
|
this.paste(event.clipboardData?.getData('text/plain') ?? '');
|
|
214
228
|
});
|
|
229
|
+
|
|
215
230
|
this.listen(input, 'copy', (event) => this.copy(event, false));
|
|
216
231
|
this.listen(input, 'cut', (event) => this.copy(event, true));
|
|
217
232
|
this.listen(viewport, 'scroll', () => this.view.scrolled());
|
|
218
233
|
this.listen(viewport, 'wheel', () => this.view.cancelScroll());
|
|
219
234
|
this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
|
|
235
|
+
|
|
220
236
|
this.listen(viewport, 'pointerleave', () => {
|
|
221
237
|
this.view.setHover(null);
|
|
222
238
|
this.view.setInlayHover(null);
|
|
223
239
|
});
|
|
240
|
+
|
|
224
241
|
this.listen(viewport, 'pointerdown', (event) => this.pointerDown(event));
|
|
225
242
|
this.listen(viewport, 'contextmenu', (event) => this.contextMenu(event));
|
|
226
243
|
this.listen(this.view.gutterElement, 'pointerdown', (event) => this.gutterDown(event));
|
|
227
244
|
this.listen(this.view.stickyElement, 'pointerdown', (event) => this.stickyDown(event));
|
|
245
|
+
|
|
228
246
|
const move = (event: Event): void => {
|
|
229
247
|
this.pointer.move(event as PointerEvent);
|
|
230
248
|
this.touch.move(event as PointerEvent);
|
|
231
249
|
};
|
|
250
|
+
|
|
232
251
|
const end = (event: Event): void => {
|
|
233
252
|
this.pointer.end(event as PointerEvent);
|
|
234
253
|
this.touch.end(event as PointerEvent);
|
|
235
254
|
this.historyGroup++;
|
|
236
255
|
};
|
|
256
|
+
|
|
237
257
|
document.addEventListener('pointermove', move);
|
|
238
258
|
document.addEventListener('pointerup', end);
|
|
239
259
|
document.addEventListener('pointercancel', end);
|
|
@@ -241,6 +261,7 @@ export class InputController {
|
|
|
241
261
|
const visualViewport = document.defaultView?.visualViewport;
|
|
242
262
|
let keyboardHeight = visualViewport?.height ?? 0;
|
|
243
263
|
let keyboardScale = visualViewport?.scale ?? 1;
|
|
264
|
+
|
|
244
265
|
const keyboard = (): void => {
|
|
245
266
|
const height = visualViewport?.height ?? 0;
|
|
246
267
|
const scale = visualViewport?.scale ?? 1;
|
|
@@ -248,11 +269,14 @@ export class InputController {
|
|
|
248
269
|
const shrunk = height < keyboardHeight && scale === keyboardScale;
|
|
249
270
|
keyboardHeight = height;
|
|
250
271
|
keyboardScale = scale;
|
|
272
|
+
|
|
251
273
|
if (shrunk && this.view.focused) {
|
|
252
274
|
this.view.revealCaret();
|
|
253
275
|
}
|
|
254
276
|
};
|
|
277
|
+
|
|
255
278
|
visualViewport?.addEventListener('resize', keyboard);
|
|
279
|
+
|
|
256
280
|
this.cleanup.push(() => {
|
|
257
281
|
visualViewport?.removeEventListener('resize', keyboard);
|
|
258
282
|
document.removeEventListener('pointermove', move);
|
|
@@ -265,30 +289,28 @@ export class InputController {
|
|
|
265
289
|
this.subscription.dispose();
|
|
266
290
|
this.pointer.dispose();
|
|
267
291
|
this.touch.dispose();
|
|
292
|
+
|
|
268
293
|
for (const remove of this.cleanup) {
|
|
269
294
|
remove();
|
|
270
295
|
}
|
|
296
|
+
|
|
271
297
|
this.cleanup.length = 0;
|
|
272
298
|
this.keyHandlers.clear();
|
|
273
299
|
}
|
|
274
300
|
|
|
275
|
-
/* Tells a person why nothing happened, when the editor is read only and has said why. */
|
|
276
301
|
private blockedByReadOnly(): boolean {
|
|
277
|
-
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
if (this.view.settings.readOnlyReason) {
|
|
281
|
-
this.view.notify(this.view.settings.readOnlyReason);
|
|
282
|
-
}
|
|
283
|
-
return true;
|
|
302
|
+
return this.view.blockedByReadOnly();
|
|
284
303
|
}
|
|
285
304
|
|
|
286
305
|
private inputGroup(): string {
|
|
287
306
|
const now = Date.now();
|
|
307
|
+
|
|
288
308
|
if (now - this.lastInputAt > TYPING_PAUSE_MS) {
|
|
289
309
|
this.historyGroup++;
|
|
290
310
|
}
|
|
311
|
+
|
|
291
312
|
this.lastInputAt = now;
|
|
313
|
+
|
|
292
314
|
return `view-input-${this.historyGroup}`;
|
|
293
315
|
}
|
|
294
316
|
|
|
@@ -296,6 +318,7 @@ export class InputController {
|
|
|
296
318
|
if (this.view.settings.readOnly) {
|
|
297
319
|
return false;
|
|
298
320
|
}
|
|
321
|
+
|
|
299
322
|
return this.view.model.applyEdits(edits, { selections, source: 'input', historyGroup: this.inputGroup() });
|
|
300
323
|
}
|
|
301
324
|
|
|
@@ -303,25 +326,28 @@ export class InputController {
|
|
|
303
326
|
if (this.blockedByReadOnly()) {
|
|
304
327
|
return;
|
|
305
328
|
}
|
|
329
|
+
|
|
306
330
|
const { model } = this.view;
|
|
307
|
-
|
|
308
|
-
this.view.ensureVisible(selection.head);
|
|
309
|
-
}
|
|
331
|
+
this.unfoldCarets();
|
|
310
332
|
const replacement = replacementEdits(model.getSelections(), this.native.normalizeNewlines(text));
|
|
311
333
|
this.applyEdits(replacement.edits, replacement.selections);
|
|
312
334
|
this.view.revealCaret();
|
|
313
335
|
}
|
|
314
336
|
|
|
337
|
+
private unfoldCarets(): void {
|
|
338
|
+
for (const selection of this.view.model.getSelections()) {
|
|
339
|
+
this.view.ensureVisible(selection.head);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
315
343
|
private paste(text: string): void {
|
|
316
344
|
if (this.blockedByReadOnly()) {
|
|
317
345
|
return;
|
|
318
346
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.view.ensureVisible(selection.head);
|
|
322
|
-
}
|
|
347
|
+
|
|
348
|
+
this.unfoldCarets();
|
|
323
349
|
this.historyGroup++;
|
|
324
|
-
model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
|
|
350
|
+
this.view.model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
|
|
325
351
|
this.historyGroup++;
|
|
326
352
|
this.view.revealCaret();
|
|
327
353
|
}
|
|
@@ -329,6 +355,7 @@ export class InputController {
|
|
|
329
355
|
private commandOptions(): CommandOptions {
|
|
330
356
|
const { settings } = this.view;
|
|
331
357
|
const keys = settings.smartKeys;
|
|
358
|
+
|
|
332
359
|
return {
|
|
333
360
|
tabSize: settings.tabSize,
|
|
334
361
|
insertSpaces: settings.insertSpaces,
|
|
@@ -350,9 +377,11 @@ export class InputController {
|
|
|
350
377
|
/* Folding is the view's, since it knows where the ranges are. */
|
|
351
378
|
private viewCommand(name: EditorViewCommand): boolean {
|
|
352
379
|
const { view } = this;
|
|
380
|
+
|
|
353
381
|
if (view.settings.folding === false && name !== 'toggleColumnMode') {
|
|
354
382
|
return false;
|
|
355
383
|
}
|
|
384
|
+
|
|
356
385
|
const run: Record<EditorViewCommand, () => void> = {
|
|
357
386
|
collapseRegion: () => view.collapseRegion(),
|
|
358
387
|
expandRegion: () => view.expandRegion(),
|
|
@@ -370,7 +399,9 @@ export class InputController {
|
|
|
370
399
|
expandAllToLevel5: () => view.expandAllToLevel(5),
|
|
371
400
|
toggleColumnMode: () => this.toggleColumnMode()
|
|
372
401
|
};
|
|
402
|
+
|
|
373
403
|
run[name]();
|
|
404
|
+
|
|
374
405
|
return true;
|
|
375
406
|
}
|
|
376
407
|
|
|
@@ -379,11 +410,14 @@ export class InputController {
|
|
|
379
410
|
if (VIEW_COMMANDS.has(name as EditorViewCommand)) {
|
|
380
411
|
return this.viewCommand(name as EditorViewCommand);
|
|
381
412
|
}
|
|
413
|
+
|
|
382
414
|
if (name === 'expandSelection' && this.selectionRanges !== null) {
|
|
383
415
|
const provider = this.selectionRanges;
|
|
384
416
|
this.expansions = this.expansions.then(() => this.expandThrough(provider));
|
|
417
|
+
|
|
385
418
|
return true;
|
|
386
419
|
}
|
|
420
|
+
|
|
387
421
|
return this.modelCommand(name as EditorCommand);
|
|
388
422
|
}
|
|
389
423
|
|
|
@@ -397,18 +431,25 @@ export class InputController {
|
|
|
397
431
|
const selections = model.getSelections();
|
|
398
432
|
const revision = model.getRevision();
|
|
399
433
|
const stretches = selections.map(({ anchor, head }) => ({ from: Math.min(anchor, head), to: Math.max(anchor, head) }));
|
|
434
|
+
|
|
400
435
|
const asked = stretches.map(({ from }) => {
|
|
401
436
|
const { line, column } = model.positionAt(from);
|
|
437
|
+
|
|
402
438
|
return { line, character: column };
|
|
403
439
|
});
|
|
440
|
+
|
|
404
441
|
const chains = await provider(asked).catch(() => null);
|
|
405
|
-
|
|
442
|
+
|
|
443
|
+
if (model.getRevision() !== revision || selectionSignature(model.getSelections()) !== selectionSignature(selections)) {
|
|
406
444
|
return;
|
|
407
445
|
}
|
|
446
|
+
|
|
408
447
|
if (chains === null) {
|
|
409
448
|
this.modelCommand('expandSelection');
|
|
449
|
+
|
|
410
450
|
return;
|
|
411
451
|
}
|
|
452
|
+
|
|
412
453
|
const ranges = stretches.map(
|
|
413
454
|
({ from, to }, index) =>
|
|
414
455
|
(chains[index] ?? [])
|
|
@@ -418,6 +459,7 @@ export class InputController {
|
|
|
418
459
|
}))
|
|
419
460
|
.find((range) => range.from <= from && range.to >= to && (range.from < from || range.to > to)) ?? null
|
|
420
461
|
);
|
|
462
|
+
|
|
421
463
|
this.historyGroup++;
|
|
422
464
|
model.expandSelectionTo(ranges);
|
|
423
465
|
this.view.revealCaret();
|
|
@@ -426,35 +468,46 @@ export class InputController {
|
|
|
426
468
|
private modelCommand(name: EditorCommand): boolean {
|
|
427
469
|
const { model } = this.view;
|
|
428
470
|
const modifies = MODIFYING.has(name);
|
|
471
|
+
|
|
429
472
|
if ((modifies || name === 'undo' || name === 'redo') && this.blockedByReadOnly()) {
|
|
430
473
|
return false;
|
|
431
474
|
}
|
|
475
|
+
|
|
432
476
|
this.historyGroup++;
|
|
477
|
+
|
|
433
478
|
if (modifies) {
|
|
434
479
|
for (const selection of model.getSelections()) {
|
|
435
480
|
this.view.ensureVisible(selection.head);
|
|
436
481
|
const position = model.positionAt(selection.head);
|
|
437
482
|
const line = model.getLine(position.line);
|
|
483
|
+
|
|
438
484
|
if ((name === 'smartBackspace' || name === 'deleteWordLeft') && selection.head === line.start && position.line > 0) {
|
|
439
485
|
this.view.ensureVisible(model.getLine(position.line - 1).start);
|
|
440
486
|
}
|
|
487
|
+
|
|
441
488
|
if ((name === 'deleteForward' || name === 'deleteWordRight') && selection.head === line.end && position.line + 1 < model.getLineCount()) {
|
|
442
489
|
this.view.ensureVisible(model.getLine(position.line + 1).start);
|
|
443
490
|
}
|
|
444
491
|
}
|
|
445
492
|
}
|
|
493
|
+
|
|
446
494
|
const folded = name === 'moveLineUp' || name === 'moveLineDown' ? this.view.getFolds().collapsed : [];
|
|
495
|
+
|
|
447
496
|
const result = model.execute(name, {
|
|
448
497
|
...this.commandOptions(),
|
|
449
498
|
onLinesMoved: (moves) => this.view.refoldMoved(folded, moves)
|
|
450
499
|
});
|
|
500
|
+
|
|
451
501
|
if (name === 'selectNextOccurrence' && model.occurrencesExhausted && this.view.settings.messages.noMoreOccurrences !== undefined) {
|
|
452
502
|
this.view.notify(this.view.settings.messages.noMoreOccurrences);
|
|
453
503
|
}
|
|
504
|
+
|
|
454
505
|
if (name === 'smartEnd' || name === 'selectSmartEnd') {
|
|
455
506
|
this.view.markRowEnd(model.getPrimary().head);
|
|
456
507
|
}
|
|
508
|
+
|
|
457
509
|
this.view.revealCaret();
|
|
510
|
+
|
|
458
511
|
return result;
|
|
459
512
|
}
|
|
460
513
|
|
|
@@ -462,9 +515,9 @@ export class InputController {
|
|
|
462
515
|
if (this.blockedByReadOnly()) {
|
|
463
516
|
return;
|
|
464
517
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
518
|
+
|
|
519
|
+
this.unfoldCarets();
|
|
520
|
+
|
|
468
521
|
// Every key goes through the model, which decides by the key before it too, as for the arrow of PHP.
|
|
469
522
|
if (text.length === 1) {
|
|
470
523
|
this.view.model.typeText(text, { ...this.commandOptions(), historyGroup: this.inputGroup() });
|
|
@@ -484,11 +537,14 @@ export class InputController {
|
|
|
484
537
|
const scrollTop = direction < 0 ? from.y - page : Math.min(layout.height - height, from.y + page);
|
|
485
538
|
this.view.scrollTo(from.x, Math.max(0, layout.lineBase(scrollTop)));
|
|
486
539
|
const distance = direction < 0 ? height : height + lineHeight - 1;
|
|
540
|
+
|
|
487
541
|
const selections = model.getSelections().map((selection, index) => {
|
|
488
542
|
this.desiredXs[index] ??= layout.caret(selection.head).x;
|
|
489
543
|
const head = layout.verticalOffset(selection.head, direction, this.desiredXs[index]!, distance);
|
|
544
|
+
|
|
490
545
|
return { anchor: extend ? selection.anchor : head, head };
|
|
491
546
|
});
|
|
547
|
+
|
|
492
548
|
this.keepDesiredXs = true;
|
|
493
549
|
model.setSelections(selections);
|
|
494
550
|
this.keepDesiredXs = false;
|
|
@@ -501,16 +557,20 @@ export class InputController {
|
|
|
501
557
|
const { view } = this;
|
|
502
558
|
const { model } = view;
|
|
503
559
|
this.block = null;
|
|
560
|
+
|
|
504
561
|
if (view.columnMode) {
|
|
505
562
|
const first = model.getSelections()[0]!;
|
|
506
563
|
const last = model.getPrimary();
|
|
507
564
|
view.setColumnMode(false);
|
|
508
565
|
model.setSelections([{ anchor: first.anchor, head: last.head }]);
|
|
566
|
+
|
|
509
567
|
return;
|
|
510
568
|
}
|
|
569
|
+
|
|
511
570
|
view.setColumnMode(true);
|
|
512
571
|
const primary = model.getPrimary();
|
|
513
572
|
model.setSelections([primary]);
|
|
573
|
+
|
|
514
574
|
if (primary.anchor !== primary.head) {
|
|
515
575
|
this.block = this.blockOf(primary, '');
|
|
516
576
|
this.extendBlock({ x: 0, y: 0 });
|
|
@@ -521,14 +581,18 @@ export class InputController {
|
|
|
521
581
|
private extendBlock(step: { x: number; y: number }): void {
|
|
522
582
|
const { model, layout } = this.view;
|
|
523
583
|
const signature = selectionSignature(model.getSelections());
|
|
584
|
+
|
|
524
585
|
if (this.block === null || (this.block.signature !== '' && this.block.signature !== signature)) {
|
|
525
586
|
this.block = this.blockOf(model.getPrimary(), signature);
|
|
526
587
|
}
|
|
588
|
+
|
|
527
589
|
const block = this.block;
|
|
590
|
+
|
|
528
591
|
block.head = {
|
|
529
592
|
x: Math.max(0, block.head.x + step.x),
|
|
530
593
|
y: Math.max(0, Math.min(layout.height - layout.metrics.lineHeight, block.head.y + step.y))
|
|
531
594
|
};
|
|
595
|
+
|
|
532
596
|
model.setSelections(columnSelections(layout, block.anchor, block.head));
|
|
533
597
|
block.signature = selectionSignature(model.getSelections());
|
|
534
598
|
}
|
|
@@ -538,52 +602,68 @@ export class InputController {
|
|
|
538
602
|
const { layout } = this.view;
|
|
539
603
|
const anchor = layout.caret(selection.anchor);
|
|
540
604
|
const head = layout.caret(selection.head);
|
|
605
|
+
|
|
541
606
|
return { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature };
|
|
542
607
|
}
|
|
543
608
|
|
|
544
609
|
private moveCarets(key: MoveKey, extend: boolean): void {
|
|
545
610
|
if (this.view.columnMode && extend && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
|
|
546
611
|
const { lineHeight, charWidth } = this.view.layout.metrics;
|
|
612
|
+
|
|
547
613
|
this.extendBlock({
|
|
548
614
|
x: key === 'ArrowLeft' ? -charWidth : key === 'ArrowRight' ? charWidth : 0,
|
|
549
615
|
y: key === 'ArrowUp' ? -lineHeight : key === 'ArrowDown' ? lineHeight : 0
|
|
550
616
|
});
|
|
617
|
+
|
|
551
618
|
this.view.revealCaret();
|
|
619
|
+
|
|
552
620
|
return;
|
|
553
621
|
}
|
|
622
|
+
|
|
554
623
|
if (key === 'PageUp' || key === 'PageDown') {
|
|
555
624
|
this.movePages(key === 'PageUp' ? -1 : 1, extend);
|
|
625
|
+
|
|
556
626
|
return;
|
|
557
627
|
}
|
|
628
|
+
|
|
558
629
|
const { model, layout } = this.view;
|
|
559
630
|
const vertical = key === 'ArrowUp' || key === 'ArrowDown';
|
|
560
631
|
const direction = key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1;
|
|
632
|
+
|
|
561
633
|
const edge = (selection: Selection): number =>
|
|
562
634
|
direction < 0 ? Math.min(selection.anchor, selection.head) : Math.max(selection.anchor, selection.head);
|
|
635
|
+
|
|
563
636
|
const selections = model.getSelections().map((selection, index) => {
|
|
564
637
|
const collapses = !extend && selection.anchor !== selection.head;
|
|
565
638
|
let head: number;
|
|
639
|
+
|
|
566
640
|
if (!vertical && collapses) {
|
|
567
641
|
head = edge(selection);
|
|
568
642
|
} else if (vertical) {
|
|
569
643
|
// A selection first gives way to the edge in the direction of the move, as the platform's Up and Down do.
|
|
570
644
|
const from = collapses ? edge(selection) : selection.head;
|
|
645
|
+
|
|
571
646
|
if (collapses) {
|
|
572
647
|
delete this.desiredXs[index];
|
|
573
648
|
}
|
|
649
|
+
|
|
574
650
|
this.desiredXs[index] ??= layout.caret(from).x;
|
|
575
651
|
head = layout.verticalOffset(from, direction, this.desiredXs[index]!);
|
|
576
652
|
} else {
|
|
577
653
|
head = layout.horizontalOffset(selection.head, direction);
|
|
578
654
|
}
|
|
655
|
+
|
|
579
656
|
return { anchor: extend ? selection.anchor : head, head };
|
|
580
657
|
});
|
|
658
|
+
|
|
581
659
|
this.keepDesiredXs = vertical;
|
|
582
660
|
model.setSelections(selections);
|
|
583
661
|
this.keepDesiredXs = false;
|
|
662
|
+
|
|
584
663
|
if (!vertical) {
|
|
585
664
|
this.desiredXs = [];
|
|
586
665
|
}
|
|
666
|
+
|
|
587
667
|
this.historyGroup++;
|
|
588
668
|
this.view.revealCaret();
|
|
589
669
|
}
|
|
@@ -593,11 +673,13 @@ export class InputController {
|
|
|
593
673
|
if (this.blockedByReadOnly()) {
|
|
594
674
|
return;
|
|
595
675
|
}
|
|
676
|
+
|
|
596
677
|
const { model } = this.view;
|
|
597
678
|
const text = model.slice(from, to);
|
|
598
679
|
this.view.ensureVisible(offset);
|
|
599
680
|
const start = copy || offset < from ? offset : offset - (to - from);
|
|
600
681
|
this.historyGroup++;
|
|
682
|
+
|
|
601
683
|
model.applyEdits(
|
|
602
684
|
copy
|
|
603
685
|
? [{ from: offset, to: offset, text }]
|
|
@@ -610,6 +692,7 @@ export class InputController {
|
|
|
610
692
|
selections: [{ anchor: start, head: start + text.length }]
|
|
611
693
|
}
|
|
612
694
|
);
|
|
695
|
+
|
|
613
696
|
this.historyGroup++;
|
|
614
697
|
this.view.revealCaret();
|
|
615
698
|
}
|
|
@@ -617,17 +700,22 @@ export class InputController {
|
|
|
617
700
|
private goToMatchingBracket(): void {
|
|
618
701
|
const { model } = this.view;
|
|
619
702
|
let changed = false;
|
|
703
|
+
|
|
620
704
|
model.setSelections(
|
|
621
705
|
model.getSelections().map((selection) => {
|
|
622
706
|
const bracket = this.view.matchingBracket(selection);
|
|
707
|
+
|
|
623
708
|
if (!bracket || bracket.mate === undefined) {
|
|
624
709
|
return selection;
|
|
625
710
|
}
|
|
711
|
+
|
|
626
712
|
const head = bracket.mate > bracket.at ? bracket.mate + 1 : bracket.mate;
|
|
627
713
|
changed = true;
|
|
714
|
+
|
|
628
715
|
return { anchor: head, head };
|
|
629
716
|
})
|
|
630
717
|
);
|
|
718
|
+
|
|
631
719
|
if (changed) {
|
|
632
720
|
this.view.revealCaret();
|
|
633
721
|
}
|
|
@@ -635,45 +723,55 @@ export class InputController {
|
|
|
635
723
|
|
|
636
724
|
private run(action: KeyAction, event: KeyboardEvent): void {
|
|
637
725
|
const { model } = this.view;
|
|
726
|
+
|
|
638
727
|
switch (action.type) {
|
|
639
728
|
case 'save':
|
|
640
729
|
event.preventDefault();
|
|
641
730
|
this.options.save();
|
|
731
|
+
|
|
642
732
|
return;
|
|
643
733
|
case 'command':
|
|
644
734
|
event.preventDefault();
|
|
645
735
|
this.command(action.command);
|
|
736
|
+
|
|
646
737
|
return;
|
|
647
738
|
case 'move':
|
|
648
739
|
event.preventDefault();
|
|
649
740
|
this.moveCarets(action.key, action.extend);
|
|
741
|
+
|
|
650
742
|
return;
|
|
651
743
|
case 'edge': {
|
|
652
744
|
event.preventDefault();
|
|
653
745
|
const head = action.end ? model.getLength() : 0;
|
|
654
746
|
model.setSelections([{ anchor: action.extend ? model.getPrimary().anchor : head, head }]);
|
|
655
747
|
this.view.revealCaret();
|
|
748
|
+
|
|
656
749
|
return;
|
|
657
750
|
}
|
|
658
751
|
case 'bracket':
|
|
659
752
|
event.preventDefault();
|
|
660
753
|
this.goToMatchingBracket();
|
|
754
|
+
|
|
661
755
|
return;
|
|
662
756
|
case 'view':
|
|
663
757
|
event.preventDefault();
|
|
664
758
|
this.viewCommand(action.command);
|
|
759
|
+
|
|
665
760
|
return;
|
|
666
761
|
case 'escape': {
|
|
667
762
|
const selections = model.getSelections();
|
|
763
|
+
|
|
668
764
|
if (selections.length > 1 || selections[0]!.anchor !== selections[0]!.head) {
|
|
669
765
|
event.preventDefault();
|
|
670
766
|
// The oldest caret stays; the newest ones go.
|
|
671
767
|
const head = selections[0]!.head;
|
|
672
768
|
model.setSelections([{ anchor: head, head }]);
|
|
769
|
+
|
|
673
770
|
if (selections.length > 1) {
|
|
674
771
|
this.view.revealCaret();
|
|
675
772
|
}
|
|
676
773
|
}
|
|
774
|
+
|
|
677
775
|
return;
|
|
678
776
|
}
|
|
679
777
|
}
|
|
@@ -700,23 +798,31 @@ export class InputController {
|
|
|
700
798
|
if (event.isComposing || this.native.composing) {
|
|
701
799
|
return;
|
|
702
800
|
}
|
|
801
|
+
|
|
703
802
|
const added = this.gesture.keydown(event);
|
|
803
|
+
|
|
704
804
|
if (added !== null) {
|
|
705
805
|
event.preventDefault();
|
|
706
806
|
this.command(added === 'above' ? 'addCaretAbove' : 'addCaretBelow');
|
|
807
|
+
|
|
707
808
|
return;
|
|
708
809
|
}
|
|
810
|
+
|
|
709
811
|
for (const handler of [...this.keyHandlers]) {
|
|
710
812
|
if (handler(event)) {
|
|
711
813
|
event.preventDefault();
|
|
814
|
+
|
|
712
815
|
return;
|
|
713
816
|
}
|
|
714
817
|
}
|
|
818
|
+
|
|
715
819
|
// The app's own shortcuts stay the app's, also where the editor has a binding for the same key.
|
|
716
820
|
if (this.options.handBack.some((chord) => chordMatches(chord, event, this.options.apple))) {
|
|
717
821
|
return;
|
|
718
822
|
}
|
|
823
|
+
|
|
719
824
|
const action = keyAction(event, this.options.apple, this.options.keymap);
|
|
825
|
+
|
|
720
826
|
if (action) {
|
|
721
827
|
this.run(action, event);
|
|
722
828
|
}
|
|
@@ -726,36 +832,45 @@ export class InputController {
|
|
|
726
832
|
if (this.view.settings.readOnly) {
|
|
727
833
|
event.preventDefault();
|
|
728
834
|
this.blockedByReadOnly();
|
|
835
|
+
|
|
729
836
|
return;
|
|
730
837
|
}
|
|
838
|
+
|
|
731
839
|
if (this.native.composing || event.isComposing) {
|
|
732
840
|
return;
|
|
733
841
|
}
|
|
842
|
+
|
|
734
843
|
switch (event.inputType) {
|
|
735
844
|
case 'insertText':
|
|
736
845
|
case 'insertReplacementText':
|
|
737
846
|
event.preventDefault();
|
|
847
|
+
|
|
738
848
|
if (event.data !== null) {
|
|
739
849
|
this.typeText(event.data);
|
|
740
850
|
}
|
|
851
|
+
|
|
741
852
|
return;
|
|
742
853
|
case 'insertLineBreak':
|
|
743
854
|
case 'insertParagraph':
|
|
744
855
|
event.preventDefault();
|
|
745
856
|
this.command('insertNewline');
|
|
857
|
+
|
|
746
858
|
return;
|
|
747
859
|
case 'deleteContentBackward':
|
|
748
860
|
event.preventDefault();
|
|
749
861
|
this.command('smartBackspace');
|
|
862
|
+
|
|
750
863
|
return;
|
|
751
864
|
case 'deleteContentForward':
|
|
752
865
|
event.preventDefault();
|
|
753
866
|
this.command('deleteForward');
|
|
867
|
+
|
|
754
868
|
return;
|
|
755
869
|
case 'historyUndo':
|
|
756
870
|
case 'historyRedo':
|
|
757
871
|
event.preventDefault();
|
|
758
872
|
this.command(event.inputType === 'historyUndo' ? 'undo' : 'redo');
|
|
873
|
+
|
|
759
874
|
return;
|
|
760
875
|
}
|
|
761
876
|
}
|
|
@@ -766,32 +881,40 @@ export class InputController {
|
|
|
766
881
|
const { model } = this.view;
|
|
767
882
|
const selections = model.getSelections();
|
|
768
883
|
const empty = selections.every((selection) => selection.anchor === selection.head);
|
|
884
|
+
|
|
769
885
|
const text = empty
|
|
770
886
|
? selections
|
|
771
887
|
.map((selection) => {
|
|
772
888
|
const { first, last } = this.view.lineSpan(model.positionAt(selection.head).line);
|
|
773
889
|
const end = model.getLine(last);
|
|
890
|
+
|
|
774
891
|
return model.slice(model.getLine(first).start, end.next) + (end.next === end.end ? '\n' : '');
|
|
775
892
|
})
|
|
776
893
|
.join('')
|
|
777
894
|
: selections.map((selection) => model.slice(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head))).join('\n');
|
|
895
|
+
|
|
778
896
|
event.clipboardData?.setData('text/plain', text);
|
|
779
897
|
lineClip = empty ? text : null;
|
|
898
|
+
|
|
780
899
|
if (!cut || this.blockedByReadOnly()) {
|
|
781
900
|
return;
|
|
782
901
|
}
|
|
902
|
+
|
|
783
903
|
this.historyGroup++;
|
|
904
|
+
|
|
784
905
|
if (empty) {
|
|
785
906
|
this.command('deleteLine');
|
|
786
907
|
} else {
|
|
787
908
|
this.replaceSelections('');
|
|
788
909
|
}
|
|
910
|
+
|
|
789
911
|
this.historyGroup++;
|
|
790
912
|
}
|
|
791
913
|
|
|
792
914
|
private pointerDown(event: PointerEvent): void {
|
|
793
915
|
this.view.cancelScroll();
|
|
794
916
|
const target = event.target as HTMLElement;
|
|
917
|
+
|
|
795
918
|
// A press a part took already, such as a pinned header, which draws itself anew and so takes its target off the page before the press bubbles here.
|
|
796
919
|
if (
|
|
797
920
|
event.defaultPrevented ||
|
|
@@ -800,32 +923,42 @@ export class InputController {
|
|
|
800
923
|
) {
|
|
801
924
|
return;
|
|
802
925
|
}
|
|
926
|
+
|
|
803
927
|
// Cancels the compatibility mouse events, whose focus change would close the keyboard a tap opened; scrolling is not affected.
|
|
804
928
|
event.preventDefault();
|
|
929
|
+
|
|
805
930
|
if (this.takenByInlayHandler(event)) {
|
|
806
931
|
return;
|
|
807
932
|
}
|
|
933
|
+
|
|
808
934
|
if (event.pointerType === 'touch') {
|
|
809
935
|
this.touch.start(event, 'text', () => this.takenByHost(event));
|
|
936
|
+
|
|
810
937
|
return;
|
|
811
938
|
}
|
|
939
|
+
|
|
812
940
|
if (this.takenByHost(event)) {
|
|
813
941
|
this.view.focus();
|
|
942
|
+
|
|
814
943
|
return;
|
|
815
944
|
}
|
|
945
|
+
|
|
816
946
|
this.pointer.start(event);
|
|
817
947
|
}
|
|
818
948
|
|
|
819
949
|
/* Whether a handler of the host took a press on a hint, which then moves neither the caret nor the focus. */
|
|
820
950
|
private takenByInlayHandler(event: PointerEvent): boolean {
|
|
821
951
|
const inlay = this.inlayHandlers.size === 0 ? null : this.view.inlayAt(event.target);
|
|
952
|
+
|
|
822
953
|
if (inlay === null) {
|
|
823
954
|
return false;
|
|
824
955
|
}
|
|
956
|
+
|
|
825
957
|
const now = Date.now();
|
|
826
958
|
const again = this.lastInlayPress?.id === inlay.id && now - this.lastInlayPress.at <= DOUBLE_PRESS_MS;
|
|
827
959
|
this.lastInlayPress = again ? null : { id: inlay.id, at: now };
|
|
828
960
|
const touch = event.pointerType === 'touch';
|
|
961
|
+
|
|
829
962
|
const click: EditorInlayClick = {
|
|
830
963
|
id: inlay.id,
|
|
831
964
|
part: inlay.part,
|
|
@@ -835,12 +968,15 @@ export class InputController {
|
|
|
835
968
|
clicks: again ? 2 : 1,
|
|
836
969
|
touch
|
|
837
970
|
};
|
|
971
|
+
|
|
838
972
|
if (![...this.inlayHandlers].some((handler) => handler(click))) {
|
|
839
973
|
return false;
|
|
840
974
|
}
|
|
975
|
+
|
|
841
976
|
if (!touch) {
|
|
842
977
|
this.view.focus();
|
|
843
978
|
}
|
|
979
|
+
|
|
844
980
|
return true;
|
|
845
981
|
}
|
|
846
982
|
|
|
@@ -849,37 +985,47 @@ export class InputController {
|
|
|
849
985
|
if (this.clickHandlers.size === 0) {
|
|
850
986
|
return false;
|
|
851
987
|
}
|
|
988
|
+
|
|
852
989
|
const offset = this.view.characterAtPoint(event.clientX, event.clientY, event.target);
|
|
990
|
+
|
|
853
991
|
if (offset === null) {
|
|
854
992
|
return false;
|
|
855
993
|
}
|
|
994
|
+
|
|
856
995
|
const click: EditorClick = {
|
|
857
996
|
position: this.positionOf(offset),
|
|
858
997
|
mod: this.options.apple ? event.metaKey : event.ctrlKey,
|
|
859
998
|
alt: event.altKey,
|
|
860
999
|
shift: event.shiftKey
|
|
861
1000
|
};
|
|
1001
|
+
|
|
862
1002
|
return [...this.clickHandlers].some((handler) => handler(click));
|
|
863
1003
|
}
|
|
864
1004
|
|
|
865
1005
|
private positionOf(offset: number): EditorPosition {
|
|
866
1006
|
const { line, column } = this.view.model.positionAt(offset);
|
|
1007
|
+
|
|
867
1008
|
return { line, character: column };
|
|
868
1009
|
}
|
|
869
1010
|
|
|
870
1011
|
private contextMenu(event: MouseEvent): void {
|
|
871
1012
|
if (this.contextListeners.size === 0 && this.touch.pressed) {
|
|
872
1013
|
event.preventDefault();
|
|
1014
|
+
|
|
873
1015
|
return;
|
|
874
1016
|
}
|
|
1017
|
+
|
|
875
1018
|
if (this.contextListeners.size === 0 || (event.target as HTMLElement | null)?.closest?.('.se-sticky')) {
|
|
876
1019
|
return;
|
|
877
1020
|
}
|
|
1021
|
+
|
|
878
1022
|
event.preventDefault();
|
|
879
1023
|
const offset = this.view.offsetAtPoint(event.clientX, event.clientY);
|
|
1024
|
+
|
|
880
1025
|
const selection = this.view.model
|
|
881
1026
|
.getSelections()
|
|
882
1027
|
.some((candidate) => Math.min(candidate.anchor, candidate.head) <= offset && offset <= Math.max(candidate.anchor, candidate.head));
|
|
1028
|
+
|
|
883
1029
|
const menu: EditorContextMenu = { position: this.positionOf(offset), inSelection: selection, x: event.clientX, y: event.clientY };
|
|
884
1030
|
emit(this.contextListeners, menu);
|
|
885
1031
|
}
|
|
@@ -887,9 +1033,11 @@ export class InputController {
|
|
|
887
1033
|
/* A press on a pinned header goes to that header. */
|
|
888
1034
|
private stickyDown(event: PointerEvent): void {
|
|
889
1035
|
const row = (event.target as HTMLElement | null)?.closest?.<HTMLElement>('.se-sticky-row');
|
|
1036
|
+
|
|
890
1037
|
if (event.button !== 0 || !row) {
|
|
891
1038
|
return;
|
|
892
1039
|
}
|
|
1040
|
+
|
|
893
1041
|
event.preventDefault();
|
|
894
1042
|
this.view.jumpToHeader(Number(row.dataset.line), Number(row.dataset.depth));
|
|
895
1043
|
this.view.focus();
|
|
@@ -898,31 +1046,44 @@ export class InputController {
|
|
|
898
1046
|
/* A press on a line number selects the line. */
|
|
899
1047
|
private gutterDown(event: PointerEvent): void {
|
|
900
1048
|
this.view.cancelScroll();
|
|
1049
|
+
|
|
901
1050
|
if (event.button !== 0) {
|
|
902
1051
|
return;
|
|
903
1052
|
}
|
|
1053
|
+
|
|
904
1054
|
event.preventDefault();
|
|
905
1055
|
/* On the press and not on click: a repaint in between replaces the gutter's buttons, and a click whose button left the DOM never arrives. */
|
|
906
1056
|
const markerId = this.view.gutterMarkerIdOf(event.target);
|
|
1057
|
+
|
|
907
1058
|
if (markerId !== null) {
|
|
908
1059
|
this.view.pressGutterMarker(markerId);
|
|
1060
|
+
|
|
909
1061
|
return;
|
|
910
1062
|
}
|
|
1063
|
+
|
|
911
1064
|
const actionLine = this.view.gutterActionLineOf(event.target);
|
|
1065
|
+
|
|
912
1066
|
if (actionLine !== null) {
|
|
913
1067
|
this.view.pressGutterAction(actionLine);
|
|
1068
|
+
|
|
914
1069
|
return;
|
|
915
1070
|
}
|
|
1071
|
+
|
|
916
1072
|
const foldLine = this.view.foldLineOf(event.target);
|
|
1073
|
+
|
|
917
1074
|
if (foldLine !== null) {
|
|
918
1075
|
this.view.toggleFold(foldLine);
|
|
919
1076
|
this.view.focus();
|
|
1077
|
+
|
|
920
1078
|
return;
|
|
921
1079
|
}
|
|
1080
|
+
|
|
922
1081
|
if (event.pointerType === 'touch') {
|
|
923
1082
|
this.touch.start(event, 'gutter');
|
|
1083
|
+
|
|
924
1084
|
return;
|
|
925
1085
|
}
|
|
1086
|
+
|
|
926
1087
|
this.pointer.start(event, 'gutter');
|
|
927
1088
|
this.view.focus();
|
|
928
1089
|
}
|