@adecore/editor 0.42.0 → 0.44.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/src/keymap.ts CHANGED
@@ -44,10 +44,15 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
44
44
  deleteLine: command('deleteLine'),
45
45
  moveLineUp: command('moveLineUp'),
46
46
  moveLineDown: command('moveLineDown'),
47
+ moveStatementUp: command('moveStatementUp'),
48
+ moveStatementDown: command('moveStatementDown'),
47
49
  toggleLineComment: command('toggleLineComment'),
48
50
  toggleBlockComment: command('toggleBlockComment'),
49
51
  joinLines: command('joinLines'),
50
52
  startNewLine: command('startNewLine'),
53
+ completeCurrentStatement: command('completeCurrentStatement'),
54
+ hippieCompletion: command('hippieCompletion'),
55
+ hippieBackwardCompletion: command('hippieBackwardCompletion'),
51
56
  startNewLineBefore: command('startNewLineBefore'),
52
57
  splitLine: command('splitLine'),
53
58
  toggleCase: command('toggleCase'),
@@ -60,6 +65,15 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
60
65
  expandSelection: command('expandSelection'),
61
66
  shrinkSelection: command('shrinkSelection'),
62
67
  matchBrace: { type: 'bracket' },
68
+ methodUp: view('methodUp'),
69
+ methodDown: view('methodDown'),
70
+ pasteAsPlainText: view('pasteAsPlainText'),
71
+ nextChange: view('nextChange'),
72
+ previousChange: view('previousChange'),
73
+ codeBlockStart: command('codeBlockStart'),
74
+ codeBlockEnd: command('codeBlockEnd'),
75
+ codeBlockStartWithSelection: command('codeBlockStartWithSelection'),
76
+ codeBlockEndWithSelection: command('codeBlockEndWithSelection'),
63
77
  selectNextOccurrence: command('selectNextOccurrence'),
64
78
  unselectOccurrence: command('unselectOccurrence'),
65
79
  selectAllOccurrences: command('selectAllOccurrences'),
package/src/layout.ts CHANGED
@@ -57,8 +57,13 @@ export interface Inlay {
57
57
  composition?: boolean;
58
58
  /* A suggestion the host drew after the caret: text in the code face with no padding, which the caret stays in front of. */
59
59
  ghost?: boolean;
60
+ /* A square of this CSS color in place of the text, such as the color a literal in the code stands for. */
61
+ swatch?: string;
60
62
  }
61
63
 
64
+ /* The side of a swatch in pixels, without its gaps. */
65
+ export const SWATCH_SIZE = 12;
66
+
62
67
  /* A row of the host's own DOM above or below a line, as tall as the host makes it. */
63
68
  export interface BlockWidget {
64
69
  id: string;
@@ -366,10 +371,13 @@ export function scanDenseLine(
366
371
  const inlay = lineInlays[nextInlay++]!;
367
372
  const shown = visibleBidi(inlay.text);
368
373
 
374
+ const gaps = (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
369
375
  const width =
370
376
  inlay.composition || inlay.ghost
371
377
  ? metrics.measureText(shown)
372
- : Math.max(8, metrics.measureInlay(shown) + 12) + (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
378
+ : inlay.swatch !== undefined
379
+ ? SWATCH_SIZE + gaps
380
+ : Math.max(8, metrics.measureInlay(shown) + 12) + gaps;
373
381
 
374
382
  geometry.inlays.push({ inlay, x, width, subRow: row });
375
383
  x += width;
package/src/outline.ts CHANGED
@@ -215,6 +215,19 @@ export class Outline {
215
215
  this.version++;
216
216
  }
217
217
 
218
+ /* Where the named blocks of the structure declare their name, in no order: the stops of Method Up and Down. */
219
+ declarationStops(document: OutlineDocument): number[] {
220
+ return this.structure.flatMap((entry) => {
221
+ if (entry.name === undefined) {
222
+ return [];
223
+ }
224
+
225
+ const header = document.getLine(document.positionAt(entry.from).line);
226
+
227
+ return [header.start + Math.max(0, header.text.indexOf(entry.name))];
228
+ });
229
+ }
230
+
218
231
  /* Outermost first. Of two blocks that start on one line, only the larger one. */
219
232
  blocks(document: OutlineDocument): readonly OutlineBlock[] {
220
233
  if (this.cache?.version !== this.version) {
package/src/paint.ts CHANGED
@@ -186,11 +186,20 @@ export function lineElement(
186
186
  }
187
187
 
188
188
  const span = document.createElement('span');
189
- span.className = box.inlay.ghost ? 'se-ghost' : box.inlay.composition ? 'se-composition' : 'se-inlay';
189
+ span.className = box.inlay.ghost
190
+ ? 'se-ghost'
191
+ : box.inlay.composition
192
+ ? 'se-composition'
193
+ : box.inlay.swatch !== undefined
194
+ ? 'se-inlay se-swatch'
195
+ : 'se-inlay';
190
196
  span.dataset.inlayId = box.inlay.id;
191
197
  const { parts } = box.inlay;
192
198
 
193
- if (parts === undefined) {
199
+ if (box.inlay.swatch !== undefined) {
200
+ span.style.backgroundColor = box.inlay.swatch;
201
+ span.setAttribute('aria-label', box.inlay.text);
202
+ } else if (parts === undefined) {
194
203
  span.textContent = visibleBidi(box.inlay.text);
195
204
  } else {
196
205
  parts.forEach((part, index) => {
@@ -0,0 +1,45 @@
1
+ import type { DocumentModel } from '@adecore/editor-core';
2
+ import type { EditorPosition, EditorPostfixOperand, EditorRange, EditorSurroundShape, EditorTextPlace, EditorUnwrapTarget } from './types.ts';
3
+
4
+ /* How an editor turns the offsets of the text model into positions and back. */
5
+ export interface Coordinates {
6
+ positionAt(offset: number): EditorPosition;
7
+ offsetAt(position: EditorPosition): number;
8
+ }
9
+
10
+ function rangeOf(coordinates: Coordinates, from: number, to: number): EditorRange {
11
+ return { start: coordinates.positionAt(from), end: coordinates.positionAt(to) };
12
+ }
13
+
14
+ export function placeAt(model: DocumentModel, coordinates: Coordinates, position: EditorPosition, language: string): EditorTextPlace {
15
+ return model.placeAt(coordinates.offsetAt(position), language);
16
+ }
17
+
18
+ export function postfixOperands(model: DocumentModel, coordinates: Coordinates, dot: EditorPosition, language: string): EditorPostfixOperand[] {
19
+ const at = coordinates.offsetAt(dot);
20
+
21
+ return model.postfixOperands(at, language).map((operand) => ({
22
+ range: rangeOf(coordinates, operand.from, operand.from + model.slice(operand.from, at).trimEnd().length),
23
+ statement: operand.statement
24
+ }));
25
+ }
26
+
27
+ export function surroundShape(model: DocumentModel, coordinates: Coordinates, range: EditorRange, language: string): EditorSurroundShape | null {
28
+ const shape = model.surroundShape(coordinates.offsetAt(range.start), coordinates.offsetAt(range.end), language);
29
+
30
+ return shape === null ? null : { range: rangeOf(coordinates, shape.from, shape.to), statements: shape.statements, expression: shape.expression };
31
+ }
32
+
33
+ export function unwrapTargets(
34
+ model: DocumentModel,
35
+ coordinates: Coordinates,
36
+ position: EditorPosition,
37
+ language: string,
38
+ indentUnit: string
39
+ ): EditorUnwrapTarget[] {
40
+ return model.unwrapTargets(coordinates.offsetAt(position), language, indentUnit).map((target) => ({
41
+ kind: target.kind,
42
+ range: rangeOf(coordinates, target.range.from, target.range.to),
43
+ edits: target.edits.map((edit) => ({ range: rangeOf(coordinates, edit.from, edit.to), text: edit.text }))
44
+ }));
45
+ }
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { EditorSearchClientOptions } from './search-client.ts';
2
2
  import type { Keymap } from './keymap-table.ts';
3
- import type { EditorCommand, FoldRole, WhitespaceCleanupOptions } from '@adecore/editor-core';
3
+ import type { EditorCommand, FoldRole, TextPlaceKind, UnwrapKind, WhitespaceCleanupOptions } from '@adecore/editor-core';
4
4
 
5
5
  export type { EditorCommand, FoldRole };
6
6
 
@@ -28,7 +28,12 @@ export type EditorViewCommand =
28
28
  | 'toggleColumnMode'
29
29
  | 'increaseFontSize'
30
30
  | 'decreaseFontSize'
31
- | 'resetFontSize';
31
+ | 'resetFontSize'
32
+ | 'methodUp'
33
+ | 'methodDown'
34
+ | 'nextChange'
35
+ | 'previousChange'
36
+ | 'pasteAsPlainText';
32
37
 
33
38
  /* How far a person may zoom the text, and whether the wheel does. */
34
39
  export interface EditorZoom {
@@ -283,6 +288,52 @@ export interface EditorContentChange {
283
288
  readonly text: string;
284
289
  }
285
290
 
291
+ /* What the text at a position is written in, with embedded regions resolved, and whether it is code, a comment or a string. */
292
+ export interface EditorTextPlace {
293
+ readonly language: string;
294
+ readonly kind: TextPlaceKind;
295
+ }
296
+
297
+ /* An expression that ends right before a dot. */
298
+ export interface EditorPostfixOperand {
299
+ /* The expression, without the whitespace before the dot. */
300
+ readonly range: EditorRange;
301
+ /* Whether it starts a statement; only the widest operand can. */
302
+ readonly statement: boolean;
303
+ }
304
+
305
+ /* What a range can be surrounded with: whole statements, one expression, both or neither. */
306
+ export interface EditorSurroundShape {
307
+ /* The range after trimming whitespace, widened past a trailing line comment for statements. */
308
+ readonly range: EditorRange;
309
+ readonly statements: boolean;
310
+ readonly expression: boolean;
311
+ }
312
+
313
+ /* A construct around a position that can be unwrapped or removed. */
314
+ export interface EditorUnwrapTarget {
315
+ readonly kind: UnwrapKind;
316
+ /* The whole construct, for a preview. */
317
+ readonly range: EditorRange;
318
+ /* Small edits in the coordinates of the text before them, which `applyEdits` takes as they are. */
319
+ readonly edits: readonly EditorContentChange[];
320
+ }
321
+
322
+ /* What a copy or cut put on the clipboard. */
323
+ export interface EditorCopy {
324
+ readonly text: string;
325
+ /* A copy of bare carets takes their lines, which paste back above the lines of bare carets. */
326
+ readonly wholeLines: boolean;
327
+ readonly cut: boolean;
328
+ }
329
+
330
+ export interface EditorPasteOptions {
331
+ /* Pastes above the lines of bare carets, as the paste of a copy of bare carets does; by default when the text is the last such copy. */
332
+ readonly wholeLines?: boolean;
333
+ /* False leaves the indentation of the text alone; the editor's indent on paste setting otherwise. */
334
+ readonly reindent?: boolean;
335
+ }
336
+
286
337
  export interface EditorApplyEditsOptions {
287
338
  readonly mapSelections?: (range: EditorRange) => EditorRange;
288
339
  readonly mergeUndo?: boolean;
@@ -472,6 +523,8 @@ export interface EditorInlayHint {
472
523
  /* A gap of 4 pixels that keeps the pill off the character before it or the one after it. */
473
524
  readonly paddingLeft?: boolean;
474
525
  readonly paddingRight?: boolean;
526
+ /* A square of this CSS color in place of the label, such as the color a literal stands for; the label is then what assistive technology reads. */
527
+ readonly swatch?: string;
475
528
  }
476
529
 
477
530
  /* A hint, or one part of its label, under the pointer. */
@@ -649,6 +702,18 @@ export interface Editor {
649
702
  setLabel(label: string): void;
650
703
  /* What a language server knows about the folds: the bodies of symbols and the ranges it folds. They follow their text through edits until set again; null forgets them. */
651
704
  setFoldHints(hints: EditorFoldHints | null): void;
705
+ /* The places Method Up and Down stop at, such as the names of a language server's symbols. They follow their text through edits until set again; null goes back to the editor's own reading of the structure. Optional so an editor from before it still fits. */
706
+ setDeclarations?(positions: readonly EditorPosition[] | null): void;
707
+ /* A command that navigates (Method Up and Down, Code Block Start and End) moved the primary caret away from `from`. Optional so an editor from before it still fits. */
708
+ onNavigate?(listener: (from: EditorPosition) => void): () => void;
709
+ /* Whether the editor has the keyboard focus. Optional so an editor from before it still fits. */
710
+ hasFocus?(): boolean;
711
+ /* The texts of the other open documents, which Hippie completion takes words from after those of this one; null takes them away. Optional so an editor from before it still fits. */
712
+ setWordSources?(source: (() => readonly string[]) | null): void;
713
+ /* Told after a copy or cut, with the text it put on the clipboard. Optional so an editor from before it still fits. */
714
+ onCopy?(listener: (copy: EditorCopy) => void): () => void;
715
+ /* Pastes `text` as a paste from the clipboard does: at every caret, with indent on paste, and refused with its notice when the editor is read-only. False when nothing was pasted. Optional so an editor from before it still fits. */
716
+ paste?(text: string, options?: EditorPasteOptions): boolean;
652
717
  /*
653
718
  * Marks that follow their lines through edits until the host sets them again. Every layer (default: the unnamed one)
654
719
  * holds its own marks and the editor draws the union, so setting one layer never clears another; an empty list
@@ -699,7 +764,8 @@ export interface Editor {
699
764
  /* Colors by what the language servers know, over what the grammar made of the text; null takes them away. */
700
765
  setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void;
701
766
  /* Hints to draw as soft pills in the text, replacing the ones set before. They follow their text through edits until set again. */
702
- setInlayHints(hints: readonly EditorInlayHint[]): void;
767
+ /* Each owner sets its own hints and keeps those of the others; ids stay unique across owners. */
768
+ setInlayHints(hints: readonly EditorInlayHint[], owner?: string): void;
703
769
  /* The pointer moved onto a hint or one of its parts, or null when it left them. Optional so an editor from before it still fits. */
704
770
  onInlayHover?(listener: (target: EditorInlayTarget | null) => void): () => void;
705
771
  /* Handlers see a press of the primary button on a hint before the editor does, the first to take it winning. */
@@ -808,6 +874,14 @@ export interface Editor {
808
874
  setReadOnly(readOnly: boolean, reason?: string): void;
809
875
  /* Whether the editor refuses edits. Optional so an editor from before it still fits. */
810
876
  isReadOnly?(): boolean;
877
+ /* What the text at a position is written in and whether it is code, a comment or a string. Optional so an editor from before it still fits. */
878
+ placeAt?(position: EditorPosition): EditorTextPlace;
879
+ /* Every expression that ends right before the `.` at `dot`, innermost first. Optional so an editor from before it still fits. */
880
+ postfixOperands?(dot: EditorPosition): readonly EditorPostfixOperand[];
881
+ /* What a range can be surrounded with; null for a blank range. Optional so an editor from before it still fits. */
882
+ surroundShape?(range: EditorRange): EditorSurroundShape | null;
883
+ /* What can be unwrapped or removed around a position, innermost first. Optional so an editor from before it still fits. */
884
+ unwrapTargets?(position: EditorPosition): readonly EditorUnwrapTarget[];
811
885
  /* Runs an editing command on every caret, as its key would. False when it changed nothing. */
812
886
  runCommand(command: EditorRunCommand): boolean;
813
887
  focus(): void;
package/src/view.ts CHANGED
@@ -298,6 +298,7 @@ export class EditorView {
298
298
  private lenses: BlockWidget[] = [];
299
299
  private readonly replacements = new Map<string, BlockReplacement[]>();
300
300
  private occurrences: Occurrence[] = [];
301
+ private hippieSource: { from: number; to: number } | null = null;
301
302
  private replacePreview: { text: string; preserveCase: boolean } | null = null;
302
303
  private dropCaret: number | null = null;
303
304
  private activeGuideCache: { key: string; guide: { column: number; first: number; last: number } | null } | undefined;
@@ -316,6 +317,7 @@ export class EditorView {
316
317
  private collapsed = new Set<number>();
317
318
  /* What a language server said about the folds, in offsets that follow the text through edits. */
318
319
  private foldHints: FoldHints | null = null;
320
+ private declarations: number[] | null = null;
319
321
  /* 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. */
320
322
  private defaultRoles: ReadonlySet<FoldRole> | null = null;
321
323
  /* The folds somebody already decided on, a person or the defaults, so a later answer of a server leaves them as they are. */
@@ -600,6 +602,16 @@ export class EditorView {
600
602
  this.render();
601
603
  }
602
604
 
605
+ /* The word Hippie completion took from this document, shown until the next key or edit; null takes the mark away. */
606
+ setHippieSource(range: { from: number; to: number } | null): void {
607
+ if (range === null && this.hippieSource === null) {
608
+ return;
609
+ }
610
+
611
+ this.hippieSource = range;
612
+ this.render();
613
+ }
614
+
603
615
  /* A name underlined as a link a press follows, with the pointer to match; null takes it away. */
604
616
  setLink(link: { from: number; to: number } | null): void {
605
617
  this.link = link;
@@ -849,6 +861,7 @@ export class EditorView {
849
861
  this.markerVersion++;
850
862
  const anchor = this.topAnchor;
851
863
  this.occurrences = [];
864
+ this.hippieSource = null;
852
865
  this.link = null;
853
866
  this.content.classList.remove('se-content-link');
854
867
  this.outline.edited(batches);
@@ -916,6 +929,10 @@ export class EditorView {
916
929
  this.foldHints = mapFoldHints(this.foldHints, map);
917
930
  }
918
931
 
932
+ if (this.declarations !== null) {
933
+ this.declarations = this.declarations.map((offset) => map(offset));
934
+ }
935
+
919
936
  this.customFolds = this.customFolds.map((fold) => mapRange(fold, map));
920
937
  this.find.mapBounds(changes);
921
938
 
@@ -1089,15 +1106,7 @@ export class EditorView {
1089
1106
  return;
1090
1107
  }
1091
1108
 
1092
- const ranges =
1093
- this.model.getLength() > FOLD_LIMIT
1094
- ? []
1095
- : this.model.getFoldingRanges({
1096
- indentation: /^(python|py|yaml|yml)$/.test(this.settings.language ?? ''),
1097
- tabSize: this.settings.tabSize,
1098
- ...(this.settings.language === undefined ? {} : { language: this.settings.language }),
1099
- ...(this.foldHints === null ? {} : { hints: this.foldHints })
1100
- });
1109
+ const ranges = this.structureRanges();
1101
1110
 
1102
1111
  this.foldRanges = this.withCustomFolds(ranges);
1103
1112
 
@@ -1115,6 +1124,20 @@ export class EditorView {
1115
1124
  this.refoldLayout();
1116
1125
  }
1117
1126
 
1127
+ /* The folding ranges the document has now, a server's included, whether or not the folds are drawn. */
1128
+ structureRanges(): FoldingRange[] {
1129
+ if (this.model.getLength() > FOLD_LIMIT) {
1130
+ return [];
1131
+ }
1132
+
1133
+ return this.model.getFoldingRanges({
1134
+ indentation: /^(python|py|yaml|yml)$/.test(this.settings.language ?? ''),
1135
+ tabSize: this.settings.tabSize,
1136
+ ...(this.settings.language === undefined ? {} : { language: this.settings.language }),
1137
+ ...(this.foldHints === null ? {} : { hints: this.foldHints })
1138
+ });
1139
+ }
1140
+
1118
1141
  /* The folds a person made of a selection, which the document's own structure does not know, among the ones it does. */
1119
1142
  private withCustomFolds(ranges: readonly FoldingRange[]): ViewFold[] {
1120
1143
  const custom = this.customFolds.flatMap((fold): ViewFold[] => {
@@ -1383,6 +1406,65 @@ export class EditorView {
1383
1406
  this.refoldLayout();
1384
1407
  }
1385
1408
 
1409
+ /* 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. */
1410
+ setDeclarations(offsets: readonly number[] | null): void {
1411
+ this.declarations = offsets === null ? null : [...new Set(offsets)].sort((left, right) => left - right);
1412
+ }
1413
+
1414
+ private methodStops(): number[] {
1415
+ if (this.declarations !== null) {
1416
+ return this.declarations;
1417
+ }
1418
+
1419
+ const sections = this.foldRanges.filter((range) => range.kind === 'section').map((range) => this.model.getLine(range.startLine).start);
1420
+
1421
+ return [...new Set([...this.outline.declarationStops(this.model), ...sections])].sort((left, right) => left - right);
1422
+ }
1423
+
1424
+ /* Moves the one caret to the first line of the next or previous change mark, of any layer. False when there is none. */
1425
+ goToChange(direction: -1 | 1): boolean {
1426
+ const line = this.model.positionAt(this.model.getPrimary().head).line;
1427
+ const { runs } = this.resolvedChanges();
1428
+
1429
+ // A run the caret is in is where it already is; a deleted run is the line its mark sits on.
1430
+ const target = direction === 1 ? runs.find((run) => run.start > line) : runs.findLast((run) => (run.kind === 'deleted' ? run.start : run.end) < line);
1431
+
1432
+ if (target === undefined) {
1433
+ return false;
1434
+ }
1435
+
1436
+ const offset = this.model.getLine(Math.min(target.start, this.model.getLineCount() - 1)).start;
1437
+ this.model.setSelections([{ anchor: offset, head: offset }]);
1438
+ this.revealOffset(offset, 'center');
1439
+
1440
+ return true;
1441
+ }
1442
+
1443
+ /* Moves the one caret to the previous or next declaration on another line. False when there is none. */
1444
+ goToMethod(direction: -1 | 1): boolean {
1445
+ // The structure is read after a pause in typing, so a press right after an edit reads it now.
1446
+ if (this.declarations === null) {
1447
+ this.refreshFolds();
1448
+ }
1449
+
1450
+ const line = this.model.positionAt(this.model.getPrimary().head).line;
1451
+ const stops = this.methodStops();
1452
+
1453
+ const target =
1454
+ direction === 1
1455
+ ? stops.find((offset) => this.model.positionAt(offset).line > line)
1456
+ : stops.findLast((offset) => this.model.positionAt(offset).line < line);
1457
+
1458
+ if (target === undefined) {
1459
+ return false;
1460
+ }
1461
+
1462
+ this.model.setSelections([{ anchor: target, head: target }]);
1463
+ this.revealOffset(target, direction === 1 ? 'centerDown' : 'centerUp');
1464
+
1465
+ return true;
1466
+ }
1467
+
1386
1468
  /* Hands over what a language server knows about the folds; null forgets it. The defaults fold what the answer names, once. */
1387
1469
  setFoldHints(hints: FoldHints | null): void {
1388
1470
  this.foldHints = hints;
@@ -2480,6 +2562,10 @@ export class EditorView {
2480
2562
  }
2481
2563
  }
2482
2564
 
2565
+ if (this.hippieSource !== null) {
2566
+ marks.push({ className: 'se-hippie-source', rects: this.paintRectangles(this.hippieSource.from, this.hippieSource.to, rows) });
2567
+ }
2568
+
2483
2569
  const marginX = this.settings.rightMargin === null ? null : this.settings.rightMargin * this.layout.metrics.charWidth;
2484
2570
 
2485
2571
  // A margin past the content would stretch the scroll extent to itself, so it shows only where the text or the view reaches it.