@adecore/editor 0.0.1 → 0.17.0-beta.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/LICENSE +110 -0
- package/README.md +82 -0
- package/dist/attribution.d.ts +28 -0
- package/dist/attribution.js +55 -0
- package/dist/code-block.d.ts +2 -0
- package/dist/code-block.js +89 -0
- package/dist/column-selection.d.ts +9 -0
- package/dist/column-selection.js +52 -0
- package/dist/controller.d.ts +64 -0
- package/dist/controller.js +772 -0
- package/dist/editor.css +813 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +604 -0
- package/dist/fake.d.ts +190 -0
- package/dist/fake.js +658 -0
- package/dist/find.d.ts +30 -0
- package/dist/find.js +108 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/input.d.ts +9 -0
- package/dist/input.js +57 -0
- package/dist/keymap-table.d.ts +102 -0
- package/dist/keymap-table.js +130 -0
- package/dist/keymap.d.ts +36 -0
- package/dist/keymap.js +163 -0
- package/dist/layout.d.ts +167 -0
- package/dist/layout.js +583 -0
- package/dist/lens.d.ts +3 -0
- package/dist/lens.js +29 -0
- package/dist/listeners.d.ts +3 -0
- package/dist/listeners.js +12 -0
- package/dist/metrics.d.ts +8 -0
- package/dist/metrics.js +54 -0
- package/dist/modifier-gesture.d.ts +24 -0
- package/dist/modifier-gesture.js +98 -0
- package/dist/native-input.d.ts +26 -0
- package/dist/native-input.js +147 -0
- package/dist/offsets.d.ts +2 -0
- package/dist/offsets.js +14 -0
- package/dist/outline.d.ts +55 -0
- package/dist/outline.js +181 -0
- package/dist/overview.d.ts +17 -0
- package/dist/overview.js +55 -0
- package/dist/paint.d.ts +86 -0
- package/dist/paint.js +481 -0
- package/dist/php-html.d.ts +9 -0
- package/dist/php-html.js +36 -0
- package/dist/pointer.d.ts +49 -0
- package/dist/pointer.js +243 -0
- package/dist/scroll-animation.d.ts +3 -0
- package/dist/scroll-animation.js +45 -0
- package/dist/scroll.d.ts +28 -0
- package/dist/scroll.js +76 -0
- package/dist/semantic.d.ts +8 -0
- package/dist/semantic.js +47 -0
- package/dist/shiki.d.ts +7 -0
- package/dist/shiki.js +106 -0
- package/dist/smart-keys.d.ts +3 -0
- package/dist/smart-keys.js +14 -0
- package/dist/testing.d.ts +34 -0
- package/dist/testing.js +117 -0
- package/dist/theme-scopes.d.ts +9 -0
- package/dist/theme-scopes.js +61 -0
- package/dist/tokens.d.ts +28 -0
- package/dist/tokens.js +152 -0
- package/dist/tracked-range.d.ts +5 -0
- package/dist/tracked-range.js +20 -0
- package/dist/types.d.ts +342 -0
- package/dist/types.js +1 -0
- package/dist/view.d.ts +382 -0
- package/dist/view.js +2134 -0
- package/package.json +63 -3
- package/src/attribution.ts +85 -0
- package/src/code-block.ts +100 -0
- package/src/column-selection.ts +63 -0
- package/src/controller.ts +838 -0
- package/src/editor.css +813 -0
- package/src/engine.ts +757 -0
- package/src/fake.ts +831 -0
- package/src/find.ts +128 -0
- package/src/index.ts +69 -0
- package/src/input.ts +63 -0
- package/src/keymap-table.ts +163 -0
- package/src/keymap.ts +204 -0
- package/src/layout.ts +754 -0
- package/src/lens.ts +32 -0
- package/src/listeners.ts +15 -0
- package/src/metrics.ts +65 -0
- package/src/modifier-gesture.ts +115 -0
- package/src/native-input.ts +179 -0
- package/src/offsets.ts +16 -0
- package/src/outline.ts +248 -0
- package/src/overview.ts +80 -0
- package/src/paint.ts +608 -0
- package/src/php-html.ts +43 -0
- package/src/pointer.ts +326 -0
- package/src/scroll-animation.ts +48 -0
- package/src/scroll.ts +102 -0
- package/src/semantic.ts +54 -0
- package/src/shiki.ts +118 -0
- package/src/smart-keys.ts +17 -0
- package/src/testing.ts +157 -0
- package/src/theme-scopes.ts +75 -0
- package/src/tokens.ts +175 -0
- package/src/tracked-range.ts +22 -0
- package/src/types.ts +656 -0
- package/src/view.ts +2436 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
import type { Keymap } from './keymap-table.ts';
|
|
2
|
+
import type { CommandOptions, EditorCommand, Selection, TextEdit } from '@adecore/editor-core';
|
|
3
|
+
import { replacementEdits } from './input.ts';
|
|
4
|
+
import { chordMatches, type KeyAction, keyAction, type MoveKey } from './keymap.ts';
|
|
5
|
+
import { DoubleModifierGesture } from './modifier-gesture.ts';
|
|
6
|
+
import { NativeInput } from './native-input.ts';
|
|
7
|
+
import { columnSelections, sameCell } from './column-selection.ts';
|
|
8
|
+
import { PointerSelection } from './pointer.ts';
|
|
9
|
+
import type {
|
|
10
|
+
EditorClick,
|
|
11
|
+
EditorClickHandler,
|
|
12
|
+
EditorContextMenu,
|
|
13
|
+
EditorKeyHandler,
|
|
14
|
+
EditorPosition,
|
|
15
|
+
EditorRunCommand,
|
|
16
|
+
EditorSelectionRanges,
|
|
17
|
+
EditorViewCommand,
|
|
18
|
+
KeyChord
|
|
19
|
+
} from './types.ts';
|
|
20
|
+
import type { EditorView } from './view.ts';
|
|
21
|
+
|
|
22
|
+
export interface ControllerOptions {
|
|
23
|
+
keymap?: Keymap;
|
|
24
|
+
handBack: readonly KeyChord[];
|
|
25
|
+
apple: boolean;
|
|
26
|
+
save(): void;
|
|
27
|
+
blur(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MODIFYING = new Set<EditorCommand>([
|
|
31
|
+
'undo',
|
|
32
|
+
'redo',
|
|
33
|
+
'deleteWordLeft',
|
|
34
|
+
'deleteWordRight',
|
|
35
|
+
'deleteCamelLeft',
|
|
36
|
+
'deleteCamelRight',
|
|
37
|
+
'duplicateLine',
|
|
38
|
+
'deleteLine',
|
|
39
|
+
'moveLineUp',
|
|
40
|
+
'moveLineDown',
|
|
41
|
+
'toggleLineComment',
|
|
42
|
+
'toggleBlockComment',
|
|
43
|
+
'startNewLine',
|
|
44
|
+
'startNewLineBefore',
|
|
45
|
+
'splitLine',
|
|
46
|
+
'joinLines',
|
|
47
|
+
'toggleCase',
|
|
48
|
+
'autoIndentLines',
|
|
49
|
+
'insertTab',
|
|
50
|
+
'indent',
|
|
51
|
+
'outdent',
|
|
52
|
+
'insertNewline',
|
|
53
|
+
'smartBackspace',
|
|
54
|
+
'deleteForward'
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const VIEW_COMMANDS = new Set<EditorViewCommand>([
|
|
58
|
+
'collapseRegion',
|
|
59
|
+
'expandRegion',
|
|
60
|
+
'collapseAllRegions',
|
|
61
|
+
'expandAllRegions',
|
|
62
|
+
'collapseRegionRecursively',
|
|
63
|
+
'expandRegionRecursively',
|
|
64
|
+
'foldSelection',
|
|
65
|
+
'collapseDocComments',
|
|
66
|
+
'expandDocComments',
|
|
67
|
+
'expandAllToLevel1',
|
|
68
|
+
'expandAllToLevel2',
|
|
69
|
+
'expandAllToLevel3',
|
|
70
|
+
'expandAllToLevel4',
|
|
71
|
+
'expandAllToLevel5',
|
|
72
|
+
'toggleColumnMode'
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/* The clipboard text of the last copy or cut of a bare caret, which pastes back as whole lines. Shared, since it can be pasted in another editor. */
|
|
76
|
+
let lineClip: string | null = null;
|
|
77
|
+
|
|
78
|
+
/* Typing in the same stretch is one undo step until a pause this long. */
|
|
79
|
+
const TYPING_PAUSE_MS = 750;
|
|
80
|
+
const PAIR_CHARACTER = /^[()[\]{}'"`;<]$/;
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* What a person does to the editor: keys, typing, the clipboard, an input method, the mouse. It turns
|
|
84
|
+
* each into a command on the model and leaves the drawing to the view.
|
|
85
|
+
*/
|
|
86
|
+
export class InputController {
|
|
87
|
+
private readonly native: NativeInput;
|
|
88
|
+
private readonly pointer: PointerSelection;
|
|
89
|
+
private readonly cleanup: (() => void)[] = [];
|
|
90
|
+
private readonly keyHandlers = new Set<EditorKeyHandler>();
|
|
91
|
+
private readonly gesture: DoubleModifierGesture;
|
|
92
|
+
private readonly clickHandlers = new Set<EditorClickHandler>();
|
|
93
|
+
private readonly contextListeners = new Set<(menu: EditorContextMenu) => void>();
|
|
94
|
+
private readonly subscription: { dispose(): void };
|
|
95
|
+
private historyGroup = 0;
|
|
96
|
+
/* The two corners of a box being grown with the keys, and the selections it made, which say whether it is still the one on screen. */
|
|
97
|
+
private block: { anchor: { x: number; y: number }; head: { x: number; y: number }; signature: string } | null = null;
|
|
98
|
+
private selectionRanges: EditorSelectionRanges | null = null;
|
|
99
|
+
/* The Extend Selection presses that wait for a server's ranges, which are answered one after the other. */
|
|
100
|
+
private expansions: Promise<void> = Promise.resolve();
|
|
101
|
+
private lastInputAt = 0;
|
|
102
|
+
private desiredXs: number[] = [];
|
|
103
|
+
private keepDesiredXs = false;
|
|
104
|
+
private readonly view: EditorView;
|
|
105
|
+
private readonly options: ControllerOptions;
|
|
106
|
+
|
|
107
|
+
constructor(view: EditorView, options: ControllerOptions) {
|
|
108
|
+
this.view = view;
|
|
109
|
+
this.options = options;
|
|
110
|
+
this.gesture = new DoubleModifierGesture(options.apple ? 'Alt' : 'Control');
|
|
111
|
+
const { model, input, viewport } = view;
|
|
112
|
+
this.native = new NativeInput(input, model, {
|
|
113
|
+
readOnly: () => view.settings.readOnly,
|
|
114
|
+
replace: (text) => this.replaceSelections(text),
|
|
115
|
+
type: (character) => this.typeText(character),
|
|
116
|
+
apply: (edit, selection) => {
|
|
117
|
+
this.applyEdits([edit], [selection]);
|
|
118
|
+
view.revealCaret();
|
|
119
|
+
},
|
|
120
|
+
preview: (text) => view.setComposition(text),
|
|
121
|
+
selectionChanged: (selection) => {
|
|
122
|
+
this.historyGroup++;
|
|
123
|
+
model.setSelections([selection]);
|
|
124
|
+
view.revealCaret();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
this.pointer = new PointerSelection({
|
|
128
|
+
model,
|
|
129
|
+
viewport,
|
|
130
|
+
offsetAt: (x, y) => view.offsetAtPoint(x, y),
|
|
131
|
+
contentPoint: (x, y) => view.contentPoint(x, y),
|
|
132
|
+
columnSelections: (from, to) => (sameCell(view.layout, from, to) ? null : columnSelections(view.layout, from, to)),
|
|
133
|
+
visualLine: (offset) => view.layout.visualLine(offset),
|
|
134
|
+
lineAt: (offset) => {
|
|
135
|
+
const row = view.layout.rowForLine(model.positionAt(offset).line);
|
|
136
|
+
return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
|
|
137
|
+
},
|
|
138
|
+
camelHumps: () => view.settings.smartKeys.camelHumps,
|
|
139
|
+
columnMode: () => view.columnMode,
|
|
140
|
+
selectionAt: (x, y) => (view.settings.readOnly ? null : view.selectionAtPoint(x, y)),
|
|
141
|
+
setDropCaret: (offset) => view.setDropCaret(offset),
|
|
142
|
+
drop: (from, to, offset, copy) => this.dropText(from, to, offset, copy),
|
|
143
|
+
copyHeld: (event) => (this.options.apple ? event.altKey : event.ctrlKey),
|
|
144
|
+
focus: () => view.focus(),
|
|
145
|
+
scrolled: () => view.requestRender()
|
|
146
|
+
});
|
|
147
|
+
this.subscription = model.subscribe(() => {
|
|
148
|
+
if (!this.keepDesiredXs) {
|
|
149
|
+
this.desiredXs = [];
|
|
150
|
+
}
|
|
151
|
+
this.native.sync();
|
|
152
|
+
});
|
|
153
|
+
this.native.sync();
|
|
154
|
+
this.attach();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private listen<Event extends keyof HTMLElementEventMap>(target: HTMLElement, name: Event, callback: (event: HTMLElementEventMap[Event]) => void): void {
|
|
158
|
+
target.addEventListener(name, callback);
|
|
159
|
+
this.cleanup.push(() => target.removeEventListener(name, callback));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private attach(): void {
|
|
163
|
+
const { input, viewport, root } = this.view;
|
|
164
|
+
const document = root.ownerDocument;
|
|
165
|
+
this.listen(input, 'keydown', (event) => this.keydown(event));
|
|
166
|
+
this.listen(input, 'beforeinput', (event) => this.beforeInput(event));
|
|
167
|
+
this.listen(input, 'input', () => this.native.onInput());
|
|
168
|
+
this.listen(input, 'compositionstart', () => {
|
|
169
|
+
this.historyGroup++;
|
|
170
|
+
this.native.startComposition();
|
|
171
|
+
});
|
|
172
|
+
this.listen(input, 'compositionend', (event) => {
|
|
173
|
+
this.native.endComposition(event.data);
|
|
174
|
+
this.historyGroup++;
|
|
175
|
+
this.view.render();
|
|
176
|
+
});
|
|
177
|
+
this.listen(input, 'select', () => this.native.readSelection());
|
|
178
|
+
this.listen(input, 'focus', () => this.view.requestRender());
|
|
179
|
+
this.listen(input, 'blur', () => {
|
|
180
|
+
this.gesture.reset();
|
|
181
|
+
this.view.requestRender();
|
|
182
|
+
});
|
|
183
|
+
this.listen(input, 'keyup', (event) => this.gesture.keyup(event));
|
|
184
|
+
this.listen(root, 'focusout', (event) => {
|
|
185
|
+
if (!(event.relatedTarget instanceof Node) || !root.contains(event.relatedTarget)) {
|
|
186
|
+
this.options.blur();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
this.listen(input, 'paste', (event) => {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
this.paste(event.clipboardData?.getData('text/plain') ?? '');
|
|
192
|
+
});
|
|
193
|
+
this.listen(input, 'copy', (event) => this.copy(event, false));
|
|
194
|
+
this.listen(input, 'cut', (event) => this.copy(event, true));
|
|
195
|
+
this.listen(viewport, 'scroll', () => this.view.scrolled());
|
|
196
|
+
this.listen(viewport, 'wheel', () => this.view.cancelScroll());
|
|
197
|
+
this.listen(viewport, 'touchstart', () => this.view.cancelScroll());
|
|
198
|
+
this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
|
|
199
|
+
this.listen(viewport, 'pointerleave', () => this.view.setHover(null));
|
|
200
|
+
this.listen(viewport, 'pointerdown', (event) => this.pointerDown(event));
|
|
201
|
+
this.listen(viewport, 'contextmenu', (event) => this.contextMenu(event));
|
|
202
|
+
this.listen(this.view.gutterElement, 'pointerdown', (event) => this.gutterDown(event));
|
|
203
|
+
this.listen(this.view.stickyElement, 'pointerdown', (event) => this.stickyDown(event));
|
|
204
|
+
const move = (event: Event): void => this.pointer.move(event as PointerEvent);
|
|
205
|
+
const end = (event: Event): void => {
|
|
206
|
+
this.pointer.end(event as PointerEvent);
|
|
207
|
+
this.historyGroup++;
|
|
208
|
+
};
|
|
209
|
+
document.addEventListener('pointermove', move);
|
|
210
|
+
document.addEventListener('pointerup', end);
|
|
211
|
+
document.addEventListener('pointercancel', end);
|
|
212
|
+
this.cleanup.push(() => {
|
|
213
|
+
document.removeEventListener('pointermove', move);
|
|
214
|
+
document.removeEventListener('pointerup', end);
|
|
215
|
+
document.removeEventListener('pointercancel', end);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
dispose(): void {
|
|
220
|
+
this.subscription.dispose();
|
|
221
|
+
this.pointer.dispose();
|
|
222
|
+
for (const remove of this.cleanup) {
|
|
223
|
+
remove();
|
|
224
|
+
}
|
|
225
|
+
this.cleanup.length = 0;
|
|
226
|
+
this.keyHandlers.clear();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Tells a person why nothing happened, when the editor is read only and has said why. */
|
|
230
|
+
private blockedByReadOnly(): boolean {
|
|
231
|
+
if (!this.view.settings.readOnly) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (this.view.settings.readOnlyReason) {
|
|
235
|
+
this.view.notify(this.view.settings.readOnlyReason);
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private inputGroup(): string {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
if (now - this.lastInputAt > TYPING_PAUSE_MS) {
|
|
243
|
+
this.historyGroup++;
|
|
244
|
+
}
|
|
245
|
+
this.lastInputAt = now;
|
|
246
|
+
return `view-input-${this.historyGroup}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private applyEdits(edits: readonly TextEdit[], selections?: readonly Selection[]): boolean {
|
|
250
|
+
if (this.view.settings.readOnly) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return this.view.model.applyEdits(edits, { selections, source: 'input', historyGroup: this.inputGroup() });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private replaceSelections(text: string): void {
|
|
257
|
+
if (this.blockedByReadOnly()) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const { model } = this.view;
|
|
261
|
+
for (const selection of model.getSelections()) {
|
|
262
|
+
this.view.ensureVisible(selection.head);
|
|
263
|
+
}
|
|
264
|
+
const replacement = replacementEdits(model.getSelections(), this.native.normalizeNewlines(text));
|
|
265
|
+
this.applyEdits(replacement.edits, replacement.selections);
|
|
266
|
+
this.view.revealCaret();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private paste(text: string): void {
|
|
270
|
+
if (this.blockedByReadOnly()) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const { model } = this.view;
|
|
274
|
+
for (const selection of model.getSelections()) {
|
|
275
|
+
this.view.ensureVisible(selection.head);
|
|
276
|
+
}
|
|
277
|
+
this.historyGroup++;
|
|
278
|
+
model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
|
|
279
|
+
this.historyGroup++;
|
|
280
|
+
this.view.revealCaret();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private commandOptions(): CommandOptions {
|
|
284
|
+
const { settings } = this.view;
|
|
285
|
+
const keys = settings.smartKeys;
|
|
286
|
+
return {
|
|
287
|
+
tabSize: settings.tabSize,
|
|
288
|
+
insertSpaces: settings.insertSpaces,
|
|
289
|
+
autoClosingBrackets: keys.autoPairBrackets,
|
|
290
|
+
autoClosingQuotes: keys.autoPairQuotes,
|
|
291
|
+
surroundSelection: keys.surroundSelection,
|
|
292
|
+
tabOutOfClosers: keys.tabOutOfClosers,
|
|
293
|
+
smartEnter: keys.smartIndentOnEnter,
|
|
294
|
+
indentOnPaste: keys.indentOnPaste,
|
|
295
|
+
smartSemicolon: keys.smartSemicolon,
|
|
296
|
+
language: settings.language ?? 'plaintext',
|
|
297
|
+
camelCase: keys.camelHumps,
|
|
298
|
+
visualLine: (offset) => this.view.rowOf(offset),
|
|
299
|
+
lineSpan: (line) => this.view.lineSpan(line)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Folding is the view's, since it knows where the ranges are. */
|
|
304
|
+
private viewCommand(name: EditorViewCommand): boolean {
|
|
305
|
+
const { view } = this;
|
|
306
|
+
const run: Record<EditorViewCommand, () => void> = {
|
|
307
|
+
collapseRegion: () => view.collapseRegion(),
|
|
308
|
+
expandRegion: () => view.expandRegion(),
|
|
309
|
+
collapseAllRegions: () => view.foldAll(true),
|
|
310
|
+
expandAllRegions: () => view.foldAll(false),
|
|
311
|
+
collapseRegionRecursively: () => view.collapseRecursively(),
|
|
312
|
+
expandRegionRecursively: () => view.expandRecursively(),
|
|
313
|
+
foldSelection: () => view.foldSelection(),
|
|
314
|
+
collapseDocComments: () => view.foldRole('doc-comment', true),
|
|
315
|
+
expandDocComments: () => view.foldRole('doc-comment', false),
|
|
316
|
+
expandAllToLevel1: () => view.expandAllToLevel(1),
|
|
317
|
+
expandAllToLevel2: () => view.expandAllToLevel(2),
|
|
318
|
+
expandAllToLevel3: () => view.expandAllToLevel(3),
|
|
319
|
+
expandAllToLevel4: () => view.expandAllToLevel(4),
|
|
320
|
+
expandAllToLevel5: () => view.expandAllToLevel(5),
|
|
321
|
+
toggleColumnMode: () => this.toggleColumnMode()
|
|
322
|
+
};
|
|
323
|
+
run[name]();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Runs a model command on every caret, with the folds that would hide its result opened first. */
|
|
328
|
+
command(name: EditorRunCommand): boolean {
|
|
329
|
+
if (VIEW_COMMANDS.has(name as EditorViewCommand)) {
|
|
330
|
+
return this.viewCommand(name as EditorViewCommand);
|
|
331
|
+
}
|
|
332
|
+
if (name === 'expandSelection' && this.selectionRanges !== null) {
|
|
333
|
+
const provider = this.selectionRanges;
|
|
334
|
+
this.expansions = this.expansions.then(() => this.expandThrough(provider));
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return this.modelCommand(name as EditorCommand);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
setSelectionRanges(provider: EditorSelectionRanges | null): void {
|
|
341
|
+
this.selectionRanges = provider;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* Extend Selection through the ranges of a language server, the editor's own rule where it has none. */
|
|
345
|
+
private async expandThrough(provider: EditorSelectionRanges): Promise<void> {
|
|
346
|
+
const { model } = this.view;
|
|
347
|
+
const selections = model.getSelections();
|
|
348
|
+
const revision = model.getRevision();
|
|
349
|
+
const stretches = selections.map(({ anchor, head }) => ({ from: Math.min(anchor, head), to: Math.max(anchor, head) }));
|
|
350
|
+
const asked = stretches.map(({ from }) => {
|
|
351
|
+
const { line, column } = model.positionAt(from);
|
|
352
|
+
return { line, character: column };
|
|
353
|
+
});
|
|
354
|
+
const chains = await provider(asked).catch(() => null);
|
|
355
|
+
if (model.getRevision() !== revision || JSON.stringify(model.getSelections()) !== JSON.stringify(selections)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (chains === null) {
|
|
359
|
+
this.modelCommand('expandSelection');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const ranges = stretches.map(
|
|
363
|
+
({ from, to }, index) =>
|
|
364
|
+
(chains[index] ?? [])
|
|
365
|
+
.map((range) => ({
|
|
366
|
+
from: model.offsetAt({ line: range.start.line, column: range.start.character }),
|
|
367
|
+
to: model.offsetAt({ line: range.end.line, column: range.end.character })
|
|
368
|
+
}))
|
|
369
|
+
.find((range) => range.from <= from && range.to >= to && (range.from < from || range.to > to)) ?? null
|
|
370
|
+
);
|
|
371
|
+
this.historyGroup++;
|
|
372
|
+
model.expandSelectionTo(ranges);
|
|
373
|
+
this.view.revealCaret();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private modelCommand(name: EditorCommand): boolean {
|
|
377
|
+
const { model } = this.view;
|
|
378
|
+
const modifies = MODIFYING.has(name) && name !== 'undo' && name !== 'redo';
|
|
379
|
+
if ((modifies || name === 'undo' || name === 'redo') && this.blockedByReadOnly()) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
this.historyGroup++;
|
|
383
|
+
if (modifies) {
|
|
384
|
+
for (const selection of model.getSelections()) {
|
|
385
|
+
this.view.ensureVisible(selection.head);
|
|
386
|
+
const position = model.positionAt(selection.head);
|
|
387
|
+
const line = model.getLine(position.line);
|
|
388
|
+
if ((name === 'smartBackspace' || name === 'deleteWordLeft') && selection.head === line.start && position.line > 0) {
|
|
389
|
+
this.view.ensureVisible(model.getLine(position.line - 1).start);
|
|
390
|
+
}
|
|
391
|
+
if ((name === 'deleteForward' || name === 'deleteWordRight') && selection.head === line.end && position.line + 1 < model.getLineCount()) {
|
|
392
|
+
this.view.ensureVisible(model.getLine(position.line + 1).start);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const folded = name === 'moveLineUp' || name === 'moveLineDown' ? this.view.getFolds().collapsed : [];
|
|
397
|
+
const result = model.execute(name, {
|
|
398
|
+
...this.commandOptions(),
|
|
399
|
+
onLinesMoved: (moves) => this.view.refoldMoved(folded, moves)
|
|
400
|
+
});
|
|
401
|
+
if (name === 'selectNextOccurrence' && model.occurrencesExhausted && this.view.settings.messages.noMoreOccurrences !== undefined) {
|
|
402
|
+
this.view.notify(this.view.settings.messages.noMoreOccurrences);
|
|
403
|
+
}
|
|
404
|
+
if (name === 'smartEnd' || name === 'selectSmartEnd') {
|
|
405
|
+
this.view.markRowEnd(model.getPrimary().head);
|
|
406
|
+
}
|
|
407
|
+
this.view.revealCaret();
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private typeText(text: string): void {
|
|
412
|
+
if (this.blockedByReadOnly()) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
for (const selection of this.view.model.getSelections()) {
|
|
416
|
+
this.view.ensureVisible(selection.head);
|
|
417
|
+
}
|
|
418
|
+
if (PAIR_CHARACTER.test(text)) {
|
|
419
|
+
this.view.model.typeText(text, { ...this.commandOptions(), historyGroup: this.inputGroup() });
|
|
420
|
+
this.view.revealCaret();
|
|
421
|
+
} else {
|
|
422
|
+
this.replaceSelections(text);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* A page scrolls whole lines, and every caret goes as far, so each stays on its row of the screen. */
|
|
427
|
+
private movePages(direction: -1 | 1, extend: boolean): void {
|
|
428
|
+
const { model, layout } = this.view;
|
|
429
|
+
const { lineHeight } = layout.metrics;
|
|
430
|
+
const height = this.view.viewportHeight;
|
|
431
|
+
const page = Math.trunc(height / lineHeight) * lineHeight;
|
|
432
|
+
const from = this.view.scrollPlace;
|
|
433
|
+
const scrollTop = direction < 0 ? from.y - page : Math.min(layout.height - height, from.y + page);
|
|
434
|
+
this.view.scrollTo(from.x, Math.max(0, layout.lineBase(scrollTop)));
|
|
435
|
+
const distance = direction < 0 ? height : height + lineHeight - 1;
|
|
436
|
+
const selections = model.getSelections().map((selection, index) => {
|
|
437
|
+
this.desiredXs[index] ??= layout.caret(selection.head).x;
|
|
438
|
+
const head = layout.verticalOffset(selection.head, direction, this.desiredXs[index]!, distance);
|
|
439
|
+
return { anchor: extend ? selection.anchor : head, head };
|
|
440
|
+
});
|
|
441
|
+
this.keepDesiredXs = true;
|
|
442
|
+
model.setSelections(selections);
|
|
443
|
+
this.keepDesiredXs = false;
|
|
444
|
+
this.historyGroup++;
|
|
445
|
+
this.view.revealCaret();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Column mode on drops every caret but the first and keeps a selection as a box; off it makes one selection of the box. */
|
|
449
|
+
private toggleColumnMode(): void {
|
|
450
|
+
const { view } = this;
|
|
451
|
+
const { model, layout } = view;
|
|
452
|
+
this.block = null;
|
|
453
|
+
if (view.columnMode) {
|
|
454
|
+
const first = model.getSelections()[0]!;
|
|
455
|
+
const last = model.getPrimary();
|
|
456
|
+
view.setColumnMode(false);
|
|
457
|
+
model.setSelections([{ anchor: first.anchor, head: last.head }]);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
view.setColumnMode(true);
|
|
461
|
+
const primary = model.getPrimary();
|
|
462
|
+
model.setSelections([primary]);
|
|
463
|
+
if (primary.anchor !== primary.head) {
|
|
464
|
+
const anchor = layout.caret(primary.anchor);
|
|
465
|
+
const head = layout.caret(primary.head);
|
|
466
|
+
this.block = { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature: '' };
|
|
467
|
+
this.extendBlock({ x: 0, y: 0 });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* Moves the corner of the box by a step and selects the columns between it and the other corner. */
|
|
472
|
+
private extendBlock(step: { x: number; y: number }): void {
|
|
473
|
+
const { model, layout } = this.view;
|
|
474
|
+
const selections = model.getSelections();
|
|
475
|
+
const signature = selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
|
|
476
|
+
if (this.block === null || (this.block.signature !== '' && this.block.signature !== signature)) {
|
|
477
|
+
const primary = model.getPrimary();
|
|
478
|
+
const anchor = layout.caret(primary.anchor);
|
|
479
|
+
const head = layout.caret(primary.head);
|
|
480
|
+
this.block = { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature };
|
|
481
|
+
}
|
|
482
|
+
const block = this.block;
|
|
483
|
+
block.head = {
|
|
484
|
+
x: Math.max(0, block.head.x + step.x),
|
|
485
|
+
y: Math.max(0, Math.min(layout.height - layout.metrics.lineHeight, block.head.y + step.y))
|
|
486
|
+
};
|
|
487
|
+
model.setSelections(columnSelections(layout, block.anchor, block.head));
|
|
488
|
+
block.signature = model
|
|
489
|
+
.getSelections()
|
|
490
|
+
.map((selection) => `${selection.anchor}:${selection.head}`)
|
|
491
|
+
.join(',');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private moveCarets(key: MoveKey, extend: boolean): void {
|
|
495
|
+
if (this.view.columnMode && extend && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
|
|
496
|
+
const { lineHeight, charWidth } = this.view.layout.metrics;
|
|
497
|
+
this.extendBlock({
|
|
498
|
+
x: key === 'ArrowLeft' ? -charWidth : key === 'ArrowRight' ? charWidth : 0,
|
|
499
|
+
y: key === 'ArrowUp' ? -lineHeight : key === 'ArrowDown' ? lineHeight : 0
|
|
500
|
+
});
|
|
501
|
+
this.view.revealCaret();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (key === 'PageUp' || key === 'PageDown') {
|
|
505
|
+
this.movePages(key === 'PageUp' ? -1 : 1, extend);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const { model, layout } = this.view;
|
|
509
|
+
const vertical = key === 'ArrowUp' || key === 'ArrowDown';
|
|
510
|
+
const direction = key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1;
|
|
511
|
+
const selections = model.getSelections().map((selection, index) => {
|
|
512
|
+
let head: number;
|
|
513
|
+
if (!vertical && !extend && selection.anchor !== selection.head) {
|
|
514
|
+
head = direction < 0 ? Math.min(selection.anchor, selection.head) : Math.max(selection.anchor, selection.head);
|
|
515
|
+
} else if (vertical) {
|
|
516
|
+
// A selection first gives way to the edge in the direction of the move, as the platform's Up and Down do.
|
|
517
|
+
const hasSelection = !extend && selection.anchor !== selection.head;
|
|
518
|
+
const from = hasSelection
|
|
519
|
+
? direction < 0
|
|
520
|
+
? Math.min(selection.anchor, selection.head)
|
|
521
|
+
: Math.max(selection.anchor, selection.head)
|
|
522
|
+
: selection.head;
|
|
523
|
+
if (hasSelection) {
|
|
524
|
+
delete this.desiredXs[index];
|
|
525
|
+
}
|
|
526
|
+
this.desiredXs[index] ??= layout.caret(from).x;
|
|
527
|
+
head = layout.verticalOffset(from, direction, this.desiredXs[index]!);
|
|
528
|
+
} else {
|
|
529
|
+
head = layout.horizontalOffset(selection.head, direction);
|
|
530
|
+
}
|
|
531
|
+
return { anchor: extend ? selection.anchor : head, head };
|
|
532
|
+
});
|
|
533
|
+
this.keepDesiredXs = vertical;
|
|
534
|
+
model.setSelections(selections);
|
|
535
|
+
this.keepDesiredXs = false;
|
|
536
|
+
if (!vertical) {
|
|
537
|
+
this.desiredXs = [];
|
|
538
|
+
}
|
|
539
|
+
this.historyGroup++;
|
|
540
|
+
this.view.revealCaret();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* Carries the text of a selection to a place in the text, or copies it there, as one step of the history, and selects it where it landed. */
|
|
544
|
+
private dropText(from: number, to: number, offset: number, copy: boolean): void {
|
|
545
|
+
if (this.blockedByReadOnly()) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const { model } = this.view;
|
|
549
|
+
const text = model.slice(from, to);
|
|
550
|
+
this.view.ensureVisible(offset);
|
|
551
|
+
const start = copy || offset < from ? offset : offset - (to - from);
|
|
552
|
+
this.historyGroup++;
|
|
553
|
+
model.applyEdits(
|
|
554
|
+
copy
|
|
555
|
+
? [{ from: offset, to: offset, text }]
|
|
556
|
+
: [
|
|
557
|
+
{ from, to, text: '' },
|
|
558
|
+
{ from: offset, to: offset, text }
|
|
559
|
+
],
|
|
560
|
+
{
|
|
561
|
+
source: 'input',
|
|
562
|
+
selections: [{ anchor: start, head: start + text.length }]
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
this.historyGroup++;
|
|
566
|
+
this.view.revealCaret();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private goToMatchingBracket(): void {
|
|
570
|
+
const { model } = this.view;
|
|
571
|
+
let changed = false;
|
|
572
|
+
model.setSelections(
|
|
573
|
+
model.getSelections().map((selection) => {
|
|
574
|
+
const bracket = this.view.matchingBracket(selection);
|
|
575
|
+
if (!bracket || bracket.mate === undefined) {
|
|
576
|
+
return selection;
|
|
577
|
+
}
|
|
578
|
+
const head = bracket.mate > bracket.at ? bracket.mate + 1 : bracket.mate;
|
|
579
|
+
changed = true;
|
|
580
|
+
return { anchor: head, head };
|
|
581
|
+
})
|
|
582
|
+
);
|
|
583
|
+
if (changed) {
|
|
584
|
+
this.view.revealCaret();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private run(action: KeyAction, event: KeyboardEvent): void {
|
|
589
|
+
const { model } = this.view;
|
|
590
|
+
switch (action.type) {
|
|
591
|
+
case 'save':
|
|
592
|
+
event.preventDefault();
|
|
593
|
+
this.options.save();
|
|
594
|
+
return;
|
|
595
|
+
case 'command':
|
|
596
|
+
event.preventDefault();
|
|
597
|
+
this.command(action.command);
|
|
598
|
+
return;
|
|
599
|
+
case 'move':
|
|
600
|
+
event.preventDefault();
|
|
601
|
+
this.moveCarets(action.key, action.extend);
|
|
602
|
+
return;
|
|
603
|
+
case 'edge': {
|
|
604
|
+
event.preventDefault();
|
|
605
|
+
const head = action.end ? model.getLength() : 0;
|
|
606
|
+
model.setSelections([{ anchor: action.extend ? model.getPrimary().anchor : head, head }]);
|
|
607
|
+
this.view.revealCaret();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
case 'bracket':
|
|
611
|
+
event.preventDefault();
|
|
612
|
+
this.goToMatchingBracket();
|
|
613
|
+
return;
|
|
614
|
+
case 'view':
|
|
615
|
+
event.preventDefault();
|
|
616
|
+
this.viewCommand(action.command);
|
|
617
|
+
return;
|
|
618
|
+
case 'escape': {
|
|
619
|
+
const selections = model.getSelections();
|
|
620
|
+
if (selections.length > 1 || selections[0]!.anchor !== selections[0]!.head) {
|
|
621
|
+
event.preventDefault();
|
|
622
|
+
// The oldest caret stays; the newest ones go.
|
|
623
|
+
const head = selections[0]!.head;
|
|
624
|
+
model.setSelections([{ anchor: head, head }]);
|
|
625
|
+
if (selections.length > 1) {
|
|
626
|
+
this.view.revealCaret();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* A handler sees the key before the editor does, the page's own shortcuts included, since a list of suggestions owns the arrows while it is open. */
|
|
635
|
+
onKeyDown(handler: EditorKeyHandler): () => void {
|
|
636
|
+
this.keyHandlers.add(handler);
|
|
637
|
+
return () => {
|
|
638
|
+
this.keyHandlers.delete(handler);
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
onClick(handler: EditorClickHandler): () => void {
|
|
643
|
+
this.clickHandlers.add(handler);
|
|
644
|
+
return () => {
|
|
645
|
+
this.clickHandlers.delete(handler);
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
onContextMenu(listener: (menu: EditorContextMenu) => void): () => void {
|
|
650
|
+
this.contextListeners.add(listener);
|
|
651
|
+
return () => {
|
|
652
|
+
this.contextListeners.delete(listener);
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private keydown(event: KeyboardEvent): void {
|
|
657
|
+
if (event.isComposing || this.native.composing) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const added = this.gesture.keydown(event);
|
|
661
|
+
if (added !== null) {
|
|
662
|
+
event.preventDefault();
|
|
663
|
+
this.command(added === 'above' ? 'addCaretAbove' : 'addCaretBelow');
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
for (const handler of [...this.keyHandlers]) {
|
|
667
|
+
if (handler(event)) {
|
|
668
|
+
event.preventDefault();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// The app's own shortcuts stay the app's, also where the editor has a binding for the same key.
|
|
673
|
+
if (this.options.handBack.some((chord) => chordMatches(chord, event, this.options.apple))) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const action = keyAction(event, this.options.apple, this.options.keymap);
|
|
677
|
+
if (action) {
|
|
678
|
+
this.run(action, event);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private beforeInput(event: InputEvent): void {
|
|
683
|
+
if (this.view.settings.readOnly) {
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
this.blockedByReadOnly();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (this.native.composing || event.isComposing) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
switch (event.inputType) {
|
|
692
|
+
case 'insertText':
|
|
693
|
+
case 'insertReplacementText':
|
|
694
|
+
event.preventDefault();
|
|
695
|
+
if (event.data !== null) {
|
|
696
|
+
this.typeText(event.data);
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
case 'insertLineBreak':
|
|
700
|
+
case 'insertParagraph':
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
this.command('insertNewline');
|
|
703
|
+
return;
|
|
704
|
+
case 'deleteContentBackward':
|
|
705
|
+
event.preventDefault();
|
|
706
|
+
this.command('smartBackspace');
|
|
707
|
+
return;
|
|
708
|
+
case 'deleteContentForward':
|
|
709
|
+
event.preventDefault();
|
|
710
|
+
this.command('deleteForward');
|
|
711
|
+
return;
|
|
712
|
+
case 'historyUndo':
|
|
713
|
+
case 'historyRedo':
|
|
714
|
+
event.preventDefault();
|
|
715
|
+
this.command(event.inputType === 'historyUndo' ? 'undo' : 'redo');
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* A cut or copy of nothing selected takes the lines the carets are on. */
|
|
721
|
+
private copy(event: ClipboardEvent, cut: boolean): void {
|
|
722
|
+
event.preventDefault();
|
|
723
|
+
const { model } = this.view;
|
|
724
|
+
const selections = model.getSelections();
|
|
725
|
+
const empty = selections.every((selection) => selection.anchor === selection.head);
|
|
726
|
+
const text = empty
|
|
727
|
+
? selections
|
|
728
|
+
.map((selection) => {
|
|
729
|
+
const { first, last } = this.view.lineSpan(model.positionAt(selection.head).line);
|
|
730
|
+
return { start: model.getLine(first).start, end: model.getLine(last).end, next: model.getLine(last).next };
|
|
731
|
+
})
|
|
732
|
+
.map((lines) => model.slice(lines.start, lines.next) + (lines.next === lines.end ? '\n' : ''))
|
|
733
|
+
.join('')
|
|
734
|
+
: selections.map((selection) => model.slice(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head))).join('\n');
|
|
735
|
+
event.clipboardData?.setData('text/plain', text);
|
|
736
|
+
lineClip = empty ? text : null;
|
|
737
|
+
if (!cut || this.blockedByReadOnly()) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
this.historyGroup++;
|
|
741
|
+
if (empty) {
|
|
742
|
+
this.command('deleteLine');
|
|
743
|
+
} else {
|
|
744
|
+
this.replaceSelections('');
|
|
745
|
+
}
|
|
746
|
+
this.historyGroup++;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private pointerDown(event: PointerEvent): void {
|
|
750
|
+
this.view.cancelScroll();
|
|
751
|
+
const target = event.target as HTMLElement;
|
|
752
|
+
if (event.button !== 0 || target.closest('button, a, input, textarea, [contenteditable], .se-block, .se-gutter, .se-sticky')) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
event.preventDefault();
|
|
756
|
+
if (this.takenByHost(event)) {
|
|
757
|
+
this.view.focus();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
this.pointer.start(event);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Whether a handler of the host took a press on a character, such as a Mod+click that follows a name. */
|
|
764
|
+
private takenByHost(event: PointerEvent): boolean {
|
|
765
|
+
if (this.clickHandlers.size === 0) {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
const offset = this.view.characterAtPoint(event.clientX, event.clientY, event.target);
|
|
769
|
+
if (offset === null) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
const click: EditorClick = {
|
|
773
|
+
position: this.positionOf(offset),
|
|
774
|
+
mod: this.options.apple ? event.metaKey : event.ctrlKey,
|
|
775
|
+
alt: event.altKey,
|
|
776
|
+
shift: event.shiftKey
|
|
777
|
+
};
|
|
778
|
+
return [...this.clickHandlers].some((handler) => handler(click));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private positionOf(offset: number): EditorPosition {
|
|
782
|
+
const { line, column } = this.view.model.positionAt(offset);
|
|
783
|
+
return { line, character: column };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private contextMenu(event: MouseEvent): void {
|
|
787
|
+
if (this.contextListeners.size === 0 || (event.target as HTMLElement | null)?.closest?.('.se-sticky')) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
event.preventDefault();
|
|
791
|
+
const offset = this.view.offsetAtPoint(event.clientX, event.clientY);
|
|
792
|
+
const selection = this.view.model
|
|
793
|
+
.getSelections()
|
|
794
|
+
.some((candidate) => Math.min(candidate.anchor, candidate.head) <= offset && offset <= Math.max(candidate.anchor, candidate.head));
|
|
795
|
+
const menu: EditorContextMenu = { position: this.positionOf(offset), inSelection: selection, x: event.clientX, y: event.clientY };
|
|
796
|
+
for (const listener of [...this.contextListeners]) {
|
|
797
|
+
listener(menu);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/* A press on a pinned header goes to that header. */
|
|
802
|
+
private stickyDown(event: PointerEvent): void {
|
|
803
|
+
const row = (event.target as HTMLElement | null)?.closest?.<HTMLElement>('.se-sticky-row');
|
|
804
|
+
if (event.button !== 0 || !row) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
event.preventDefault();
|
|
808
|
+
this.view.jumpToHeader(Number(row.dataset.line), Number(row.dataset.depth));
|
|
809
|
+
this.view.focus();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/* A press on a line number selects the line. */
|
|
813
|
+
private gutterDown(event: PointerEvent): void {
|
|
814
|
+
if (event.button !== 0) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
event.preventDefault();
|
|
818
|
+
/* 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. */
|
|
819
|
+
const markerId = this.view.gutterMarkerIdOf(event.target);
|
|
820
|
+
if (markerId !== null) {
|
|
821
|
+
this.view.pressGutterMarker(markerId);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const actionLine = this.view.gutterActionLineOf(event.target);
|
|
825
|
+
if (actionLine !== null) {
|
|
826
|
+
this.view.pressGutterAction(actionLine);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const foldLine = this.view.foldLineOf(event.target);
|
|
830
|
+
if (foldLine !== null) {
|
|
831
|
+
this.view.toggleFold(foldLine);
|
|
832
|
+
this.view.focus();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
this.pointer.start(event, 'gutter');
|
|
836
|
+
this.view.focus();
|
|
837
|
+
}
|
|
838
|
+
}
|