@adecore/editor-react 0.23.0 → 0.24.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 (130) hide show
  1. package/dist/AnchoredPopup.d.ts +2 -1
  2. package/dist/AnchoredPopup.js +2 -2
  3. package/dist/CallHierarchyPanel.js +6 -2
  4. package/dist/CodeAuthorsCard.d.ts +4 -0
  5. package/dist/CodeAuthorsCard.js +8 -4
  6. package/dist/CompletionPopup.js +7 -3
  7. package/dist/EditorView.d.ts +8 -3
  8. package/dist/EditorView.js +79 -9
  9. package/dist/HoverCard.d.ts +3 -1
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.d.ts +8 -4
  12. package/dist/LanguagePopups.js +4 -4
  13. package/dist/PanelResizeHandle.d.ts +8 -0
  14. package/dist/PanelResizeHandle.js +71 -0
  15. package/dist/PeekPanel.js +6 -2
  16. package/dist/PickPopup.js +2 -1
  17. package/dist/RenameCard.js +10 -8
  18. package/dist/RenderedDocs.d.ts +10 -4
  19. package/dist/RenderedDocs.js +10 -6
  20. package/dist/call-hierarchy-model.js +3 -9
  21. package/dist/call-hierarchy.js +17 -17
  22. package/dist/code-actions.js +5 -2
  23. package/dist/completion-model.js +3 -1
  24. package/dist/completion.d.ts +4 -2
  25. package/dist/completion.js +49 -27
  26. package/dist/diagnostics-model.d.ts +1 -0
  27. package/dist/diagnostics-model.js +1 -1
  28. package/dist/diagnostics.d.ts +1 -0
  29. package/dist/diagnostics.js +28 -10
  30. package/dist/diff-overlays.js +2 -2
  31. package/dist/editor-language.d.ts +3 -1
  32. package/dist/editor-language.js +11 -1
  33. package/dist/format-on-type.js +2 -3
  34. package/dist/highlights.js +3 -2
  35. package/dist/history.js +3 -2
  36. package/dist/holder.d.ts +3 -3
  37. package/dist/holder.js +2 -1
  38. package/dist/host-types.d.ts +5 -0
  39. package/dist/hover.d.ts +5 -0
  40. package/dist/hover.js +13 -5
  41. package/dist/index.d.ts +6 -2
  42. package/dist/index.js +3 -1
  43. package/dist/key-priority.d.ts +23 -0
  44. package/dist/key-priority.js +39 -0
  45. package/dist/locales/en.json +5 -1
  46. package/dist/locales/nl.json +5 -1
  47. package/dist/models.d.ts +2 -2
  48. package/dist/models.js +2 -2
  49. package/dist/navigation.js +3 -2
  50. package/dist/panel-size.d.ts +6 -0
  51. package/dist/panel-size.js +25 -0
  52. package/dist/peek.d.ts +3 -1
  53. package/dist/peek.js +8 -3
  54. package/dist/pick.js +2 -1
  55. package/dist/popup-focus.d.ts +5 -0
  56. package/dist/popup-focus.js +12 -0
  57. package/dist/popups.d.ts +1 -1
  58. package/dist/project-language.d.ts +4 -0
  59. package/dist/project-language.js +23 -4
  60. package/dist/project-problems.d.ts +10 -1
  61. package/dist/project-problems.js +34 -10
  62. package/dist/proposal-model.d.ts +0 -1
  63. package/dist/proposal-model.js +0 -8
  64. package/dist/rename-model.d.ts +2 -1
  65. package/dist/rename-model.js +57 -0
  66. package/dist/rename.d.ts +1 -0
  67. package/dist/rename.js +21 -5
  68. package/dist/rendered-docs.d.ts +2 -0
  69. package/dist/rendered-docs.js +15 -8
  70. package/dist/signature.js +4 -3
  71. package/dist/snippet-session.d.ts +1 -1
  72. package/dist/snippet-session.js +8 -2
  73. package/dist/symbol-picker.js +3 -2
  74. package/dist/timers.d.ts +1 -0
  75. package/dist/timers.js +4 -0
  76. package/dist/use-panel-body.d.ts +6 -0
  77. package/dist/use-panel-body.js +13 -0
  78. package/package.json +6 -6
  79. package/src/AnchoredPopup.tsx +4 -2
  80. package/src/CallHierarchyPanel.tsx +9 -3
  81. package/src/CodeAuthorsCard.tsx +21 -12
  82. package/src/CompletionPopup.tsx +11 -1
  83. package/src/EditorView.tsx +122 -11
  84. package/src/HoverCard.tsx +6 -1
  85. package/src/LanguagePopups.tsx +23 -4
  86. package/src/PanelResizeHandle.tsx +104 -0
  87. package/src/PeekPanel.tsx +9 -3
  88. package/src/PickPopup.tsx +9 -1
  89. package/src/RenameCard.tsx +15 -9
  90. package/src/RenderedDocs.tsx +30 -8
  91. package/src/call-hierarchy-model.ts +15 -22
  92. package/src/call-hierarchy.ts +17 -17
  93. package/src/code-actions.ts +5 -2
  94. package/src/completion-model.ts +3 -1
  95. package/src/completion.ts +48 -28
  96. package/src/diagnostics-model.ts +1 -1
  97. package/src/diagnostics.ts +29 -13
  98. package/src/diff-overlays.tsx +12 -8
  99. package/src/editor-language.ts +13 -2
  100. package/src/format-on-type.ts +2 -3
  101. package/src/highlights.ts +3 -2
  102. package/src/history.ts +3 -2
  103. package/src/holder.ts +5 -4
  104. package/src/host-types.ts +9 -0
  105. package/src/hover.ts +19 -5
  106. package/src/index.ts +6 -2
  107. package/src/key-priority.ts +49 -0
  108. package/src/locales/en.json +5 -1
  109. package/src/locales/nl.json +5 -1
  110. package/src/models.ts +1 -1
  111. package/src/navigation.ts +3 -2
  112. package/src/panel-size.ts +28 -0
  113. package/src/peek.ts +8 -3
  114. package/src/pick.ts +2 -1
  115. package/src/popup-focus.ts +17 -0
  116. package/src/popups.ts +1 -2
  117. package/src/project-language.ts +25 -4
  118. package/src/project-problems.ts +46 -11
  119. package/src/proposal-model.ts +0 -9
  120. package/src/rename-model.ts +60 -1
  121. package/src/rename.ts +21 -5
  122. package/src/rendered-docs.ts +17 -8
  123. package/src/signature.ts +4 -3
  124. package/src/snippet-session.ts +9 -3
  125. package/src/symbol-picker.ts +3 -2
  126. package/src/timers.ts +5 -0
  127. package/src/use-panel-body.ts +17 -0
  128. package/dist/relative-time.d.ts +0 -1
  129. package/dist/relative-time.js +0 -9
  130. package/src/relative-time.ts +0 -10
@@ -1,6 +1,8 @@
1
1
  import type { Diagnostic } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.ts';
2
3
  import type { EditorContentChange, EditorPosition, EditorReveal } from '@adecore/editor';
3
4
  import type { EditorLanguage } from './editor-language.ts';
5
+ import { sameRange } from './hover-content.ts';
4
6
  import { isShortcut } from './shortcut-keys.ts';
5
7
  import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, severityOf, shiftRange, type Problem } from './diagnostics-model.ts';
6
8
 
@@ -27,19 +29,19 @@ export class DiagnosticsFeature {
27
29
  const reports = project.service.onDiagnostics((report) => {
28
30
  if (report.uri === uri) {
29
31
  this.reports.set(report.source, report.diagnostics);
30
- this.rebuild(true);
32
+ this.rebuild();
31
33
  }
32
34
  });
33
35
  const edits = editor.onTextChange((change) => this.shift(change.changes));
34
36
  // These keys are the editor's while it has the keyboard; elsewhere the menu's commands answer.
35
- const keys = editor.onKeyDown((event) => {
37
+ const keys = language.onKey((event) => {
36
38
  const next = isShortcut(this.language.shortcuts.nextProblem, event);
37
39
  if (!next && !isShortcut(this.language.shortcuts.previousProblem, event)) {
38
40
  return false;
39
41
  }
40
42
  this.step(next ? 1 : -1);
41
43
  return true;
42
- });
44
+ }, KEY_PRIORITY.diagnostics);
43
45
  language.onDispose(() => {
44
46
  keys();
45
47
  reports.dispose();
@@ -99,23 +101,37 @@ export class DiagnosticsFeature {
99
101
  editor.announce(project.i18n.t('editor:language.announce.problem', { severity, message: problem.diagnostic.message }));
100
102
  }
101
103
 
104
+ /* The problems follow the edit as the markers do. Their number does not change, so nobody is told. */
102
105
  private shift(changes: readonly EditorContentChange[]): void {
106
+ let moved = false;
103
107
  for (const [server, diagnostics] of this.reports) {
104
- this.reports.set(
105
- server,
106
- diagnostics.map((diagnostic) => ({ ...diagnostic, range: shiftRange(diagnostic.range, changes) }))
107
- );
108
+ let shifted: Diagnostic[] | null = null;
109
+ for (const [index, diagnostic] of diagnostics.entries()) {
110
+ const range = shiftRange(diagnostic.range, changes);
111
+ if (!sameRange(range, diagnostic.range)) {
112
+ shifted ??= [...diagnostics];
113
+ shifted[index] = { ...diagnostic, range };
114
+ }
115
+ }
116
+ if (shifted !== null) {
117
+ this.reports.set(server, shifted);
118
+ moved = true;
119
+ }
120
+ }
121
+ if (moved) {
122
+ this.collect();
108
123
  }
109
- this.rebuild(false);
110
124
  }
111
125
 
112
- private rebuild(draw: boolean): void {
113
- this.list = [...this.reports].flatMap(([server, diagnostics]) => diagnostics.map((diagnostic): Problem => ({ diagnostic, server })));
114
- if (draw) {
115
- this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
116
- }
126
+ private rebuild(): void {
127
+ this.collect();
128
+ this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
117
129
  for (const listener of [...this.listeners]) {
118
130
  listener();
119
131
  }
120
132
  }
133
+
134
+ private collect(): void {
135
+ this.list = [...this.reports].flatMap(([server, diagnostics]) => diagnostics.map((diagnostic): Problem => ({ diagnostic, server })));
136
+ }
121
137
  }
@@ -1,7 +1,7 @@
1
1
  import { useSyncExternalStore } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { Button } from '@adecore/ui';
4
+ import { Button, Tooltip } from '@adecore/ui';
5
5
  import { formatNumber } from '@adecore/ui/format';
6
6
  import type { Editor, EditorEngine } from '@adecore/editor';
7
7
  import type { DiffDisplay } from '@adecore/merge';
@@ -38,13 +38,17 @@ export function DiffOverlays({
38
38
  const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
39
39
  return createPortal(
40
40
  <div className="adecore-editor-diff-gap">
41
- {row.unfoldable ? (
42
- <Button
43
- size="xs"
44
- disabled={blocked?.has(row.id)}
45
- title={blocked?.has(row.id) ? t('diff.contextLimit') : undefined}
46
- onClick={() => unfold(row.id)}
47
- >
41
+ {row.unfoldable && blocked?.has(row.id) === true ? (
42
+ <Tooltip label={t('diff.contextLimit')}>
43
+ {/* A disabled button gets no pointer events, so the hint hangs on a wrapper. */}
44
+ <span className="inline-flex">
45
+ <Button size="xs" disabled>
46
+ {text}
47
+ </Button>
48
+ </span>
49
+ </Tooltip>
50
+ ) : row.unfoldable ? (
51
+ <Button size="xs" onClick={() => unfold(row.id)}>
48
52
  {text}
49
53
  </Button>
50
54
  ) : (
@@ -1,5 +1,5 @@
1
1
  import { KEYMAP, type Keymap } from '@adecore/editor/keymap';
2
- import type { Editor, EditorPosition } from '@adecore/editor';
2
+ import type { Editor, EditorKeyHandler, EditorPosition } from '@adecore/editor';
3
3
  import type { Location } from '@adecore/lsp';
4
4
  import { CallHierarchyFeature } from './call-hierarchy.ts';
5
5
  import { CodeActionsFeature } from './code-actions.ts';
@@ -13,6 +13,7 @@ import { FormatOnTypeFeature } from './format-on-type.ts';
13
13
  import { HighlightsFeature } from './highlights.ts';
14
14
  import { HistoryFeature } from './history.ts';
15
15
  import { HoverFeature } from './hover.ts';
16
+ import { KeyDispatcher } from './key-priority.ts';
16
17
  import { NavigationFeature, locationRow } from './navigation.ts';
17
18
  import { PeekFeature } from './peek.ts';
18
19
  import { PickFeature } from './pick.ts';
@@ -73,6 +74,7 @@ export class EditorLanguage {
73
74
  readonly formatOnType: FormatOnTypeFeature;
74
75
  readonly renderedDocs: RenderedDocsFeature;
75
76
  private readonly disposers: Array<() => void> = [];
77
+ private readonly keys = new KeyDispatcher();
76
78
  private disposed = false;
77
79
 
78
80
  constructor(project: ProjectLanguage, editor: Editor, uri: string, languageId: string, timers: Timers = realTimers, options: EditorLanguageOptions = {}) {
@@ -83,6 +85,7 @@ export class EditorLanguage {
83
85
  this.project = project;
84
86
  this.languageId = languageId;
85
87
  this.document = project.acquire(uri, languageId, editor);
88
+ this.onDispose(editor.onKeyDown((event) => this.keys.dispatch(event)));
86
89
  this.pick = new PickFeature(this, timers);
87
90
  this.diagnostics = new DiagnosticsFeature(this);
88
91
  this.hover = new HoverFeature(this, timers);
@@ -97,7 +100,7 @@ export class EditorLanguage {
97
100
  this.codeActions = new CodeActionsFeature(this, timers);
98
101
  this.rename = new RenameFeature(this);
99
102
  this.navigation = new NavigationFeature(this);
100
- this.peek = new PeekFeature(this);
103
+ this.peek = new PeekFeature(this, timers);
101
104
  this.callHierarchy = new CallHierarchyFeature(this, timers);
102
105
  this.symbolPicker = new SymbolPickerFeature(this);
103
106
  this.contextMenu = new ContextMenuFeature(this);
@@ -177,6 +180,14 @@ export class EditorLanguage {
177
180
  };
178
181
  }
179
182
 
183
+ /*
184
+ * Lets `handler` see the editor's keys before every handler of a lower `priority`, the first to take a key
185
+ * winning. `KEY_PRIORITY` holds the features'; the default, 0, comes after all of them.
186
+ */
187
+ onKey(handler: EditorKeyHandler, priority = 0): () => void {
188
+ return this.keys.add(handler, priority);
189
+ }
190
+
180
191
  /* Runs `dispose` when the editor lets go of its document. */
181
192
  onDispose(dispose: () => void): void {
182
193
  this.disposers.push(dispose);
@@ -26,12 +26,11 @@ function overlaps(edits: readonly TextEdit[]): boolean {
26
26
  * second caret, a read-only editor or a snippet session, whose tab stops an edit could move. An answer is applied
27
27
  * only while the text is exactly what it was asked about, and the caret where it was.
28
28
  *
29
- * The edits join the undo step of the typed character, so one undo takes both back. On by default where the server offers it; `configure(false)`
30
- * turns it off for a host that has a setting for it.
29
+ * 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)`.
31
30
  */
32
31
  export class FormatOnTypeFeature {
33
32
  private readonly language: EditorLanguage;
34
- private enabled = true;
33
+ private enabled = false;
35
34
  /* Raised by every change of the text, so an answer for an older text is told from the one for the text that is. */
36
35
  private revision = 0;
37
36
  private controller = new AbortController();
package/src/highlights.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { DocumentHighlight } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.ts';
2
3
  import type { EditorHighlight, EditorHighlightKind, EditorPosition, EditorRange } from '@adecore/editor';
3
4
  import { comparePositions, rangeHolds } from './diagnostics-model.ts';
4
5
  import { isIdentifierCharacter } from './completion-model.ts';
@@ -70,13 +71,13 @@ export class HighlightsFeature {
70
71
  this.current = [];
71
72
  this.refresher.later();
72
73
  }),
73
- editor.onKeyDown((event) => {
74
+ language.onKey((event) => {
74
75
  const next = isShortcut(this.language.shortcuts.nextHighlight, event);
75
76
  if (!next && !isShortcut(this.language.shortcuts.previousHighlight, event)) {
76
77
  return false;
77
78
  }
78
79
  return this.step(next ? 1 : -1);
79
- })
80
+ }, KEY_PRIORITY.highlights)
80
81
  ];
81
82
  language.onDispose(() => {
82
83
  for (const off of offs) {
package/src/history.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { EditorLanguage } from './editor-language.ts';
2
+ import { KEY_PRIORITY } from './key-priority.ts';
2
3
  import type { Place } from './navigation-history.ts';
3
4
  import { isShortcut } from './shortcut-keys.ts';
4
5
 
@@ -8,7 +9,7 @@ export class HistoryFeature {
8
9
 
9
10
  constructor(language: EditorLanguage) {
10
11
  this.language = language;
11
- const off = language.editor.onKeyDown((event) => {
12
+ const off = language.onKey((event) => {
12
13
  if (isShortcut(this.language.shortcuts.historyBack, event)) {
13
14
  this.back();
14
15
  return true;
@@ -22,7 +23,7 @@ export class HistoryFeature {
22
23
  return true;
23
24
  }
24
25
  return false;
25
- });
26
+ }, KEY_PRIORITY.history);
26
27
  language.onDispose(off);
27
28
  }
28
29
 
package/src/holder.ts CHANGED
@@ -1,15 +1,16 @@
1
- export function createHolder<T>() {
1
+ /* One value that an effect sets and a render reads through `useSyncExternalStore`, without a state update inside the effect. */
2
+ export function createHolder<T>(): { get(): T | null; set(value: T | null): void; subscribe(listener: () => void): () => void } {
2
3
  let value: T | null = null;
3
4
  const listeners = new Set<() => void>();
4
5
  return {
5
6
  get: () => value,
6
- set: (next: T | null) => {
7
+ set: (next) => {
7
8
  value = next;
8
- for (const listener of listeners) {
9
+ for (const listener of [...listeners]) {
9
10
  listener();
10
11
  }
11
12
  },
12
- subscribe: (listener: () => void) => {
13
+ subscribe: (listener) => {
13
14
  listeners.add(listener);
14
15
  return () => {
15
16
  listeners.delete(listener);
package/src/host-types.ts CHANGED
@@ -4,6 +4,7 @@ import type { i18n } from 'i18next';
4
4
  import type { Keymap } from '@adecore/editor/keymap';
5
5
  import type { ProjectFiles } from './workspace-edit.ts';
6
6
  import type { Place } from './navigation-history.ts';
7
+ import type { ProblemSource } from './project-problems.ts';
7
8
 
8
9
  export interface DiskText {
9
10
  text: string;
@@ -35,6 +36,8 @@ export interface RenameSuggestionsRequest {
35
36
  uri: string;
36
37
  languageId: string;
37
38
  name: string;
39
+ /* What the name belongs to, in words (`function`, `class`, `type parameter`), when the outline of the file lists it. */
40
+ kind?: string;
38
41
  context: string;
39
42
  uses: readonly string[];
40
43
  }
@@ -61,6 +64,12 @@ export interface LanguageHost {
61
64
  readonly files?: ProjectFiles;
62
65
  readonly apple?: boolean;
63
66
  readonly keymap?: Keymap;
67
+ /* The height in pixels of the list-and-preview area of the peek and the call hierarchy, so a person's drag outlives the panel; 180 without one. */
68
+ readonly panelBodyHeight?: number;
69
+ /* A person finished resizing a peek or call hierarchy panel; the height is already within what the editor allows. */
70
+ onPanelResize?(height: number): void;
71
+ /* Where `project.problems` reads its reports, such as a machine that also reports files no editor here holds; the service's diagnostics without one. */
72
+ readonly problemSource?: ProblemSource;
64
73
  openPlace?(place: Place): void;
65
74
  pathOfUri?(uri: string): string | null;
66
75
  serverNames?(uri: string): readonly string[];
package/src/hover.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { EditorHover, EditorPosition, EditorRange } from '@adecore/editor';
2
+ import { KEY_PRIORITY } from './key-priority.ts';
2
3
  import type { EditorLanguage } from './editor-language.ts';
3
4
  import { rangeHolds, type Problem } from './diagnostics-model.ts';
4
5
  import { hoverTextOf, isEmptyHover, locationsOf, type HoverText } from './hover-content.ts';
@@ -14,6 +15,11 @@ const HIDE_DELAY_MS = 250;
14
15
  const COUNT_DELAY_MS = 500;
15
16
  const TOAST_ID = 'language-hover';
16
17
 
18
+ export interface HoverSettings {
19
+ /* Counts the references of the symbol on a card that stays up, which can be a search over the whole project; on unless false. */
20
+ readonly referenceCount?: boolean;
21
+ }
22
+
17
23
  /*
18
24
  * The card under a character the pointer rests on: the problems there, and what the language servers
19
25
  * say about the symbol. It opens after the pointer has stayed a moment, stays while the pointer is on
@@ -28,6 +34,7 @@ export class HoverFeature {
28
34
  private hideTimer: unknown;
29
35
  private countTimer: unknown;
30
36
  private inCard = false;
37
+ private referenceCount = true;
31
38
  /* Counts every request, so an answer that arrives after the pointer moved on is dropped. */
32
39
  private request = 0;
33
40
  private lookup: AbortController | null = null;
@@ -62,17 +69,17 @@ export class HoverFeature {
62
69
  this.hide();
63
70
  }
64
71
  }),
65
- editor.onKeyDown((event) => {
72
+ language.onKey((event) => {
66
73
  if (isShortcut(this.language.shortcuts.quickInfo, event)) {
67
74
  this.quickInfo();
68
75
  return true;
69
76
  }
70
- if (event.key === 'Escape' && this.language.popups.getState().hover !== null && this.language.popups.getState().callHierarchy == null) {
77
+ if (event.key === 'Escape' && this.language.popups.getState().hover !== null && this.language.popups.getState().callHierarchy === null) {
71
78
  this.hide();
72
79
  return true;
73
80
  }
74
81
  return false;
75
- })
82
+ }, KEY_PRIORITY.hover)
76
83
  ];
77
84
  language.onDispose(() => {
78
85
  for (const off of offs) {
@@ -82,6 +89,13 @@ export class HoverFeature {
82
89
  });
83
90
  }
84
91
 
92
+ configure(settings: HoverSettings): void {
93
+ this.referenceCount = settings.referenceCount !== false;
94
+ if (!this.referenceCount) {
95
+ this.timers.clear(this.countTimer);
96
+ }
97
+ }
98
+
85
99
  /* The pointer entered or left the card, which is where a link in it is clicked. */
86
100
  holdCard(inside: boolean): void {
87
101
  this.inCard = inside;
@@ -236,7 +250,7 @@ export class HoverFeature {
236
250
  }
237
251
  this.pinned = pinned;
238
252
  this.present(position, problems, info, problems[0]?.diagnostic.range ?? range);
239
- if (info !== null) {
253
+ if (info !== null && this.referenceCount) {
240
254
  this.scheduleCount(position, request, controller.signal);
241
255
  }
242
256
  }
@@ -270,7 +284,7 @@ export class HoverFeature {
270
284
  /* How many places use the symbol, which can take a while in a large project and so arrives after the card does. */
271
285
  private async count(position: EditorPosition, key: string, request: number, signal: AbortSignal): Promise<void> {
272
286
  const { service } = this.language.project;
273
- if (request !== this.request || !service.supports('textDocument/references', this.language.uri)) {
287
+ if (request !== this.request || !this.referenceCount || !service.supports('textDocument/references', this.language.uri)) {
274
288
  return;
275
289
  }
276
290
  const found = await service.references(this.language.uri, position, false, { signal }).catch(() => null);
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { EditorLanguage, type EditorLanguageOptions } from './editor-language.ts';
2
+ export { KEY_PRIORITY } from './key-priority.ts';
3
+ export { realTimers, type Timers } from './timers.ts';
2
4
  export { DEFAULT_RANGE_LINES, DEFAULT_RANGE_CHARACTERS, type SemanticTokensOptions } from './semantic-tokens.ts';
3
5
  export { ProjectLanguage, type FileOperationTarget } from './project-language.ts';
4
6
  export type { LanguageDocumentHandle } from './project-language.ts';
@@ -15,6 +17,7 @@ export { EditorView } from './EditorView.tsx';
15
17
  export { createHolder } from './holder.ts';
16
18
  export type { EditorViewProps } from './EditorView.tsx';
17
19
  export { LanguagePopups } from './LanguagePopups.tsx';
20
+ export type { LanguagePopupsProps } from './LanguagePopups.tsx';
18
21
  export { AnchoredPopup } from './AnchoredPopup.tsx';
19
22
  export { HoverCard } from './HoverCard.tsx';
20
23
  export { CompletionPopup } from './CompletionPopup.tsx';
@@ -36,13 +39,14 @@ export type { EditorRendering } from './rendering-context.ts';
36
39
  export { FindReplace } from './FindReplace.tsx';
37
40
  export { ChangeReview } from './ChangeReview.tsx';
38
41
  export type { ChangeReviewProps } from './ChangeReview.tsx';
39
- export { ProjectProblems, countsOf, compareRows } from './project-problems.ts';
40
- export type { ProblemFile, ProblemRow } from './project-problems.ts';
42
+ export { ProjectProblems, serviceProblems, countsOf, compareRows } from './project-problems.ts';
43
+ export type { ProblemFile, ProblemRow, ProblemReport, ProblemSource } from './project-problems.ts';
41
44
  export { applyWorkspaceEdit, supportedResourceOperations } from './workspace-edit.ts';
42
45
  export type { ProjectFiles, StagedFile, WorkspaceEditHost } from './workspace-edit.ts';
43
46
  export { useProblemCounts } from './use-problem-counts.ts';
44
47
  export type { ProblemCounts } from './diagnostics.ts';
45
48
  export type { BlameSource, CodeVisionSettings } from './code-vision.ts';
49
+ export type { HoverSettings } from './hover.ts';
46
50
  export type { Problem } from './diagnostics-model.ts';
47
51
  export type {
48
52
  PopupState,
@@ -0,0 +1,49 @@
1
+ import type { EditorKeyHandler } from '@adecore/editor';
2
+
3
+ /*
4
+ * Where each feature of an editor sees a key through `EditorLanguage.onKey`, the highest first. A hundred
5
+ * apart, so a handler of the app can stand between two features; 0, the default, comes after all of them.
6
+ */
7
+ export const KEY_PRIORITY = {
8
+ pick: 1500,
9
+ diagnostics: 1400,
10
+ hover: 1300,
11
+ completion: 1200,
12
+ snippets: 1100,
13
+ signature: 1000,
14
+ highlights: 900,
15
+ codeActions: 800,
16
+ rename: 700,
17
+ navigation: 600,
18
+ peek: 500,
19
+ callHierarchy: 400,
20
+ symbolPicker: 300,
21
+ history: 200,
22
+ renderedDocs: 100
23
+ } as const;
24
+
25
+ interface Entry {
26
+ readonly handler: EditorKeyHandler;
27
+ readonly priority: number;
28
+ }
29
+
30
+ /* Hands a key to its handlers by priority until one takes it; of two of the same priority the one added first. */
31
+ export class KeyDispatcher {
32
+ private readonly entries: Entry[] = [];
33
+
34
+ add(handler: EditorKeyHandler, priority: number): () => void {
35
+ const entry = { handler, priority };
36
+ const index = this.entries.findIndex((other) => other.priority < priority);
37
+ this.entries.splice(index === -1 ? this.entries.length : index, 0, entry);
38
+ return () => {
39
+ const at = this.entries.indexOf(entry);
40
+ if (at !== -1) {
41
+ this.entries.splice(at, 1);
42
+ }
43
+ };
44
+ }
45
+
46
+ dispatch(event: KeyboardEvent): boolean {
47
+ return [...this.entries].some((entry) => entry.handler(event));
48
+ }
49
+ }
@@ -82,6 +82,9 @@
82
82
  "symbol": "Matches"
83
83
  }
84
84
  },
85
+ "panel": {
86
+ "resize": "Resize panel"
87
+ },
85
88
  "peek": {
86
89
  "unavailable": "No language server finds references in this file",
87
90
  "none": "No references found",
@@ -175,7 +178,8 @@
175
178
  "details": "Details",
176
179
  "none": "No suggestions",
177
180
  "transformBusy": "Snippet transforms are busy; try again",
178
- "transformFailed": "Snippet transform could not be applied"
181
+ "transformFailed": "Snippet transform could not be applied",
182
+ "failed": "Could not insert the suggestion: {{message}}"
179
183
  },
180
184
  "history": {
181
185
  "recent": "Recent locations"
@@ -82,6 +82,9 @@
82
82
  "symbol": "Overeenkomsten"
83
83
  }
84
84
  },
85
+ "panel": {
86
+ "resize": "Paneel vergroten of verkleinen"
87
+ },
85
88
  "peek": {
86
89
  "unavailable": "Geen language server vindt referenties in dit bestand",
87
90
  "none": "Geen referenties gevonden",
@@ -175,7 +178,8 @@
175
178
  "details": "Details",
176
179
  "none": "Geen suggesties",
177
180
  "transformBusy": "Snippettransformaties zijn bezig; probeer het opnieuw",
178
- "transformFailed": "Snippettransformatie kon niet worden toegepast"
181
+ "transformFailed": "Snippettransformatie kon niet worden toegepast",
182
+ "failed": "De suggestie kon niet worden ingevoegd: {{message}}"
179
183
  },
180
184
  "history": {
181
185
  "recent": "Recente plekken"
package/src/models.ts CHANGED
@@ -37,6 +37,7 @@ export {
37
37
  markerOf,
38
38
  comparePositions,
39
39
  shiftPosition,
40
+ endOfInsertion,
40
41
  shiftRange,
41
42
  rangeHolds,
42
43
  problemsAt,
@@ -159,7 +160,6 @@ export {
159
160
  type InlineAnswer,
160
161
  parseAnswer,
161
162
  fitReplacement,
162
- endOfInsertion,
163
163
  type DiffSegment,
164
164
  diffSegments,
165
165
  emphasisOf,
package/src/navigation.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { StaleResultError, fileUriToPath, type LanguageService, type Location, type LocationLink, type NavigationResult, type Range } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.ts';
2
3
  import type { EditorPosition } from '@adecore/editor';
3
4
  import { basenameOf } from './paths.ts';
4
5
  import type { EditorLanguage } from './editor-language.ts';
@@ -76,14 +77,14 @@ export class NavigationFeature {
76
77
  void this.go('definition', click.position);
77
78
  return true;
78
79
  }),
79
- editor.onKeyDown((event) => {
80
+ language.onKey((event) => {
80
81
  const found = SHORTCUT_KINDS.find(([, name]) => isShortcut(this.language.shortcuts[name], event));
81
82
  if (found === undefined) {
82
83
  return false;
83
84
  }
84
85
  void this.go(found[0]);
85
86
  return true;
86
- })
87
+ }, KEY_PRIORITY.navigation)
87
88
  ];
88
89
  language.onDispose(() => {
89
90
  for (const off of offs) {
@@ -0,0 +1,28 @@
1
+ /* The height of the list-and-preview area of a peek or a call hierarchy, which is what a person drags; the bands and the title bar around it stay. */
2
+ export const PANEL_BODY_HEIGHT = 180;
3
+ /* Four rows of 22px, so a list never shrinks to nothing. */
4
+ export const PANEL_BODY_MIN = 88;
5
+ /* What the bands and the title bar take of the panel, which the share of the editor has to leave room for. */
6
+ const PANEL_CHROME = 63;
7
+ /* The share of the editor's height a panel may take, so the line it belongs to stays in view. */
8
+ const PANEL_MAX_SHARE = 0.8;
9
+ const PANEL_FALLBACK_AVAILABLE = 800;
10
+ export const PANEL_KEY_STEP = 22;
11
+
12
+ /* The tallest body for an editor that is `available` pixels high; never below the minimum, so a tiny editor still gets a usable panel. */
13
+ export function panelBodyMax(available: number): number {
14
+ return Math.max(PANEL_BODY_MIN, Math.floor(available * PANEL_MAX_SHARE) - PANEL_CHROME);
15
+ }
16
+
17
+ export function clampPanelBody(height: number, available: number = PANEL_FALLBACK_AVAILABLE): number {
18
+ if (!Number.isFinite(height)) {
19
+ return PANEL_BODY_HEIGHT;
20
+ }
21
+ return Math.min(panelBodyMax(available), Math.max(PANEL_BODY_MIN, Math.round(height)));
22
+ }
23
+
24
+ /* The height of the editor's scrolling area around a panel's row; the fallback stands in where nothing is laid out. */
25
+ export function availableHeightOf(container: HTMLElement | null): number {
26
+ const height = container?.closest<HTMLElement>('.se-viewport')?.clientHeight ?? 0;
27
+ return height > 0 ? height : PANEL_FALLBACK_AVAILABLE;
28
+ }
package/src/peek.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { StaleResultError, type Location, type Range } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.ts';
2
3
  import type { EditorPosition } from '@adecore/editor';
3
4
  import type { EditorLanguage } from './editor-language.ts';
4
5
  import { uniquePlaces } from './navigation.ts';
@@ -6,6 +7,7 @@ import { PEEK_READ_FILES, definitionSnippetOf, peekFilesOf, snippetOf, type Peek
6
7
  import type { PeekView } from './popups.ts';
7
8
  import { isShortcut } from './shortcut-keys.ts';
8
9
  import { messageOf } from './error-message.ts';
10
+ import { realTimers, revealRow, type Timers } from './timers.ts';
9
11
 
10
12
  const TOAST_ID = 'language-peek';
11
13
  /* 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. */
@@ -29,6 +31,7 @@ function placeKey(location: Location): string {
29
31
  */
30
32
  export class PeekFeature {
31
33
  private readonly language: EditorLanguage;
34
+ private readonly timers: Timers;
32
35
  private kind: PeekKind = 'references';
33
36
  /* The whole declaration of each place of a definition peek, where the server gave it. */
34
37
  private targets = new Map<string, Range>();
@@ -38,11 +41,12 @@ export class PeekFeature {
38
41
  private line = 0;
39
42
  private token = 0;
40
43
 
41
- constructor(language: EditorLanguage) {
44
+ constructor(language: EditorLanguage, timers: Timers = realTimers) {
42
45
  this.language = language;
46
+ this.timers = timers;
43
47
  const { editor } = language;
44
48
  const offs = [
45
- editor.onKeyDown((event) => {
49
+ language.onKey((event) => {
46
50
  if (isShortcut(this.language.shortcuts.peekReferences, event)) {
47
51
  void this.open();
48
52
  return true;
@@ -52,7 +56,7 @@ export class PeekFeature {
52
56
  return true;
53
57
  }
54
58
  return this.key(event);
55
- }),
59
+ }, KEY_PRIORITY.peek),
56
60
  editor.onTextChange(() => this.close())
57
61
  ];
58
62
  language.onDispose(() => {
@@ -201,6 +205,7 @@ export class PeekFeature {
201
205
  const view = this.language.popups.getState().peek;
202
206
  if (view !== null && view.container !== container) {
203
207
  this.language.popups.setState({ peek: { ...view, container } });
208
+ revealRow(this.timers, container);
204
209
  }
205
210
  }
206
211
 
package/src/pick.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { EditorPosition } from '@adecore/editor';
2
+ import { KEY_PRIORITY } from './key-priority.ts';
2
3
  import type { EditorLanguage } from './editor-language.ts';
3
4
  import type { PickGroup, PickPreview } from './popups.ts';
4
5
  import { realTimers, type Timers } from './timers.ts';
@@ -45,7 +46,7 @@ export class PickFeature {
45
46
  this.timers = timers;
46
47
  const { editor } = language;
47
48
  const offs = [
48
- editor.onKeyDown((event) => this.key(event)),
49
+ language.onKey((event) => this.key(event), KEY_PRIORITY.pick),
49
50
  editor.onTextChange(() => this.close()),
50
51
  editor.onCaret(() => this.close()),
51
52
  editor.onBlur(() => this.close())
@@ -0,0 +1,17 @@
1
+ import type { FocusEvent, MouseEvent } from 'react';
2
+
3
+ /*
4
+ * The handlers for a popup list whose rows the keyboard never enters: the text it belongs to keeps the
5
+ * focus, and the active row is only `aria-activedescendant` or a highlight. Chromium reports that row as
6
+ * the focused element, so a tool that focuses it before it types would otherwise pull the keys out of the
7
+ * text. A press on the list moves no focus, and a row that gets it anyway hands it straight back.
8
+ */
9
+ export function keepFocusOutOfList(focusBack: () => void): {
10
+ onMouseDown(event: MouseEvent): void;
11
+ onFocus(event: FocusEvent): void;
12
+ } {
13
+ return {
14
+ onMouseDown: (event) => event.preventDefault(),
15
+ onFocus: () => focusBack()
16
+ };
17
+ }
package/src/popups.ts CHANGED
@@ -203,8 +203,7 @@ export interface PopupState {
203
203
  readonly pick: PickView | null;
204
204
  readonly rename: RenameView | null;
205
205
  readonly peek: PeekView | null;
206
- /* Optional so a host that builds a `PopupState` literal from before the call hierarchy still fits; absent reads as closed. */
207
- readonly callHierarchy?: CallHierarchyView | null;
206
+ readonly callHierarchy: CallHierarchyView | null;
208
207
  readonly symbols: SymbolsView | null;
209
208
  readonly menu: MenuView | null;
210
209
  readonly authors: AuthorsView | null;