@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
@@ -2,9 +2,10 @@ import { disposeSnippetTransforms } from './snippet-transform-queue.js';
2
2
  import i18next, {} from 'i18next';
3
3
  import { fileUriToPath, pathToFileUri } from '@adecore/lsp';
4
4
  import { NavigationHistory } from './navigation-history.js';
5
- import { ProjectProblems } from './project-problems.js';
5
+ import { ProjectProblems, serviceProblems } from './project-problems.js';
6
6
  import { applyWorkspaceEdit } from './workspace-edit.js';
7
7
  import { relativeTo } from './paths.js';
8
+ import { PANEL_BODY_HEIGHT } from './panel-size.js';
8
9
  /* Sends one editor's edits to the document, holding them until the document is open. */
9
10
  class DocumentSync {
10
11
  ready;
@@ -71,6 +72,9 @@ export class ProjectLanguage {
71
72
  history = new NavigationHistory();
72
73
  holders = new Map();
73
74
  files;
75
+ /* The body height of the peek and call hierarchy panels, shared by the editors of the project. */
76
+ panelBodyHeight;
77
+ panelListeners = new Set();
74
78
  pendingCaret = null;
75
79
  lastPlace = null;
76
80
  disposed = false;
@@ -80,7 +84,21 @@ export class ProjectLanguage {
80
84
  this.i18n = host.i18n ?? i18next;
81
85
  this.folder = host.folder ?? '';
82
86
  this.files = host.files ?? null;
83
- this.problems = new ProjectProblems(service, (uri) => this.pathOfLocation(uri) ?? uri);
87
+ this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
88
+ this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, (uri) => this.pathOfLocation(uri) ?? uri));
89
+ }
90
+ /* Open panels of other editors of the project follow a resize. */
91
+ onPanelBodyChange(listener) {
92
+ this.panelListeners.add(listener);
93
+ return () => this.panelListeners.delete(listener);
94
+ }
95
+ resizePanelBody(height) {
96
+ if (height === this.panelBodyHeight) {
97
+ return;
98
+ }
99
+ this.panelBodyHeight = height;
100
+ this.panelListeners.forEach((listener) => listener(height));
101
+ this.host.onPanelResize?.(height);
84
102
  }
85
103
  pathOfLocation(uri) {
86
104
  if (this.host.pathOfUri !== undefined) {
@@ -193,9 +211,10 @@ export class ProjectLanguage {
193
211
  void holder.sync.ready
194
212
  .catch(() => undefined)
195
213
  .then(() => {
196
- if (!this.holders.has(uri)) {
197
- return this.service.closeDocument(uri);
214
+ if (this.holders.has(uri)) {
215
+ return undefined;
198
216
  }
217
+ return this.service.closeDocument(uri);
199
218
  })
200
219
  .catch(() => undefined);
201
220
  }
@@ -9,12 +9,21 @@ export interface ProblemFile {
9
9
  readonly rows: readonly ProblemRow[];
10
10
  }
11
11
  export declare function compareRows(left: ProblemRow, right: ProblemRow): number;
12
+ export interface ProblemReport {
13
+ readonly path: string;
14
+ readonly server: string;
15
+ readonly diagnostics: readonly Diagnostic[];
16
+ }
17
+ export type ProblemSource = (listener: (report: ProblemReport) => void) => () => void;
18
+ export declare function serviceProblems(service: LanguageService, pathOf?: (uri: string) => string): ProblemSource;
12
19
  export declare class ProjectProblems {
13
20
  private readonly reports;
21
+ private readonly files;
14
22
  private readonly listeners;
15
23
  private readonly stop;
24
+ private order;
16
25
  private snapshot;
17
- constructor(service: LanguageService, pathOf?: (uri: string) => string);
26
+ constructor(source: ProblemSource | LanguageService, pathOf?: (uri: string) => string);
18
27
  report(path: string, server: string, diagnostics: readonly Diagnostic[]): void;
19
28
  getSnapshot: () => readonly ProblemFile[];
20
29
  subscribe: (listener: () => void) => (() => void);
@@ -5,22 +5,38 @@ export function compareRows(left, right) {
5
5
  return (SEVERITY_ORDER[severityOf(left.diagnostic)] - SEVERITY_ORDER[severityOf(right.diagnostic)] ||
6
6
  comparePositions(left.diagnostic.range.start, right.diagnostic.range.start));
7
7
  }
8
+ /* The diagnostics a service reports, by the stored path `pathOf` gives each URI. */
9
+ export function serviceProblems(service, pathOf = (uri) => uri) {
10
+ return (listener) => {
11
+ const subscription = service.onDiagnostics((report) => listener({ path: pathOf(report.uri), server: report.source, diagnostics: report.diagnostics }));
12
+ return () => subscription.dispose();
13
+ };
14
+ }
8
15
  /*
9
16
  * Each report replaces the previous diagnostics from that source for the file. Files with no
10
17
  * remaining problems drop out. Hints remain editor annotations rather than listed problems.
11
18
  */
12
19
  export class ProjectProblems {
13
20
  reports = new Map();
21
+ files = new Map();
14
22
  listeners = new Set();
15
23
  stop;
24
+ /* The paths of `files` in order; null once a file came or went. */
25
+ order = [];
26
+ /* Made on first read after a report, so a project nobody lists does no sorting. */
16
27
  snapshot = [];
17
- constructor(service, pathOf = (uri) => uri) {
18
- const subscription = service.onDiagnostics((report) => this.report(pathOf(report.uri), report.source, report.diagnostics));
19
- this.stop = () => subscription.dispose();
28
+ /* A service stands for `serviceProblems(service, pathOf)`. */
29
+ constructor(source, pathOf) {
30
+ const listen = typeof source === 'function' ? source : serviceProblems(source, pathOf);
31
+ this.stop = listen((report) => this.report(report.path, report.server, report.diagnostics));
20
32
  }
21
33
  report(path, server, diagnostics) {
22
34
  const kept = diagnostics.filter((diagnostic) => severityOf(diagnostic) !== 'hint');
23
35
  const servers = this.reports.get(path) ?? new Map();
36
+ if (kept.length === 0 && !servers.has(server)) {
37
+ // A clean file reported clean again, as servers do after every edit.
38
+ return;
39
+ }
24
40
  if (kept.length === 0) {
25
41
  servers.delete(server);
26
42
  }
@@ -29,22 +45,30 @@ export class ProjectProblems {
29
45
  }
30
46
  if (servers.size === 0) {
31
47
  this.reports.delete(path);
48
+ this.files.delete(path);
49
+ this.order = null;
32
50
  }
33
51
  else {
52
+ if (!this.files.has(path)) {
53
+ this.order = null;
54
+ }
34
55
  this.reports.set(path, servers);
56
+ const rows = [...servers].flatMap(([name, list]) => list.map((diagnostic) => ({ diagnostic, server: name }))).sort(compareRows);
57
+ this.files.set(path, { path, rows });
35
58
  }
36
- this.snapshot = [...this.reports]
37
- .map(([file, byServer]) => ({
38
- path: file,
39
- rows: [...byServer].flatMap(([name, list]) => list.map((diagnostic) => ({ diagnostic, server: name }))).sort(compareRows)
40
- }))
41
- .sort((left, right) => left.path.localeCompare(right.path));
59
+ this.snapshot = null;
42
60
  for (const listener of [...this.listeners]) {
43
61
  listener();
44
62
  }
45
63
  }
46
64
  /* The same array until the next report, which is what a store a component reads must hand out. */
47
- getSnapshot = () => this.snapshot;
65
+ getSnapshot = () => {
66
+ if (this.snapshot === null) {
67
+ this.order ??= [...this.files.keys()].sort((left, right) => left.localeCompare(right));
68
+ this.snapshot = this.order.map((path) => this.files.get(path));
69
+ }
70
+ return this.snapshot;
71
+ };
48
72
  subscribe = (listener) => {
49
73
  this.listeners.add(listener);
50
74
  return () => {
@@ -21,7 +21,6 @@ export interface InlineAnswer {
21
21
  }
22
22
  export declare function parseAnswer(text: string): InlineAnswer;
23
23
  export declare function fitReplacement(selected: string, replacement: string): string;
24
- export declare function endOfInsertion(start: EditorPosition, text: string): EditorPosition;
25
24
  export interface DiffSegment {
26
25
  readonly kind: 'same' | 'added' | 'removed';
27
26
  readonly text: string;
@@ -94,14 +94,6 @@ export function fitReplacement(selected, replacement) {
94
94
  }
95
95
  return breaks === '\n' ? fitted : fitted.replace(/\n/g, breaks);
96
96
  }
97
- /* Where the caret stands after `text` was typed at `start`. */
98
- export function endOfInsertion(start, text) {
99
- const lines = text.split('\n');
100
- if (lines.length === 1) {
101
- return { line: start.line, character: start.character + text.length };
102
- }
103
- return { line: start.line + lines.length - 1, character: lines.at(-1).length };
104
- }
105
97
  function linesOf(text) {
106
98
  const lines = text.replace(/\r\n/g, '\n').split('\n');
107
99
  return lines.length > 1 && lines.at(-1) === '' ? lines.slice(0, -1) : lines;
@@ -1,5 +1,5 @@
1
1
  import type { EditorPosition, EditorRange } from '@adecore/editor';
2
- import type { Location, PrepareRenameResult, TextEdit, WorkspaceEdit } from '@adecore/lsp';
2
+ import type { DocumentSymbolResult, Location, PrepareRenameResult, TextEdit, WorkspaceEdit } from '@adecore/lsp';
3
3
  export declare function wordRangeAt(lineText: string, position: EditorPosition): EditorRange | null;
4
4
  export interface RenameTarget {
5
5
  readonly range: EditorRange;
@@ -12,6 +12,7 @@ export interface Occurrences {
12
12
  readonly files: number;
13
13
  }
14
14
  export declare function occurrencesOf(locations: readonly Location[], uri: string): Occurrences;
15
+ export declare function renameKindOf(result: DocumentSymbolResult, name: string, places: readonly EditorRange[]): string | undefined;
15
16
  export interface RenameRow {
16
17
  readonly line: number;
17
18
  readonly before: string;
@@ -1,4 +1,5 @@
1
1
  import { isIdentifierCharacter } from './completion-model.js';
2
+ import { rangeHolds } from './diagnostics-model.js';
2
3
  import { textEntriesOf } from './workspace-edit.js';
3
4
  /* The identifier the caret is in or touches; null where there is none. */
4
5
  export function wordRangeAt(lineText, position) {
@@ -36,6 +37,62 @@ export function occurrencesOf(locations, uri) {
36
37
  files: new Set(locations.map((location) => location.uri)).size
37
38
  };
38
39
  }
40
+ /* LSP's SymbolKind in words, indexed by its number. */
41
+ const SYMBOL_KINDS = [
42
+ '',
43
+ 'file',
44
+ 'module',
45
+ 'namespace',
46
+ 'package',
47
+ 'class',
48
+ 'method',
49
+ 'property',
50
+ 'field',
51
+ 'constructor',
52
+ 'enum',
53
+ 'interface',
54
+ 'function',
55
+ 'variable',
56
+ 'constant',
57
+ 'string',
58
+ 'number',
59
+ 'boolean',
60
+ 'array',
61
+ 'object',
62
+ 'key',
63
+ 'null',
64
+ 'enum member',
65
+ 'struct',
66
+ 'event',
67
+ 'operator',
68
+ 'type parameter'
69
+ ];
70
+ /*
71
+ * The kind of the symbol a rename names, in words, from the outline of the open file: the symbol whose name
72
+ * holds one of the places of the name there. Undefined when the outline does not list it, as with most locals.
73
+ */
74
+ export function renameKindOf(result, name, places) {
75
+ if (result === null || result.length === 0) {
76
+ return undefined;
77
+ }
78
+ const named = (range) => places.some((place) => rangeHolds(range, place.start));
79
+ if ('range' in result[0]) {
80
+ let found;
81
+ const walk = (symbols) => {
82
+ for (const symbol of symbols) {
83
+ if (named(symbol.selectionRange)) {
84
+ found = symbol;
85
+ }
86
+ walk(symbol.children ?? []);
87
+ }
88
+ };
89
+ walk(result);
90
+ return found === undefined ? undefined : SYMBOL_KINDS[found.kind];
91
+ }
92
+ // A flat list only gives the whole span of a symbol, so the name has to match as well.
93
+ const found = result.find((symbol) => 'location' in symbol && symbol.name === name && named(symbol.location.range));
94
+ return found === undefined ? undefined : SYMBOL_KINDS[found.kind];
95
+ }
39
96
  function lineStarts(text) {
40
97
  const starts = [0];
41
98
  for (let index = 0; index < text.length; index++) {
package/dist/rename.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare class RenameFeature {
14
14
  private finish;
15
15
  private light;
16
16
  private suggest;
17
+ private kindOf;
17
18
  private linesOf;
18
19
  private publish;
19
20
  private tell;
package/dist/rename.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { StaleResultError } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.js';
2
3
  import { lineTextOf } from './line-text.js';
3
- import { occurrencesOf, renamePreviewOf, renameTargetOf, wordRangeAt } from './rename-model.js';
4
+ import { occurrencesOf, renameKindOf, renamePreviewOf, renameTargetOf, wordRangeAt } from './rename-model.js';
4
5
  import { isShortcut } from './shortcut-keys.js';
5
6
  import { resourceOperationsOf } from './workspace-edit.js';
6
7
  import { messageOf } from './error-message.js';
@@ -22,13 +23,13 @@ export class RenameFeature {
22
23
  this.language = language;
23
24
  const { editor } = language;
24
25
  const offs = [
25
- editor.onKeyDown((event) => {
26
+ language.onKey((event) => {
26
27
  if (!isShortcut(this.language.shortcuts.rename, event)) {
27
28
  return false;
28
29
  }
29
30
  void this.start();
30
31
  return true;
31
- }),
32
+ }, KEY_PRIORITY.rename),
32
33
  editor.onTextChange(() => this.cancel())
33
34
  ];
34
35
  language.onDispose(() => {
@@ -192,8 +193,9 @@ export class RenameFeature {
192
193
  start: { line: Math.max(0, line - CONTEXT_LINES), character: 0 },
193
194
  end: { line: line + CONTEXT_LINES, character: Number.MAX_SAFE_INTEGER }
194
195
  });
195
- const uses = await this.linesOf(locations ?? []);
196
- const names = await suggestNames({ uri, languageId, name: session.original, context, uses }, session.names.signal);
196
+ const [uses, kind] = await Promise.all([this.linesOf(locations ?? []), this.kindOf(session, locations ?? [])]);
197
+ const request = { uri, languageId, name: session.original, ...(kind === undefined ? {} : { kind }), context, uses };
198
+ const names = await suggestNames(request, session.names.signal);
197
199
  if (this.session === session && names.length > 0) {
198
200
  this.publish({ suggestions: names });
199
201
  }
@@ -202,6 +204,20 @@ export class RenameFeature {
202
204
  // Suggestions do not prevent a manual rename.
203
205
  }
204
206
  }
207
+ /* So a suggester knows a function from a variable; a rename on a use finds the kind through the declaration's place in this file. */
208
+ async kindOf(session, locations) {
209
+ const { project, uri } = this.language;
210
+ if (!project.service.supports('textDocument/documentSymbol', uri)) {
211
+ return undefined;
212
+ }
213
+ try {
214
+ const symbols = await project.service.documentSymbols(uri, { signal: session.names.signal, parallel: true });
215
+ return renameKindOf(symbols, session.original, [session.target.range, ...occurrencesOf(locations, uri).inFile]);
216
+ }
217
+ catch {
218
+ return undefined;
219
+ }
220
+ }
205
221
  /* The text of the first lines that name the symbol, in the order the servers listed them, reading at most a few other files. */
206
222
  async linesOf(locations) {
207
223
  const { project } = this.language;
@@ -4,6 +4,7 @@ import { type ParsedDocComment } from './doc-comment-model.ts';
4
4
  import { type Timers } from './timers.ts';
5
5
  export interface RenderedDocsSettings {
6
6
  readonly renderDocComments: boolean;
7
+ readonly enabled?: boolean;
7
8
  }
8
9
  export interface RenderedDocEntry {
9
10
  readonly id: string;
@@ -15,6 +16,7 @@ export declare class RenderedDocsFeature {
15
16
  private readonly language;
16
17
  private readonly timers;
17
18
  private settings;
19
+ private enabled;
18
20
  private comments;
19
21
  private overrides;
20
22
  private readonly containers;
@@ -1,4 +1,5 @@
1
1
  import { findDocComments } from '@adecore/editor';
2
+ import { KEY_PRIORITY } from './key-priority.js';
2
3
  import { parseDocComment } from './doc-comment-model.js';
3
4
  import { isShortcut } from './shortcut-keys.js';
4
5
  import { realTimers } from './timers.js';
@@ -44,6 +45,7 @@ export class RenderedDocsFeature {
44
45
  language;
45
46
  timers;
46
47
  settings = { renderDocComments: false };
48
+ enabled = true;
47
49
  comments = [];
48
50
  overrides = [];
49
51
  containers = new Map();
@@ -85,9 +87,9 @@ export class RenderedDocsFeature {
85
87
  this.toggleAt(line);
86
88
  }
87
89
  }),
88
- editor.onKeyDown((event) => {
90
+ language.onKey((event) => {
89
91
  return isShortcut(this.language.shortcuts.toggleRenderedDocs, event) && this.toggle();
90
- }),
92
+ }, KEY_PRIORITY.renderedDocs),
91
93
  editor.onViewChange(() => {
92
94
  this.sweep();
93
95
  this.releaseRevealed();
@@ -113,11 +115,16 @@ export class RenderedDocsFeature {
113
115
  }
114
116
  /* Whether every documentation comment is drawn as text until a person switches it. Changing it puts every comment back under it. */
115
117
  configure(settings) {
116
- if (settings.renderDocComments === this.settings.renderDocComments) {
118
+ const enabled = settings.enabled !== false;
119
+ if (settings.renderDocComments === this.settings.renderDocComments && enabled === this.enabled) {
117
120
  return;
118
121
  }
122
+ const sameRendering = settings.renderDocComments === this.settings.renderDocComments;
119
123
  this.settings = settings;
120
- this.clearOverrides();
124
+ this.enabled = enabled;
125
+ if (!sameRendering) {
126
+ this.clearOverrides();
127
+ }
121
128
  this.publish();
122
129
  }
123
130
  /* The documentation comments of the document, from the last time it settled. */
@@ -132,7 +139,7 @@ export class RenderedDocsFeature {
132
139
  toggle() {
133
140
  this.settle();
134
141
  const comment = this.at(this.language.editor.getCaret().line);
135
- if (comment === undefined || !this.canRender(comment)) {
142
+ if (!this.enabled || comment === undefined || !this.canRender(comment)) {
136
143
  return false;
137
144
  }
138
145
  this.setRendered(comment, !this.wantsRendered(comment));
@@ -142,7 +149,7 @@ export class RenderedDocsFeature {
142
149
  toggleAt(startLine) {
143
150
  this.settle();
144
151
  const comment = this.comments.find((candidate) => candidate.startLine === startLine);
145
- if (comment === undefined || !this.canRender(comment)) {
152
+ if (!this.enabled || comment === undefined || !this.canRender(comment)) {
146
153
  return false;
147
154
  }
148
155
  this.setRendered(comment, !this.wantsRendered(comment));
@@ -186,7 +193,7 @@ export class RenderedDocsFeature {
186
193
  return comment.startLine > 0 || comment.endLine < this.language.editor.positionAt(Number.MAX_SAFE_INTEGER).line;
187
194
  }
188
195
  wantsRendered(comment) {
189
- if (!this.canRender(comment)) {
196
+ if (!this.enabled || !this.canRender(comment)) {
190
197
  return false;
191
198
  }
192
199
  const start = startOf(comment);
@@ -325,7 +332,7 @@ export class RenderedDocsFeature {
325
332
  this.bump();
326
333
  }
327
334
  this.markerLines.clear();
328
- const markers = this.comments.map((comment) => {
335
+ const markers = (this.enabled ? this.comments : []).map((comment) => {
329
336
  const id = `docs:${comment.startLine}`;
330
337
  this.markerLines.set(id, comment.startLine);
331
338
  const on = this.wantsRendered(comment);
package/dist/signature.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { KEY_PRIORITY } from './key-priority.js';
1
2
  import { signatureViewOf } from './signature-model.js';
2
3
  import { isShortcut } from './shortcut-keys.js';
3
4
  import { realTimers } from './timers.js';
@@ -24,8 +25,8 @@ export class SignatureFeature {
24
25
  editor.onTextChange((change) => this.edited(change)),
25
26
  editor.onCaret(() => this.retrigger()),
26
27
  editor.onBlur(() => this.close()),
27
- editor.onKeyDown((event) => {
28
- if (event.key === 'Escape' && this.isOpen && this.language.popups.getState().callHierarchy == null) {
28
+ language.onKey((event) => {
29
+ if (event.key === 'Escape' && this.isOpen && this.language.popups.getState().callHierarchy === null) {
29
30
  this.close();
30
31
  return true;
31
32
  }
@@ -37,7 +38,7 @@ export class SignatureFeature {
37
38
  return this.cycle(event.key === 'ArrowDown' ? 1 : -1);
38
39
  }
39
40
  return false;
40
- })
41
+ }, KEY_PRIORITY.signature)
41
42
  ];
42
43
  language.onDispose(() => {
43
44
  for (const off of offs) {
@@ -10,7 +10,7 @@ export interface SnippetInsertion {
10
10
  readonly stops: readonly SnippetStop[];
11
11
  readonly fields?: readonly SnippetField[];
12
12
  }
13
- type SnippetHost = Pick<EditorLanguage, 'editor' | 'onDispose'> & Partial<Pick<EditorLanguage, 'popups' | 'pick' | 'project'>>;
13
+ type SnippetHost = Pick<EditorLanguage, 'editor' | 'onDispose'> & Partial<Pick<EditorLanguage, 'popups' | 'pick' | 'project' | 'onKey'>>;
14
14
  export declare class SnippetFeature {
15
15
  private readonly language;
16
16
  private frames;
@@ -3,6 +3,7 @@ import { SnippetFieldTransforms } from './snippet-field-transforms.js';
3
3
  import { SnippetTransformError } from './snippet-transform-client.js';
4
4
  import { acquireSnippetTransforms } from './snippet-transform-queue.js';
5
5
  import { realTimers } from './timers.js';
6
+ import { KEY_PRIORITY } from './key-priority.js';
6
7
  import { MAX_TRANSFORM_CHARACTERS } from './snippet-transform-protocol.js';
7
8
  import { comparePositions, rangeHolds, shiftPosition } from './diagnostics-model.js';
8
9
  export function positionInInsertion(start, text, offset) {
@@ -128,7 +129,12 @@ export class SnippetFeature {
128
129
  this.transforms = language.project === undefined ? null : acquireSnippetTransforms(language.project, timers);
129
130
  const { editor } = language;
130
131
  this.fieldTransforms = new SnippetFieldTransforms(editor, this.transforms, (targets, values) => this.applyFieldTransforms(targets, values));
131
- const offs = [editor.onKeyDown((event) => this.key(event)), editor.onTextChange((change) => this.edited(change)), editor.onCaret(() => this.moved())];
132
+ const key = (event) => this.key(event);
133
+ const offs = [
134
+ language.onKey === undefined ? editor.onKeyDown(key) : language.onKey(key, KEY_PRIORITY.snippets),
135
+ editor.onTextChange((change) => this.edited(change)),
136
+ editor.onCaret(() => this.moved())
137
+ ];
132
138
  language.onDispose(() => {
133
139
  this.transforms?.dispose();
134
140
  this.end();
@@ -395,7 +401,7 @@ export class SnippetFeature {
395
401
  if (event.key === 'Escape') {
396
402
  const popups = this.language.popups?.getState();
397
403
  if (popups !== undefined &&
398
- [popups.completion, popups.signature, popups.hover, popups.pick, popups.peek, popups.callHierarchy ?? null].some((popup) => popup !== null)) {
404
+ [popups.completion, popups.signature, popups.hover, popups.pick, popups.peek, popups.callHierarchy].some((popup) => popup !== null)) {
399
405
  return false;
400
406
  }
401
407
  const active = this.isActive;
@@ -1,4 +1,5 @@
1
1
  import { StaleResultError } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.js';
2
3
  import { basenameOf } from './paths.js';
3
4
  import { entriesOf, workspaceEntriesOf } from './symbol-picker-model.js';
4
5
  import { isShortcut } from './shortcut-keys.js';
@@ -13,13 +14,13 @@ export class SymbolPickerFeature {
13
14
  language;
14
15
  constructor(language) {
15
16
  this.language = language;
16
- const off = language.editor.onKeyDown((event) => {
17
+ const off = language.onKey((event) => {
17
18
  if (!isShortcut(this.language.shortcuts.goToSymbol, event)) {
18
19
  return false;
19
20
  }
20
21
  void this.open();
21
22
  return true;
22
- });
23
+ }, KEY_PRIORITY.symbolPicker);
23
24
  language.onDispose(() => {
24
25
  off();
25
26
  this.close();
package/dist/timers.d.ts CHANGED
@@ -12,3 +12,4 @@ export declare class ManualTimers implements Timers {
12
12
  advance(milliseconds: number): void;
13
13
  get size(): number;
14
14
  }
15
+ export declare function revealRow(timers: Timers, container: HTMLElement): void;
package/dist/timers.js CHANGED
@@ -32,3 +32,7 @@ export class ManualTimers {
32
32
  return this.pending.size;
33
33
  }
34
34
  }
35
+ /* Scrolls a row the editor opened under the caret line into view, which may sit at the bottom of the view with the row below the fold. The element is in the page a turn later. */
36
+ export function revealRow(timers, container) {
37
+ timers.set(() => container.scrollIntoView?.({ block: 'nearest' }), 0);
38
+ }
@@ -0,0 +1,6 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ export declare function usePanelBody(language: EditorLanguage): {
3
+ height: number;
4
+ set(height: number): void;
5
+ commit(height: number): void;
6
+ };
@@ -0,0 +1,13 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { clampPanelBody } from './panel-size.js';
3
+ /* The body height of a panel, from the project that remembers it for the next one; `commit` hands it to the host. */
4
+ export function usePanelBody(language) {
5
+ const { project } = language;
6
+ const [height, set] = useState(() => clampPanelBody(project.panelBodyHeight));
7
+ useEffect(() => project.onPanelBodyChange((value) => set(clampPanelBody(value))), [project]);
8
+ return {
9
+ height,
10
+ set,
11
+ commit: (value) => project.resizePanelBody(value)
12
+ };
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-react",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "description": "Language-enabled editors, feature coordination and React popups over injected language services.",
6
6
  "license": "FSL-1.1-MIT",
@@ -62,9 +62,9 @@
62
62
  "test": "bun test --conditions=source"
63
63
  },
64
64
  "dependencies": {
65
- "@adecore/editor": "0.23.0",
66
- "@adecore/lsp": "0.23.0",
67
- "@adecore/merge": "0.23.0",
65
+ "@adecore/editor": "0.24.0",
66
+ "@adecore/lsp": "0.24.0",
67
+ "@adecore/merge": "0.24.0",
68
68
  "lucide-react": "^1.47.0",
69
69
  "react-markdown": "^10.1.0",
70
70
  "remark-breaks": "^4.0.0",
@@ -72,14 +72,14 @@
72
72
  "zustand": "^5.0.15"
73
73
  },
74
74
  "peerDependencies": {
75
- "@adecore/ui": "^0.23.0",
75
+ "@adecore/ui": "^0.24.0",
76
76
  "i18next": "^26.4.2",
77
77
  "react": "^19.3.0",
78
78
  "react-dom": "^19.3.0",
79
79
  "react-i18next": "^17.0.15"
80
80
  },
81
81
  "devDependencies": {
82
- "@adecore/ui": "workspace:*",
82
+ "@adecore/ui": "0.24.0",
83
83
  "@types/bun": "^1.4.2",
84
84
  "@types/react": "^19.3.0",
85
85
  "@types/react-dom": "^19.3.0",
@@ -7,6 +7,8 @@ export interface AnchoredPopupProps {
7
7
  /* The character the popup belongs to, in the page's pixels. */
8
8
  rect: EditorRect;
9
9
  className?: string;
10
+ /* What the card does with content past its room: `scroll` it, or `clip` it for content that scrolls on its own. `scroll` without one. */
11
+ overflow?: 'scroll' | 'clip';
10
12
  children: ReactNode;
11
13
  placement?: PlaceOptions;
12
14
  onPointerEnter?(): void;
@@ -17,7 +19,7 @@ export interface AnchoredPopupProps {
17
19
  * A card next to a character of the editor, portaled to the page: inside a scaled canvas node it would be
18
20
  * drawn at that scale and clipped by the node. Placed from the character's screen rectangle, so it holds at any zoom.
19
21
  */
20
- export function AnchoredPopup({ rect, className, children, placement, onPointerEnter, onPointerLeave }: AnchoredPopupProps) {
22
+ export function AnchoredPopup({ rect, className, overflow = 'scroll', children, placement, onPointerEnter, onPointerLeave }: AnchoredPopupProps) {
21
23
  const element = useRef<HTMLDivElement>(null);
22
24
 
23
25
  // Every render, since the character moves when the editor scrolls and the card's own size follows its content.
@@ -38,7 +40,7 @@ export function AnchoredPopup({ rect, className, children, placement, onPointerE
38
40
  return createPortal(
39
41
  <div
40
42
  ref={element}
41
- className={`fixed top-0 left-0 z-(--z-popup) overflow-y-auto rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow) ${className ?? ''}`}
43
+ className={`fixed top-0 left-0 z-(--z-popup) ${overflow === 'clip' ? 'overflow-hidden' : 'overflow-y-auto'} rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow) ${className ?? ''}`}
42
44
  style={{ visibility: 'hidden' }}
43
45
  onPointerEnter={onPointerEnter}
44
46
  onPointerLeave={onPointerLeave}