@adecore/editor-react 0.23.1 → 0.24.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 (109) hide show
  1. package/dist/AnchoredPopup.d.ts +2 -1
  2. package/dist/AnchoredPopup.js +2 -2
  3. package/dist/CodeAuthorsCard.d.ts +4 -0
  4. package/dist/CodeAuthorsCard.js +8 -4
  5. package/dist/CompletionPopup.js +7 -3
  6. package/dist/EditorView.d.ts +8 -3
  7. package/dist/EditorView.js +79 -9
  8. package/dist/HoverCard.d.ts +3 -1
  9. package/dist/HoverCard.js +2 -2
  10. package/dist/LanguagePopups.d.ts +8 -4
  11. package/dist/LanguagePopups.js +4 -4
  12. package/dist/PickPopup.js +2 -1
  13. package/dist/RenameCard.js +10 -8
  14. package/dist/RenderedDocs.d.ts +10 -4
  15. package/dist/RenderedDocs.js +10 -6
  16. package/dist/call-hierarchy.js +15 -14
  17. package/dist/code-actions.js +5 -2
  18. package/dist/completion-model.js +3 -1
  19. package/dist/completion.d.ts +4 -2
  20. package/dist/completion.js +49 -27
  21. package/dist/diagnostics-model.d.ts +1 -0
  22. package/dist/diagnostics-model.js +1 -1
  23. package/dist/diagnostics.d.ts +1 -0
  24. package/dist/diagnostics.js +28 -10
  25. package/dist/diff-overlays.js +2 -2
  26. package/dist/editor-language.d.ts +3 -1
  27. package/dist/editor-language.js +10 -0
  28. package/dist/format-on-type.js +2 -3
  29. package/dist/highlights.js +3 -2
  30. package/dist/history.js +3 -2
  31. package/dist/holder.d.ts +3 -3
  32. package/dist/holder.js +2 -1
  33. package/dist/host-types.d.ts +3 -0
  34. package/dist/hover.d.ts +5 -0
  35. package/dist/hover.js +13 -5
  36. package/dist/index.d.ts +6 -2
  37. package/dist/index.js +3 -1
  38. package/dist/key-priority.d.ts +23 -0
  39. package/dist/key-priority.js +39 -0
  40. package/dist/locales/en.json +2 -1
  41. package/dist/locales/nl.json +2 -1
  42. package/dist/models.d.ts +2 -2
  43. package/dist/models.js +2 -2
  44. package/dist/navigation.js +3 -2
  45. package/dist/peek.js +3 -2
  46. package/dist/pick.js +2 -1
  47. package/dist/popup-focus.d.ts +5 -0
  48. package/dist/popup-focus.js +12 -0
  49. package/dist/popups.d.ts +1 -1
  50. package/dist/project-language.js +5 -4
  51. package/dist/project-problems.d.ts +10 -1
  52. package/dist/project-problems.js +34 -10
  53. package/dist/proposal-model.d.ts +0 -1
  54. package/dist/proposal-model.js +0 -8
  55. package/dist/rename-model.d.ts +2 -1
  56. package/dist/rename-model.js +57 -0
  57. package/dist/rename.d.ts +1 -0
  58. package/dist/rename.js +21 -5
  59. package/dist/rendered-docs.js +3 -2
  60. package/dist/signature.js +4 -3
  61. package/dist/snippet-session.d.ts +1 -1
  62. package/dist/snippet-session.js +8 -2
  63. package/dist/symbol-picker.js +3 -2
  64. package/package.json +6 -6
  65. package/src/AnchoredPopup.tsx +4 -2
  66. package/src/CodeAuthorsCard.tsx +21 -12
  67. package/src/CompletionPopup.tsx +11 -1
  68. package/src/EditorView.tsx +122 -11
  69. package/src/HoverCard.tsx +6 -1
  70. package/src/LanguagePopups.tsx +23 -4
  71. package/src/PickPopup.tsx +9 -1
  72. package/src/RenameCard.tsx +15 -9
  73. package/src/RenderedDocs.tsx +30 -8
  74. package/src/call-hierarchy.ts +15 -14
  75. package/src/code-actions.ts +5 -2
  76. package/src/completion-model.ts +3 -1
  77. package/src/completion.ts +48 -28
  78. package/src/diagnostics-model.ts +1 -1
  79. package/src/diagnostics.ts +29 -13
  80. package/src/diff-overlays.tsx +12 -8
  81. package/src/editor-language.ts +12 -1
  82. package/src/format-on-type.ts +2 -3
  83. package/src/highlights.ts +3 -2
  84. package/src/history.ts +3 -2
  85. package/src/holder.ts +5 -4
  86. package/src/host-types.ts +5 -0
  87. package/src/hover.ts +19 -5
  88. package/src/index.ts +6 -2
  89. package/src/key-priority.ts +49 -0
  90. package/src/locales/en.json +2 -1
  91. package/src/locales/nl.json +2 -1
  92. package/src/models.ts +1 -1
  93. package/src/navigation.ts +3 -2
  94. package/src/peek.ts +3 -2
  95. package/src/pick.ts +2 -1
  96. package/src/popup-focus.ts +17 -0
  97. package/src/popups.ts +1 -2
  98. package/src/project-language.ts +5 -4
  99. package/src/project-problems.ts +46 -11
  100. package/src/proposal-model.ts +0 -9
  101. package/src/rename-model.ts +60 -1
  102. package/src/rename.ts +21 -5
  103. package/src/rendered-docs.ts +3 -2
  104. package/src/signature.ts +4 -3
  105. package/src/snippet-session.ts +9 -3
  106. package/src/symbol-picker.ts +3 -2
  107. package/dist/relative-time.d.ts +0 -1
  108. package/dist/relative-time.js +0 -9
  109. package/src/relative-time.ts +0 -10
@@ -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);
@@ -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
  }
@@ -65,6 +68,8 @@ export interface LanguageHost {
65
68
  readonly panelBodyHeight?: number;
66
69
  /* A person finished resizing a peek or call hierarchy panel; the height is already within what the editor allows. */
67
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;
68
73
  openPlace?(place: Place): void;
69
74
  pathOfUri?(uri: string): string | null;
70
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
+ }
@@ -178,7 +178,8 @@
178
178
  "details": "Details",
179
179
  "none": "No suggestions",
180
180
  "transformBusy": "Snippet transforms are busy; try again",
181
- "transformFailed": "Snippet transform could not be applied"
181
+ "transformFailed": "Snippet transform could not be applied",
182
+ "failed": "Could not insert the suggestion: {{message}}"
182
183
  },
183
184
  "history": {
184
185
  "recent": "Recent locations"
@@ -178,7 +178,8 @@
178
178
  "details": "Details",
179
179
  "none": "Geen suggesties",
180
180
  "transformBusy": "Snippettransformaties zijn bezig; probeer het opnieuw",
181
- "transformFailed": "Snippettransformatie kon niet worden toegepast"
181
+ "transformFailed": "Snippettransformatie kon niet worden toegepast",
182
+ "failed": "De suggestie kon niet worden ingevoegd: {{message}}"
182
183
  },
183
184
  "history": {
184
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) {
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';
@@ -45,7 +46,7 @@ export class PeekFeature {
45
46
  this.timers = timers;
46
47
  const { editor } = language;
47
48
  const offs = [
48
- editor.onKeyDown((event) => {
49
+ language.onKey((event) => {
49
50
  if (isShortcut(this.language.shortcuts.peekReferences, event)) {
50
51
  void this.open();
51
52
  return true;
@@ -55,7 +56,7 @@ export class PeekFeature {
55
56
  return true;
56
57
  }
57
58
  return this.key(event);
58
- }),
59
+ }, KEY_PRIORITY.peek),
59
60
  editor.onTextChange(() => this.close())
60
61
  ];
61
62
  language.onDispose(() => {
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;
@@ -3,7 +3,7 @@ import i18next, { type i18n } from 'i18next';
3
3
  import { fileUriToPath, pathToFileUri, type ApplyWorkspaceEditResult, type ContentChange, type LanguageService, type WorkspaceEdit } from '@adecore/lsp';
4
4
  import type { Editor, EditorPosition } from '@adecore/editor';
5
5
  import { NavigationHistory, type Place } from './navigation-history.ts';
6
- import { ProjectProblems } from './project-problems.ts';
6
+ import { ProjectProblems, serviceProblems } from './project-problems.ts';
7
7
  import { applyWorkspaceEdit, type ProjectFiles } from './workspace-edit.ts';
8
8
  import type { LanguageHost } from './host-types.ts';
9
9
  import { relativeTo } from './paths.ts';
@@ -113,7 +113,7 @@ export class ProjectLanguage {
113
113
  this.folder = host.folder ?? '';
114
114
  this.files = host.files ?? null;
115
115
  this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
116
- this.problems = new ProjectProblems(service, (uri) => this.pathOfLocation(uri) ?? uri);
116
+ this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, (uri) => this.pathOfLocation(uri) ?? uri));
117
117
  }
118
118
 
119
119
  /* Open panels of other editors of the project follow a resize. */
@@ -254,9 +254,10 @@ export class ProjectLanguage {
254
254
  void holder.sync.ready
255
255
  .catch(() => undefined)
256
256
  .then(() => {
257
- if (!this.holders.has(uri)) {
258
- return this.service.closeDocument(uri);
257
+ if (this.holders.has(uri)) {
258
+ return undefined;
259
259
  }
260
+ return this.service.closeDocument(uri);
260
261
  })
261
262
  .catch(() => undefined);
262
263
  } else if (holder.sync.editor === editor) {
@@ -24,24 +24,51 @@ export function compareRows(left: ProblemRow, right: ProblemRow): number {
24
24
  );
25
25
  }
26
26
 
27
+ /* One report of a file's problems; `path` is a stored path, as `ProblemFile.path` is. */
28
+ export interface ProblemReport {
29
+ readonly path: string;
30
+ readonly server: string;
31
+ readonly diagnostics: readonly Diagnostic[];
32
+ }
33
+
34
+ /* Hands every report to `listener` until the function it returns runs. */
35
+ export type ProblemSource = (listener: (report: ProblemReport) => void) => () => void;
36
+
37
+ /* The diagnostics a service reports, by the stored path `pathOf` gives each URI. */
38
+ export function serviceProblems(service: LanguageService, pathOf: (uri: string) => string = (uri) => uri): ProblemSource {
39
+ return (listener) => {
40
+ const subscription = service.onDiagnostics((report) => listener({ path: pathOf(report.uri), server: report.source, diagnostics: report.diagnostics }));
41
+ return () => subscription.dispose();
42
+ };
43
+ }
44
+
27
45
  /*
28
46
  * Each report replaces the previous diagnostics from that source for the file. Files with no
29
47
  * remaining problems drop out. Hints remain editor annotations rather than listed problems.
30
48
  */
31
49
  export class ProjectProblems {
32
50
  private readonly reports = new Map<string, Map<string, readonly Diagnostic[]>>();
51
+ private readonly files = new Map<string, ProblemFile>();
33
52
  private readonly listeners = new Set<() => void>();
34
53
  private readonly stop: () => void;
35
- private snapshot: readonly ProblemFile[] = [];
54
+ /* The paths of `files` in order; null once a file came or went. */
55
+ private order: string[] | null = [];
56
+ /* Made on first read after a report, so a project nobody lists does no sorting. */
57
+ private snapshot: readonly ProblemFile[] | null = [];
36
58
 
37
- constructor(service: LanguageService, pathOf: (uri: string) => string = (uri) => uri) {
38
- const subscription = service.onDiagnostics((report) => this.report(pathOf(report.uri), report.source, report.diagnostics));
39
- this.stop = () => subscription.dispose();
59
+ /* A service stands for `serviceProblems(service, pathOf)`. */
60
+ constructor(source: ProblemSource | LanguageService, pathOf?: (uri: string) => string) {
61
+ const listen = typeof source === 'function' ? source : serviceProblems(source, pathOf);
62
+ this.stop = listen((report) => this.report(report.path, report.server, report.diagnostics));
40
63
  }
41
64
 
42
65
  report(path: string, server: string, diagnostics: readonly Diagnostic[]): void {
43
66
  const kept = diagnostics.filter((diagnostic) => severityOf(diagnostic) !== 'hint');
44
67
  const servers = this.reports.get(path) ?? new Map<string, readonly Diagnostic[]>();
68
+ if (kept.length === 0 && !servers.has(server)) {
69
+ // A clean file reported clean again, as servers do after every edit.
70
+ return;
71
+ }
45
72
  if (kept.length === 0) {
46
73
  servers.delete(server);
47
74
  } else {
@@ -49,22 +76,30 @@ export class ProjectProblems {
49
76
  }
50
77
  if (servers.size === 0) {
51
78
  this.reports.delete(path);
79
+ this.files.delete(path);
80
+ this.order = null;
52
81
  } else {
82
+ if (!this.files.has(path)) {
83
+ this.order = null;
84
+ }
53
85
  this.reports.set(path, servers);
86
+ const rows = [...servers].flatMap(([name, list]) => list.map((diagnostic): ProblemRow => ({ diagnostic, server: name }))).sort(compareRows);
87
+ this.files.set(path, { path, rows });
54
88
  }
55
- this.snapshot = [...this.reports]
56
- .map(([file, byServer]): ProblemFile => ({
57
- path: file,
58
- rows: [...byServer].flatMap(([name, list]) => list.map((diagnostic): ProblemRow => ({ diagnostic, server: name }))).sort(compareRows)
59
- }))
60
- .sort((left, right) => left.path.localeCompare(right.path));
89
+ this.snapshot = null;
61
90
  for (const listener of [...this.listeners]) {
62
91
  listener();
63
92
  }
64
93
  }
65
94
 
66
95
  /* The same array until the next report, which is what a store a component reads must hand out. */
67
- getSnapshot = (): readonly ProblemFile[] => this.snapshot;
96
+ getSnapshot = (): readonly ProblemFile[] => {
97
+ if (this.snapshot === null) {
98
+ this.order ??= [...this.files.keys()].sort((left, right) => left.localeCompare(right));
99
+ this.snapshot = this.order.map((path) => this.files.get(path)!);
100
+ }
101
+ return this.snapshot;
102
+ };
68
103
 
69
104
  subscribe = (listener: () => void): (() => void) => {
70
105
  this.listeners.add(listener);
@@ -139,15 +139,6 @@ export function fitReplacement(selected: string, replacement: string): string {
139
139
  return breaks === '\n' ? fitted : fitted.replace(/\n/g, breaks);
140
140
  }
141
141
 
142
- /* Where the caret stands after `text` was typed at `start`. */
143
- export function endOfInsertion(start: EditorPosition, text: string): EditorPosition {
144
- const lines = text.split('\n');
145
- if (lines.length === 1) {
146
- return { line: start.line, character: start.character + text.length };
147
- }
148
- return { line: start.line + lines.length - 1, character: lines.at(-1)!.length };
149
- }
150
-
151
142
  /* One stretch of the proposal as the card draws it. */
152
143
  export interface DiffSegment {
153
144
  readonly kind: 'same' | 'added' | 'removed';