@adecore/editor 0.21.1 → 0.22.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/testing.js CHANGED
@@ -40,28 +40,31 @@ export function createPage() {
40
40
  host: document.getElementById('host')
41
41
  };
42
42
  }
43
+ function createEvent(window, type) {
44
+ return new window.Event(type, { bubbles: true, cancelable: true });
45
+ }
43
46
  /* Fires a keydown at the element and says whether the editor took the key. */
44
47
  export function press(window, target, key, options = {}) {
45
- const event = new window.Event('keydown', { bubbles: true, cancelable: true });
48
+ const event = createEvent(window, 'keydown');
46
49
  Object.assign(event, { key, ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, isComposing: false, ...options });
47
50
  target.dispatchEvent(event);
48
51
  return event.defaultPrevented;
49
52
  }
50
53
  /* Fires a keyup at the element, the other half of `press`. */
51
54
  export function release(window, target, key, options = {}) {
52
- const event = new window.Event('keyup', { bubbles: true, cancelable: true });
55
+ const event = createEvent(window, 'keyup');
53
56
  Object.assign(event, { key, ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, isComposing: false, ...options });
54
57
  target.dispatchEvent(event);
55
58
  }
56
59
  /* What a browser sends before it puts text in a textarea. */
57
60
  export function typeInto(window, target, text) {
58
- const event = new window.Event('beforeinput', { bubbles: true, cancelable: true });
61
+ const event = createEvent(window, 'beforeinput');
59
62
  Object.assign(event, { inputType: 'insertText', data: text, isComposing: false });
60
63
  target.dispatchEvent(event);
61
64
  }
62
65
  /* A clipboard event with the data it carries; what a copy or cut sets is read back through `data`. */
63
66
  export function clipboard(window, target, type, data = { text: '' }) {
64
- const event = new window.Event(type, { bubbles: true, cancelable: true });
67
+ const event = createEvent(window, type);
65
68
  Object.assign(event, {
66
69
  clipboardData: {
67
70
  getData: () => data.text,
@@ -75,7 +78,7 @@ export function clipboard(window, target, type, data = { text: '' }) {
75
78
  }
76
79
  /* A pointer event at a point of the screen; the page has no scroll or scale, so it is also the point of the content. */
77
80
  export function pointer(window, target, type, x, y, options = {}) {
78
- const event = new window.Event(type, { bubbles: true, cancelable: true });
81
+ const event = createEvent(window, type);
79
82
  Object.assign(event, {
80
83
  clientX: x,
81
84
  clientY: y,
package/dist/tokens.d.ts CHANGED
@@ -24,5 +24,6 @@ export declare class TokenCache {
24
24
  from: number;
25
25
  to: number;
26
26
  } | null;
27
+ private forgetPivot;
27
28
  covers(line: number): boolean;
28
29
  }
package/dist/tokens.js CHANGED
@@ -32,8 +32,7 @@ export class TokenCache {
32
32
  this.lines = this.source.getLineCount();
33
33
  this.entries = [];
34
34
  this.frontier = 0;
35
- this.reusableFrom = Number.POSITIVE_INFINITY;
36
- this.pivotLine = -1;
35
+ this.forgetPivot();
37
36
  }
38
37
  /* How far down the document has colors that are right for the text. */
39
38
  get colored() {
@@ -71,8 +70,7 @@ export class TokenCache {
71
70
  if (first === undefined) {
72
71
  this.entries = [];
73
72
  this.frontier = 0;
74
- this.reusableFrom = Number.POSITIVE_INFINITY;
75
- this.pivotLine = -1;
73
+ this.forgetPivot();
76
74
  return;
77
75
  }
78
76
  const firstLine = lineOf(first.from);
@@ -87,8 +85,7 @@ export class TokenCache {
87
85
  this.frontier = Math.min(this.frontier, firstLine);
88
86
  if (last === undefined || lastOld < firstLine || this.entries.length <= firstLine) {
89
87
  this.entries.length = Math.min(this.entries.length, firstLine);
90
- this.reusableFrom = Number.POSITIVE_INFINITY;
91
- this.pivotLine = -1;
88
+ this.forgetPivot();
92
89
  return;
93
90
  }
94
91
  const old = this.entries[lastOld];
@@ -109,8 +106,7 @@ export class TokenCache {
109
106
  }
110
107
  if (tokenizer.stale?.() === true) {
111
108
  this.frontier = 0;
112
- this.reusableFrom = Number.POSITIVE_INFINITY;
113
- this.pivotLine = -1;
109
+ this.forgetPivot();
114
110
  }
115
111
  const count = this.source.getLineCount();
116
112
  const limit = Math.min(until, count - 1);
@@ -135,8 +131,7 @@ export class TokenCache {
135
131
  (previous !== undefined && line >= this.reusableFrom && tokenizer.sameState(previous.state, next.state));
136
132
  if (settled) {
137
133
  this.frontier = this.entries.length;
138
- this.reusableFrom = Number.POSITIVE_INFINITY;
139
- this.pivotLine = -1;
134
+ this.forgetPivot();
140
135
  break;
141
136
  }
142
137
  if (now() > deadline) {
@@ -145,6 +140,10 @@ export class TokenCache {
145
140
  }
146
141
  return this.frontier > from ? { from, to: this.frontier - 1 } : null;
147
142
  }
143
+ forgetPivot() {
144
+ this.reusableFrom = Number.POSITIVE_INFINITY;
145
+ this.pivotLine = -1;
146
+ }
148
147
  /* Whether lines through `line` are colored for the text as it is now. */
149
148
  covers(line) {
150
149
  return this.frontier > line || this.frontier >= this.source.getLineCount();
package/dist/view.d.ts CHANGED
@@ -171,6 +171,7 @@ export declare class EditorView {
171
171
  private get overscan();
172
172
  private get viewportWidth();
173
173
  private modelChanged;
174
+ private followEdit;
174
175
  private mapTracked;
175
176
  trackRange(from: number, to: number): {
176
177
  get(): {
package/dist/view.js CHANGED
@@ -1,9 +1,10 @@
1
- import { replacementText, scanBrackets } from '@adecore/editor-core';
1
+ import { indentationColumn, replacementText, scanBrackets } from '@adecore/editor-core';
2
2
  import { FindController } from './find.js';
3
3
  import { EditorLayout, RIGHT_PADDING } from './layout.js';
4
4
  import { createMetrics, readEditorFont } from './metrics.js';
5
5
  import { AttributionRuns, colorValue, tintValue } from './attribution.js';
6
6
  import { renderCodeBlock } from './code-block.js';
7
+ import { emit, subscribe } from './listeners.js';
7
8
  import { mapOffset } from './offsets.js';
8
9
  import { mapTrackedRange } from './tracked-range.js';
9
10
  import { scrollPosition } from './scroll.js';
@@ -12,11 +13,23 @@ import { Outline, scopeChain, stickyCover, stickyPlacements, structuralEntries }
12
13
  import { overviewTicks, paintOverview } from './overview.js';
13
14
  import { paintCarets, paintGutter, paintOver, paintOverlays, paintRemoteCarets, paintSigns, paintSticky, RowPainter } from './paint.js';
14
15
  import { overlayTokens } from './semantic.js';
15
- import { indentationColumn } from '@adecore/editor-core';
16
16
  import { TokenCache } from './tokens.js';
17
17
  function sameRect(left, right) {
18
18
  return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
19
19
  }
20
+ function mapRange(range, changes) {
21
+ return { ...range, from: mapOffset(range.from, changes), to: mapOffset(range.to, changes) };
22
+ }
23
+ function mapFoldHints(hints, changes) {
24
+ return {
25
+ symbols: hints.symbols?.map((hint) => mapRange(hint, changes)) ?? [],
26
+ ranges: hints.ranges?.map((hint) => ({
27
+ ...mapRange(hint, changes),
28
+ ...(hint.head === undefined ? {} : { head: mapOffset(hint.head, changes) }),
29
+ ...(hint.tail === undefined ? {} : { tail: mapOffset(hint.tail, changes) })
30
+ })) ?? []
31
+ };
32
+ }
20
33
  /* The lines past the visible ones that are colored ahead of the scroll, and how long one slice of that work may take. */
21
34
  const COLOR_MARGIN = 80;
22
35
  const MIN_OVERSCAN = 400;
@@ -436,37 +449,7 @@ export class EditorView {
436
449
  this.layoutGhostParts();
437
450
  }
438
451
  for (const changes of batches) {
439
- this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: mapOffset(inlay.at, changes) }));
440
- for (const [owner, blocks] of this.blocks) {
441
- this.blocks.set(owner, blocks.map((block) => ({ ...block, at: mapOffset(block.at, changes) })));
442
- }
443
- this.lenses = this.lenses.map((lens) => ({ ...lens, at: mapOffset(lens.at, changes) }));
444
- this.collapsed = new Set([...this.collapsed].map((anchor) => mapOffset(anchor, changes)));
445
- this.settled = new Set([...this.settled].map((anchor) => mapOffset(anchor, changes)));
446
- if (this.foldHints !== null) {
447
- this.foldHints = {
448
- symbols: this.foldHints.symbols?.map((hint) => ({ ...hint, from: mapOffset(hint.from, changes), to: mapOffset(hint.to, changes) })) ?? [],
449
- ranges: this.foldHints.ranges?.map((hint) => ({
450
- ...hint,
451
- from: mapOffset(hint.from, changes),
452
- to: mapOffset(hint.to, changes),
453
- ...(hint.head === undefined ? {} : { head: mapOffset(hint.head, changes) }),
454
- ...(hint.tail === undefined ? {} : { tail: mapOffset(hint.tail, changes) })
455
- })) ?? []
456
- };
457
- }
458
- this.customFolds = this.customFolds.map((fold) => ({ from: mapOffset(fold.from, changes), to: mapOffset(fold.to, changes) }));
459
- this.find.mapBounds(changes);
460
- this.changeMarks = this.changeMarks.map((mark) => ({ ...mark, from: mapOffset(mark.from, changes), to: mapOffset(mark.to, changes) }));
461
- this.markers = this.markers.map((marker) => ({ ...marker, from: mapOffset(marker.from, changes), to: mapOffset(marker.to, changes) }));
462
- this.mapTracked(changes);
463
- this.attribution.map(changes);
464
- this.highlights.map(changes);
465
- this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: mapOffset(caret.at, changes) }));
466
- for (const [owner, entries] of this.lineActions) {
467
- this.lineActions.set(owner, entries.map((entry) => ({ ...entry, at: mapOffset(entry.at, changes) })));
468
- }
469
- this.semantic = this.semantic.map((token) => ({ ...token, from: mapOffset(token.from, changes), to: mapOffset(token.to, changes) }));
452
+ this.followEdit(changes);
470
453
  }
471
454
  this.foldRanges = this.mappedFolds(batches);
472
455
  this.semanticVersion++;
@@ -496,6 +479,31 @@ export class EditorView {
496
479
  }
497
480
  this.requestRender();
498
481
  }
482
+ /* Moves everything anchored to an offset along with one batch of changes. */
483
+ followEdit(changes) {
484
+ this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: mapOffset(inlay.at, changes) }));
485
+ for (const [owner, blocks] of this.blocks) {
486
+ this.blocks.set(owner, blocks.map((block) => ({ ...block, at: mapOffset(block.at, changes) })));
487
+ }
488
+ this.lenses = this.lenses.map((lens) => ({ ...lens, at: mapOffset(lens.at, changes) }));
489
+ this.collapsed = new Set([...this.collapsed].map((anchor) => mapOffset(anchor, changes)));
490
+ this.settled = new Set([...this.settled].map((anchor) => mapOffset(anchor, changes)));
491
+ if (this.foldHints !== null) {
492
+ this.foldHints = mapFoldHints(this.foldHints, changes);
493
+ }
494
+ this.customFolds = this.customFolds.map((fold) => mapRange(fold, changes));
495
+ this.find.mapBounds(changes);
496
+ this.changeMarks = this.changeMarks.map((mark) => mapRange(mark, changes));
497
+ this.markers = this.markers.map((marker) => mapRange(marker, changes));
498
+ this.mapTracked(changes);
499
+ this.attribution.map(changes);
500
+ this.highlights.map(changes);
501
+ this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: mapOffset(caret.at, changes) }));
502
+ for (const [owner, entries] of this.lineActions) {
503
+ this.lineActions.set(owner, entries.map((entry) => ({ ...entry, at: mapOffset(entry.at, changes) })));
504
+ }
505
+ this.semantic = this.semantic.map((token) => mapRange(token, changes));
506
+ }
499
507
  mapTracked(changes) {
500
508
  for (const entry of this.tracked) {
501
509
  const mapped = mapTrackedRange(entry.from, entry.to, changes);
@@ -622,9 +630,7 @@ export class EditorView {
622
630
  this.outline.setStructure(structuralEntries(ranges, this.model, (from, to) => this.model.slice(from, to), (offset) => this.brackets().pairs.get(offset)));
623
631
  const anchors = new Set(this.foldRanges.map((range) => range.from));
624
632
  this.collapsed = new Set([...this.collapsed].filter((anchor) => anchors.has(anchor)));
625
- this.anchorScroll(() => this.layout.configure({ folds: this.foldStates() }));
626
- this.moveHiddenCarets();
627
- this.render();
633
+ this.refoldLayout();
628
634
  }
629
635
  /* The folds a person made of a selection, which the document's own structure does not know, among the ones it does. */
630
636
  withCustomFolds(ranges) {
@@ -968,9 +974,11 @@ export class EditorView {
968
974
  if (!rect) {
969
975
  return { x: clientX - this.gutterWidth + this.viewport.scrollLeft, y: clientY + this.viewport.scrollTop };
970
976
  }
971
- const scaleX = this.viewport.offsetWidth > 0 ? rect.width / this.viewport.offsetWidth : 1;
972
- const scaleY = this.viewport.offsetHeight > 0 ? rect.height / this.viewport.offsetHeight : 1;
973
- return { x: (clientX - rect.left) / scaleX - this.gutterWidth + this.viewport.scrollLeft, y: (clientY - rect.top) / scaleY + this.viewport.scrollTop };
977
+ const scale = this.screenScale(rect);
978
+ return {
979
+ x: (clientX - rect.left) / scale.x - this.gutterWidth + this.viewport.scrollLeft,
980
+ y: (clientY - rect.top) / scale.y + this.viewport.scrollTop
981
+ };
974
982
  }
975
983
  offsetAtPoint(clientX, clientY) {
976
984
  const point = this.contentPoint(clientX, clientY);
@@ -1059,12 +1067,8 @@ export class EditorView {
1059
1067
  }
1060
1068
  }
1061
1069
  onHover(listener) {
1062
- this.hoverListeners.add(listener);
1063
- return () => {
1064
- this.hoverListeners.delete(listener);
1065
- };
1070
+ return subscribe(this.hoverListeners, listener);
1066
1071
  }
1067
- /* The pointer moved over the editor; a drag in progress is not a hover. */
1068
1072
  /*
1069
1073
  * The other places the selected text is, found without a language server: only for one selection that
1070
1074
  * holds no line break and is not blank, and not at all when there are more than a few of them.
@@ -1268,6 +1272,7 @@ export class EditorView {
1268
1272
  this.dropCaret = offset;
1269
1273
  this.render();
1270
1274
  }
1275
+ /* The pointer moved over the editor; a drag in progress is not a hover. */
1271
1276
  hoverMoved(event) {
1272
1277
  this.content.classList.toggle('se-over-selection', !this.settings.readOnly && event.buttons === 0 && this.selectionAtPoint(event.clientX, event.clientY) !== null);
1273
1278
  this.setHover(event.buttons === 0 ? this.characterAtPoint(event.clientX, event.clientY, event.target) : null);
@@ -1277,15 +1282,10 @@ export class EditorView {
1277
1282
  return;
1278
1283
  }
1279
1284
  this.hoverOffset = offset;
1280
- for (const listener of [...this.hoverListeners]) {
1281
- listener(offset);
1282
- }
1285
+ emit(this.hoverListeners, offset);
1283
1286
  }
1284
1287
  onViewChange(listener) {
1285
- this.viewListeners.add(listener);
1286
- return () => {
1287
- this.viewListeners.delete(listener);
1288
- };
1288
+ return subscribe(this.viewListeners, listener);
1289
1289
  }
1290
1290
  announceView() {
1291
1291
  const key = `${this.scrollTop}|${this.scrollLeft}|${this.clientWidth}|${this.clientHeight}|${this.layout.height}`;
@@ -1293,9 +1293,7 @@ export class EditorView {
1293
1293
  return;
1294
1294
  }
1295
1295
  this.viewKey = key;
1296
- for (const listener of [...this.viewListeners]) {
1297
- listener();
1298
- }
1296
+ emit(this.viewListeners);
1299
1297
  }
1300
1298
  caretOf(head) {
1301
1299
  return this.layout.caret(head, head === this.ghost?.at ? 'before' : 'after', head === this.rowEndCaret);
@@ -1304,14 +1302,12 @@ export class EditorView {
1304
1302
  get scrollPlace() {
1305
1303
  return this.scrollRun?.to ?? { x: this.viewport.scrollLeft, y: this.viewport.scrollTop };
1306
1304
  }
1307
- /* Stops a scroll on its way where it is. */
1308
1305
  cancelScroll() {
1309
1306
  if (this.scrollRun !== null) {
1310
1307
  this.document.defaultView?.cancelAnimationFrame?.(this.scrollRun.frame);
1311
1308
  this.scrollRun = null;
1312
1309
  }
1313
1310
  }
1314
- /* Whether a person asked their system for less motion. */
1315
1311
  reducedMotion() {
1316
1312
  return this.document.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
1317
1313
  }
@@ -1440,15 +1436,10 @@ export class EditorView {
1440
1436
  this.render();
1441
1437
  }
1442
1438
  onFind(listener) {
1443
- this.findListeners.add(listener);
1444
- return () => {
1445
- this.findListeners.delete(listener);
1446
- };
1439
+ return subscribe(this.findListeners, listener);
1447
1440
  }
1448
1441
  announceFind() {
1449
- for (const listener of [...this.findListeners]) {
1450
- listener(this.find.state);
1451
- }
1442
+ emit(this.findListeners, this.find.state);
1452
1443
  }
1453
1444
  revealFind() {
1454
1445
  const mark = this.find.currentMark;
@@ -1837,10 +1828,7 @@ export class EditorView {
1837
1828
  return this.attribution.linesIn(first, last, this.model.getLine(first).start, this.model.getLine(last).end, (offset) => this.model.positionAt(offset).line);
1838
1829
  }
1839
1830
  onAttributionHover(listener) {
1840
- this.attributionListeners.add(listener);
1841
- return () => {
1842
- this.attributionListeners.delete(listener);
1843
- };
1831
+ return subscribe(this.attributionListeners, listener);
1844
1832
  }
1845
1833
  /* The bar is three pixels wide, so the pointer finds it a few pixels to either side. */
1846
1834
  wireAttribution() {
@@ -1860,9 +1848,7 @@ export class EditorView {
1860
1848
  return;
1861
1849
  }
1862
1850
  this.attributionHover = next;
1863
- for (const listener of [...this.attributionListeners]) {
1864
- listener(next);
1865
- }
1851
+ emit(this.attributionListeners, next);
1866
1852
  }
1867
1853
  attributionUnder(clientX, clientY) {
1868
1854
  if (this.attribution.size === 0) {
@@ -1973,10 +1959,7 @@ export class EditorView {
1973
1959
  this.render();
1974
1960
  }
1975
1961
  onScope(listener) {
1976
- this.scopeListeners.add(listener);
1977
- return () => {
1978
- this.scopeListeners.delete(listener);
1979
- };
1962
+ return subscribe(this.scopeListeners, listener);
1980
1963
  }
1981
1964
  announceScope() {
1982
1965
  const line = this.model.positionAt(this.model.getPrimary().head).line;
@@ -1991,9 +1974,7 @@ export class EditorView {
1991
1974
  return;
1992
1975
  }
1993
1976
  this.scopeKey = key;
1994
- for (const listener of [...this.scopeListeners]) {
1995
- listener(scope);
1996
- }
1977
+ emit(this.scopeListeners, scope);
1997
1978
  }
1998
1979
  stickyLimit() {
1999
1980
  return Math.min(STICKY_MAX_LINES, Math.floor(this.viewportHeight / this.layout.metrics.lineHeight / STICKY_ROW_SHARE));
@@ -2061,10 +2042,7 @@ export class EditorView {
2061
2042
  this.render();
2062
2043
  }
2063
2044
  onGutterAction(listener) {
2064
- this.gutterActionListeners.add(listener);
2065
- return () => {
2066
- this.gutterActionListeners.delete(listener);
2067
- };
2045
+ return subscribe(this.gutterActionListeners, listener);
2068
2046
  }
2069
2047
  setGutterMarkers(owner, markers) {
2070
2048
  const before = this.gutterMarkers.get(owner) ?? [];
@@ -2081,10 +2059,7 @@ export class EditorView {
2081
2059
  this.render();
2082
2060
  }
2083
2061
  onGutterMarker(listener) {
2084
- this.gutterMarkerListeners.add(listener);
2085
- return () => {
2086
- this.gutterMarkerListeners.delete(listener);
2087
- };
2062
+ return subscribe(this.gutterMarkerListeners, listener);
2088
2063
  }
2089
2064
  /* The id of the host's gutter marker when the target is it. */
2090
2065
  gutterMarkerIdOf(target) {
@@ -2092,11 +2067,9 @@ export class EditorView {
2092
2067
  return button?.dataset.gutterMarker ?? null;
2093
2068
  }
2094
2069
  pressGutterMarker(id) {
2095
- for (const listener of [...this.gutterMarkerListeners]) {
2096
- listener(id);
2097
- }
2070
+ emit(this.gutterMarkerListeners, id);
2098
2071
  }
2099
- /* One marker per line: where two owners mark the same line, the owner named first stays. */
2072
+ /* One marker per line: where two owners mark the same line, the owner that sorts first stays. */
2100
2073
  markersByLine() {
2101
2074
  const byLine = new Map();
2102
2075
  for (const owner of [...this.gutterMarkers.keys()].sort()) {
@@ -2114,9 +2087,7 @@ export class EditorView {
2114
2087
  return button ? Number(button.dataset.gutterAction) : null;
2115
2088
  }
2116
2089
  pressGutterAction(line) {
2117
- for (const listener of [...this.gutterActionListeners]) {
2118
- listener(line);
2119
- }
2090
+ emit(this.gutterActionListeners, line);
2120
2091
  }
2121
2092
  /* Whether a click at this point of the gutter is on a fold control, and which line it folds. */
2122
2093
  foldLineOf(target) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -39,7 +39,7 @@
39
39
  "test": "bun test --conditions=source"
40
40
  },
41
41
  "dependencies": {
42
- "@adecore/editor-core": "0.21.1",
42
+ "@adecore/editor-core": "0.22.0",
43
43
  "linkedom": "^0.18.13",
44
44
  "shiki": "^4.4.3"
45
45
  },
package/src/code-block.ts CHANGED
@@ -25,28 +25,7 @@ export function renderCodeBlock(
25
25
  if (options.color !== undefined) {
26
26
  row.style.background = tintValue(options.color);
27
27
  }
28
- const gutter = document.createElement('div');
29
- gutter.className = 'se-code-gutter';
30
- if (options.firstLine !== undefined) {
31
- const number = document.createElement('span');
32
- number.textContent = String(options.firstLine + index);
33
- gutter.append(number);
34
- }
35
- if (options.sign) {
36
- const sign = document.createElement('span');
37
- sign.className = 'se-line-sign';
38
- sign.textContent = options.sign;
39
- if (options.color !== undefined) {
40
- sign.style.color = colorValue(options.color);
41
- }
42
- gutter.append(sign);
43
- }
44
- if (options.color !== undefined) {
45
- const bar = document.createElement('span');
46
- bar.className = 'se-attribution';
47
- bar.style.background = colorValue(options.color);
48
- gutter.append(bar);
49
- }
28
+ const gutter = codeGutter(document, options, index);
50
29
  const code = document.createElement('div');
51
30
  code.className = 'se-code-text';
52
31
  if (options.faded) {
@@ -63,6 +42,32 @@ export function renderCodeBlock(
63
42
  container.append(block);
64
43
  }
65
44
 
45
+ function codeGutter(document: Document, options: EditorCodeBlockOptions, index: number): HTMLElement {
46
+ const gutter = document.createElement('div');
47
+ gutter.className = 'se-code-gutter';
48
+ if (options.firstLine !== undefined) {
49
+ const number = document.createElement('span');
50
+ number.textContent = String(options.firstLine + index);
51
+ gutter.append(number);
52
+ }
53
+ if (options.sign) {
54
+ const sign = document.createElement('span');
55
+ sign.className = 'se-line-sign';
56
+ sign.textContent = options.sign;
57
+ if (options.color !== undefined) {
58
+ sign.style.color = colorValue(options.color);
59
+ }
60
+ gutter.append(sign);
61
+ }
62
+ if (options.color !== undefined) {
63
+ const bar = document.createElement('span');
64
+ bar.className = 'se-attribution';
65
+ bar.style.background = colorValue(options.color);
66
+ gutter.append(bar);
67
+ }
68
+ return gutter;
69
+ }
70
+
66
71
  function fill(element: HTMLElement, line: string, tokens: readonly LineToken[] | null, emphasis: ReadonlyArray<readonly [number, number]>): void {
67
72
  if (tokens === null && emphasis.length === 0) {
68
73
  element.textContent = line;