@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
package/src/lens.ts CHANGED
@@ -12,17 +12,20 @@ export function lensSignature(entries: readonly EditorCodeVisionEntry[]): string
12
12
  export function fillLens(container: HTMLElement, entries: readonly EditorCodeVisionEntry[], lookup: (id: string) => EditorCodeVisionEntry | undefined): void {
13
13
  const document = container.ownerDocument;
14
14
  container.classList.add('se-lens');
15
+
15
16
  for (const entry of entries) {
16
17
  const button = document.createElement('button');
17
18
  button.type = 'button';
18
19
  button.className = 'se-lens-entry';
19
20
  button.tabIndex = -1;
21
+
20
22
  if (entry.icon !== undefined) {
21
23
  const icon = document.createElement('span');
22
24
  icon.className = `se-lens-icon se-lens-icon-${entry.icon}`;
23
25
  icon.setAttribute('aria-hidden', 'true');
24
26
  button.append(icon);
25
27
  }
28
+
26
29
  button.append(entry.text);
27
30
  // The editor keeps the focus, so the caret stays where it was and a popover can hand the keyboard back.
28
31
  button.addEventListener('pointerdown', (event) => event.preventDefault());
package/src/listeners.ts CHANGED
@@ -2,6 +2,7 @@ export type Listener = () => void;
2
2
 
3
3
  export function subscribe<T>(listeners: Set<T>, listener: T): () => void {
4
4
  listeners.add(listener);
5
+
5
6
  return () => {
6
7
  listeners.delete(listener);
7
8
  };
package/src/metrics.ts CHANGED
@@ -11,53 +11,67 @@ const FALLBACK: EditorFont = { family: 'monospace', size: 13, lineHeight: 20 };
11
11
 
12
12
  function pixels(value: string): number | null {
13
13
  const parsed = Number.parseFloat(value);
14
+
14
15
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
15
16
  }
16
17
 
17
18
  /* The code face the page gives the editor's element, in whole pixels so a line is as tall as the viewer's. */
18
19
  export function readEditorFont(element: HTMLElement): EditorFont {
19
20
  const view = element.ownerDocument.defaultView;
21
+
20
22
  if (!view?.getComputedStyle) {
21
23
  return FALLBACK;
22
24
  }
25
+
23
26
  const style = view.getComputedStyle(element);
24
27
  const size = pixels(style.fontSize) ?? FALLBACK.size;
25
28
  const lineHeight = pixels(style.lineHeight) ?? Math.round(size * 1.5);
29
+
26
30
  return { family: style.fontFamily || FALLBACK.family, size, lineHeight: Math.round(lineHeight) };
27
31
  }
28
32
 
29
33
  /* Character widths from a canvas, which sets the text in the same face the page draws it in. Without a canvas a character is 0.6 of the size. */
30
34
  export function createMetrics(font: EditorFont, tabSize: number, document: Document): LayoutMetrics {
31
35
  let context: CanvasRenderingContext2D | null = null;
36
+
32
37
  try {
33
38
  context = document.createElement('canvas').getContext('2d');
34
39
  } catch {
35
40
  context = null;
36
41
  }
42
+
37
43
  const codeFont = `${font.size}px ${font.family}`;
38
44
  const inlayFont = `${Math.max(12, font.size - 1)}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`;
39
45
  const cache = new Map<string, number>();
46
+
40
47
  const measure = (text: string, face: string, fallback: number): number => {
41
48
  if (!context) {
42
49
  return Array.from(text).length * fallback;
43
50
  }
51
+
44
52
  context.font = face;
45
53
  context.fontKerning = 'none';
54
+
46
55
  return context.measureText(text).width;
47
56
  };
57
+
48
58
  return {
49
59
  lineHeight: font.lineHeight,
50
60
  charWidth: measure('M', codeFont, font.size * 0.6),
51
61
  tabSize,
52
62
  measureText(text) {
53
63
  let width = cache.get(text);
64
+
54
65
  if (width === undefined) {
55
66
  width = measure(text, codeFont, font.size * 0.6);
67
+
56
68
  if (cache.size > 2000) {
57
69
  cache.clear();
58
70
  }
71
+
59
72
  cache.set(text, width);
60
73
  }
74
+
61
75
  return width;
62
76
  },
63
77
  measureInlay: (text) => measure(text, inlayFont, font.size * 0.5)
@@ -46,6 +46,7 @@ export class DoubleModifierGesture {
46
46
  /* Another modifier held with the one that counts spoils the gesture. */
47
47
  private spoiled(event: GestureEvent): boolean {
48
48
  const other = this.modifier === 'Alt' ? event.ctrlKey : event.altKey;
49
+
49
50
  return other || event.metaKey || event.shiftKey;
50
51
  }
51
52
 
@@ -54,24 +55,34 @@ export class DoubleModifierGesture {
54
55
  if (event.repeat === true) {
55
56
  return null;
56
57
  }
58
+
57
59
  if (this.spoiled(event)) {
58
60
  this.reset();
61
+
59
62
  return null;
60
63
  }
64
+
61
65
  if (this.firstPressed && event.timeStamp - this.last > FORGET_MS) {
62
66
  this.reset();
63
67
  }
68
+
64
69
  this.step(event, 'press');
70
+
65
71
  return null;
66
72
  }
73
+
67
74
  if (this.armed) {
68
75
  const held = this.modifier === 'Alt' ? event.altKey : event.ctrlKey;
76
+
69
77
  if (held && !this.spoiled(event) && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
70
78
  return event.key === 'ArrowUp' ? 'above' : 'below';
71
79
  }
80
+
72
81
  return null;
73
82
  }
83
+
74
84
  this.reset();
85
+
75
86
  return null;
76
87
  }
77
88
 
@@ -79,8 +90,10 @@ export class DoubleModifierGesture {
79
90
  if (event.key === this.modifier) {
80
91
  if (this.spoiled(event)) {
81
92
  this.reset();
93
+
82
94
  return;
83
95
  }
96
+
84
97
  this.step(event, 'release');
85
98
  } else if (!this.armed) {
86
99
  this.reset();
@@ -90,8 +103,10 @@ export class DoubleModifierGesture {
90
103
  private step(event: GestureEvent, kind: 'press' | 'release'): void {
91
104
  if (this.firstPressed && event.timeStamp - this.last > TAP_MS) {
92
105
  this.reset();
106
+
93
107
  return;
94
108
  }
109
+
95
110
  if (kind === 'press') {
96
111
  if (!this.firstPressed) {
97
112
  this.reset();
@@ -103,8 +118,10 @@ export class DoubleModifierGesture {
103
118
  } else {
104
119
  this.reset();
105
120
  }
121
+
106
122
  return;
107
123
  }
124
+
108
125
  if (this.firstPressed && !this.firstReleased) {
109
126
  this.firstReleased = true;
110
127
  this.last = event.timeStamp;
@@ -49,6 +49,7 @@ export class NativeInput {
49
49
  if (this.composing) {
50
50
  return;
51
51
  }
52
+
52
53
  const selection = this.model.getPrimary();
53
54
  const caretCount = this.model.getSelectionCount();
54
55
  const length = this.model.getLength();
@@ -56,33 +57,41 @@ export class NativeInput {
56
57
  const end = Math.max(selection.anchor, selection.head);
57
58
  let from = 0;
58
59
  let to = length;
60
+
59
61
  if (length > CONTEXT) {
60
62
  from = Math.max(0, Math.min(start, selection.head - 2048));
61
63
  to = Math.min(length, end + 2048);
64
+
62
65
  if (to - from > CONTEXT) {
63
66
  from = Math.max(0, selection.head - CONTEXT / 2);
64
67
  to = Math.min(length, from + CONTEXT);
65
68
  }
69
+
66
70
  // A boundary must not split a surrogate pair or a CRLF.
67
71
  if (from > 0 && /[\uDC00-\uDFFF\n]/.test(this.model.slice(from, from + 1))) {
68
72
  from--;
69
73
  }
74
+
70
75
  if (to < length && /[\uDC00-\uDFFF\n]/.test(this.model.slice(to, to + 1))) {
71
76
  to++;
72
77
  }
73
78
  }
79
+
74
80
  const source = this.model.slice(from, to);
75
81
  const value = textareaText(source);
76
82
  const nativeStart = textareaOffset(source, Math.max(0, start - from));
77
83
  const nativeEnd = textareaOffset(source, Math.max(0, end - from));
78
84
  this.context = { from, source, value, start: nativeStart, end: nativeEnd };
79
85
  this.writing = true;
86
+
80
87
  if (this.element.value !== value) {
81
88
  this.element.value = value;
82
89
  }
90
+
83
91
  this.element.setSelectionRange(nativeStart, nativeEnd, selection.head < selection.anchor ? 'backward' : 'forward');
84
92
  this.writing = false;
85
93
  const position = this.model.positionAt(selection.head);
94
+
86
95
  this.element.setAttribute(
87
96
  'aria-description',
88
97
  `Line ${position.line + 1}, column ${position.column + 1}. ${caretCount} caret${caretCount === 1 ? '' : 's'}.`
@@ -94,11 +103,14 @@ export class NativeInput {
94
103
  if (this.writing || this.composing || this.element.value !== this.context.value) {
95
104
  return;
96
105
  }
106
+
97
107
  const start = this.element.selectionStart;
98
108
  const end = this.element.selectionEnd;
109
+
99
110
  if (start === this.context.start && end === this.context.end) {
100
111
  return;
101
112
  }
113
+
102
114
  const from = this.context.from + modelOffset(this.context.source, start);
103
115
  const to = this.context.from + modelOffset(this.context.source, end);
104
116
  this.host.selectionChanged(this.element.selectionDirection === 'backward' ? { anchor: to, head: from } : { anchor: from, head: to });
@@ -107,26 +119,35 @@ export class NativeInput {
107
119
  onInput(): void {
108
120
  if (this.composing) {
109
121
  this.host.preview(this.composedText());
122
+
110
123
  return;
111
124
  }
125
+
112
126
  if (this.host.readOnly()) {
113
127
  this.sync();
128
+
114
129
  return;
115
130
  }
131
+
116
132
  const context = this.context;
117
133
  const change = findChange(context.value, this.element.value);
134
+
118
135
  if (change.from === change.to && !change.text) {
119
136
  return;
120
137
  }
138
+
121
139
  if (this.model.getSelections().length > 1) {
122
140
  this.host.replace(this.replacedText(context));
141
+
123
142
  return;
124
143
  }
144
+
125
145
  const edit = {
126
146
  from: context.from + modelOffset(context.source, change.from),
127
147
  to: context.from + modelOffset(context.source, change.to),
128
148
  text: this.normalizeNewlines(change.text)
129
149
  };
150
+
130
151
  const next = context.source.slice(0, edit.from - context.from) + edit.text + context.source.slice(edit.to - context.from);
131
152
  const start = context.from + modelOffset(next, this.element.selectionStart);
132
153
  const end = context.from + modelOffset(next, this.element.selectionEnd);
@@ -137,6 +158,7 @@ export class NativeInput {
137
158
  if (this.host.readOnly()) {
138
159
  return;
139
160
  }
161
+
140
162
  this.compositionContext = { ...this.context };
141
163
  this.composing = true;
142
164
  }
@@ -145,12 +167,14 @@ export class NativeInput {
145
167
  if (!this.composing) {
146
168
  return;
147
169
  }
170
+
148
171
  const text = this.composedText();
149
172
  const context = this.compositionContext!;
150
173
  const unchanged = this.element.value === context.value;
151
174
  this.composing = false;
152
175
  this.compositionContext = undefined;
153
176
  this.host.preview(undefined);
177
+
154
178
  if (data && !unchanged && !this.host.readOnly()) {
155
179
  if (DEAD_KEY_RESULT.test(text)) {
156
180
  for (const character of text) {
@@ -160,6 +184,7 @@ export class NativeInput {
160
184
  this.host.replace(text);
161
185
  }
162
186
  }
187
+
163
188
  this.sync();
164
189
  }
165
190
 
@@ -167,6 +192,7 @@ export class NativeInput {
167
192
  normalizeNewlines(text: string): string {
168
193
  const first = this.model.getLine(0);
169
194
  const newline = this.model.slice(first.end, first.next) === '\r\n' ? '\r\n' : '\n';
195
+
170
196
  return textareaText(text).replace(/\n/g, newline);
171
197
  }
172
198
 
@@ -177,6 +203,7 @@ export class NativeInput {
177
203
  /* What the textarea now holds where the selection of `context` was, the text after it kept. */
178
204
  private replacedText(context: InputContext): string {
179
205
  const suffix = context.value.length - context.end;
206
+
180
207
  return this.element.value.slice(context.start, Math.max(context.start, this.element.value.length - suffix));
181
208
  }
182
209
  }
package/src/offsets.ts CHANGED
@@ -3,38 +3,48 @@ import type { DocumentChange } from '@adecore/editor-core';
3
3
  /* Where an offset is after a batch of changes. One inside a replaced range lands after the new text. */
4
4
  export function mapOffset(offset: number, changes: readonly DocumentChange[]): number {
5
5
  let delta = 0;
6
+
6
7
  for (const change of changes) {
7
8
  if (offset < change.from) {
8
9
  break;
9
10
  }
11
+
10
12
  if (offset < change.to || (offset === change.from && change.from === change.to)) {
11
13
  return change.from + delta + change.insertedLength;
12
14
  }
15
+
13
16
  delta += change.insertedLength - (change.to - change.from);
14
17
  }
18
+
15
19
  return offset + delta;
16
20
  }
17
21
 
18
22
  /* Build once per sorted, nonoverlapping batch when many offsets need to follow it. */
19
23
  export function offsetMapper(changes: readonly DocumentChange[]): (offset: number) => number {
20
24
  const deltas = new Float64Array(changes.length + 1);
25
+
21
26
  for (let index = 0; index < changes.length; index++) {
22
27
  const change = changes[index]!;
23
28
  deltas[index + 1] = deltas[index]! + change.insertedLength - (change.to - change.from);
24
29
  }
30
+
25
31
  return (offset) => {
26
32
  let low = 0,
27
33
  high = changes.length;
34
+
28
35
  while (low < high) {
29
36
  const middle = (low + high) >>> 1;
30
37
  const change = changes[middle]!;
38
+
31
39
  if (change.to < offset || (change.to === offset && change.from !== change.to)) {
32
40
  low = middle + 1;
33
41
  } else {
34
42
  high = middle;
35
43
  }
36
44
  }
45
+
37
46
  const change = changes[low];
47
+
38
48
  return change !== undefined && change.from <= offset ? change.from + deltas[low]! + change.insertedLength : offset + deltas[low]!;
39
49
  };
40
50
  }
package/src/outline.ts CHANGED
@@ -27,10 +27,13 @@ export interface OutlineDocument {
27
27
 
28
28
  const MODIFIERS =
29
29
  '(?:export\\s+|default\\s+|declare\\s+|abstract\\s+|async\\s+|pub(?:\\([^)]*\\))?\\s+|public\\s+|private\\s+|protected\\s+|static\\s+|final\\s+|readonly\\s+|override\\s+|unsafe\\s+|extern\\s+)*';
30
+
30
31
  const DECLARATION = new RegExp(
31
32
  `^${MODIFIERS}(function\\*?|class|interface|enum|type|namespace|module|trait|impl|struct|fn|def|func|object|record)\\s+([A-Za-z_$][\\w$]*)`
32
33
  );
34
+
33
35
  const VARIABLE = new RegExp(`^${MODIFIERS}(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)`);
36
+
34
37
  const KEYWORDS = new Set([
35
38
  'if',
36
39
  'for',
@@ -49,8 +52,10 @@ const KEYWORDS = new Set([
49
52
  'super',
50
53
  'finally'
51
54
  ]);
55
+
52
56
  const METHOD = /^(?:(?:public|private|protected|static|async|get|set|override|readonly|abstract)\s+)*\*?\s*([A-Za-z_$#][\w$]*)\s*[(<]/;
53
57
  const PROPERTY = /^["']?([\w$-]+)["']?\s*:\s*[{[(]?$/;
58
+
54
59
  const KINDS: Record<string, string> = {
55
60
  'function*': 'function',
56
61
  fn: 'function',
@@ -62,30 +67,39 @@ const KINDS: Record<string, string> = {
62
67
  trait: 'interface',
63
68
  impl: 'class'
64
69
  };
70
+
65
71
  const LABEL_LIMIT = 48;
66
72
 
67
73
  /* What a header line declares, when it declares something a person would look for by name. */
68
74
  export function describeHeader(text: string): { name?: string; kind?: string } {
69
75
  const line = text.trim();
70
76
  const declaration = DECLARATION.exec(line);
77
+
71
78
  if (declaration) {
72
79
  return { name: declaration[2]!, kind: KINDS[declaration[1]!] ?? declaration[1]! };
73
80
  }
81
+
74
82
  const variable = VARIABLE.exec(line);
83
+
75
84
  if (variable) {
76
85
  return { name: variable[1]!, kind: 'variable' };
77
86
  }
87
+
78
88
  const method = METHOD.exec(line);
89
+
79
90
  if (method && !KEYWORDS.has(method[1]!)) {
80
91
  return { name: method[1]!, kind: 'method' };
81
92
  }
93
+
82
94
  const property = PROPERTY.exec(line);
95
+
83
96
  return property ? { name: property[1]!, kind: 'property' } : {};
84
97
  }
85
98
 
86
99
  /* The header line as a label: what a breadcrumb shows for a block that declares nothing. */
87
100
  export function headerLabel(text: string): string {
88
101
  const label = text.trim().replace(/[\s{(:]+$/, '');
102
+
89
103
  return label.length > LABEL_LIMIT ? `${label.slice(0, LABEL_LIMIT - 1)}…` : label;
90
104
  }
91
105
 
@@ -101,27 +115,34 @@ export function structuralEntries(
101
115
  mate: (offset: number) => number | undefined
102
116
  ): OutlineEntry[] {
103
117
  const entries: OutlineEntry[] = [];
118
+
104
119
  for (const range of ranges) {
105
120
  if (range.kind === 'comment' || (range.kind === 'bracket' && slice(range.from, range.from + 1) !== '{')) {
106
121
  continue;
107
122
  }
123
+
108
124
  let headerLine = range.startLine;
109
125
  const header = document.getLine(headerLine);
110
126
  const lead = header.text.length - header.text.trimStart().length;
127
+
111
128
  if (/^[)\]]/.test(header.text.slice(lead))) {
112
129
  const opener = mate(header.start + lead);
130
+
113
131
  if (opener !== undefined && opener < header.start) {
114
132
  headerLine = document.positionAt(opener).line;
115
133
  }
116
134
  }
135
+
117
136
  const start = document.getLine(headerLine);
118
137
  entries.push({ from: start.start, to: document.getLine(range.endLine).end, ...describeHeader(start.text) });
119
138
  }
139
+
120
140
  return entries;
121
141
  }
122
142
 
123
143
  function toBlock(entry: OutlineEntry, document: OutlineDocument): OutlineBlock {
124
144
  const startLine = document.positionAt(entry.from).line;
145
+
125
146
  return {
126
147
  startLine,
127
148
  endLine: Math.max(startLine, document.positionAt(entry.to).line),
@@ -149,15 +170,21 @@ export class Outline {
149
170
  /* Blocks the host drew from its own source, in one-based lines; null goes back to the structure. */
150
171
  setProvided(blocks: readonly EditorBlock[] | null, document: OutlineDocument): void {
151
172
  this.version++;
173
+
152
174
  if (blocks === null) {
153
175
  this.provided = null;
176
+
154
177
  return;
155
178
  }
179
+
156
180
  const last = document.getLineCount() - 1;
181
+
157
182
  const clamp = (line: number): number => Math.min(last, Math.max(0, Math.trunc(line) - 1));
183
+
158
184
  this.provided = blocks.map((block) => {
159
185
  const start = document.getLine(clamp(block.startLine));
160
186
  const header = block.name === undefined ? headerLabel(start.text) : block.name;
187
+
161
188
  return {
162
189
  from: start.start,
163
190
  to: document.getLine(Math.max(clamp(block.startLine), clamp(block.endLine))).end,
@@ -169,16 +196,20 @@ export class Outline {
169
196
 
170
197
  edited(batches: readonly (readonly DocumentChange[])[]): void {
171
198
  const mappers = batches.map(offsetMapper);
199
+
172
200
  const move = (entries: OutlineEntry[]): OutlineEntry[] =>
173
201
  entries.map((entry) => {
174
202
  let from = entry.from;
175
203
  let to = entry.to;
204
+
176
205
  for (const map of mappers) {
177
206
  from = map(from);
178
207
  to = map(to);
179
208
  }
209
+
180
210
  return { ...entry, from, to };
181
211
  });
212
+
182
213
  this.structure = move(this.structure);
183
214
  this.provided = this.provided === null ? null : move(this.provided);
184
215
  this.version++;
@@ -190,8 +221,10 @@ export class Outline {
190
221
  const sorted = (this.provided ?? this.structure)
191
222
  .map((entry) => toBlock(entry, document))
192
223
  .sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
224
+
193
225
  this.cache = { version: this.version, blocks: sorted.filter((block, index) => index === 0 || block.startLine !== sorted[index - 1]!.startLine) };
194
226
  }
227
+
195
228
  return this.cache.blocks;
196
229
  }
197
230
  }
@@ -220,9 +253,11 @@ export interface StickyPlacement {
220
253
  */
221
254
  export function stickyPlacements(blocks: readonly OutlineBlock[], scrollTop: number, lineHeight: number, max: number, rows: StickyRows): StickyPlacement[] {
222
255
  const placements: StickyPlacement[] = [];
256
+
223
257
  for (let depth = 0; depth < max; depth++) {
224
258
  const pinnedAt = scrollTop + depth * lineHeight;
225
259
  const parent = placements.at(-1)?.block;
260
+
226
261
  const next = blocks.find(
227
262
  (block) =>
228
263
  (parent === undefined || (block.startLine > parent.startLine && block.endLine <= parent.endLine)) &&
@@ -230,11 +265,14 @@ export function stickyPlacements(blocks: readonly OutlineBlock[], scrollTop: num
230
265
  rows.top(block.startLine) < pinnedAt &&
231
266
  rows.bottom(block.endLine) > pinnedAt
232
267
  );
268
+
233
269
  if (next === undefined) {
234
270
  break;
235
271
  }
272
+
236
273
  placements.push({ block: next, depth, offset: Math.min(0, rows.bottom(next.endLine) - (pinnedAt + lineHeight)) });
237
274
  }
275
+
238
276
  return placements;
239
277
  }
240
278
 
package/src/overview.ts CHANGED
@@ -28,7 +28,9 @@ export function overviewTicks(spans: readonly OverviewSpan[], contentHeight: num
28
28
  if (trackHeight <= 0) {
29
29
  return [];
30
30
  }
31
+
31
32
  const scale = trackHeight / Math.max(contentHeight, trackHeight);
33
+
32
34
  const ticks = spans
33
35
  .map((span): OverviewTick => ({
34
36
  kind: span.kind,
@@ -38,18 +40,24 @@ export function overviewTicks(spans: readonly OverviewSpan[], contentHeight: num
38
40
  ...(span.title === undefined ? {} : { title: span.title })
39
41
  }))
40
42
  .sort((left, right) => left.y - right.y);
43
+
41
44
  const merged: OverviewTick[] = [];
42
45
  const last = new Map<OverviewKind, OverviewTick>();
46
+
43
47
  for (const tick of ticks) {
44
48
  const before = last.get(tick.kind);
49
+
45
50
  if (before && tick.y <= before.y + before.height) {
46
51
  before.height = Math.max(before.height, tick.y + tick.height - before.y);
52
+
47
53
  continue;
48
54
  }
55
+
49
56
  const next = { ...tick };
50
57
  last.set(tick.kind, next);
51
58
  merged.push(next);
52
59
  }
60
+
53
61
  return merged;
54
62
  }
55
63
 
@@ -58,7 +66,9 @@ const PAINT_ORDER: Partial<Record<OverviewKind, number>> = { warning: 1, error:
58
66
 
59
67
  export function paintOverview(container: HTMLElement, ticks: readonly OverviewTick[]): void {
60
68
  const document = container.ownerDocument;
69
+
61
70
  const order = (tick: OverviewTick): number => PAINT_ORDER[tick.kind] ?? 0;
71
+
62
72
  container.replaceChildren(
63
73
  ...[...ticks]
64
74
  .sort((left, right) => order(left) - order(right))
@@ -67,12 +77,15 @@ export function paintOverview(container: HTMLElement, ticks: readonly OverviewTi
67
77
  element.className = `se-tick se-tick-${tick.kind}`;
68
78
  element.style.top = `${tick.y}px`;
69
79
  element.style.height = `${tick.height}px`;
80
+
70
81
  if (tick.offset !== undefined) {
71
82
  element.setAttribute('data-offset', String(tick.offset));
72
83
  }
84
+
73
85
  if (tick.title !== undefined) {
74
86
  element.setAttribute('data-title', tick.title);
75
87
  }
88
+
76
89
  return element;
77
90
  })
78
91
  );