@adecore/editor-react 0.39.0 → 0.40.1

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 (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
@@ -10,6 +10,7 @@ export interface ParsedDocComment {
10
10
  }
11
11
  export declare const DOC_LINK_PREFIX = "#doc-link:";
12
12
  export declare function decodeEntities(text: string): string;
13
+ export declare function dedent(text: string): string;
13
14
  export declare function safeAddress(address: string): string | null;
14
15
  export declare function htmlToMarkdown(source: string): string;
15
16
  export declare function inlineTagsToMarkdown(markdown: string): string;
@@ -170,7 +170,7 @@ export function decodeEntities(text) {
170
170
  return ENTITIES[code.toLowerCase()] ?? match;
171
171
  });
172
172
  }
173
- function dedent(text) {
173
+ export function dedent(text) {
174
174
  const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
175
175
  const indents = lines.filter(line => line.trim() !== '').map(line => line.length - line.trimStart().length);
176
176
  const common = indents.length === 0 ? 0 : Math.min(...indents);
package/dist/folding.js CHANGED
@@ -92,10 +92,9 @@ export class FoldingFeature {
92
92
  });
93
93
  }
94
94
  publish() {
95
- const hints = {
95
+ this.language.editor.setFoldHints({
96
96
  symbols: this.symbols,
97
97
  ranges: this.ranges
98
- };
99
- this.language.editor.setFoldHints(hints);
98
+ });
100
99
  }
101
100
  }
@@ -10,17 +10,13 @@ function overlaps(edits) {
10
10
  return ordered.some((edit, index) => index > 0 && comparePositions(ordered[index - 1].range.end, edit.range.start) > 0);
11
11
  }
12
12
  /*
13
- * Formatting while typing: after a person types one of the characters the server named in
14
- * `firstTriggerCharacter` and `moreTriggerCharacter`, the server is asked what to change around the caret, and its
15
- * edits are applied.
13
+ * Formatting while typing: after one of the server's trigger characters is typed, the server is asked what
14
+ * to change around the caret, and its edits join the undo step of the typed character.
16
15
  *
17
- * Only typing counts: one change that is the trigger character alone, the closer the editor moved back to its
18
- * opener's indentation, or a line break with its indentation for `\n`, which the editor reports as typed though
19
- * Enter runs as a command. A character the editor types over an existing closer changes no text and asks nothing. A paste, undo, a completion or an external change does not ask, and neither does a
20
- * second caret, a read-only editor or a snippet session, whose tab stops an edit could move. An answer is applied
21
- * only while the text is exactly what it was asked about, and the caret where it was.
22
- *
23
- * The edits join the undo step of the typed character, so one undo takes both back. Off by default; a host enables it explicitly with `configure(true)`.
16
+ * Only typing counts: the trigger character alone, the closer the editor moved back to its opener's
17
+ * indentation, or a line break with its indentation for `\n`. A paste, undo, a completion, an external change,
18
+ * a second caret, a read-only editor or a snippet session (whose tab stops an edit could move) asks nothing.
19
+ * An answer is applied only while the text and the caret are what it was asked about. Off until `configure(true)`.
24
20
  */
25
21
  export class FormatOnTypeFeature {
26
22
  language;
@@ -1,4 +1,3 @@
1
- import type { EditorRange } from '@adecore/editor';
2
1
  import type { Hover, Location, NavigationResult } from '@adecore/lsp';
3
2
  export interface SignatureBlock {
4
3
  readonly language: string;
@@ -13,7 +12,6 @@ export declare function hoverTextOf(hover: Hover): HoverText;
13
12
  export declare function escapeMarkdown(text: string): string;
14
13
  export declare function isEmptyHover(text: HoverText): boolean;
15
14
  export declare function locationsOf(result: NavigationResult): Location[];
16
- export declare function sameRange(left: EditorRange, right: EditorRange): boolean;
17
15
  export interface DocTag {
18
16
  readonly name: string;
19
17
  readonly markdown: string;
@@ -1,3 +1,4 @@
1
+ import { dedent } from './doc-comment-model.js';
1
2
  const FENCE = /^```([\w+#.-]*)[^\n]*\n([\s\S]*?)\n```[ \t]*(?:\n|$)/;
2
3
  /*
3
4
  * The leading fenced blocks of a markdown text as signatures, and what follows them, minus the rule
@@ -72,9 +73,6 @@ export function locationsOf(result) {
72
73
  range: entry.targetSelectionRange
73
74
  } : entry);
74
75
  }
75
- export function sameRange(left, right) {
76
- return left.start.line === right.start.line && left.start.character === right.start.character && left.end.line === right.end.line && left.end.character === right.end.character;
77
- }
78
76
  const TAG_LINE = /^(?:_@([\w-]+)_|\*@([\w-]+)\*|@([\w-]+))(?:\s+|$)([\s\S]*)$/;
79
77
  /* Tags whose text is a block of its own, which a row of the tag list has no room for. */
80
78
  const BLOCK_TAGS = new Set(['example']);
@@ -145,12 +143,6 @@ const INLINE_TAG = /\{@(see|link|linkplain)\s+([^}\s]+)(?:\s+([^}]+?))?\s*\}/g;
145
143
  function decodeEntities(text) {
146
144
  return text.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#0?39;/g, "'").replace(/&amp;/g, '&');
147
145
  }
148
- function dedent(text) {
149
- const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
150
- const indents = lines.filter(line => line.trim() !== '').map(line => line.length - line.trimStart().length);
151
- const common = indents.length === 0 ? 0 : Math.min(...indents);
152
- return lines.map(line => line.slice(common)).join('\n');
153
- }
154
146
  /*
155
147
  * What a docblock writes for a reader of its source, written for one of a hover: a `<code>` or `<pre>`
156
148
  * example as a block in the symbol's language, and an inline `{@see Name}` or `{@link Name label}` as
@@ -6,7 +6,6 @@ import { type Timers } from './timers.ts';
6
6
  export declare function hintLabelOf(hint: InlayHint): string;
7
7
  export declare function drawableHints(hints: readonly InlayHint[]): InlayHint[];
8
8
  export declare function editorHintOf(hint: InlayHint, id: string): EditorInlayHint;
9
- export declare function editorHintsOf(hints: readonly InlayHint[]): EditorInlayHint[];
10
9
  export declare function withMargin(visible: EditorRange, endOfDocument: EditorPosition, endOfLine: (line: number) => EditorPosition): EditorRange;
11
10
  export declare function covers(asked: EditorRange, visible: EditorRange): boolean;
12
11
  export declare function tooltipTextOf(tooltip: string | MarkupContent | undefined): HoverText | null;
@@ -1,3 +1,5 @@
1
+ import { followModKey } from './definition-link.js';
2
+ import { comparePositions } from './diagnostics-model.js';
1
3
  import { hoverTextOf, isEmptyHover } from './hover-content.js';
2
4
  import { SHOW_DELAY_MS } from './hover.js';
3
5
  import { messageOf } from './error-message.js';
@@ -12,13 +14,11 @@ const PAUSE_MS = 300;
12
14
  const DOUBLE_PRESS_MS = 500;
13
15
  /* Lines asked for above and below what is in view, so a little scrolling stays inside what is already drawn. */
14
16
  const MARGIN_LINES = 60;
15
- /* A hint's label is a string or a list of parts, which are read as one text. */
16
17
  export function hintLabelOf(hint) {
17
18
  return typeof hint.label === 'string' ? hint.label : hint.label.map(part => part.value).join('');
18
19
  }
19
- /* The hints worth drawing, in the order of the text. */
20
20
  export function drawableHints(hints) {
21
- return hints.filter(hint => hintLabelOf(hint) !== '').sort((left, right) => left.position.line - right.position.line || left.position.character - right.position.character);
21
+ return hints.filter(hint => hintLabelOf(hint) !== '').sort((left, right) => comparePositions(left.position, right.position));
22
22
  }
23
23
  /* A hint as the editor draws it: a label with parts keeps them apart so each can be pointed at. */
24
24
  export function editorHintOf(hint, id) {
@@ -37,10 +37,6 @@ export function editorHintOf(hint, id) {
37
37
  } : {})
38
38
  };
39
39
  }
40
- export function editorHintsOf(hints) {
41
- return drawableHints(hints).map((hint, index) => editorHintOf(hint, `hint-${index}`));
42
- }
43
- /* The lines in view with a margin of lines around them, from the start of the first to the end of the last. */
44
40
  export function withMargin(visible, endOfDocument, endOfLine) {
45
41
  const first = Math.max(0, visible.start.line - MARGIN_LINES);
46
42
  const last = Math.min(endOfDocument.line, visible.end.line + MARGIN_LINES);
@@ -52,7 +48,6 @@ export function withMargin(visible, endOfDocument, endOfLine) {
52
48
  end: last === endOfDocument.line ? endOfDocument : endOfLine(last)
53
49
  };
54
50
  }
55
- /* Whether what was asked for already holds every line in view. */
56
51
  export function covers(asked, visible) {
57
52
  return asked.start.line <= visible.start.line && asked.end.line >= visible.end.line;
58
53
  }
@@ -69,7 +64,6 @@ export function tooltipTextOf(tooltip) {
69
64
  });
70
65
  return isEmptyHover(text) ? null : text;
71
66
  }
72
- /* One part of a hint's label, or null for a hint whose label is a string. */
73
67
  export function partOf(hint, index) {
74
68
  return index === null || typeof hint.label === 'string' ? null : hint.label[index] ?? null;
75
69
  }
@@ -94,7 +88,7 @@ export function lacksInfo(hint, index) {
94
88
  export function editsFit(editor, edits) {
95
89
  const end = editor.positionAt(Number.MAX_SAFE_INTEGER);
96
90
  const inside = position => Number.isInteger(position.line) && Number.isInteger(position.character) && position.line >= 0 && position.character >= 0 && position.line <= end.line && position.character <= lineTextOf(editor, position.line).length;
97
- const before = (left, right) => left.line < right.line || left.line === right.line && left.character <= right.character;
91
+ const before = (left, right) => comparePositions(left, right) <= 0;
98
92
  const ordered = [...edits].sort((left, right) => before(left.range.start, right.range.start) ? -1 : 1);
99
93
  return ordered.every((edit, index) => {
100
94
  const next = ordered[index + 1];
@@ -123,17 +117,12 @@ export function actionOf(hint, click) {
123
117
  }
124
118
  /*
125
119
  * Types and parameter names the servers infer, asked for the lines in view and a margin around them. The
126
- * last ones stay until the answer comes, so nothing blinks while typing; scrolling asks again only once the
127
- * view leaves the lines that were asked for.
120
+ * last ones stay until the answer comes, so nothing blinks while typing.
128
121
  *
129
- * A hint can be acted on with the pointer. The server is asked to resolve a hint as the pointer arrives, and
130
- * resting on it for the hover delay shows its tooltip. Mod+click on a part follows its place, a click on a
131
- * part runs its command and a double click inserts its text edits as one undo step, never together with the
132
- * command: when a hint has both, the command waits for the double press interval. A press on a hint that
133
- * offers nothing (yet) moves the caret as beside the text. A finger has no hover, so a tap shows the
134
- * tooltip and does nothing more. All of it is for the pointer: the hints are not part
135
- * of the text and stay out of the keyboard's way, and Quick Info on the caret shows the tooltip of a hint that
136
- * sits there.
122
+ * Mod+click on a part follows its place, a click runs its command and a double click inserts its text edits
123
+ * as one undo step, never together with the command. A press on a hint that offers nothing (yet) moves the
124
+ * caret; a tap only shows the tooltip. The hints stay out of the keyboard's way: Quick Info on the caret
125
+ * shows the tooltip of a hint that sits there.
137
126
  */
138
127
  export class InlayHintsFeature {
139
128
  language;
@@ -203,23 +192,7 @@ export class InlayHintsFeature {
203
192
  if (editor.onInlayClick !== undefined) {
204
193
  offs.push(editor.onInlayClick(click => this.pressed(click)));
205
194
  }
206
- if (typeof window !== 'undefined') {
207
- const modKey = language.apple ? 'Meta' : 'Control';
208
- const keys = event => {
209
- if (event.key === modKey) {
210
- this.setModHeld(event.type === 'keydown');
211
- }
212
- };
213
- const lost = () => this.setModHeld(false);
214
- window.addEventListener('keydown', keys);
215
- window.addEventListener('keyup', keys);
216
- window.addEventListener('blur', lost);
217
- offs.push(() => {
218
- window.removeEventListener('keydown', keys);
219
- window.removeEventListener('keyup', keys);
220
- window.removeEventListener('blur', lost);
221
- });
222
- }
195
+ offs.push(followModKey(language.apple, held => this.setModHeld(held)));
223
196
  language.onDispose(() => {
224
197
  for (const off of offs) {
225
198
  off();
@@ -240,7 +213,7 @@ export class InlayHintsFeature {
240
213
  }
241
214
  /* The tooltip of a hint that sits at a position, as a card; false when there is none. This is how the keyboard reaches it. */
242
215
  async showTooltipAt(position) {
243
- const found = [...this.entries].find(([, entry]) => entry.epoch === this.epoch && sameSpot(entry.hint.position, position));
216
+ const found = [...this.entries].find(([, entry]) => entry.epoch === this.epoch && comparePositions(entry.hint.position, position) === 0);
244
217
  if (found === undefined) {
245
218
  return false;
246
219
  }
@@ -447,7 +420,4 @@ export class InlayHintsFeature {
447
420
  });
448
421
  }
449
422
  }
450
- }
451
- function sameSpot(left, right) {
452
- return left.line === right.line && left.character === right.character;
453
423
  }
@@ -5,6 +5,7 @@ import { rangeHolds } from './diagnostics-model.js';
5
5
  import { placesOfName } from './symbol-links.js';
6
6
  import { isShortcut } from './shortcut-keys.js';
7
7
  import { messageOf } from './error-message.js';
8
+ import { placeKey, uniqueBy } from './unique.js';
8
9
  const METHODS = {
9
10
  definition: 'textDocument/definition',
10
11
  declaration: 'textDocument/declaration',
@@ -21,9 +22,8 @@ const SHORTCUT_KINDS = [['definition', 'goToDefinition'], ['typeDefinition', 'go
21
22
  const TOAST_ID = 'language-navigation';
22
23
  /* The places of a result, each once: the two servers of a Vue file may both know the same one. `target` is the whole declaration when the server gave it. */
23
24
  export function uniquePlaces(result) {
24
- const seen = new Set();
25
25
  const list = result === null ? [] : Array.isArray(result) ? result : [result];
26
- return list.map(entry => 'targetUri' in entry ? {
26
+ return uniqueBy(list.map(entry => 'targetUri' in entry ? {
27
27
  location: {
28
28
  uri: entry.targetUri,
29
29
  range: entry.targetSelectionRange
@@ -32,19 +32,9 @@ export function uniquePlaces(result) {
32
32
  } : {
33
33
  location: entry,
34
34
  target: null
35
- }).filter(({
35
+ }), ({
36
36
  location
37
- }) => {
38
- const {
39
- start
40
- } = location.range;
41
- const key = `${location.uri}\0${start.line}\0${start.character}`;
42
- if (seen.has(key)) {
43
- return false;
44
- }
45
- seen.add(key);
46
- return true;
47
- });
37
+ }) => placeKey(location));
48
38
  }
49
39
  /* The places of a result, each once. */
50
40
  export function uniqueLocations(result) {
package/dist/peek.js CHANGED
@@ -5,6 +5,7 @@ import { PEEK_READ_FILES, definitionSnippetOf, peekFilesOf, snippetOf } from './
5
5
  import { isShortcut } from './shortcut-keys.js';
6
6
  import { messageOf } from './error-message.js';
7
7
  import { realTimers, revealRow } from './timers.js';
8
+ import { placeKey, uniqueBy } from './unique.js';
8
9
  const TOAST_ID = 'language-peek';
9
10
  /* The block as the panel draws it: a hatched band, the header, eight lines of code and a hatched band, until the editor has measured it. */
10
11
  export const PEEK_HEIGHT = 241;
@@ -20,9 +21,6 @@ const KINDS = {
20
21
  none: 'noDefinition'
21
22
  }
22
23
  };
23
- function placeKey(location) {
24
- return `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`;
25
- }
26
24
  /*
27
25
  * The references of the name at the caret, between the lines of the file under the line it is on. The
28
26
  * editor keeps the keyboard: arrows pick a reference, Enter goes to it and Escape closes the peek; typing
@@ -95,15 +93,7 @@ export class PeekFeature {
95
93
  const {
96
94
  targets
97
95
  } = found;
98
- const seen = new Set();
99
- const unique = found.locations.filter(location => {
100
- const key = placeKey(location);
101
- if (seen.has(key)) {
102
- return false;
103
- }
104
- seen.add(key);
105
- return true;
106
- });
96
+ const unique = uniqueBy(found.locations, placeKey);
107
97
  if (unique.length === 0) {
108
98
  this.tell(this.say(KINDS[kind].none));
109
99
  return;
@@ -1,5 +1,7 @@
1
- import type { FocusEvent, MouseEvent } from 'react';
1
+ import { type FocusEvent, type MouseEvent } from 'react';
2
+ import type { EditorLanguage } from './editor-language.ts';
2
3
  export declare function keepFocusOutOfList(focusBack: () => void): {
3
4
  onMouseDown(event: MouseEvent): void;
4
5
  onFocus(event: FocusEvent): void;
5
6
  };
7
+ export declare function useInputPopup(language: EditorLanguage, controls: string, activeDescendant: string | undefined): void;
@@ -1,3 +1,5 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useLayoutEffect } from 'react';
1
3
  /*
2
4
  * The handlers for a popup list whose rows the keyboard never enters: the text it belongs to keeps the
3
5
  * focus, and the active row is only `aria-activedescendant` or a highlight. Chromium reports that row as
@@ -9,4 +11,35 @@ export function keepFocusOutOfList(focusBack) {
9
11
  onMouseDown: event => event.preventDefault(),
10
12
  onFocus: () => focusBack()
11
13
  };
14
+ }
15
+ /* The focus stays in the text, so the input is what names the popup's list and its active option. */
16
+ export function useInputPopup(language, controls, activeDescendant) {
17
+ const $ = _c(8);
18
+ let t0;
19
+ if ($[0] !== activeDescendant || $[1] !== controls || $[2] !== language.editor) {
20
+ t0 = () => {
21
+ language.editor.setInputPopup({
22
+ controls,
23
+ activeDescendant
24
+ });
25
+ return () => language.editor.setInputPopup(null);
26
+ };
27
+ $[0] = activeDescendant;
28
+ $[1] = controls;
29
+ $[2] = language.editor;
30
+ $[3] = t0;
31
+ } else {
32
+ t0 = $[3];
33
+ }
34
+ let t1;
35
+ if ($[4] !== activeDescendant || $[5] !== controls || $[6] !== language) {
36
+ t1 = [language, controls, activeDescendant];
37
+ $[4] = activeDescendant;
38
+ $[5] = controls;
39
+ $[6] = language;
40
+ $[7] = t1;
41
+ } else {
42
+ t1 = $[7];
43
+ }
44
+ useLayoutEffect(t0, t1);
12
45
  }
@@ -7,4 +7,3 @@ export declare class RecentChoices {
7
7
  recency(item: CompletionItem): number;
8
8
  }
9
9
  export declare function recentChoicesOf(languageId: string): RecentChoices;
10
- export declare function forgetRecentChoices(): void;
@@ -32,8 +32,4 @@ export function recentChoicesOf(languageId) {
32
32
  stores.set(languageId, store);
33
33
  }
34
34
  return store;
35
- }
36
- /* For a test that must not hear the choices an earlier one made. */
37
- export function forgetRecentChoices() {
38
- stores.clear();
39
35
  }
@@ -39,6 +39,7 @@ export declare class RenderedDocsFeature {
39
39
  isRendered(comment: DocComment): boolean;
40
40
  toggle(): boolean;
41
41
  toggleAt(startLine: number): boolean;
42
+ private switch;
42
43
  entries(): readonly RenderedDocEntry[];
43
44
  getVersion(): number;
44
45
  getEntries(): readonly RenderedDocEntry[];
@@ -1,6 +1,8 @@
1
1
  import { findDocComments } from '@adecore/editor';
2
+ import { comparePositions } from './diagnostics-model.js';
2
3
  import { KEY_PRIORITY } from './key-priority.js';
3
4
  import { parseDocComment } from './doc-comment-model.js';
5
+ import { lineTextOf } from './line-text.js';
4
6
  import { isShortcut } from './shortcut-keys.js';
5
7
  import { realTimers } from './timers.js';
6
8
  const OWNER = 'rendered-docs';
@@ -22,7 +24,7 @@ function touches(range, comment) {
22
24
  if (range.end.line < comment.startLine || range.start.line > comment.endLine) {
23
25
  return false;
24
26
  }
25
- const empty = range.start.line === range.end.line && range.start.character === range.end.character;
27
+ const empty = comparePositions(range.start, range.end) === 0;
26
28
  if (!empty && range.end.line === comment.startLine && range.end.character === 0) {
27
29
  return false;
28
30
  }
@@ -30,13 +32,13 @@ function touches(range, comment) {
30
32
  }
31
33
  /* The first words of what a comment says, to tell its row from the others for a person who hears it. */
32
34
  function summaryOf(body) {
33
- const lead = body.split('\n').findIndex(line => /^\s*@/.test(line));
34
- const text = (lead === -1 ? body : body.split('\n').slice(0, lead).join('\n')).replace(/[`*_#>[\]()]/g, ' ');
35
- const words = text.split(/\s+/).filter(word => word !== '');
36
- return words.slice(0, SUMMARY_WORDS).join(' ');
35
+ const lines = body.split('\n');
36
+ const lead = lines.findIndex(line => /^\s*@/.test(line));
37
+ const text = (lead === -1 ? body : lines.slice(0, lead).join('\n')).replace(/[`*_#>[\]()]/g, ' ');
38
+ return text.split(/\s+/).filter(word => word !== '').slice(0, SUMMARY_WORDS).join(' ');
37
39
  }
38
40
  function samePlace(range, position) {
39
- return range !== null && range.start.line === position.line && range.start.character === position.character;
41
+ return range !== null && comparePositions(range.start, position) === 0;
40
42
  }
41
43
  /*
42
44
  * Documentation comments drawn as formatted text in place of their lines, the way the Reader mode of the
@@ -105,10 +107,7 @@ export class RenderedDocsFeature {
105
107
  }
106
108
  this.timers.clear(this.settleTimer);
107
109
  this.timers.clear(this.caretTimer);
108
- for (const override of this.overrides) {
109
- override.tracked.dispose();
110
- }
111
- this.overrides = [];
110
+ this.clearOverrides();
112
111
  editor.setReplacements?.([], OWNER);
113
112
  editor.setGutterMarkers([], OWNER);
114
113
  this.containers.clear();
@@ -141,17 +140,14 @@ export class RenderedDocsFeature {
141
140
  /* Switches the comment the caret is in between formatted and source. False when the caret is in none. */
142
141
  toggle() {
143
142
  this.settle();
144
- const comment = this.at(this.language.editor.getCaret().line);
145
- if (!this.enabled || comment === undefined || !this.canRender(comment)) {
146
- return false;
147
- }
148
- this.setRendered(comment, !this.wantsRendered(comment));
149
- return true;
143
+ return this.switch(this.at(this.language.editor.getCaret().line));
150
144
  }
151
145
  /* Switches the comment that starts on a line, which is what its gutter button does. */
152
146
  toggleAt(startLine) {
153
147
  this.settle();
154
- const comment = this.comments.find(candidate => candidate.startLine === startLine);
148
+ return this.switch(this.comments.find(candidate => candidate.startLine === startLine));
149
+ }
150
+ switch(comment) {
155
151
  if (!this.enabled || comment === undefined || !this.canRender(comment)) {
156
152
  return false;
157
153
  }
@@ -267,16 +263,7 @@ export class RenderedDocsFeature {
267
263
  if (line < 0) {
268
264
  return;
269
265
  }
270
- const text = editor.textInRange({
271
- start: {
272
- line,
273
- character: 0
274
- },
275
- end: {
276
- line,
277
- character: Number.MAX_SAFE_INTEGER
278
- }
279
- });
266
+ const text = lineTextOf(editor, line);
280
267
  const character = line > comment.endLine ? /^[ \t]*/.exec(text)?.[0].length ?? 0 : text.length;
281
268
  const place = {
282
269
  line,
@@ -344,7 +331,7 @@ export class RenderedDocsFeature {
344
331
  this.bump();
345
332
  }
346
333
  parse(comment) {
347
- const key = `${comment.body}`;
334
+ const key = comment.body;
348
335
  let result = this.parsed.get(key);
349
336
  if (result === undefined) {
350
337
  if (this.parsed.size >= MAX_PARSED) {
@@ -1,6 +1,7 @@
1
1
  import type { Editor, EditorContentChange, EditorRange } from '@adecore/editor';
2
2
  import type { SnippetTransform } from './snippet.ts';
3
3
  import type { SnippetTransformLease } from './snippet-transform-queue.ts';
4
+ import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
4
5
  export interface FieldTransformTarget {
5
6
  readonly id: symbol;
6
7
  range: EditorRange;
@@ -12,6 +13,10 @@ export interface FieldTransformTarget {
12
13
  readonly order: readonly number[];
13
14
  readonly ancestors?: readonly symbol[];
14
15
  }
16
+ export declare function jobTable(): {
17
+ readonly jobs: SnippetTransformJob[];
18
+ readonly slotOf: (job: SnippetTransformJob) => number;
19
+ };
15
20
  export declare class SnippetFieldTransforms {
16
21
  private readonly entries;
17
22
  private applying;
@@ -1,4 +1,23 @@
1
1
  import { comparePositions, shiftPosition } from './diagnostics-model.js';
2
+ /* Each distinct job once, so a transform that several ranges share is computed once; `slotOf` is its index in `jobs`. */
3
+ export function jobTable() {
4
+ const jobs = [];
5
+ const keys = new Map();
6
+ const slotOf = job => {
7
+ const key = JSON.stringify(job);
8
+ let slot = keys.get(key);
9
+ if (slot === undefined) {
10
+ slot = jobs.length;
11
+ keys.set(key, slot);
12
+ jobs.push(job);
13
+ }
14
+ return slot;
15
+ };
16
+ return {
17
+ jobs,
18
+ slotOf
19
+ };
20
+ }
2
21
  /* Navigation frames can pop before a worker replies; their tracked targets live independently. */
3
22
  export class SnippetFieldTransforms {
4
23
  entries = new Set();
@@ -36,22 +55,14 @@ export class SnippetFieldTransforms {
36
55
  cancelled: false
37
56
  };
38
57
  this.entries.add(entry);
39
- const jobs = [],
40
- keys = new Map();
41
- const slots = targets.map(target => {
42
- const job = {
43
- ...target.transform,
44
- value: target.value
45
- },
46
- key = JSON.stringify(job);
47
- let slot = keys.get(key);
48
- if (slot === undefined) {
49
- slot = jobs.length;
50
- keys.set(key, slot);
51
- jobs.push(job);
52
- }
53
- return slot;
54
- });
58
+ const {
59
+ jobs,
60
+ slotOf
61
+ } = jobTable();
62
+ const slots = targets.map(target => slotOf({
63
+ ...target.transform,
64
+ value: target.value
65
+ }));
55
66
  const valid = () => !entry.cancelled && entry.current() && targets.every(target => this.editor.textInRange(target.range) === target.value);
56
67
  void this.lease.request(jobs, {
57
68
  kind: 'exit',
@@ -5,16 +5,10 @@ import { acquireSnippetTransforms } from './snippet-transform-queue.js';
5
5
  import { realTimers } from './timers.js';
6
6
  import { KEY_PRIORITY } from './key-priority.js';
7
7
  import { MAX_TRANSFORM_CHARACTERS } from './snippet-transform-protocol.js';
8
- import { comparePositions, rangeHolds, shiftPosition } from './diagnostics-model.js';
8
+ import { comparePositions, endOfInsertion, rangeHolds, sameRange, shiftPosition } from './diagnostics-model.js';
9
+ import { textBeforeOf } from './line-text.js';
9
10
  export function positionInInsertion(start, text, offset) {
10
- const lines = text.slice(0, offset).split(/\r\n|\r|\n/);
11
- return lines.length === 1 ? {
12
- line: start.line,
13
- character: start.character + offset
14
- } : {
15
- line: start.line + lines.length - 1,
16
- character: lines[lines.length - 1].length
17
- };
11
+ return endOfInsertion(start, text.slice(0, offset));
18
12
  }
19
13
  function mapRange(range, changes, grow) {
20
14
  let {
@@ -198,7 +192,7 @@ export class SnippetFeature {
198
192
  off();
199
193
  }
200
194
  const now = this.language.editor.getSelections();
201
- return valid && current() && now.length === selections.length && now.every((range, index) => comparePositions(range.start, selections[index].start) === 0 && comparePositions(range.end, selections[index].end) === 0);
195
+ return valid && current() && now.length === selections.length && now.every((range, index) => sameRange(range, selections[index]));
202
196
  }
203
197
  get isActive() {
204
198
  return this.frames.length > 0;
@@ -489,13 +483,7 @@ export class SnippetFeature {
489
483
  const groups = [];
490
484
  let characters = 0;
491
485
  for (const [index, target] of targets.entries()) {
492
- const before = editor.textInRange({
493
- start: {
494
- line: target.range.start.line,
495
- character: 0
496
- },
497
- end: target.range.start
498
- });
486
+ const before = textBeforeOf(editor, target.range.start);
499
487
  const prefix = /^[\t ]*/.exec(before)?.[0] ?? '';
500
488
  const value = values[index];
501
489
  let newlines = 0;
@@ -586,7 +574,7 @@ export class SnippetFeature {
586
574
  const captured = [...this.placed, ...active, ...trackedFrames.flatMap(frame => frame.instances.flatMap(instance => instance.fields.flatMap(field => field.ranges.filter(range => !transformIds.has(range.id) || comparePositions(range.start, range.end) < 0))))];
587
575
  const ancestorsOf = new Map(targets.map(target => [target, new Set([...(target.ancestors ?? []), ...target.frame.instances.flatMap(instance => instance.fields.flatMap(field => target.owner === instance && target.parents?.includes(field.index) === true ? field.ranges.filter(range => rangeHolds(range, target.range.start) && rangeHolds(range, target.range.end)).map(range => range.id) : []))])]));
588
576
  const map = (range, parent) => {
589
- const tracked = 'order' in range ? range : captured.find(candidate => comparePositions(candidate.start, range.start) === 0 && comparePositions(candidate.end, range.end) === 0);
577
+ const tracked = 'order' in range ? range : captured.find(candidate => sameRange(candidate, range));
590
578
  let {
591
579
  start,
592
580
  end
@@ -665,7 +653,7 @@ export class SnippetFeature {
665
653
  for (const field of instance.fields) {
666
654
  const primary = field.ranges[field.primary];
667
655
  field.ranges = field.ranges.filter(range => !range.parents?.includes(index) || !owned.some(edit => comparePositions(edit.range.start, edit.range.end) < 0 && (comparePositions(range.start, range.end) === 0 ? rangeHolds(edit.range, range.start) : comparePositions(edit.range.start, range.end) < 0 && comparePositions(range.start, edit.range.end) < 0)));
668
- field.primary = Math.max(0, field.ranges.findIndex(range => range === primary));
656
+ field.primary = Math.max(0, field.ranges.indexOf(primary));
669
657
  field.ranges = field.ranges.map(range => {
670
658
  const replacement = this.detached?.ranges.get(range.id);
671
659
  if (replacement !== undefined) {
@@ -712,7 +700,7 @@ export class SnippetFeature {
712
700
  if (!this.isActive) {
713
701
  return;
714
702
  }
715
- const extras = new Set(change.changes.filter(change => this.extras.some(extra => comparePositions(extra.range.start, change.range.start) === 0 && comparePositions(extra.range.end, change.range.end) === 0 && extra.text === change.text)));
703
+ const extras = new Set(change.changes.filter(change => this.extras.some(extra => sameRange(extra.range, change.range) && extra.text === change.text)));
716
704
  for (const frame of this.frames) {
717
705
  if (!this.mapFrame(frame, change.changes, extras)) {
718
706
  const caret = this.language.editor.getCaret();