@adecore/editor 0.42.0 → 0.43.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/dist/controller.d.ts +9 -2
- package/dist/controller.js +82 -8
- package/dist/editor.css +13 -0
- package/dist/engine.js +30 -3
- package/dist/fake.d.ts +19 -2
- package/dist/fake.js +65 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/keymap-table.d.ts +20 -1
- package/dist/keymap-table.js +24 -3
- package/dist/keymap.js +14 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +6 -1
- package/dist/outline.d.ts +1 -0
- package/dist/outline.js +10 -0
- package/dist/paint.js +12 -2
- package/dist/types.d.ts +18 -2
- package/dist/view.d.ts +11 -0
- package/dist/view.js +73 -8
- package/package.json +2 -2
- package/src/controller.ts +110 -9
- package/src/editor.css +13 -0
- package/src/engine.ts +37 -3
- package/src/fake.ts +85 -3
- package/src/index.ts +3 -0
- package/src/keymap-table.ts +44 -4
- package/src/keymap.ts +14 -0
- package/src/layout.ts +9 -1
- package/src/outline.ts +13 -0
- package/src/paint.ts +11 -2
- package/src/types.ts +37 -2
- package/src/view.ts +95 -9
package/dist/layout.js
CHANGED
|
@@ -11,6 +11,8 @@ export function subRowBand(top, subRows, lineHeight, window, block = 1) {
|
|
|
11
11
|
last: Math.min(subRows - 1, Math.ceil((last + 1) / block) * block - 1)
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
/* The side of a swatch in pixels, without its gaps. */
|
|
15
|
+
export const SWATCH_SIZE = 12;
|
|
14
16
|
export const TEXT_PADDING = 0;
|
|
15
17
|
export const BOTTOM_PADDING = 8;
|
|
16
18
|
export const RIGHT_PADDING = 16;
|
|
@@ -154,9 +156,12 @@ export function scanDenseLine(line, metrics, limit, lineInlays) {
|
|
|
154
156
|
flushRun(offset);
|
|
155
157
|
const inlay = lineInlays[nextInlay++];
|
|
156
158
|
const shown = visibleBidi(inlay.text);
|
|
159
|
+
const gaps = (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
|
|
157
160
|
const width = inlay.composition || inlay.ghost
|
|
158
161
|
? metrics.measureText(shown)
|
|
159
|
-
:
|
|
162
|
+
: inlay.swatch !== undefined
|
|
163
|
+
? SWATCH_SIZE + gaps
|
|
164
|
+
: Math.max(8, metrics.measureInlay(shown) + 12) + gaps;
|
|
160
165
|
geometry.inlays.push({ inlay, x, width, subRow: row });
|
|
161
166
|
x += width;
|
|
162
167
|
}
|
package/dist/outline.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare class Outline {
|
|
|
37
37
|
setStructure(entries: OutlineEntry[]): void;
|
|
38
38
|
setProvided(blocks: readonly EditorBlock[] | null, document: OutlineDocument): void;
|
|
39
39
|
edited(batches: readonly (readonly DocumentChange[])[]): void;
|
|
40
|
+
declarationStops(document: OutlineDocument): number[];
|
|
40
41
|
blocks(document: OutlineDocument): readonly OutlineBlock[];
|
|
41
42
|
}
|
|
42
43
|
export interface StickyRows {
|
package/dist/outline.js
CHANGED
|
@@ -140,6 +140,16 @@ export class Outline {
|
|
|
140
140
|
this.provided = this.provided === null ? null : move(this.provided);
|
|
141
141
|
this.version++;
|
|
142
142
|
}
|
|
143
|
+
/* Where the named blocks of the structure declare their name, in no order: the stops of Method Up and Down. */
|
|
144
|
+
declarationStops(document) {
|
|
145
|
+
return this.structure.flatMap((entry) => {
|
|
146
|
+
if (entry.name === undefined) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const header = document.getLine(document.positionAt(entry.from).line);
|
|
150
|
+
return [header.start + Math.max(0, header.text.indexOf(entry.name))];
|
|
151
|
+
});
|
|
152
|
+
}
|
|
143
153
|
/* Outermost first. Of two blocks that start on one line, only the larger one. */
|
|
144
154
|
blocks(document) {
|
|
145
155
|
if (this.cache?.version !== this.version) {
|
package/dist/paint.js
CHANGED
|
@@ -102,10 +102,20 @@ export function lineElement(document, geometry, tokens, lineHeight, band, whites
|
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
104
|
const span = document.createElement('span');
|
|
105
|
-
span.className = box.inlay.ghost
|
|
105
|
+
span.className = box.inlay.ghost
|
|
106
|
+
? 'se-ghost'
|
|
107
|
+
: box.inlay.composition
|
|
108
|
+
? 'se-composition'
|
|
109
|
+
: box.inlay.swatch !== undefined
|
|
110
|
+
? 'se-inlay se-swatch'
|
|
111
|
+
: 'se-inlay';
|
|
106
112
|
span.dataset.inlayId = box.inlay.id;
|
|
107
113
|
const { parts } = box.inlay;
|
|
108
|
-
if (
|
|
114
|
+
if (box.inlay.swatch !== undefined) {
|
|
115
|
+
span.style.backgroundColor = box.inlay.swatch;
|
|
116
|
+
span.setAttribute('aria-label', box.inlay.text);
|
|
117
|
+
}
|
|
118
|
+
else if (parts === undefined) {
|
|
109
119
|
span.textContent = visibleBidi(box.inlay.text);
|
|
110
120
|
}
|
|
111
121
|
else {
|
package/dist/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { EditorCommand, FoldRole, WhitespaceCleanupOptions } from '@adecore
|
|
|
4
4
|
export type { EditorCommand, FoldRole };
|
|
5
5
|
export type EditorWhitespaceCleanup = WhitespaceCleanupOptions;
|
|
6
6
|
export type EditorFoldOutline = 'off' | 'hover' | 'always';
|
|
7
|
-
export type EditorViewCommand = 'collapseRegion' | 'expandRegion' | 'collapseAllRegions' | 'expandAllRegions' | 'collapseRegionRecursively' | 'expandRegionRecursively' | 'foldSelection' | 'collapseDocComments' | 'expandDocComments' | 'expandAllToLevel1' | 'expandAllToLevel2' | 'expandAllToLevel3' | 'expandAllToLevel4' | 'expandAllToLevel5' | 'toggleColumnMode' | 'increaseFontSize' | 'decreaseFontSize' | 'resetFontSize';
|
|
7
|
+
export type EditorViewCommand = 'collapseRegion' | 'expandRegion' | 'collapseAllRegions' | 'expandAllRegions' | 'collapseRegionRecursively' | 'expandRegionRecursively' | 'foldSelection' | 'collapseDocComments' | 'expandDocComments' | 'expandAllToLevel1' | 'expandAllToLevel2' | 'expandAllToLevel3' | 'expandAllToLevel4' | 'expandAllToLevel5' | 'toggleColumnMode' | 'increaseFontSize' | 'decreaseFontSize' | 'resetFontSize' | 'methodUp' | 'methodDown' | 'nextChange' | 'previousChange' | 'pasteAsPlainText';
|
|
8
8
|
export interface EditorZoom {
|
|
9
9
|
readonly min?: number;
|
|
10
10
|
readonly max?: number;
|
|
@@ -139,6 +139,15 @@ export interface EditorContentChange {
|
|
|
139
139
|
readonly range: EditorRange;
|
|
140
140
|
readonly text: string;
|
|
141
141
|
}
|
|
142
|
+
export interface EditorCopy {
|
|
143
|
+
readonly text: string;
|
|
144
|
+
readonly wholeLines: boolean;
|
|
145
|
+
readonly cut: boolean;
|
|
146
|
+
}
|
|
147
|
+
export interface EditorPasteOptions {
|
|
148
|
+
readonly wholeLines?: boolean;
|
|
149
|
+
readonly reindent?: boolean;
|
|
150
|
+
}
|
|
142
151
|
export interface EditorApplyEditsOptions {
|
|
143
152
|
readonly mapSelections?: (range: EditorRange) => EditorRange;
|
|
144
153
|
readonly mergeUndo?: boolean;
|
|
@@ -240,6 +249,7 @@ export interface EditorInlayHint {
|
|
|
240
249
|
readonly parts?: readonly string[];
|
|
241
250
|
readonly paddingLeft?: boolean;
|
|
242
251
|
readonly paddingRight?: boolean;
|
|
252
|
+
readonly swatch?: string;
|
|
243
253
|
}
|
|
244
254
|
export interface EditorInlayTarget {
|
|
245
255
|
readonly id: string;
|
|
@@ -336,6 +346,12 @@ export interface Editor {
|
|
|
336
346
|
setTodoWords?(words: readonly string[]): void;
|
|
337
347
|
setLabel(label: string): void;
|
|
338
348
|
setFoldHints(hints: EditorFoldHints | null): void;
|
|
349
|
+
setDeclarations?(positions: readonly EditorPosition[] | null): void;
|
|
350
|
+
onNavigate?(listener: (from: EditorPosition) => void): () => void;
|
|
351
|
+
hasFocus?(): boolean;
|
|
352
|
+
setWordSources?(source: (() => readonly string[]) | null): void;
|
|
353
|
+
onCopy?(listener: (copy: EditorCopy) => void): () => void;
|
|
354
|
+
paste?(text: string, options?: EditorPasteOptions): boolean;
|
|
339
355
|
setChangeMarks(marks: readonly EditorChangeMark[], layer?: string): void;
|
|
340
356
|
setBlocks(blocks: readonly EditorBlock[] | null): void;
|
|
341
357
|
onScope(listener: (scope: readonly EditorBlock[]) => void): () => void;
|
|
@@ -351,7 +367,7 @@ export interface Editor {
|
|
|
351
367
|
setHighlights(highlights: readonly EditorHighlight[]): void;
|
|
352
368
|
setLink(range: EditorRange | null): void;
|
|
353
369
|
setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void;
|
|
354
|
-
setInlayHints(hints: readonly EditorInlayHint[]): void;
|
|
370
|
+
setInlayHints(hints: readonly EditorInlayHint[], owner?: string): void;
|
|
355
371
|
onInlayHover?(listener: (target: EditorInlayTarget | null) => void): () => void;
|
|
356
372
|
onInlayClick?(handler: EditorInlayClickHandler): () => void;
|
|
357
373
|
setInlayLink?(target: {
|
package/dist/view.d.ts
CHANGED
|
@@ -143,6 +143,7 @@ export declare class EditorView {
|
|
|
143
143
|
private lenses;
|
|
144
144
|
private readonly replacements;
|
|
145
145
|
private occurrences;
|
|
146
|
+
private hippieSource;
|
|
146
147
|
private replacePreview;
|
|
147
148
|
private dropCaret;
|
|
148
149
|
private activeGuideCache;
|
|
@@ -157,6 +158,7 @@ export declare class EditorView {
|
|
|
157
158
|
private customFolds;
|
|
158
159
|
private collapsed;
|
|
159
160
|
private foldHints;
|
|
161
|
+
private declarations;
|
|
160
162
|
private defaultRoles;
|
|
161
163
|
private settled;
|
|
162
164
|
private foldTimer;
|
|
@@ -188,6 +190,10 @@ export declare class EditorView {
|
|
|
188
190
|
setLenses(lenses: readonly BlockWidget[]): void;
|
|
189
191
|
private allBlocks;
|
|
190
192
|
setOccurrences(occurrences: readonly Occurrence[]): void;
|
|
193
|
+
setHippieSource(range: {
|
|
194
|
+
from: number;
|
|
195
|
+
to: number;
|
|
196
|
+
} | null): void;
|
|
191
197
|
setLink(link: {
|
|
192
198
|
from: number;
|
|
193
199
|
to: number;
|
|
@@ -239,6 +245,7 @@ export declare class EditorView {
|
|
|
239
245
|
private foldStates;
|
|
240
246
|
private scheduleFolds;
|
|
241
247
|
refreshFolds(): void;
|
|
248
|
+
structureRanges(): FoldingRange[];
|
|
242
249
|
private withCustomFolds;
|
|
243
250
|
private closerOf;
|
|
244
251
|
private foldAt;
|
|
@@ -274,6 +281,10 @@ export declare class EditorView {
|
|
|
274
281
|
endLine: number;
|
|
275
282
|
}[];
|
|
276
283
|
} | null, defaults?: readonly FoldRole[]): void;
|
|
284
|
+
setDeclarations(offsets: readonly number[] | null): void;
|
|
285
|
+
private methodStops;
|
|
286
|
+
goToChange(direction: -1 | 1): boolean;
|
|
287
|
+
goToMethod(direction: -1 | 1): boolean;
|
|
277
288
|
setFoldHints(hints: FoldHints | null): void;
|
|
278
289
|
private applyFoldDefaults;
|
|
279
290
|
foldRole(role: FoldRole, collapse: boolean): void;
|
package/dist/view.js
CHANGED
|
@@ -168,6 +168,7 @@ export class EditorView {
|
|
|
168
168
|
lenses = [];
|
|
169
169
|
replacements = new Map();
|
|
170
170
|
occurrences = [];
|
|
171
|
+
hippieSource = null;
|
|
171
172
|
replacePreview = null;
|
|
172
173
|
dropCaret = null;
|
|
173
174
|
activeGuideCache;
|
|
@@ -186,6 +187,7 @@ export class EditorView {
|
|
|
186
187
|
collapsed = new Set();
|
|
187
188
|
/* What a language server said about the folds, in offsets that follow the text through edits. */
|
|
188
189
|
foldHints = null;
|
|
190
|
+
declarations = null;
|
|
189
191
|
/* The roles that fold by themselves while the file is as it was opened; null once a person edited or the file opened with folds of its own. */
|
|
190
192
|
defaultRoles = null;
|
|
191
193
|
/* The folds somebody already decided on, a person or the defaults, so a later answer of a server leaves them as they are. */
|
|
@@ -427,6 +429,14 @@ export class EditorView {
|
|
|
427
429
|
this.occurrences = [...occurrences];
|
|
428
430
|
this.render();
|
|
429
431
|
}
|
|
432
|
+
/* The word Hippie completion took from this document, shown until the next key or edit; null takes the mark away. */
|
|
433
|
+
setHippieSource(range) {
|
|
434
|
+
if (range === null && this.hippieSource === null) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
this.hippieSource = range;
|
|
438
|
+
this.render();
|
|
439
|
+
}
|
|
430
440
|
/* A name underlined as a link a press follows, with the pointer to match; null takes it away. */
|
|
431
441
|
setLink(link) {
|
|
432
442
|
this.link = link;
|
|
@@ -621,6 +631,7 @@ export class EditorView {
|
|
|
621
631
|
this.markerVersion++;
|
|
622
632
|
const anchor = this.topAnchor;
|
|
623
633
|
this.occurrences = [];
|
|
634
|
+
this.hippieSource = null;
|
|
624
635
|
this.link = null;
|
|
625
636
|
this.content.classList.remove('se-content-link');
|
|
626
637
|
this.outline.edited(batches);
|
|
@@ -668,6 +679,9 @@ export class EditorView {
|
|
|
668
679
|
if (this.foldHints !== null) {
|
|
669
680
|
this.foldHints = mapFoldHints(this.foldHints, map);
|
|
670
681
|
}
|
|
682
|
+
if (this.declarations !== null) {
|
|
683
|
+
this.declarations = this.declarations.map((offset) => map(offset));
|
|
684
|
+
}
|
|
671
685
|
this.customFolds = this.customFolds.map((fold) => mapRange(fold, map));
|
|
672
686
|
this.find.mapBounds(changes);
|
|
673
687
|
for (const [layer, marks] of this.changeLayers) {
|
|
@@ -801,20 +815,25 @@ export class EditorView {
|
|
|
801
815
|
if (this.disposed || this.settings.folding === false) {
|
|
802
816
|
return;
|
|
803
817
|
}
|
|
804
|
-
const ranges = this.
|
|
805
|
-
? []
|
|
806
|
-
: this.model.getFoldingRanges({
|
|
807
|
-
indentation: /^(python|py|yaml|yml)$/.test(this.settings.language ?? ''),
|
|
808
|
-
tabSize: this.settings.tabSize,
|
|
809
|
-
...(this.settings.language === undefined ? {} : { language: this.settings.language }),
|
|
810
|
-
...(this.foldHints === null ? {} : { hints: this.foldHints })
|
|
811
|
-
});
|
|
818
|
+
const ranges = this.structureRanges();
|
|
812
819
|
this.foldRanges = this.withCustomFolds(ranges);
|
|
813
820
|
this.outline.setStructure(structuralEntries(ranges, this.model, (from, to) => this.model.slice(from, to), (offset) => this.model.matchBracket(offset, this.settings.language)?.mate));
|
|
814
821
|
const anchors = new Set(this.foldRanges.map((range) => range.from));
|
|
815
822
|
this.collapsed = new Set([...this.collapsed].filter((anchor) => anchors.has(anchor)));
|
|
816
823
|
this.refoldLayout();
|
|
817
824
|
}
|
|
825
|
+
/* The folding ranges the document has now, a server's included, whether or not the folds are drawn. */
|
|
826
|
+
structureRanges() {
|
|
827
|
+
if (this.model.getLength() > FOLD_LIMIT) {
|
|
828
|
+
return [];
|
|
829
|
+
}
|
|
830
|
+
return this.model.getFoldingRanges({
|
|
831
|
+
indentation: /^(python|py|yaml|yml)$/.test(this.settings.language ?? ''),
|
|
832
|
+
tabSize: this.settings.tabSize,
|
|
833
|
+
...(this.settings.language === undefined ? {} : { language: this.settings.language }),
|
|
834
|
+
...(this.foldHints === null ? {} : { hints: this.foldHints })
|
|
835
|
+
});
|
|
836
|
+
}
|
|
818
837
|
/* The folds a person made of a selection, which the document's own structure does not know, among the ones it does. */
|
|
819
838
|
withCustomFolds(ranges) {
|
|
820
839
|
const custom = this.customFolds.flatMap((fold) => {
|
|
@@ -1023,6 +1042,49 @@ export class EditorView {
|
|
|
1023
1042
|
this.applyFoldDefaults();
|
|
1024
1043
|
this.refoldLayout();
|
|
1025
1044
|
}
|
|
1045
|
+
/* The places Method Up and Down stop at; they follow their text through edits until set again, and null goes back to the editor's own reading. */
|
|
1046
|
+
setDeclarations(offsets) {
|
|
1047
|
+
this.declarations = offsets === null ? null : [...new Set(offsets)].sort((left, right) => left - right);
|
|
1048
|
+
}
|
|
1049
|
+
methodStops() {
|
|
1050
|
+
if (this.declarations !== null) {
|
|
1051
|
+
return this.declarations;
|
|
1052
|
+
}
|
|
1053
|
+
const sections = this.foldRanges.filter((range) => range.kind === 'section').map((range) => this.model.getLine(range.startLine).start);
|
|
1054
|
+
return [...new Set([...this.outline.declarationStops(this.model), ...sections])].sort((left, right) => left - right);
|
|
1055
|
+
}
|
|
1056
|
+
/* Moves the one caret to the first line of the next or previous change mark, of any layer. False when there is none. */
|
|
1057
|
+
goToChange(direction) {
|
|
1058
|
+
const line = this.model.positionAt(this.model.getPrimary().head).line;
|
|
1059
|
+
const { runs } = this.resolvedChanges();
|
|
1060
|
+
// A run the caret is in is where it already is; a deleted run is the line its mark sits on.
|
|
1061
|
+
const target = direction === 1 ? runs.find((run) => run.start > line) : runs.findLast((run) => (run.kind === 'deleted' ? run.start : run.end) < line);
|
|
1062
|
+
if (target === undefined) {
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
const offset = this.model.getLine(Math.min(target.start, this.model.getLineCount() - 1)).start;
|
|
1066
|
+
this.model.setSelections([{ anchor: offset, head: offset }]);
|
|
1067
|
+
this.revealOffset(offset, 'center');
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
/* Moves the one caret to the previous or next declaration on another line. False when there is none. */
|
|
1071
|
+
goToMethod(direction) {
|
|
1072
|
+
// The structure is read after a pause in typing, so a press right after an edit reads it now.
|
|
1073
|
+
if (this.declarations === null) {
|
|
1074
|
+
this.refreshFolds();
|
|
1075
|
+
}
|
|
1076
|
+
const line = this.model.positionAt(this.model.getPrimary().head).line;
|
|
1077
|
+
const stops = this.methodStops();
|
|
1078
|
+
const target = direction === 1
|
|
1079
|
+
? stops.find((offset) => this.model.positionAt(offset).line > line)
|
|
1080
|
+
: stops.findLast((offset) => this.model.positionAt(offset).line < line);
|
|
1081
|
+
if (target === undefined) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
this.model.setSelections([{ anchor: target, head: target }]);
|
|
1085
|
+
this.revealOffset(target, direction === 1 ? 'centerDown' : 'centerUp');
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1026
1088
|
/* Hands over what a language server knows about the folds; null forgets it. The defaults fold what the answer names, once. */
|
|
1027
1089
|
setFoldHints(hints) {
|
|
1028
1090
|
this.foldHints = hints;
|
|
@@ -1866,6 +1928,9 @@ export class EditorView {
|
|
|
1866
1928
|
});
|
|
1867
1929
|
}
|
|
1868
1930
|
}
|
|
1931
|
+
if (this.hippieSource !== null) {
|
|
1932
|
+
marks.push({ className: 'se-hippie-source', rects: this.paintRectangles(this.hippieSource.from, this.hippieSource.to, rows) });
|
|
1933
|
+
}
|
|
1869
1934
|
const marginX = this.settings.rightMargin === null ? null : this.settings.rightMargin * this.layout.metrics.charWidth;
|
|
1870
1935
|
// A margin past the content would stretch the scroll extent to itself, so it shows only where the text or the view reaches it.
|
|
1871
1936
|
if (marginX !== null && marginX < Math.max(this.layout.width, this.viewportWidth)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"test": "bun test --conditions=source"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@adecore/editor-core": "0.
|
|
57
|
+
"@adecore/editor-core": "0.43.0",
|
|
58
58
|
"linkedom": "^0.18.14",
|
|
59
59
|
"shiki": "^4.5.0"
|
|
60
60
|
},
|
package/src/controller.ts
CHANGED
|
@@ -15,7 +15,9 @@ import type {
|
|
|
15
15
|
EditorInlayClick,
|
|
16
16
|
EditorInlayClickHandler,
|
|
17
17
|
EditorContextMenu,
|
|
18
|
+
EditorCopy,
|
|
18
19
|
EditorKeyHandler,
|
|
20
|
+
EditorPasteOptions,
|
|
19
21
|
EditorPosition,
|
|
20
22
|
EditorRunCommand,
|
|
21
23
|
EditorSelectionRanges,
|
|
@@ -45,9 +47,12 @@ const MODIFYING = new Set<EditorCommand>([
|
|
|
45
47
|
'deleteLine',
|
|
46
48
|
'moveLineUp',
|
|
47
49
|
'moveLineDown',
|
|
50
|
+
'moveStatementUp',
|
|
51
|
+
'moveStatementDown',
|
|
48
52
|
'toggleLineComment',
|
|
49
53
|
'toggleBlockComment',
|
|
50
54
|
'startNewLine',
|
|
55
|
+
'completeCurrentStatement',
|
|
51
56
|
'startNewLineBefore',
|
|
52
57
|
'splitLine',
|
|
53
58
|
'joinLines',
|
|
@@ -63,9 +68,13 @@ const MODIFYING = new Set<EditorCommand>([
|
|
|
63
68
|
'reverseLines',
|
|
64
69
|
'uniqueLines',
|
|
65
70
|
'convertIndentsToSpaces',
|
|
66
|
-
'convertIndentsToTabs'
|
|
71
|
+
'convertIndentsToTabs',
|
|
72
|
+
'hippieCompletion',
|
|
73
|
+
'hippieBackwardCompletion'
|
|
67
74
|
]);
|
|
68
75
|
|
|
76
|
+
const CODE_BLOCK_COMMANDS = new Set<EditorCommand>(['codeBlockStart', 'codeBlockEnd', 'codeBlockStartWithSelection', 'codeBlockEndWithSelection']);
|
|
77
|
+
|
|
69
78
|
const VIEW_COMMANDS = new Set<EditorViewCommand>([
|
|
70
79
|
'collapseRegion',
|
|
71
80
|
'expandRegion',
|
|
@@ -84,7 +93,12 @@ const VIEW_COMMANDS = new Set<EditorViewCommand>([
|
|
|
84
93
|
'toggleColumnMode',
|
|
85
94
|
'increaseFontSize',
|
|
86
95
|
'decreaseFontSize',
|
|
87
|
-
'resetFontSize'
|
|
96
|
+
'resetFontSize',
|
|
97
|
+
'methodUp',
|
|
98
|
+
'methodDown',
|
|
99
|
+
'nextChange',
|
|
100
|
+
'previousChange',
|
|
101
|
+
'pasteAsPlainText'
|
|
88
102
|
]);
|
|
89
103
|
|
|
90
104
|
const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0 } as const satisfies Partial<Record<EditorViewCommand, -1 | 0 | 1>>;
|
|
@@ -92,6 +106,14 @@ const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0
|
|
|
92
106
|
/* 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. */
|
|
93
107
|
let lineClip: string | null = null;
|
|
94
108
|
|
|
109
|
+
/*
|
|
110
|
+
* Makes `text` the copy that pastes back as whole lines (or not), for a paste from a history that kept what the
|
|
111
|
+
* copy was. The clipboard itself is the caller's.
|
|
112
|
+
*/
|
|
113
|
+
export function rememberCopy(text: string, wholeLines: boolean): void {
|
|
114
|
+
lineClip = wholeLines ? text : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
95
117
|
function selectionSignature(selections: readonly Selection[]): string {
|
|
96
118
|
return selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
|
|
97
119
|
}
|
|
@@ -111,6 +133,9 @@ export class InputController {
|
|
|
111
133
|
private readonly touch: TouchSelection;
|
|
112
134
|
private readonly cleanup: (() => void)[] = [];
|
|
113
135
|
private readonly keyHandlers = new Set<EditorKeyHandler>();
|
|
136
|
+
private readonly navigateListeners = new Set<(from: number) => void>();
|
|
137
|
+
private readonly copyListeners = new Set<(copy: EditorCopy) => void>();
|
|
138
|
+
private wordSources: (() => readonly string[]) | null = null;
|
|
114
139
|
private readonly gesture: DoubleModifierGesture;
|
|
115
140
|
private readonly clickHandlers = new Set<EditorClickHandler>();
|
|
116
141
|
private readonly inlayHandlers = new Set<EditorInlayClickHandler>();
|
|
@@ -240,7 +265,7 @@ export class InputController {
|
|
|
240
265
|
|
|
241
266
|
this.listen(input, 'paste', (event) => {
|
|
242
267
|
event.preventDefault();
|
|
243
|
-
this.
|
|
268
|
+
this.pasteText(event.clipboardData?.getData('text/plain') ?? '');
|
|
244
269
|
});
|
|
245
270
|
|
|
246
271
|
this.listen(input, 'copy', (event) => this.copy(event, false));
|
|
@@ -312,6 +337,8 @@ export class InputController {
|
|
|
312
337
|
|
|
313
338
|
this.cleanup.length = 0;
|
|
314
339
|
this.keyHandlers.clear();
|
|
340
|
+
this.navigateListeners.clear();
|
|
341
|
+
this.copyListeners.clear();
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
private blockedByReadOnly(): boolean {
|
|
@@ -356,16 +383,30 @@ export class InputController {
|
|
|
356
383
|
}
|
|
357
384
|
}
|
|
358
385
|
|
|
359
|
-
|
|
386
|
+
/* Pastes as a paste from the clipboard does; false when a read-only editor refused. */
|
|
387
|
+
pasteText(text: string, options: EditorPasteOptions = {}): boolean {
|
|
360
388
|
if (this.blockedByReadOnly()) {
|
|
361
|
-
return;
|
|
389
|
+
return false;
|
|
362
390
|
}
|
|
363
391
|
|
|
364
392
|
this.unfoldCarets();
|
|
365
393
|
this.historyGroup++;
|
|
366
|
-
|
|
394
|
+
|
|
395
|
+
const pasted = this.view.model.paste(this.native.normalizeNewlines(text), {
|
|
396
|
+
...this.commandOptions(),
|
|
397
|
+
...(options.reindent === false ? { indentOnPaste: false } : {}),
|
|
398
|
+
wholeLines: options.wholeLines ?? (text !== '' && text === lineClip)
|
|
399
|
+
});
|
|
400
|
+
|
|
367
401
|
this.historyGroup++;
|
|
368
402
|
this.view.revealCaret();
|
|
403
|
+
|
|
404
|
+
return pasted;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Told what a copy or cut put on the clipboard. */
|
|
408
|
+
onCopy(listener: (copy: EditorCopy) => void): () => void {
|
|
409
|
+
return subscribe(this.copyListeners, listener);
|
|
369
410
|
}
|
|
370
411
|
|
|
371
412
|
private commandOptions(): CommandOptions {
|
|
@@ -381,15 +422,23 @@ export class InputController {
|
|
|
381
422
|
tabOutOfClosers: keys.tabOutOfClosers,
|
|
382
423
|
smartEnter: keys.smartIndentOnEnter,
|
|
383
424
|
indentOnPaste: keys.indentOnPaste,
|
|
425
|
+
indentGuides: settings.guides,
|
|
384
426
|
smartSemicolon: keys.smartSemicolon,
|
|
385
427
|
smartArrow: keys.smartArrow,
|
|
386
428
|
language: settings.language ?? 'plaintext',
|
|
387
429
|
camelCase: keys.camelHumps,
|
|
388
430
|
visualLine: (offset) => this.view.rowOf(offset),
|
|
389
|
-
lineSpan: (line) => this.view.lineSpan(line)
|
|
431
|
+
lineSpan: (line) => this.view.lineSpan(line),
|
|
432
|
+
structure: () => this.view.structureRanges(),
|
|
433
|
+
...(this.wordSources === null ? {} : { wordSources: this.wordSources })
|
|
390
434
|
};
|
|
391
435
|
}
|
|
392
436
|
|
|
437
|
+
/* The texts Hippie completion takes words from after those of this document; null takes them away. */
|
|
438
|
+
setWordSources(source: (() => readonly string[]) | null): void {
|
|
439
|
+
this.wordSources = source;
|
|
440
|
+
}
|
|
441
|
+
|
|
393
442
|
/* Folding is the view's, since it knows where the ranges are. */
|
|
394
443
|
private viewCommand(name: EditorViewCommand): boolean {
|
|
395
444
|
const { view } = this;
|
|
@@ -400,11 +449,43 @@ export class InputController {
|
|
|
400
449
|
return true;
|
|
401
450
|
}
|
|
402
451
|
|
|
452
|
+
if (name === 'methodUp' || name === 'methodDown') {
|
|
453
|
+
const from = view.model.getPrimary().head;
|
|
454
|
+
const moved = view.goToMethod(name === 'methodUp' ? -1 : 1);
|
|
455
|
+
|
|
456
|
+
if (moved) {
|
|
457
|
+
emit(this.navigateListeners, from);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return moved;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (name === 'pasteAsPlainText') {
|
|
464
|
+
// The clipboard is read once a person asks, and an editor with no way to read it does nothing.
|
|
465
|
+
if (this.blockedByReadOnly() || globalThis.navigator?.clipboard?.readText === undefined) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
void navigator.clipboard.readText().then(
|
|
470
|
+
(text) => this.pasteText(text, { reindent: false }),
|
|
471
|
+
() => undefined
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (name === 'nextChange' || name === 'previousChange') {
|
|
478
|
+
return view.goToChange(name === 'nextChange' ? 1 : -1);
|
|
479
|
+
}
|
|
480
|
+
|
|
403
481
|
if (view.settings.folding === false && name !== 'toggleColumnMode') {
|
|
404
482
|
return false;
|
|
405
483
|
}
|
|
406
484
|
|
|
407
|
-
const run: Record<
|
|
485
|
+
const run: Record<
|
|
486
|
+
Exclude<EditorViewCommand, keyof typeof ZOOM_STEPS | 'methodUp' | 'methodDown' | 'nextChange' | 'previousChange' | 'pasteAsPlainText'>,
|
|
487
|
+
() => void
|
|
488
|
+
> = {
|
|
408
489
|
collapseRegion: () => view.collapseRegion(),
|
|
409
490
|
expandRegion: () => view.expandRegion(),
|
|
410
491
|
collapseAllRegions: () => view.foldAll(true),
|
|
@@ -529,7 +610,11 @@ export class InputController {
|
|
|
529
610
|
}
|
|
530
611
|
}
|
|
531
612
|
|
|
532
|
-
const
|
|
613
|
+
const before = model.getPrimary().head;
|
|
614
|
+
const folded =
|
|
615
|
+
name === 'moveLineUp' || name === 'moveLineDown' || name === 'moveStatementUp' || name === 'moveStatementDown'
|
|
616
|
+
? this.view.getFolds().collapsed
|
|
617
|
+
: [];
|
|
533
618
|
|
|
534
619
|
const result = model.execute(name, {
|
|
535
620
|
...this.commandOptions(),
|
|
@@ -544,11 +629,24 @@ export class InputController {
|
|
|
544
629
|
this.view.markRowEnd(model.getPrimary().head);
|
|
545
630
|
}
|
|
546
631
|
|
|
632
|
+
if (name === 'hippieCompletion' || name === 'hippieBackwardCompletion') {
|
|
633
|
+
this.view.setHippieSource(model.hippieSource);
|
|
634
|
+
}
|
|
635
|
+
|
|
547
636
|
this.view.revealCaret();
|
|
548
637
|
|
|
638
|
+
if (CODE_BLOCK_COMMANDS.has(name) && model.getPrimary().head !== before) {
|
|
639
|
+
emit(this.navigateListeners, before);
|
|
640
|
+
}
|
|
641
|
+
|
|
549
642
|
return result;
|
|
550
643
|
}
|
|
551
644
|
|
|
645
|
+
/* Told the offset the primary caret left when a command that navigates moved it. */
|
|
646
|
+
onNavigate(listener: (from: number) => void): () => void {
|
|
647
|
+
return subscribe(this.navigateListeners, listener);
|
|
648
|
+
}
|
|
649
|
+
|
|
552
650
|
private typeText(text: string): void {
|
|
553
651
|
if (this.blockedByReadOnly()) {
|
|
554
652
|
return;
|
|
@@ -837,6 +935,8 @@ export class InputController {
|
|
|
837
935
|
return;
|
|
838
936
|
}
|
|
839
937
|
|
|
938
|
+
this.view.setHippieSource(null);
|
|
939
|
+
|
|
840
940
|
const added = this.gesture.keydown(event);
|
|
841
941
|
|
|
842
942
|
if (added !== null) {
|
|
@@ -933,6 +1033,7 @@ export class InputController {
|
|
|
933
1033
|
|
|
934
1034
|
event.clipboardData?.setData('text/plain', text);
|
|
935
1035
|
lineClip = empty ? text : null;
|
|
1036
|
+
emit(this.copyListeners, { text, wholeLines: empty, cut });
|
|
936
1037
|
|
|
937
1038
|
if (!cut || this.blockedByReadOnly()) {
|
|
938
1039
|
return;
|
package/src/editor.css
CHANGED
|
@@ -652,11 +652,16 @@
|
|
|
652
652
|
.se-find-match,
|
|
653
653
|
.se-find-current,
|
|
654
654
|
.se-occurrence,
|
|
655
|
+
.se-hippie-source,
|
|
655
656
|
.se-bracket-match,
|
|
656
657
|
.se-bracket-unmatched {
|
|
657
658
|
position: absolute;
|
|
658
659
|
border-radius: var(--radius-xs);
|
|
659
660
|
}
|
|
661
|
+
.se-hippie-source {
|
|
662
|
+
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
663
|
+
border-radius: var(--radius-sm);
|
|
664
|
+
}
|
|
660
665
|
.se-find-match {
|
|
661
666
|
background: var(--find-match);
|
|
662
667
|
}
|
|
@@ -747,6 +752,14 @@
|
|
|
747
752
|
white-space: pre;
|
|
748
753
|
cursor: default;
|
|
749
754
|
}
|
|
755
|
+
.se-swatch {
|
|
756
|
+
height: 12px;
|
|
757
|
+
margin-top: calc((var(--code-line-height, 20px) - 12px) / 2);
|
|
758
|
+
padding: 0;
|
|
759
|
+
border-radius: var(--radius-sm);
|
|
760
|
+
box-shadow: inset 0 0 0 1px var(--border);
|
|
761
|
+
cursor: pointer;
|
|
762
|
+
}
|
|
750
763
|
.se-inlay-link {
|
|
751
764
|
text-decoration: underline;
|
|
752
765
|
text-underline-offset: 2px;
|