@adecore/editor 0.38.0 → 0.40.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.
Files changed (53) hide show
  1. package/dist/controller.d.ts +1 -0
  2. package/dist/controller.js +17 -28
  3. package/dist/engine.js +2 -9
  4. package/dist/find.js +12 -13
  5. package/dist/layout.js +7 -2
  6. package/dist/paint.d.ts +2 -0
  7. package/dist/paint.js +23 -20
  8. package/dist/view.d.ts +3 -2
  9. package/dist/view.js +46 -59
  10. package/package.json +6 -6
  11. package/src/attribution.ts +14 -0
  12. package/src/code-block.ts +25 -0
  13. package/src/column-selection.ts +10 -0
  14. package/src/controller.ts +189 -28
  15. package/src/engine.ts +87 -9
  16. package/src/fake.ts +79 -0
  17. package/src/find-search.ts +20 -0
  18. package/src/find.ts +135 -14
  19. package/src/geometry-stops.ts +10 -0
  20. package/src/grammar-preparation.ts +23 -0
  21. package/src/index.ts +1 -0
  22. package/src/input.ts +13 -0
  23. package/src/keymap-table.ts +9 -0
  24. package/src/keymap.ts +25 -0
  25. package/src/layout-changes.ts +7 -0
  26. package/src/layout.ts +219 -3
  27. package/src/lens.ts +3 -0
  28. package/src/listeners.ts +1 -0
  29. package/src/metrics.ts +14 -0
  30. package/src/modifier-gesture.ts +17 -0
  31. package/src/native-input.ts +27 -0
  32. package/src/offsets.ts +10 -0
  33. package/src/outline.ts +38 -0
  34. package/src/overview.ts +13 -0
  35. package/src/paint.ts +140 -20
  36. package/src/php-html.ts +1 -0
  37. package/src/pointer.ts +50 -0
  38. package/src/scroll-animation.ts +10 -0
  39. package/src/scroll.ts +7 -0
  40. package/src/search-client.ts +32 -0
  41. package/src/search-worker.ts +38 -0
  42. package/src/semantic.ts +12 -0
  43. package/src/shiki-document.ts +19 -0
  44. package/src/shiki-worker.ts +68 -0
  45. package/src/shiki.ts +37 -0
  46. package/src/testing.ts +17 -0
  47. package/src/theme-scopes.ts +14 -0
  48. package/src/tokens.ts +31 -0
  49. package/src/touch.ts +16 -0
  50. package/src/tracked-range.ts +5 -0
  51. package/src/uniform-geometry.ts +10 -0
  52. package/src/view.ts +481 -75
  53. package/src/worker-client.ts +54 -0
@@ -37,6 +37,7 @@ export declare class InputController {
37
37
  private inputGroup;
38
38
  private applyEdits;
39
39
  private replaceSelections;
40
+ private unfoldCarets;
40
41
  private paste;
41
42
  private commandOptions;
42
43
  private viewCommand;
@@ -88,6 +88,10 @@ export class InputController {
88
88
  this.options = options;
89
89
  this.gesture = new DoubleModifierGesture(options.apple ? 'Alt' : 'Control');
90
90
  const { model, input, viewport } = view;
91
+ const lineAt = (offset) => {
92
+ const row = view.layout.rowForLine(model.positionAt(offset).line);
93
+ return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
94
+ };
91
95
  this.native = new NativeInput(input, model, {
92
96
  readOnly: () => view.settings.readOnly,
93
97
  replace: (text) => this.replaceSelections(text),
@@ -110,10 +114,7 @@ export class InputController {
110
114
  contentPoint: (x, y) => view.contentPoint(x, y),
111
115
  columnSelections: (from, to) => (sameCell(view.layout, from, to) ? null : columnSelections(view.layout, from, to)),
112
116
  visualLine: (offset) => view.layout.visualLine(offset),
113
- lineAt: (offset) => {
114
- const row = view.layout.rowForLine(model.positionAt(offset).line);
115
- return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
116
- },
117
+ lineAt,
117
118
  camelHumps: () => view.settings.smartKeys.camelHumps,
118
119
  columnMode: () => view.columnMode,
119
120
  selectionAt: (x, y) => (view.settings.readOnly ? null : view.selectionAtPoint(x, y)),
@@ -127,10 +128,7 @@ export class InputController {
127
128
  model,
128
129
  offsetAt: (x, y) => view.offsetAtPoint(x, y),
129
130
  camelHumps: () => view.settings.smartKeys.camelHumps,
130
- lineAt: (offset) => {
131
- const row = view.layout.rowForLine(model.positionAt(offset).line);
132
- return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
133
- },
131
+ lineAt,
134
132
  focus: () => view.focus()
135
133
  });
136
134
  this.subscription = model.subscribe(() => {
@@ -237,15 +235,8 @@ export class InputController {
237
235
  this.cleanup.length = 0;
238
236
  this.keyHandlers.clear();
239
237
  }
240
- /* Tells a person why nothing happened, when the editor is read only and has said why. */
241
238
  blockedByReadOnly() {
242
- if (!this.view.settings.readOnly) {
243
- return false;
244
- }
245
- if (this.view.settings.readOnlyReason) {
246
- this.view.notify(this.view.settings.readOnlyReason);
247
- }
248
- return true;
239
+ return this.view.blockedByReadOnly();
249
240
  }
250
241
  inputGroup() {
251
242
  const now = Date.now();
@@ -266,23 +257,23 @@ export class InputController {
266
257
  return;
267
258
  }
268
259
  const { model } = this.view;
269
- for (const selection of model.getSelections()) {
270
- this.view.ensureVisible(selection.head);
271
- }
260
+ this.unfoldCarets();
272
261
  const replacement = replacementEdits(model.getSelections(), this.native.normalizeNewlines(text));
273
262
  this.applyEdits(replacement.edits, replacement.selections);
274
263
  this.view.revealCaret();
275
264
  }
265
+ unfoldCarets() {
266
+ for (const selection of this.view.model.getSelections()) {
267
+ this.view.ensureVisible(selection.head);
268
+ }
269
+ }
276
270
  paste(text) {
277
271
  if (this.blockedByReadOnly()) {
278
272
  return;
279
273
  }
280
- const { model } = this.view;
281
- for (const selection of model.getSelections()) {
282
- this.view.ensureVisible(selection.head);
283
- }
274
+ this.unfoldCarets();
284
275
  this.historyGroup++;
285
- model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
276
+ this.view.model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
286
277
  this.historyGroup++;
287
278
  this.view.revealCaret();
288
279
  }
@@ -358,7 +349,7 @@ export class InputController {
358
349
  return { line, character: column };
359
350
  });
360
351
  const chains = await provider(asked).catch(() => null);
361
- if (model.getRevision() !== revision || JSON.stringify(model.getSelections()) !== JSON.stringify(selections)) {
352
+ if (model.getRevision() !== revision || selectionSignature(model.getSelections()) !== selectionSignature(selections)) {
362
353
  return;
363
354
  }
364
355
  if (chains === null) {
@@ -413,9 +404,7 @@ export class InputController {
413
404
  if (this.blockedByReadOnly()) {
414
405
  return;
415
406
  }
416
- for (const selection of this.view.model.getSelections()) {
417
- this.view.ensureVisible(selection.head);
418
- }
407
+ this.unfoldCarets();
419
408
  // Every key goes through the model, which decides by the key before it too, as for the arrow of PHP.
420
409
  if (text.length === 1) {
421
410
  this.view.model.typeText(text, { ...this.commandOptions(), historyGroup: this.inputGroup() });
package/dist/engine.js CHANGED
@@ -164,8 +164,7 @@ class SmartEditor {
164
164
  return;
165
165
  }
166
166
  this.caretOffset = head;
167
- const position = this.positionAt(head);
168
- emit(this.carets, position);
167
+ emit(this.carets, this.positionAt(head));
169
168
  }
170
169
  getCaret() {
171
170
  return this.positionAt(this.model.getPrimary().head);
@@ -483,13 +482,7 @@ class SmartEditor {
483
482
  }
484
483
  }
485
484
  blockedByReadOnly() {
486
- if (!this.settings.readOnly) {
487
- return false;
488
- }
489
- if (this.settings.readOnlyReason) {
490
- this.view.notify(this.settings.readOnlyReason);
491
- }
492
- return true;
485
+ return this.view.blockedByReadOnly();
493
486
  }
494
487
  onFind(listener) {
495
488
  return this.view.onFind(listener);
package/dist/find.js CHANGED
@@ -360,14 +360,7 @@ export class FindController {
360
360
  }
361
361
  this.pendingAnchor = undefined;
362
362
  this.pendingStep = undefined;
363
- this.failed =
364
- error instanceof EditorSearchError
365
- ? error.kind === 'syntax'
366
- ? 'invalid'
367
- : error.kind === 'timeout' || error.kind === 'unavailable'
368
- ? error.kind
369
- : 'error'
370
- : 'error';
363
+ this.failed = searchFailure(error);
371
364
  this.regexCache = { revision, key, complete: false };
372
365
  this.options?.changed();
373
366
  });
@@ -534,7 +527,8 @@ export class FindController {
534
527
  this.options?.changed();
535
528
  }
536
529
  async replaceOne(replacement, preserveCase, canApply) {
537
- const query = this.query, mark = this.currentMark;
530
+ const query = this.query;
531
+ const mark = this.currentMark;
538
532
  if (query === null || mark === null || this.search === undefined || !canApply()) {
539
533
  return false;
540
534
  }
@@ -574,8 +568,7 @@ export class FindController {
574
568
  }
575
569
  catch (error) {
576
570
  if (valid()) {
577
- const failure = searchFailure(error);
578
- this.reportReplaceFailed(error instanceof EditorSearchError && error.kind === 'range' ? 'limit' : failure === 'invalid' ? 'error' : failure);
571
+ this.reportReplaceFailed(replaceFailure(error));
579
572
  }
580
573
  return false;
581
574
  }
@@ -629,8 +622,7 @@ export class FindController {
629
622
  }
630
623
  catch (error) {
631
624
  if (!this.disposed && epoch === this.epoch) {
632
- const failure = searchFailure(error);
633
- this.replaceFailed = error instanceof EditorSearchError && error.kind === 'range' ? 'limit' : failure === 'invalid' ? 'error' : failure;
625
+ this.replaceFailed = replaceFailure(error);
634
626
  }
635
627
  return 0;
636
628
  }
@@ -656,3 +648,10 @@ function searchFailure(error) {
656
648
  : 'error'
657
649
  : 'error';
658
650
  }
651
+ function replaceFailure(error) {
652
+ if (error instanceof EditorSearchError && error.kind === 'range') {
653
+ return 'limit';
654
+ }
655
+ const failure = searchFailure(error);
656
+ return failure === 'invalid' ? 'error' : failure;
657
+ }
package/dist/layout.js CHANGED
@@ -417,7 +417,13 @@ export class EditorLayout {
417
417
  for (const block of this.blocks) {
418
418
  const line = this.document.positionAt(block.at).line;
419
419
  const target = block.placement === 'above' ? above : below;
420
- target.set(line, [...(target.get(line) ?? []), block]);
420
+ const list = target.get(line);
421
+ if (list === undefined) {
422
+ target.set(line, [block]);
423
+ }
424
+ else {
425
+ list.push(block);
426
+ }
421
427
  }
422
428
  const folds = new Map();
423
429
  for (const fold of this.folds) {
@@ -703,7 +709,6 @@ export class EditorLayout {
703
709
  rowForLine(line) {
704
710
  return this.rows[this.lineRows[Math.max(0, Math.min(this.lineRows.length - 1, line))]];
705
711
  }
706
- /* Whether a fold hides the line. */
707
712
  isHidden(line) {
708
713
  const row = this.rowForLine(line);
709
714
  return row.kind === 'text' && line > row.line && line <= row.lastLine;
package/dist/paint.d.ts CHANGED
@@ -24,6 +24,8 @@ export declare class RowPainter {
24
24
  private buildText;
25
25
  private appendTail;
26
26
  private paintReplacement;
27
+ private hostEntry;
28
+ private measure;
27
29
  private paintBlock;
28
30
  private fillBlock;
29
31
  clear(): void;
package/dist/paint.js CHANGED
@@ -221,7 +221,7 @@ export class RowPainter {
221
221
  chip.className = 'se-fold-chip';
222
222
  chip.textContent = '…';
223
223
  chip.tabIndex = -1;
224
- const chipLeft = geometry.rowEnds[geometry.rowEnds.length - 1] + FOLD_GAP;
224
+ const chipLeft = geometry.rowEnds.at(-1) + FOLD_GAP;
225
225
  const chipWidth = Math.ceil(this.layout.metrics.charWidth) + FOLD_CHIP_PADDING;
226
226
  chip.style.left = `${chipLeft}px`;
227
227
  chip.style.top = `${(geometry.subRows - 1) * lineHeight}px`;
@@ -265,8 +265,7 @@ export class RowPainter {
265
265
  }
266
266
  replacement.render(element);
267
267
  this.code.append(element);
268
- entry = { element, text: '', tokens: null, layoutVersion: 0, whitespace: false, folded: false, tail: '', tailTokens: null, revision };
269
- this.rows.set(row.key, entry);
268
+ entry = this.hostEntry(row.key, element, revision);
270
269
  }
271
270
  else if (entry.revision !== revision) {
272
271
  entry.element.replaceChildren();
@@ -277,10 +276,19 @@ export class RowPainter {
277
276
  entry.element.style.top = `${row.top}px`;
278
277
  entry.element.style.left = `${indent}px`;
279
278
  entry.element.style.width = `${Math.max(80, paint.viewportWidth - indent)}px`;
280
- entry.element.style.height = '';
281
- this.observer?.observe(entry.element);
282
- const height = entry.element.getBoundingClientRect?.().height ?? 0;
283
- return height > 0 && this.layout.setMeasuredHeight(row.key, height);
279
+ return this.measure(row.key, entry.element);
280
+ }
281
+ hostEntry(key, element, revision) {
282
+ const entry = { element, text: '', tokens: null, layoutVersion: 0, whitespace: false, folded: false, tail: '', tailTokens: null, revision };
283
+ this.rows.set(key, entry);
284
+ return entry;
285
+ }
286
+ /* Lets the host's content set the height, and returns whether that moved the layout. */
287
+ measure(key, element) {
288
+ element.style.height = '';
289
+ this.observer?.observe(element);
290
+ const height = element.getBoundingClientRect?.().height ?? 0;
291
+ return height > 0 && this.layout.setMeasuredHeight(key, height);
284
292
  }
285
293
  paintBlock(row, paint) {
286
294
  const { widget } = row;
@@ -296,8 +304,7 @@ export class RowPainter {
296
304
  if (!widget.lens && !widget.fixed) {
297
305
  this.observer?.observe(element);
298
306
  }
299
- entry = { element, text: '', tokens: null, layoutVersion: 0, whitespace: false, folded: false, tail: '', tailTokens: null, revision };
300
- this.rows.set(row.key, entry);
307
+ entry = this.hostEntry(row.key, element, revision);
301
308
  }
302
309
  else if (entry.revision !== revision) {
303
310
  entry.element.replaceChildren();
@@ -322,10 +329,7 @@ export class RowPainter {
322
329
  entry.element.style.height = `${row.height}px`;
323
330
  return false;
324
331
  }
325
- entry.element.style.height = '';
326
- this.observer?.observe(entry.element);
327
- const height = entry.element.getBoundingClientRect?.().height ?? 0;
328
- return height > 0 && this.layout.setMeasuredHeight(row.key, height);
332
+ return this.measure(row.key, entry.element);
329
333
  }
330
334
  fillBlock(element, widget) {
331
335
  if (widget.render) {
@@ -407,15 +411,14 @@ export function paintGutter(container, layout, rows, paint) {
407
411
  }
408
412
  item.append(button);
409
413
  }
410
- const sign = paint.highlights.get(row.line)?.sign;
411
- if (sign) {
414
+ if (highlight?.sign) {
412
415
  const mark = document.createElement('span');
413
416
  mark.className = 'se-line-sign';
414
- mark.textContent = sign;
417
+ mark.textContent = highlight.sign;
415
418
  if (label !== null) {
416
419
  mark.setAttribute('aria-hidden', 'true');
417
420
  }
418
- mark.style.color = colorValue(paint.highlights.get(row.line).color);
421
+ mark.style.color = colorValue(highlight.color);
419
422
  item.append(mark);
420
423
  }
421
424
  if (highlight?.bar !== undefined) {
@@ -495,7 +498,6 @@ export function paintOver(container, layers) {
495
498
  }
496
499
  container.replaceChildren(fragment);
497
500
  }
498
- /* The carets, above the text. */
499
501
  export function paintCarets(container, carets) {
500
502
  const document = container.ownerDocument;
501
503
  container.replaceChildren(...carets.map((rect) => box(document, rect.drop ? 'se-drop-caret' : rect.primary ? 'se-caret se-primary-caret' : 'se-caret', { ...rect, width: 2 })));
@@ -571,14 +573,15 @@ function appendTabSigns(element, geometry, lineHeight, band) {
571
573
  if (geometry.text[offsetOfStop(geometry, stop)] !== '\t') {
572
574
  continue;
573
575
  }
576
+ const row = rowOfStop(geometry, stop);
574
577
  const left = xOfStop(geometry, stop);
575
- const right = rowOfStop(geometry, stop + 1) === rowOfStop(geometry, stop) ? xOfStop(geometry, stop + 1) : geometry.rowEnds[rowOfStop(geometry, stop)];
578
+ const right = rowOfStop(geometry, stop + 1) === row ? xOfStop(geometry, stop + 1) : geometry.rowEnds[row];
576
579
  if (right - left < 3) {
577
580
  continue;
578
581
  }
579
582
  const sign = arrowSign(document, right - left - 2, lineHeight, 'se-tab-sign');
580
583
  sign.style.left = `${left + 1}px`;
581
- sign.style.top = `${rowOfStop(geometry, stop) * lineHeight}px`;
584
+ sign.style.top = `${row * lineHeight}px`;
582
585
  element.append(sign);
583
586
  }
584
587
  }
package/dist/view.d.ts CHANGED
@@ -194,6 +194,7 @@ export declare class EditorView {
194
194
  setMarkers(markers: readonly ViewMarker[]): void;
195
195
  setComposition(text: string | undefined): void;
196
196
  notify(message: string): void;
197
+ blockedByReadOnly(): boolean;
197
198
  private foldName;
198
199
  announce(message: string): void;
199
200
  setInputPopup(popup: EditorInputPopup | null): void;
@@ -317,6 +318,7 @@ export declare class EditorView {
317
318
  private wrapSigns;
318
319
  private indentOf;
319
320
  private effectiveIndent;
321
+ private nearestIndent;
320
322
  private activeGuide;
321
323
  private scopeGuide;
322
324
  private caretOnGuide;
@@ -393,10 +395,9 @@ export declare class EditorView {
393
395
  color: string;
394
396
  }[]): void;
395
397
  setLineHighlights(highlights: readonly EditorLineHighlight[]): void;
396
- private highlightedLines;
398
+ private runLines;
397
399
  private highlightMarks;
398
400
  renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions): void;
399
- private attributedLines;
400
401
  onAttributionHover(listener: (hover: {
401
402
  id: string;
402
403
  rect: EditorRect;
package/dist/view.js CHANGED
@@ -20,6 +20,9 @@ import { TokenCache } from './tokens.js';
20
20
  function sameRect(left, right) {
21
21
  return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
22
22
  }
23
+ function closestElement(target, selector) {
24
+ return target?.closest?.(selector) ?? null;
25
+ }
23
26
  function mapRange(range, map) {
24
27
  return { ...range, from: map(range.from), to: map(range.to) };
25
28
  }
@@ -453,8 +456,7 @@ export class EditorView {
453
456
  continue;
454
457
  }
455
458
  const line = this.model.positionAt(token.from).line;
456
- const start = this.model.getLine(line).start;
457
- const end = this.model.getLine(line).end;
459
+ const { start, end } = this.model.getLine(line);
458
460
  if (token.to > end) {
459
461
  continue;
460
462
  }
@@ -486,6 +488,16 @@ export class EditorView {
486
488
  this.notice.hidden = true;
487
489
  }, NOTICE_MS);
488
490
  }
491
+ /* Tells a person why nothing happened, when the editor is read only and has said why. */
492
+ blockedByReadOnly() {
493
+ if (!this.settings.readOnly) {
494
+ return false;
495
+ }
496
+ if (this.settings.readOnlyReason) {
497
+ this.notify(this.settings.readOnlyReason);
498
+ }
499
+ return true;
500
+ }
489
501
  foldName(line, collapsed) {
490
502
  const template = (collapsed ? this.settings.messages.unfold : this.settings.messages.fold) ??
491
503
  (collapsed ? 'Unfold lines {start} to {end}' : 'Fold lines {start} to {end}');
@@ -1095,16 +1107,16 @@ export class EditorView {
1095
1107
  this.anchorScroll(() => this.layout.configure({ wrapWidth: width }));
1096
1108
  }
1097
1109
  }
1098
- bracketAt(from, to, head) {
1099
- if (to - from > 1) {
1110
+ bracketAt({ anchor, head }) {
1111
+ if (Math.abs(head - anchor) > 1) {
1100
1112
  return undefined;
1101
1113
  }
1102
- const candidates = from !== to ? [from] : [head, head - 1];
1114
+ const candidates = anchor !== head ? [Math.min(anchor, head)] : [head, head - 1];
1103
1115
  return candidates.find((at) => at >= 0 && /[()[\]{}]/.test(this.model.slice(at, at + 1)));
1104
1116
  }
1105
1117
  /* The offset a bracket at the caret pairs with, if it is one. */
1106
1118
  matchingBracket(selection) {
1107
- const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
1119
+ const at = this.bracketAt(selection);
1108
1120
  return at === undefined ? null : { at, mate: this.model.matchBracket(at, this.settings.language)?.mate };
1109
1121
  }
1110
1122
  /* Draws the highlights of the brackets at the carets that the last frame ran out of lexing for. */
@@ -1151,7 +1163,7 @@ export class EditorView {
1151
1163
  }
1152
1164
  /* The offset of the character under a point of the page, or null over anything that is not text. */
1153
1165
  characterAtPoint(clientX, clientY, target) {
1154
- if (target?.closest?.('.se-inlay, .se-block, .se-line-action, .se-gutter, .se-sticky, .se-fold-chip, .se-overview')) {
1166
+ if (closestElement(target, '.se-inlay, .se-block, .se-line-action, .se-gutter, .se-sticky, .se-fold-chip, .se-overview')) {
1155
1167
  return null;
1156
1168
  }
1157
1169
  const point = this.contentPoint(clientX, clientY);
@@ -1292,17 +1304,18 @@ export class EditorView {
1292
1304
  if (own !== null) {
1293
1305
  return own;
1294
1306
  }
1307
+ return Math.min(this.nearestIndent(line, -1, GUIDE_SCAN_LINES), this.nearestIndent(line, 1, GUIDE_SCAN_LINES));
1308
+ }
1309
+ /* The indentation of the first line that is not blank within `reach` lines of `line` in `direction`, or 0. */
1310
+ nearestIndent(line, direction, reach) {
1295
1311
  const last = this.model.getLineCount() - 1;
1296
- const nearest = (direction) => {
1297
- for (let at = line + direction, steps = 0; at >= 0 && at <= last && steps < GUIDE_SCAN_LINES; at += direction, steps++) {
1298
- const indent = this.indentOf(at);
1299
- if (indent !== null) {
1300
- return indent;
1301
- }
1312
+ for (let at = line + direction, steps = 0; at >= 0 && at <= last && steps < reach; at += direction, steps++) {
1313
+ const indent = this.indentOf(at);
1314
+ if (indent !== null) {
1315
+ return indent;
1302
1316
  }
1303
- return 0;
1304
- };
1305
- return Math.min(nearest(-1), nearest(1));
1317
+ }
1318
+ return 0;
1306
1319
  }
1307
1320
  /* The scope guide of the caret, worked out again only when the text or the caret's line is not the one it was for. */
1308
1321
  activeGuide() {
@@ -1319,22 +1332,8 @@ export class EditorView {
1319
1332
  const indent = this.effectiveIndent(line);
1320
1333
  const count = this.model.getLineCount();
1321
1334
  const deeper = (at, column) => at >= 0 && at < count && this.effectiveIndent(at) > column;
1322
- const next = (() => {
1323
- for (let at = line + 1; at < count && at < line + GUIDE_SCAN_LINES; at++) {
1324
- if (this.indentOf(at) !== null) {
1325
- return this.indentOf(at);
1326
- }
1327
- }
1328
- return 0;
1329
- })();
1330
- const previous = (() => {
1331
- for (let at = line - 1; at >= 0 && at > line - GUIDE_SCAN_LINES; at--) {
1332
- if (this.indentOf(at) !== null) {
1333
- return this.indentOf(at);
1334
- }
1335
- }
1336
- return 0;
1337
- })();
1335
+ const next = this.nearestIndent(line, 1, GUIDE_SCAN_LINES - 1);
1336
+ const previous = this.nearestIndent(line, -1, GUIDE_SCAN_LINES - 1);
1338
1337
  // A line that opens a block, or closes one, is at the edge of its guide: the one that sits at its own indentation.
1339
1338
  const opens = indent % size === 0 && (next > indent || previous > indent) && this.indentOf(line) !== null;
1340
1339
  const column = opens ? indent : indent > 0 ? Math.ceil(indent / size - 1) * size : -1;
@@ -1441,7 +1440,7 @@ export class EditorView {
1441
1440
  }
1442
1441
  /* The hint, and the part of its label, an event happened on, or null anywhere else. */
1443
1442
  inlayAt(target) {
1444
- const element = target?.closest?.('.se-inlay');
1443
+ const element = closestElement(target, '.se-inlay');
1445
1444
  if (!element?.dataset.inlayId) {
1446
1445
  return null;
1447
1446
  }
@@ -1806,13 +1805,13 @@ export class EditorView {
1806
1805
  const lastRow = rows.at(-1);
1807
1806
  const firstLine = rows[0]?.line ?? 0;
1808
1807
  const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
1809
- const highlighted = this.highlightedLines(firstLine, lastLine);
1808
+ const highlighted = this.runLines(this.highlights, firstLine, lastLine);
1810
1809
  const shownFrom = this.model.offsetAt({ line: firstLine, column: 0 });
1811
1810
  const shownTo = lastLine + 1 < this.model.getLineCount() ? this.model.offsetAt({ line: lastLine + 1, column: 0 }) : this.model.getLength();
1812
1811
  paintGutter(this.gutterLines, this.layout, rows, {
1813
1812
  lineNumber: (line) => this.lineNumberLabel(line),
1814
- changes: this.changedLines(rows),
1815
- attribution: this.attributedLines(firstLine, lastLine),
1813
+ changes: this.changedLines(firstLine, lastLine),
1814
+ attribution: this.runLines(this.attribution, firstLine, lastLine),
1816
1815
  highlights: highlighted,
1817
1816
  activeLines: new Set(selections
1818
1817
  .filter((selection) => selection.head >= shownFrom && selection.head <= shownTo)
@@ -1893,7 +1892,7 @@ export class EditorView {
1893
1892
  if (index < selections.length - 1 && (selection.head < visibleFrom - 1 || selection.head > visibleTo + 1)) {
1894
1893
  continue;
1895
1894
  }
1896
- const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
1895
+ const at = this.bracketAt(selection);
1897
1896
  if (at === undefined) {
1898
1897
  continue;
1899
1898
  }
@@ -1916,9 +1915,7 @@ export class EditorView {
1916
1915
  }
1917
1916
  }
1918
1917
  }
1919
- const selected = selections.flatMap((selection) => selection.anchor === selection.head
1920
- ? []
1921
- : this.paintRectangles(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), rows));
1918
+ const selected = selections.flatMap(({ anchor, head }) => anchor === head ? [] : this.paintRectangles(Math.min(anchor, head), Math.max(anchor, head), rows));
1922
1919
  paintOverlays(this.overlays, { currentLine, selections: selected, focused, marks });
1923
1920
  paintOver(this.over, [
1924
1921
  { className: 'se-faded', rects: faded },
@@ -2006,12 +2003,8 @@ export class EditorView {
2006
2003
  this.changeCache = { version: this.changeVersion, lines, runs };
2007
2004
  return this.changeCache;
2008
2005
  }
2009
- /* How each line on screen differs. */
2010
- changedLines(rows) {
2006
+ changedLines(first, last) {
2011
2007
  const { lines } = this.resolvedChanges();
2012
- const first = rows[0]?.line ?? 0;
2013
- const lastRow = rows.at(-1);
2014
- const last = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
2015
2008
  const visible = new Map();
2016
2009
  for (let line = first; line <= last; line++) {
2017
2010
  const kind = lines.get(line);
@@ -2112,8 +2105,8 @@ export class EditorView {
2112
2105
  this.highlights.set(highlights.map((highlight) => ({ id: '', ...highlight })), this.model.getLineCount(), (line) => this.model.getLine(line));
2113
2106
  this.render();
2114
2107
  }
2115
- highlightedLines(first, last) {
2116
- return this.highlights.linesIn(first, last, this.model.getLine(first).start, this.model.getLine(last).end, (offset) => this.model.positionAt(offset).line);
2108
+ runLines(runs, first, last) {
2109
+ return runs.linesIn(first, last, this.model.getLine(first).start, this.model.getLine(last).end, (offset) => this.model.positionAt(offset).line);
2117
2110
  }
2118
2111
  /* One box per row, so a row of the host's between two tinted lines stays clear. */
2119
2112
  highlightMarks(rows, lines) {
@@ -2147,9 +2140,6 @@ export class EditorView {
2147
2140
  renderCode(container, text, options) {
2148
2141
  renderCodeBlock(container, text, options.tokens === undefined ? this.tokens.tokenizeText(text.split(/\r?\n/)) : options.tokens, this.settings.tabSize, options);
2149
2142
  }
2150
- attributedLines(first, last) {
2151
- return this.attribution.linesIn(first, last, this.model.getLine(first).start, this.model.getLine(last).end, (offset) => this.model.positionAt(offset).line);
2152
- }
2153
2143
  onAttributionHover(listener) {
2154
2144
  return subscribe(this.attributionListeners, listener);
2155
2145
  }
@@ -2186,7 +2176,7 @@ export class EditorView {
2186
2176
  if (row.kind !== 'text') {
2187
2177
  return null;
2188
2178
  }
2189
- const bar = this.attributedLines(row.line, row.line).get(row.line);
2179
+ const bar = this.runLines(this.attribution, row.line, row.line).get(row.line);
2190
2180
  if (bar === undefined) {
2191
2181
  return null;
2192
2182
  }
@@ -2203,7 +2193,7 @@ export class EditorView {
2203
2193
  }
2204
2194
  /* A problem's tick says what the problem is when the pointer rests on it, and a press goes to it. The rest of the track lets the pointer through. */
2205
2195
  wireOverview() {
2206
- const tickOf = (event) => event.target?.closest?.('.se-tick[data-offset]') ?? null;
2196
+ const tickOf = (event) => closestElement(event.target, '.se-tick[data-offset]');
2207
2197
  this.overview.addEventListener('pointerover', (event) => {
2208
2198
  const tick = tickOf(event);
2209
2199
  const title = tick?.getAttribute('data-title');
@@ -2395,10 +2385,8 @@ export class EditorView {
2395
2385
  onGutterMarker(listener) {
2396
2386
  return subscribe(this.gutterMarkerListeners, listener);
2397
2387
  }
2398
- /* The id of the host's gutter marker when the target is it. */
2399
2388
  gutterMarkerIdOf(target) {
2400
- const button = target?.closest?.('[data-gutter-marker]');
2401
- return button?.dataset.gutterMarker ?? null;
2389
+ return closestElement(target, '[data-gutter-marker]')?.dataset.gutterMarker ?? null;
2402
2390
  }
2403
2391
  pressGutterMarker(id) {
2404
2392
  emit(this.gutterMarkerListeners, id);
@@ -2415,17 +2403,16 @@ export class EditorView {
2415
2403
  }
2416
2404
  return byLine;
2417
2405
  }
2418
- /* The line of the host's gutter button when the target is it. */
2419
2406
  gutterActionLineOf(target) {
2420
- const button = target?.closest?.('[data-gutter-action]');
2407
+ const button = closestElement(target, '[data-gutter-action]');
2421
2408
  return button ? Number(button.dataset.gutterAction) : null;
2422
2409
  }
2423
2410
  pressGutterAction(line) {
2424
2411
  emit(this.gutterActionListeners, line);
2425
2412
  }
2426
- /* Whether a click at this point of the gutter is on a fold control, and which line it folds. */
2413
+ /* The line a fold control in the gutter folds. */
2427
2414
  foldLineOf(target) {
2428
- const button = target?.closest?.('[data-fold-line]');
2415
+ const button = closestElement(target, '[data-fold-line]');
2429
2416
  return button ? Number(button.dataset.foldLine) : null;
2430
2417
  }
2431
2418
  dispose() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -54,13 +54,13 @@
54
54
  "test": "bun test --conditions=source"
55
55
  },
56
56
  "dependencies": {
57
- "@adecore/editor-core": "0.38.0",
58
- "linkedom": "^0.18.13",
59
- "shiki": "^4.4.3"
57
+ "@adecore/editor-core": "0.40.0",
58
+ "linkedom": "^0.18.14",
59
+ "shiki": "^4.5.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@types/bun": "^1.4.2",
63
- "@types/node": "^24.13.6",
62
+ "@types/bun": "^1.4.4",
63
+ "@types/node": "^24.19.2",
64
64
  "typescript": "^7.0.2"
65
65
  },
66
66
  "license": "FSL-1.1-MIT",