@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
package/src/history.ts CHANGED
@@ -9,21 +9,29 @@ export class HistoryFeature {
9
9
 
10
10
  constructor(language: EditorLanguage) {
11
11
  this.language = language;
12
+
12
13
  const off = language.onKey((event) => {
13
14
  if (isShortcut(this.language.shortcuts.historyBack, event)) {
14
15
  this.back();
16
+
15
17
  return true;
16
18
  }
19
+
17
20
  if (isShortcut(this.language.shortcuts.historyForward, event)) {
18
21
  this.forward();
22
+
19
23
  return true;
20
24
  }
25
+
21
26
  if (isShortcut(this.language.shortcuts.recentLocations, event)) {
22
27
  this.recent();
28
+
23
29
  return true;
24
30
  }
31
+
25
32
  return false;
26
33
  }, KEY_PRIORITY.history);
34
+
27
35
  language.onDispose(off);
28
36
  }
29
37
 
@@ -39,6 +47,7 @@ export class HistoryFeature {
39
47
  /* Lists the places of the history under the caret; choosing one jumps there like any other jump. */
40
48
  recent(): void {
41
49
  const places = this.language.project.history.recent(this.language.place);
50
+
42
51
  this.language.locations(
43
52
  places.map((place) => ({ uri: place.uri, range: { start: place.position, end: place.position } })),
44
53
  this.language.editor.getCaret(),
@@ -50,7 +59,9 @@ export class HistoryFeature {
50
59
  if (place === null) {
51
60
  return false;
52
61
  }
62
+
53
63
  this.language.visit(place);
64
+
54
65
  return true;
55
66
  }
56
67
  }
package/src/holder.ts CHANGED
@@ -2,16 +2,19 @@
2
2
  export function createHolder<T>(): { get(): T | null; set(value: T | null): void; subscribe(listener: () => void): () => void } {
3
3
  let value: T | null = null;
4
4
  const listeners = new Set<() => void>();
5
+
5
6
  return {
6
7
  get: () => value,
7
8
  set: (next) => {
8
9
  value = next;
10
+
9
11
  for (const listener of [...listeners]) {
10
12
  listener();
11
13
  }
12
14
  },
13
15
  subscribe: (listener) => {
14
16
  listeners.add(listener);
17
+
15
18
  return () => {
16
19
  listeners.delete(listener);
17
20
  };
@@ -1,5 +1,5 @@
1
- import type { EditorRange } from '@adecore/editor';
2
1
  import type { Hover, Location, LocationLink, MarkedString, NavigationResult } from '@adecore/lsp';
2
+ import { dedent } from './doc-comment-model.ts';
3
3
 
4
4
  export interface SignatureBlock {
5
5
  readonly language: string;
@@ -22,10 +22,12 @@ const FENCE = /^```([\w+#.-]*)[^\n]*\n([\s\S]*?)\n```[ \t]*(?:\n|$)/;
22
22
  export function splitSignatures(markdown: string): HoverText {
23
23
  const signatures: SignatureBlock[] = [];
24
24
  let rest = markdown.replace(/^\s+/, '');
25
+
25
26
  for (let match = FENCE.exec(rest); match !== null; match = FENCE.exec(rest)) {
26
27
  signatures.push({ language: match[1] || 'text', code: match[2]! });
27
28
  rest = rest.slice(match[0].length).replace(/^\s+/, '');
28
29
  }
30
+
29
31
  return { signatures, markdown: rest.replace(/^(?:-{3,}|\*{3,})\s*(?:\n|$)/, '').trim() };
30
32
  }
31
33
 
@@ -33,14 +35,17 @@ function markedStringText(content: MarkedString): HoverText {
33
35
  if (typeof content !== 'string') {
34
36
  return { signatures: [{ language: content.language || 'text', code: content.value }], markdown: '' };
35
37
  }
38
+
36
39
  return splitSignatures(content);
37
40
  }
38
41
 
39
42
  /* The text of a hover, whichever of the three shapes the protocol allows it in. */
40
43
  export function hoverTextOf(hover: Hover): HoverText {
41
44
  const { contents } = hover;
45
+
42
46
  if (Array.isArray(contents)) {
43
47
  const parts = contents.map(markedStringText);
48
+
44
49
  return {
45
50
  signatures: parts.flatMap((part) => part.signatures),
46
51
  markdown: parts
@@ -49,9 +54,11 @@ export function hoverTextOf(hover: Hover): HoverText {
49
54
  .join('\n\n')
50
55
  };
51
56
  }
57
+
52
58
  if (typeof contents === 'string' || 'language' in contents) {
53
59
  return markedStringText(contents);
54
60
  }
61
+
55
62
  return contents.kind === 'plaintext' ? { signatures: [], markdown: escapeMarkdown(contents.value).trim() } : splitSignatures(contents.value);
56
63
  }
57
64
 
@@ -73,17 +80,10 @@ export function locationsOf(result: NavigationResult): Location[] {
73
80
  if (result === null) {
74
81
  return [];
75
82
  }
83
+
76
84
  const list = Array.isArray(result) ? (result as (Location | LocationLink)[]) : [result];
77
- return list.map((entry) => (isLink(entry) ? { uri: entry.targetUri, range: entry.targetSelectionRange } : entry));
78
- }
79
85
 
80
- export function sameRange(left: EditorRange, right: EditorRange): boolean {
81
- return (
82
- left.start.line === right.start.line &&
83
- left.start.character === right.start.character &&
84
- left.end.line === right.end.line &&
85
- left.end.character === right.end.character
86
- );
86
+ return list.map((entry) => (isLink(entry) ? { uri: entry.targetUri, range: entry.targetSelectionRange } : entry));
87
87
  }
88
88
 
89
89
  /* One `@tag` of a docblock, the way a server writes it into a hover: `_@since_ 1.0` or `*@param* `name` - what it is`. */
@@ -105,20 +105,25 @@ const BLOCK_TAGS = new Set(['example']);
105
105
  export function splitDocTags(markdown: string): { readonly markdown: string; readonly tags: readonly DocTag[] } {
106
106
  const tags: DocTag[] = [];
107
107
  const kept: string[] = [];
108
+
108
109
  for (const paragraph of markdown.split(/\n{2,}/)) {
109
110
  const lines = paragraph.split('\n');
110
111
  const matches = lines.map((line) => TAG_LINE.exec(line.trim()));
111
112
  const names = matches.map((match) => (match === null ? null : (match[1] ?? match[2] ?? match[3])!));
113
+
112
114
  if (paragraph.trim() === '' || names.some((name) => name === null || BLOCK_TAGS.has(name))) {
113
115
  kept.push(paragraph);
116
+
114
117
  continue;
115
118
  }
119
+
116
120
  matches.forEach((match, index) => {
117
121
  // The TypeScript server puts a dash between a tag, its parameter and its text.
118
122
  const text = match![4]!.replace(/^[\u2014\u2013-]\s*/, '').replace(/^(`[^`]+`)\s+[\u2014\u2013-]\s+/, '$1 ');
119
123
  tags.push({ name: names[index]!, markdown: text.trim() });
120
124
  });
121
125
  }
126
+
122
127
  // By name, and in the order written within one name, so parameters keep the order of the signature.
123
128
  return { markdown: kept.join('\n\n').trim(), tags: tags.toSorted((left, right) => left.name.localeCompare(right.name)) };
124
129
  }
@@ -147,11 +152,14 @@ const BASELINE = /!\[Baseline icon\]\([^)\s]*\)[ \t]*_([^\n]+?)_[ \t]*(?:\n|$)/;
147
152
  /* The Baseline status a text carries, which the card draws with a mark of its own, and the text without it. */
148
153
  function takeBaseline(markdown: string): { baseline: Baseline | null; rest: string } {
149
154
  const match = BASELINE.exec(markdown);
155
+
150
156
  if (match === null) {
151
157
  return { baseline: null, rest: markdown };
152
158
  }
159
+
153
160
  const text = match[1]!.trim();
154
161
  const level = text.startsWith('Widely') ? 'widely' : text.startsWith('Newly') ? 'newly' : 'limited';
162
+
155
163
  return { baseline: { level, text }, rest: (markdown.slice(0, match.index) + markdown.slice(match.index + match[0].length)).replace(/\n{3,}/g, '\n\n') };
156
164
  }
157
165
 
@@ -176,13 +184,6 @@ function decodeEntities(text: string): string {
176
184
  .replace(/&amp;/g, '&');
177
185
  }
178
186
 
179
- function dedent(text: string): string {
180
- const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
181
- const indents = lines.filter((line) => line.trim() !== '').map((line) => line.length - line.trimStart().length);
182
- const common = indents.length === 0 ? 0 : Math.min(...indents);
183
- return lines.map((line) => line.slice(common)).join('\n');
184
- }
185
-
186
187
  /*
187
188
  * What a docblock writes for a reader of its source, written for one of a hover: a `<code>` or `<pre>`
188
189
  * example as a block in the symbol's language, and an inline `{@see Name}` or `{@link Name label}` as
@@ -192,6 +193,7 @@ export function docblockMarkdown(markdown: string, language: string): string {
192
193
  return markdown
193
194
  .replace(HTML_CODE, (_match, _tag: string, body: string) => {
194
195
  const code = decodeEntities(body);
196
+
195
197
  return code.includes('\n') ? `\n\n\`\`\`${language}\n${dedent(code)}\n\`\`\`\n\n` : `\`${code.trim()}\``;
196
198
  })
197
199
  .replace(INLINE_TAG, (_match, _tag: string, target: string, label: string | undefined) => (label === undefined ? `\`${target}\`` : label))
@@ -205,17 +207,22 @@ export type MarkdownPart = { readonly kind: 'prose'; readonly text: string } | {
205
207
  export function markdownParts(markdown: string): MarkdownPart[] {
206
208
  const parts: MarkdownPart[] = [];
207
209
  let rest = markdown;
210
+
208
211
  for (let match = ANY_FENCE.exec(rest); match !== null; match = ANY_FENCE.exec(rest)) {
209
212
  const prose = rest.slice(0, match.index).trim();
213
+
210
214
  if (prose !== '') {
211
215
  parts.push({ kind: 'prose', text: prose });
212
216
  }
217
+
213
218
  parts.push({ kind: 'code', block: { language: match[1] || 'text', code: match[2]! } });
214
219
  rest = rest.slice(match.index + match[0].length);
215
220
  }
221
+
216
222
  if (rest.trim() !== '') {
217
223
  parts.push({ kind: 'prose', text: rest.trim() });
218
224
  }
225
+
219
226
  return parts;
220
227
  }
221
228
 
@@ -227,18 +234,23 @@ function unescapeMarkdown(text: string): string {
227
234
  function sectionOf(chunk: string, leading: readonly SignatureBlock[]): HoverSection {
228
235
  let rest = chunk.trim();
229
236
  const title = TITLE.exec(rest);
237
+
230
238
  if (title !== null) {
231
239
  rest = rest.slice(title[0].length).trim();
232
240
  }
241
+
233
242
  const signatures = [...leading];
234
243
  // A section's own snippet is its first block; one further down after the prose, such as an example, stays prose.
235
244
  const fence = ANY_FENCE.exec(rest);
245
+
236
246
  if (fence !== null && !/@example/.test(rest.slice(0, fence.index))) {
237
247
  signatures.push({ language: fence[1] || 'text', code: fence[2]! });
238
248
  rest = (rest.slice(0, fence.index) + rest.slice(fence.index + fence[0].length)).trim();
239
249
  }
250
+
240
251
  const taken = takeBaseline(rest);
241
252
  const doc = splitDocTags(docblockMarkdown(taken.rest, signatures[0]?.language ?? 'text'));
253
+
242
254
  return {
243
255
  title: title === null ? null : unescapeMarkdown((title[1] ?? title[2])!.trim()),
244
256
  signatures: signatures.map(withoutOpenTag),
@@ -253,14 +265,19 @@ export function hoverSectionsOf(text: HoverText): HoverSection[] {
253
265
  const chunks = text.markdown === '' ? [''] : text.markdown.split(RULE);
254
266
  const sections = chunks.map((chunk, index) => sectionOf(chunk, index === 0 ? text.signatures : []));
255
267
  const seen = new Set<string>();
268
+
256
269
  return sections.filter((section) => {
257
270
  const empty =
258
271
  section.title === null && section.signatures.length === 0 && section.markdown === '' && section.tags.length === 0 && section.baseline === null;
272
+
259
273
  const key = JSON.stringify([section.title, section.signatures, section.markdown, section.baseline]);
274
+
260
275
  if (empty || seen.has(key)) {
261
276
  return false;
262
277
  }
278
+
263
279
  seen.add(key);
280
+
264
281
  return true;
265
282
  });
266
283
  }
package/src/hover.ts CHANGED
@@ -10,9 +10,12 @@ import { isShortcut } from './shortcut-keys.ts';
10
10
  import { realTimers, type Timers } from './timers.ts';
11
11
 
12
12
  export const SHOW_DELAY_MS = 300;
13
+
13
14
  const HIDE_DELAY_MS = 250;
15
+
14
16
  /* How long a card has to stay up before the references of its symbol are counted, which can be a request over the whole project. */
15
17
  const COUNT_DELAY_MS = 500;
18
+
16
19
  const TOAST_ID = 'language-hover';
17
20
 
18
21
  export interface HoverSettings {
@@ -28,18 +31,24 @@ export interface HoverSettings {
28
31
  export class HoverFeature {
29
32
  private readonly language: EditorLanguage;
30
33
  private readonly timers: Timers;
34
+
31
35
  /* The counts asked for since the text last changed, by the symbol's position, so moving back to a name does not ask again. */
32
36
  private readonly counts = new Map<string, number>();
37
+
33
38
  private showTimer: unknown;
34
39
  private hideTimer: unknown;
35
40
  private countTimer: unknown;
36
41
  private inCard = false;
37
42
  private referenceCount = true;
43
+
38
44
  /* Counts every request, so an answer that arrives after the pointer moved on is dropped. */
39
45
  private request = 0;
46
+
40
47
  private lookup: AbortController | null = null;
48
+
41
49
  /* A card the keyboard opened: scrolling does not take it away, since it was never under a pointer. Moving the caret does. */
42
50
  private pinned = false;
51
+
43
52
  /* The card is being read: an explanation runs in it or is shown, so the pointer going elsewhere neither closes nor replaces it. */
44
53
  private held = false;
45
54
 
@@ -47,6 +56,7 @@ export class HoverFeature {
47
56
  this.language = language;
48
57
  this.timers = timers;
49
58
  const { editor } = language;
59
+
50
60
  const offs = [
51
61
  editor.onHover((hover) => this.moved(hover)),
52
62
  editor.onTextChange(() => {
@@ -57,6 +67,7 @@ export class HoverFeature {
57
67
  if (this.held) {
58
68
  this.hide();
59
69
  }
70
+
60
71
  return false;
61
72
  }),
62
73
  editor.onViewChange(() => {
@@ -72,25 +83,32 @@ export class HoverFeature {
72
83
  language.onKey((event) => {
73
84
  if (isShortcut(this.language.shortcuts.quickInfo, event)) {
74
85
  this.quickInfo();
86
+
75
87
  return true;
76
88
  }
89
+
77
90
  if (event.key === 'Escape' && this.language.popups.getState().hover !== null && this.language.popups.getState().callHierarchy === null) {
78
91
  this.hide();
92
+
79
93
  return true;
80
94
  }
95
+
81
96
  return false;
82
97
  }, KEY_PRIORITY.hover)
83
98
  ];
99
+
84
100
  language.onDispose(() => {
85
101
  for (const off of offs) {
86
102
  off();
87
103
  }
104
+
88
105
  this.hide();
89
106
  });
90
107
  }
91
108
 
92
109
  configure(settings: HoverSettings): void {
93
110
  this.referenceCount = settings.referenceCount !== false;
111
+
94
112
  if (!this.referenceCount) {
95
113
  this.timers.clear(this.countTimer);
96
114
  }
@@ -99,6 +117,7 @@ export class HoverFeature {
99
117
  /* The pointer entered or left the card, which is where a link in it is clicked. */
100
118
  holdCard(inside: boolean): void {
101
119
  this.inCard = inside;
120
+
102
121
  if (inside) {
103
122
  this.timers.clear(this.hideTimer);
104
123
  } else {
@@ -116,12 +135,15 @@ export class HoverFeature {
116
135
  /* The problems at a position as a card, at once; what the servers say about the symbol is left for the pointer and Quick Info. */
117
136
  showProblemsAt(position: EditorPosition): boolean {
118
137
  const problems = this.language.diagnostics.at(position);
138
+
119
139
  if (problems.length === 0) {
120
140
  return false;
121
141
  }
142
+
122
143
  this.hide();
123
144
  this.pinned = true;
124
145
  this.present(position, problems, null, problems[0]!.diagnostic.range);
146
+
125
147
  return true;
126
148
  }
127
149
 
@@ -159,6 +181,7 @@ export class HoverFeature {
159
181
  this.timers.clear(this.hideTimer);
160
182
  this.timers.clear(this.countTimer);
161
183
  this.inCard = false;
184
+
162
185
  if (this.language.popups.getState().hover !== null) {
163
186
  this.language.popups.setState({ hover: null });
164
187
  }
@@ -168,17 +191,24 @@ export class HoverFeature {
168
191
  if (this.held) {
169
192
  return;
170
193
  }
194
+
171
195
  this.timers.clear(this.showTimer);
196
+
172
197
  if (hover === null) {
173
198
  this.request++;
174
199
  this.scheduleHide();
200
+
175
201
  return;
176
202
  }
203
+
177
204
  const shown = this.language.popups.getState().hover;
205
+
178
206
  if (shown !== null && rangeHolds(shown.subject, hover.position)) {
179
207
  this.timers.clear(this.hideTimer);
208
+
180
209
  return;
181
210
  }
211
+
182
212
  this.scheduleHide();
183
213
  const request = ++this.request;
184
214
  this.showTimer = this.timers.set(() => void this.show(hover.position, request, false), SHOW_DELAY_MS);
@@ -186,9 +216,11 @@ export class HoverFeature {
186
216
 
187
217
  private scheduleHide(): void {
188
218
  this.timers.clear(this.hideTimer);
219
+
189
220
  if (this.language.popups.getState().hover === null || this.inCard || this.held) {
190
221
  return;
191
222
  }
223
+
192
224
  this.hideTimer = this.timers.set(() => {
193
225
  if (!this.inCard) {
194
226
  this.hide();
@@ -200,23 +232,30 @@ export class HoverFeature {
200
232
  private async ask(position: EditorHover['position'], signal: AbortSignal): Promise<{ info: HoverInfo | null; range: EditorRange | null }> {
201
233
  const { service } = this.language.project;
202
234
  const { uri } = this.language;
235
+
203
236
  if (!service.supports('textDocument/hover', uri)) {
204
237
  return { info: null, range: null };
205
238
  }
239
+
206
240
  const [hover, definition] = await Promise.all([
207
241
  service.hover(uri, position, { signal }).catch(() => null),
208
242
  service.supports('textDocument/definition', uri) ? service.definition(uri, position, { signal }).catch(() => null) : null
209
243
  ]);
244
+
210
245
  if (hover === null) {
211
246
  return { info: null, range: null };
212
247
  }
248
+
213
249
  const text = hoverTextOf(hover);
250
+
214
251
  if (isEmptyHover(text)) {
215
252
  return { info: null, range: null };
216
253
  }
254
+
217
255
  const { editor } = this.language;
218
256
  const line = lineTextOf(editor, position.line);
219
257
  const word = wordRangeAt(line, position);
258
+
220
259
  return {
221
260
  info: {
222
261
  text,
@@ -234,22 +273,30 @@ export class HoverFeature {
234
273
  const controller = new AbortController();
235
274
  this.lookup = controller;
236
275
  const { info, range } = await this.ask(position, controller.signal);
276
+
237
277
  if (request !== this.request) {
238
278
  return;
239
279
  }
280
+
240
281
  if (problems.length === 0 && info === null) {
241
282
  this.hide();
283
+
242
284
  if (pinned && (await this.language.inlayHints.showTooltipAt(position))) {
243
285
  this.pinned = true;
286
+
244
287
  return;
245
288
  }
289
+
246
290
  if (pinned) {
247
291
  this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.hover.none') });
248
292
  }
293
+
249
294
  return;
250
295
  }
296
+
251
297
  this.pinned = pinned;
252
298
  this.present(position, problems, info, problems[0]?.diagnostic.range ?? range);
299
+
253
300
  if (info !== null && this.referenceCount) {
254
301
  this.scheduleCount(position, request, controller.signal);
255
302
  }
@@ -258,6 +305,7 @@ export class HoverFeature {
258
305
  private present(position: EditorPosition, problems: readonly Problem[], info: HoverInfo | null, range: EditorRange | null): void {
259
306
  const subject = range ?? { start: position, end: position };
260
307
  this.timers.clear(this.hideTimer);
308
+
261
309
  this.language.popups.setState({
262
310
  hover: {
263
311
  anchor: subject.start.line === position.line ? subject.start : position,
@@ -273,10 +321,13 @@ export class HoverFeature {
273
321
  private scheduleCount(position: EditorPosition, request: number, signal: AbortSignal): void {
274
322
  const key = `${position.line}:${this.wordStart(position)}`;
275
323
  const known = this.counts.get(key);
324
+
276
325
  if (known !== undefined) {
277
326
  this.showCount(known, request);
327
+
278
328
  return;
279
329
  }
330
+
280
331
  this.timers.clear(this.countTimer);
281
332
  this.countTimer = this.timers.set(() => void this.count(position, key, request, signal), COUNT_DELAY_MS);
282
333
  }
@@ -284,19 +335,24 @@ export class HoverFeature {
284
335
  /* How many places use the symbol, which can take a while in a large project and so arrives after the card does. */
285
336
  private async count(position: EditorPosition, key: string, request: number, signal: AbortSignal): Promise<void> {
286
337
  const { service } = this.language.project;
338
+
287
339
  if (request !== this.request || !this.referenceCount || !service.supports('textDocument/references', this.language.uri)) {
288
340
  return;
289
341
  }
342
+
290
343
  const found = await service.references(this.language.uri, position, false, { signal }).catch(() => null);
344
+
291
345
  if (found === null) {
292
346
  return;
293
347
  }
348
+
294
349
  this.counts.set(key, found.length);
295
350
  this.showCount(found.length, request);
296
351
  }
297
352
 
298
353
  private showCount(count: number, request: number): void {
299
354
  const shown = this.language.popups.getState().hover;
355
+
300
356
  if (request === this.request && shown?.info != null) {
301
357
  this.language.popups.setState({ hover: { ...shown, info: { ...shown.info, references: count } } });
302
358
  }
@@ -305,6 +361,7 @@ export class HoverFeature {
305
361
  /* Where the word at a position starts, which names the symbol for the cache. */
306
362
  wordStart(position: EditorPosition): number {
307
363
  const line = lineTextOf(this.language.editor, position.line);
364
+
308
365
  return wordRangeAt(line, position)?.start.character ?? position.character;
309
366
  }
310
367
  }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export { realTimers, type Timers } from './timers.ts';
4
4
  export { type SemanticTokensOptions } from './semantic-tokens.ts';
5
5
  export { ProjectLanguage, type FileOperationTarget } from './project-language.ts';
6
6
  export type { LanguageDocumentHandle } from './project-language.ts';
7
+
7
8
  export type {
8
9
  SnippetVariableContext,
9
10
  LanguageHost,
@@ -13,6 +14,7 @@ export type {
13
14
  EditorNotification,
14
15
  RenameSuggestionsRequest
15
16
  } from './host-types.ts';
17
+
16
18
  export { EditorView } from './EditorView.tsx';
17
19
  export { createHolder } from './holder.ts';
18
20
  export type { EditorViewProps } from './EditorView.tsx';
@@ -36,6 +38,7 @@ export { useProblemCounts } from './use-problem-counts.ts';
36
38
  export type { ProblemCounts } from './diagnostics.ts';
37
39
  export type { BlameSource, CodeVisionSettings } from './code-vision.ts';
38
40
  export type { HoverSettings } from './hover.ts';
41
+
39
42
  export type {
40
43
  PopupState,
41
44
  PopupStore,
@@ -57,6 +60,7 @@ export type {
57
60
  MenuView,
58
61
  AuthorsView
59
62
  } from './popups.ts';
63
+
60
64
  export { RowHost, LineActionHost } from './row-host.ts';
61
65
  export type { HostedRow, HostedAction } from './row-host.ts';
62
66
  export { type HighlightLayers, highlightLayers } from './highlight-layers.ts';