@adecore/editor-react 0.0.1 → 0.17.0-beta.2

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 (264) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +60 -0
  3. package/dist/AnchoredPopup.d.ts +12 -0
  4. package/dist/AnchoredPopup.js +27 -0
  5. package/dist/AttributionCard.d.ts +14 -0
  6. package/dist/AttributionCard.js +5 -0
  7. package/dist/BaselineStatus.d.ts +4 -0
  8. package/dist/BaselineStatus.js +13 -0
  9. package/dist/ChangeReview.d.ts +12 -0
  10. package/dist/ChangeReview.js +27 -0
  11. package/dist/CodeAuthorsCard.d.ts +6 -0
  12. package/dist/CodeAuthorsCard.js +22 -0
  13. package/dist/CompletionPopup.d.ts +8 -0
  14. package/dist/CompletionPopup.js +103 -0
  15. package/dist/EditorContextMenu.d.ts +8 -0
  16. package/dist/EditorContextMenu.js +23 -0
  17. package/dist/EditorView.d.ts +16 -0
  18. package/dist/EditorView.js +27 -0
  19. package/dist/FindReplace.d.ts +6 -0
  20. package/dist/FindReplace.js +44 -0
  21. package/dist/HoverCard.d.ts +11 -0
  22. package/dist/HoverCard.js +38 -0
  23. package/dist/HoverSections.d.ts +21 -0
  24. package/dist/HoverSections.js +82 -0
  25. package/dist/LanguagePopups.d.ts +6 -0
  26. package/dist/LanguagePopups.js +38 -0
  27. package/dist/Markdown.d.ts +3 -0
  28. package/dist/Markdown.js +7 -0
  29. package/dist/PathText.d.ts +4 -0
  30. package/dist/PathText.js +12 -0
  31. package/dist/PeekPanel.d.ts +6 -0
  32. package/dist/PeekPanel.js +111 -0
  33. package/dist/PickPopup.d.ts +8 -0
  34. package/dist/PickPopup.js +33 -0
  35. package/dist/RenameCard.d.ts +9 -0
  36. package/dist/RenameCard.js +108 -0
  37. package/dist/SignatureCard.d.ts +4 -0
  38. package/dist/SignatureCard.js +8 -0
  39. package/dist/SymbolPicker.d.ts +6 -0
  40. package/dist/SymbolPicker.js +108 -0
  41. package/dist/code-actions-model.d.ts +23 -0
  42. package/dist/code-actions-model.js +114 -0
  43. package/dist/code-actions.d.ts +30 -0
  44. package/dist/code-actions.js +326 -0
  45. package/dist/code-authors.d.ts +18 -0
  46. package/dist/code-authors.js +79 -0
  47. package/dist/code-vision-model.d.ts +21 -0
  48. package/dist/code-vision-model.js +183 -0
  49. package/dist/code-vision.d.ts +48 -0
  50. package/dist/code-vision.js +236 -0
  51. package/dist/completion-call.d.ts +24 -0
  52. package/dist/completion-call.js +110 -0
  53. package/dist/completion-model.d.ts +51 -0
  54. package/dist/completion-model.js +353 -0
  55. package/dist/completion.d.ts +43 -0
  56. package/dist/completion.js +463 -0
  57. package/dist/conflict-model.d.ts +15 -0
  58. package/dist/conflict-model.js +36 -0
  59. package/dist/context-menu.d.ts +10 -0
  60. package/dist/context-menu.js +55 -0
  61. package/dist/definition-link.d.ts +14 -0
  62. package/dist/definition-link.js +99 -0
  63. package/dist/diagnostics-model.d.ts +15 -0
  64. package/dist/diagnostics-model.js +83 -0
  65. package/dist/diagnostics.d.ts +23 -0
  66. package/dist/diagnostics.js +95 -0
  67. package/dist/editor-language.d.ts +65 -0
  68. package/dist/editor-language.js +170 -0
  69. package/dist/editor-react.css +43 -0
  70. package/dist/find-query.d.ts +26 -0
  71. package/dist/find-query.js +51 -0
  72. package/dist/folding.d.ts +16 -0
  73. package/dist/folding.js +95 -0
  74. package/dist/highlight-layers.d.ts +10 -0
  75. package/dist/highlight-layers.js +30 -0
  76. package/dist/highlights.d.ts +16 -0
  77. package/dist/highlights.js +102 -0
  78. package/dist/history.d.ts +8 -0
  79. package/dist/history.js +46 -0
  80. package/dist/holder.d.ts +5 -0
  81. package/dist/holder.js +19 -0
  82. package/dist/host-types.d.ts +45 -0
  83. package/dist/host-types.js +1 -0
  84. package/dist/hover-content.d.ts +45 -0
  85. package/dist/hover-content.js +198 -0
  86. package/dist/hover.d.ts +32 -0
  87. package/dist/hover.js +260 -0
  88. package/dist/index.d.ts +44 -0
  89. package/dist/index.js +28 -0
  90. package/dist/inlay-hints.d.ts +13 -0
  91. package/dist/inlay-hints.js +71 -0
  92. package/dist/language-ids.d.ts +3 -0
  93. package/dist/language-ids.js +49 -0
  94. package/dist/line-edits.d.ts +8 -0
  95. package/dist/line-edits.js +39 -0
  96. package/dist/locales/en.json +171 -0
  97. package/dist/locales/nl.json +171 -0
  98. package/dist/models.d.ts +20 -0
  99. package/dist/models.js +20 -0
  100. package/dist/navigation-history.d.ts +16 -0
  101. package/dist/navigation-history.js +53 -0
  102. package/dist/navigation.d.ts +23 -0
  103. package/dist/navigation.js +155 -0
  104. package/dist/paths.d.ts +1 -0
  105. package/dist/paths.js +3 -0
  106. package/dist/peek-model.d.ts +28 -0
  107. package/dist/peek-model.js +104 -0
  108. package/dist/peek.d.ts +30 -0
  109. package/dist/peek.js +215 -0
  110. package/dist/pick.d.ts +34 -0
  111. package/dist/pick.js +166 -0
  112. package/dist/popup-placement.d.ts +23 -0
  113. package/dist/popup-placement.js +34 -0
  114. package/dist/popups.d.ts +133 -0
  115. package/dist/popups.js +14 -0
  116. package/dist/project-language.d.ts +36 -0
  117. package/dist/project-language.js +190 -0
  118. package/dist/project-problems.d.ts +24 -0
  119. package/dist/project-problems.js +63 -0
  120. package/dist/proposal-model.d.ts +36 -0
  121. package/dist/proposal-model.js +197 -0
  122. package/dist/recent-choices.d.ts +10 -0
  123. package/dist/recent-choices.js +39 -0
  124. package/dist/refresher.d.ts +13 -0
  125. package/dist/refresher.js +50 -0
  126. package/dist/relative-time.d.ts +1 -0
  127. package/dist/relative-time.js +9 -0
  128. package/dist/rename-model.d.ts +25 -0
  129. package/dist/rename-model.js +93 -0
  130. package/dist/rename.d.ts +21 -0
  131. package/dist/rename.js +257 -0
  132. package/dist/rendering-context.d.ts +6 -0
  133. package/dist/rendering-context.js +5 -0
  134. package/dist/rendering.d.ts +6 -0
  135. package/dist/rendering.js +5 -0
  136. package/dist/review-group.d.ts +16 -0
  137. package/dist/review-group.js +54 -0
  138. package/dist/row-host.d.ts +42 -0
  139. package/dist/row-host.js +120 -0
  140. package/dist/selection-ranges.d.ts +7 -0
  141. package/dist/selection-ranges.js +26 -0
  142. package/dist/semantic-model.d.ts +4 -0
  143. package/dist/semantic-model.js +76 -0
  144. package/dist/semantic-tokens.d.ts +6 -0
  145. package/dist/semantic-tokens.js +38 -0
  146. package/dist/shortcut-keys.d.ts +91 -0
  147. package/dist/shortcut-keys.js +12 -0
  148. package/dist/signature-model.d.ts +18 -0
  149. package/dist/signature-model.js +33 -0
  150. package/dist/signature.d.ts +22 -0
  151. package/dist/signature.js +150 -0
  152. package/dist/snippet-session.d.ts +17 -0
  153. package/dist/snippet-session.js +110 -0
  154. package/dist/snippet.d.ts +11 -0
  155. package/dist/snippet.js +134 -0
  156. package/dist/symbol-links.d.ts +6 -0
  157. package/dist/symbol-links.js +50 -0
  158. package/dist/symbol-picker-model.d.ts +44 -0
  159. package/dist/symbol-picker-model.js +146 -0
  160. package/dist/symbol-picker.d.ts +12 -0
  161. package/dist/symbol-picker.js +76 -0
  162. package/dist/symbols.d.ts +11 -0
  163. package/dist/symbols.js +95 -0
  164. package/dist/testing.d.ts +57 -0
  165. package/dist/testing.js +156 -0
  166. package/dist/timers.d.ts +14 -0
  167. package/dist/timers.js +34 -0
  168. package/dist/use-problem-counts.d.ts +3 -0
  169. package/dist/use-problem-counts.js +20 -0
  170. package/dist/view-state.d.ts +21 -0
  171. package/dist/view-state.js +63 -0
  172. package/dist/word-diff.d.ts +1 -0
  173. package/dist/word-diff.js +32 -0
  174. package/dist/workspace-edit.d.ts +21 -0
  175. package/dist/workspace-edit.js +147 -0
  176. package/package.json +74 -2
  177. package/src/AnchoredPopup.tsx +51 -0
  178. package/src/AttributionCard.tsx +36 -0
  179. package/src/BaselineStatus.tsx +20 -0
  180. package/src/ChangeReview.tsx +43 -0
  181. package/src/CodeAuthorsCard.tsx +81 -0
  182. package/src/CompletionPopup.tsx +196 -0
  183. package/src/EditorContextMenu.tsx +67 -0
  184. package/src/EditorView.tsx +47 -0
  185. package/src/FindReplace.tsx +89 -0
  186. package/src/HoverCard.tsx +121 -0
  187. package/src/HoverSections.tsx +146 -0
  188. package/src/LanguagePopups.tsx +65 -0
  189. package/src/Markdown.tsx +11 -0
  190. package/src/PathText.tsx +18 -0
  191. package/src/PeekPanel.tsx +222 -0
  192. package/src/PickPopup.tsx +102 -0
  193. package/src/RenameCard.tsx +219 -0
  194. package/src/SignatureCard.tsx +42 -0
  195. package/src/SymbolPicker.tsx +219 -0
  196. package/src/code-actions-model.ts +146 -0
  197. package/src/code-actions.ts +384 -0
  198. package/src/code-authors.ts +104 -0
  199. package/src/code-vision-model.ts +230 -0
  200. package/src/code-vision.ts +269 -0
  201. package/src/completion-call.ts +139 -0
  202. package/src/completion-model.ts +440 -0
  203. package/src/completion.ts +500 -0
  204. package/src/conflict-model.ts +56 -0
  205. package/src/context-menu.ts +63 -0
  206. package/src/definition-link.ts +106 -0
  207. package/src/diagnostics-model.ts +102 -0
  208. package/src/diagnostics.ts +117 -0
  209. package/src/editor-language.ts +189 -0
  210. package/src/editor-react.css +43 -0
  211. package/src/find-query.ts +74 -0
  212. package/src/folding.ts +110 -0
  213. package/src/highlight-layers.ts +37 -0
  214. package/src/highlights.ts +123 -0
  215. package/src/history.ts +56 -0
  216. package/src/holder.ts +19 -0
  217. package/src/host-types.ts +52 -0
  218. package/src/hover-content.ts +264 -0
  219. package/src/hover.ts +280 -0
  220. package/src/index.ts +63 -0
  221. package/src/inlay-hints.ts +85 -0
  222. package/src/language-ids.ts +54 -0
  223. package/src/line-edits.ts +48 -0
  224. package/src/locales/en.json +171 -0
  225. package/src/locales/nl.json +171 -0
  226. package/src/models.ts +157 -0
  227. package/src/navigation-history.ts +69 -0
  228. package/src/navigation.ts +168 -0
  229. package/src/paths.ts +3 -0
  230. package/src/peek-model.ts +147 -0
  231. package/src/peek.ts +247 -0
  232. package/src/pick.ts +197 -0
  233. package/src/popup-placement.ts +64 -0
  234. package/src/popups.ts +204 -0
  235. package/src/project-language.ts +223 -0
  236. package/src/project-problems.ts +87 -0
  237. package/src/proposal-model.ts +259 -0
  238. package/src/recent-choices.ts +48 -0
  239. package/src/refresher.ts +56 -0
  240. package/src/relative-time.ts +10 -0
  241. package/src/rename-model.ts +130 -0
  242. package/src/rename.ts +290 -0
  243. package/src/rendering-context.ts +12 -0
  244. package/src/rendering.tsx +6 -0
  245. package/src/review-group.ts +70 -0
  246. package/src/row-host.ts +158 -0
  247. package/src/selection-ranges.ts +32 -0
  248. package/src/semantic-model.ts +80 -0
  249. package/src/semantic-tokens.ts +46 -0
  250. package/src/shortcut-keys.ts +21 -0
  251. package/src/signature-model.ts +49 -0
  252. package/src/signature.ts +164 -0
  253. package/src/snippet-session.ts +122 -0
  254. package/src/snippet.ts +147 -0
  255. package/src/symbol-links.ts +72 -0
  256. package/src/symbol-picker-model.ts +198 -0
  257. package/src/symbol-picker.ts +85 -0
  258. package/src/symbols.ts +110 -0
  259. package/src/testing.ts +240 -0
  260. package/src/timers.ts +45 -0
  261. package/src/use-problem-counts.ts +25 -0
  262. package/src/view-state.ts +88 -0
  263. package/src/word-diff.ts +35 -0
  264. package/src/workspace-edit.ts +194 -0
@@ -0,0 +1,28 @@
1
+ import type { Location, Range } from '@adecore/lsp';
2
+ export interface NameRange {
3
+ readonly start: number;
4
+ readonly end: number;
5
+ }
6
+ export interface PeekPlace {
7
+ readonly id: string;
8
+ readonly location: Location;
9
+ readonly line: number;
10
+ readonly text: string;
11
+ readonly name: NameRange | null;
12
+ }
13
+ export interface PeekFile {
14
+ readonly uri: string;
15
+ readonly places: readonly PeekPlace[];
16
+ }
17
+ export declare const PEEK_READ_FILES = 30;
18
+ export declare function peekFilesOf(locations: readonly Location[], current: string, textOf: (uri: string) => string | null): PeekFile[];
19
+ export interface PeekSnippet {
20
+ readonly startLine: number;
21
+ readonly text: string;
22
+ readonly active: number;
23
+ readonly name: NameRange | null;
24
+ }
25
+ export declare function snippetOf(text: string, line: number, range: Range): PeekSnippet;
26
+ export declare function definitionSnippetOf(text: string, line: number, declaration: Range | null, range: Range): PeekSnippet;
27
+ export declare function distinguishingFolders(paths: readonly string[]): string[];
28
+ export declare function visualColumnOf(line: string, character: number, tabSize: number): number;
@@ -0,0 +1,104 @@
1
+ /* The most files whose text is read for the list; a name used everywhere shows the rest as places without a line. */
2
+ export const PEEK_READ_FILES = 30;
3
+ /* The part of `line` a range covers, which runs to the end of the line when the range goes on. */
4
+ function columnsOf(range, line, length) {
5
+ if (range.start.line !== line) {
6
+ return null;
7
+ }
8
+ const end = Math.min(length, range.end.line === line ? range.end.character : length);
9
+ return range.start.character < end ? { start: range.start.character, end } : null;
10
+ }
11
+ /* The name of a place within its line once the indentation is cut off. */
12
+ function trimmedNameOf(raw, range, line) {
13
+ const leading = raw.length - raw.trimStart().length;
14
+ const columns = columnsOf(range, line, raw.length);
15
+ const length = raw.trim().length;
16
+ if (columns === null || columns.start < leading || columns.start >= leading + length) {
17
+ return null;
18
+ }
19
+ return { start: columns.start - leading, end: Math.min(length, columns.end - leading) };
20
+ }
21
+ /*
22
+ * The references grouped by file, the file the peek was opened in first and the others in the order the
23
+ * server gave them, each file's places by line. `textOf` answers null for a file whose text is not at hand.
24
+ */
25
+ export function peekFilesOf(locations, current, textOf) {
26
+ const order = [];
27
+ const byFile = new Map();
28
+ for (const location of locations) {
29
+ if (!byFile.has(location.uri)) {
30
+ order.push(location.uri);
31
+ byFile.set(location.uri, []);
32
+ }
33
+ byFile.get(location.uri).push(location);
34
+ }
35
+ const uris = [...order.filter((uri) => uri === current), ...order.filter((uri) => uri !== current)];
36
+ return uris.map((uri, fileIndex) => {
37
+ const text = textOf(uri);
38
+ const lines = text === null ? null : text.split(/\r\n|\r|\n/);
39
+ const places = [...byFile.get(uri)]
40
+ .sort((left, right) => left.range.start.line - right.range.start.line || left.range.start.character - right.range.start.character)
41
+ .map((location, index) => {
42
+ const line = location.range.start.line;
43
+ const raw = lines?.[line] ?? '';
44
+ return {
45
+ id: `${fileIndex}:${index}`,
46
+ location,
47
+ line,
48
+ text: raw.trim(),
49
+ name: lines === null ? null : trimmedNameOf(raw, location.range, line)
50
+ };
51
+ });
52
+ return { uri, places };
53
+ });
54
+ }
55
+ const BEFORE = 3;
56
+ const AFTER = 8;
57
+ export function snippetOf(text, line, range) {
58
+ const lines = text.split(/\r\n|\r|\n/);
59
+ const startLine = Math.max(0, line - BEFORE);
60
+ const end = Math.min(lines.length, line + AFTER + 1);
61
+ return { startLine, text: lines.slice(startLine, end).join('\n'), active: line - startLine, name: columnsOf(range, line, (lines[line] ?? '').length) };
62
+ }
63
+ const DEFINITION_LINES = 24;
64
+ /*
65
+ * The source of a definition: the lines of the whole declaration when the server gave them, else a stretch
66
+ * from just above the name, cut at a screenful since a class is longer than a peek is tall.
67
+ */
68
+ export function definitionSnippetOf(text, line, declaration, range) {
69
+ const lines = text.split(/\r\n|\r|\n/);
70
+ const startLine = declaration === null ? Math.max(0, line - 2) : Math.min(declaration.start.line, line);
71
+ const end = Math.min(lines.length - 1, declaration === null ? line + DEFINITION_LINES / 2 : Math.min(declaration.end.line, startLine + DEFINITION_LINES));
72
+ return { startLine, text: lines.slice(startLine, end + 1).join('\n'), active: line - startLine, name: columnsOf(range, line, (lines[line] ?? '').length) };
73
+ }
74
+ /*
75
+ * For each path, the trailing folders that tell it from the other paths with the same file name, as few
76
+ * as it takes; empty for a file whose name is its own.
77
+ */
78
+ export function distinguishingFolders(paths) {
79
+ const folders = paths.map((path) => path.split('/').slice(0, -1));
80
+ const names = paths.map((path) => path.slice(path.lastIndexOf('/') + 1));
81
+ return paths.map((path, index) => {
82
+ const same = paths.map((_, other) => other).filter((other) => names[other] === names[index] && paths[other] !== path);
83
+ if (same.length === 0) {
84
+ return '';
85
+ }
86
+ const rivals = [...new Set(same.map((other) => paths[other]))].map((other) => folders[paths.indexOf(other)]);
87
+ const mine = folders[index];
88
+ for (let depth = 1; depth <= mine.length; depth++) {
89
+ const tail = mine.slice(-depth).join('/');
90
+ if (rivals.every((rival) => rival.slice(-depth).join('/') !== tail)) {
91
+ return tail;
92
+ }
93
+ }
94
+ return mine.join('/');
95
+ });
96
+ }
97
+ /* The column a character stands at once tabs are expanded to their stops, which is where a monospace face draws it. */
98
+ export function visualColumnOf(line, character, tabSize) {
99
+ let column = 0;
100
+ for (let i = 0; i < Math.min(character, line.length); i++) {
101
+ column += line[i] === '\t' ? tabSize - (column % tabSize) : 1;
102
+ }
103
+ return column;
104
+ }
package/dist/peek.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { EditorPosition } from '@adecore/editor';
2
+ import type { EditorLanguage } from './editor-language.ts';
3
+ import type { PeekView } from './popups.ts';
4
+ export declare const PEEK_HEIGHT = 241;
5
+ type PeekKind = PeekView['kind'];
6
+ export declare class PeekFeature {
7
+ private readonly language;
8
+ private kind;
9
+ private targets;
10
+ private texts;
11
+ private places;
12
+ private order;
13
+ private line;
14
+ private token;
15
+ constructor(language: EditorLanguage);
16
+ get isOpen(): boolean;
17
+ open(position?: EditorPosition, kind?: PeekKind): Promise<void>;
18
+ openDefinition(position?: EditorPosition): Promise<void>;
19
+ private fetch;
20
+ close(): void;
21
+ select(id: string): void;
22
+ toggleFile(uri: string): void;
23
+ go(id: string): void;
24
+ private mounted;
25
+ private key;
26
+ private step;
27
+ private tell;
28
+ private say;
29
+ }
30
+ export {};
package/dist/peek.js ADDED
@@ -0,0 +1,215 @@
1
+ import { StaleResultError } from '@adecore/lsp';
2
+ import { uniquePlaces } from './navigation.js';
3
+ import { PEEK_READ_FILES, definitionSnippetOf, peekFilesOf, snippetOf } from './peek-model.js';
4
+ import { isShortcut } from './shortcut-keys.js';
5
+ const TOAST_ID = 'language-peek';
6
+ /* 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. */
7
+ export const PEEK_HEIGHT = 241;
8
+ const KINDS = {
9
+ references: { method: 'textDocument/references', unavailable: 'unavailable', none: 'none' },
10
+ definitions: { method: 'textDocument/definition', unavailable: 'unavailableDefinition', none: 'noDefinition' }
11
+ };
12
+ function placeKey(location) {
13
+ return `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`;
14
+ }
15
+ /*
16
+ * The references of the name at the caret, between the lines of the file under the line it is on. The
17
+ * editor keeps the keyboard: arrows pick a reference, Enter goes to it and Escape closes the peek; typing
18
+ * closes it too. A click on a place shows its code and a second one goes there.
19
+ */
20
+ export class PeekFeature {
21
+ language;
22
+ kind = 'references';
23
+ /* The whole declaration of each place of a definition peek, where the server gave it. */
24
+ targets = new Map();
25
+ texts = new Map();
26
+ places = new Map();
27
+ order = [];
28
+ line = 0;
29
+ token = 0;
30
+ constructor(language) {
31
+ this.language = language;
32
+ const { editor } = language;
33
+ const offs = [
34
+ editor.onKeyDown((event) => {
35
+ if (isShortcut(this.language.shortcuts.peekReferences, event)) {
36
+ void this.open();
37
+ return true;
38
+ }
39
+ if (isShortcut(this.language.shortcuts.peekDefinition, event)) {
40
+ void this.openDefinition();
41
+ return true;
42
+ }
43
+ return this.key(event);
44
+ }),
45
+ editor.onTextChange(() => this.close())
46
+ ];
47
+ language.onDispose(() => {
48
+ for (const off of offs) {
49
+ off();
50
+ }
51
+ this.close();
52
+ });
53
+ }
54
+ get isOpen() {
55
+ return this.language.popups.getState().peek !== null;
56
+ }
57
+ async open(position = this.language.editor.getCaret(), kind = 'references') {
58
+ const { editor, project, uri } = this.language;
59
+ const method = KINDS[kind].method;
60
+ if (!project.service.supports(method, uri)) {
61
+ this.tell(this.say(KINDS[kind].unavailable));
62
+ return;
63
+ }
64
+ const token = ++this.token;
65
+ let found;
66
+ try {
67
+ found = await this.fetch(kind, position);
68
+ }
69
+ catch (error) {
70
+ if (!(error instanceof StaleResultError)) {
71
+ this.tell(this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
72
+ }
73
+ return;
74
+ }
75
+ if (token !== this.token) {
76
+ return;
77
+ }
78
+ const { targets } = found;
79
+ const unique = found.locations.filter((location, index) => found.locations.findIndex((other) => other.uri === location.uri &&
80
+ other.range.start.line === location.range.start.line &&
81
+ other.range.start.character === location.range.start.character) === index);
82
+ if (unique.length === 0) {
83
+ this.tell(this.say(KINDS[kind].none));
84
+ return;
85
+ }
86
+ const uris = [...new Set(unique.map((location) => location.uri))].slice(0, PEEK_READ_FILES);
87
+ const texts = new Map();
88
+ await Promise.all(uris.map(async (target) => {
89
+ const text = await project.readText(target);
90
+ if (text !== null) {
91
+ texts.set(target, text);
92
+ }
93
+ }));
94
+ if (token !== this.token) {
95
+ return;
96
+ }
97
+ this.texts = texts;
98
+ this.kind = kind;
99
+ this.targets = targets;
100
+ const files = peekFilesOf(unique, uri, (target) => texts.get(target) ?? null);
101
+ this.places = new Map(files.flatMap((file) => file.places.map((place) => [place.id, place.location])));
102
+ this.order = files.flatMap((file) => file.places.map((place) => place.id));
103
+ // The references open on the first place that is not the one the caret is on, which is what a person came to see.
104
+ const first = kind === 'definitions'
105
+ ? files[0].places[0]
106
+ : (files.flatMap((file) => file.places).find((place) => !(place.location.uri === uri && place.line === position.line)) ?? files[0].places[0]);
107
+ this.line = position.line;
108
+ this.language.popups.setState({ peek: { kind, container: null, files, active: first.id, collapsed: [], count: unique.length, preview: null } });
109
+ this.select(first.id);
110
+ // The editor draws a row once per id, so a peek opened over another needs an id of its own to get a container.
111
+ editor.setWidgets([{ id: `peek-${token}`, line: this.line, height: PEEK_HEIGHT, render: (container) => this.mounted(container) }]);
112
+ // The keys come through the editor, which a click on the hover's link or a menu command has taken the focus from.
113
+ editor.focus();
114
+ }
115
+ /* The definition of the name at the caret, as its source between the lines of the file. */
116
+ openDefinition(position = this.language.editor.getCaret()) {
117
+ return this.open(position, 'definitions');
118
+ }
119
+ async fetch(kind, position) {
120
+ const { project, uri } = this.language;
121
+ if (kind === 'references') {
122
+ // Without the declaration, so the list holds what the usages count above a declaration counts.
123
+ return { locations: (await project.service.references(uri, position, false)) ?? [], targets: new Map() };
124
+ }
125
+ const places = uniquePlaces(await project.service.definition(uri, position));
126
+ const targets = new Map(places.flatMap(({ location, target }) => (target === null ? [] : [[placeKey(location), target]])));
127
+ return { locations: places.map(({ location }) => location), targets };
128
+ }
129
+ close() {
130
+ this.token++;
131
+ if (this.language.popups.getState().peek === null) {
132
+ return;
133
+ }
134
+ this.language.popups.setState({ peek: null });
135
+ this.language.editor.setWidgets([]);
136
+ this.language.editor.focus();
137
+ }
138
+ /* Shows the code around a place. */
139
+ select(id) {
140
+ const view = this.language.popups.getState().peek;
141
+ const location = this.places.get(id);
142
+ if (view === null || location === undefined) {
143
+ return;
144
+ }
145
+ const text = this.texts.get(location.uri);
146
+ const line = location.range.start.line;
147
+ const snippet = text === undefined
148
+ ? null
149
+ : this.kind === 'definitions'
150
+ ? definitionSnippetOf(text, line, this.targets.get(placeKey(location)) ?? null, location.range)
151
+ : snippetOf(text, line, location.range);
152
+ this.language.popups.setState({ peek: { ...view, active: id, preview: snippet === null ? null : { ...snippet, uri: location.uri } } });
153
+ }
154
+ /* Folds the places of a file away from the list, or brings them back. */
155
+ toggleFile(uri) {
156
+ const view = this.language.popups.getState().peek;
157
+ if (view !== null) {
158
+ const collapsed = view.collapsed.includes(uri) ? view.collapsed.filter((other) => other !== uri) : [...view.collapsed, uri];
159
+ this.language.popups.setState({ peek: { ...view, collapsed } });
160
+ }
161
+ }
162
+ /* Goes to a place and closes the peek. */
163
+ go(id) {
164
+ const location = this.places.get(id);
165
+ if (location !== undefined) {
166
+ this.close();
167
+ this.language.goTo(location);
168
+ }
169
+ }
170
+ mounted(container) {
171
+ const view = this.language.popups.getState().peek;
172
+ if (view !== null && view.container !== container) {
173
+ this.language.popups.setState({ peek: { ...view, container } });
174
+ }
175
+ }
176
+ key(event) {
177
+ const view = this.language.popups.getState().peek;
178
+ if (view === null || event.metaKey || event.ctrlKey || event.altKey) {
179
+ return false;
180
+ }
181
+ switch (event.key) {
182
+ case 'ArrowDown':
183
+ this.step(view, 1);
184
+ return true;
185
+ case 'ArrowUp':
186
+ this.step(view, -1);
187
+ return true;
188
+ case 'Enter':
189
+ this.go(view.active);
190
+ return true;
191
+ case 'Escape':
192
+ this.close();
193
+ return true;
194
+ default:
195
+ return false;
196
+ }
197
+ }
198
+ /* The next place in the list a person can see, which a folded file has none of. */
199
+ step(view, direction) {
200
+ const index = this.order.indexOf(view.active);
201
+ for (let next = index + direction; next >= 0 && next < this.order.length; next += direction) {
202
+ const id = this.order[next];
203
+ if (!view.collapsed.includes(this.places.get(id).uri)) {
204
+ this.select(id);
205
+ return;
206
+ }
207
+ }
208
+ }
209
+ tell(title) {
210
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title });
211
+ }
212
+ say(key, options) {
213
+ return this.language.project.i18n.t(`editor:language.peek.${key}`, options);
214
+ }
215
+ }
package/dist/pick.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { EditorPosition } from '@adecore/editor';
2
+ import type { EditorLanguage } from './editor-language.ts';
3
+ import type { PickGroup, PickPreview } from './popups.ts';
4
+ import { type Timers } from './timers.ts';
5
+ export interface PickSpec {
6
+ readonly anchor: EditorPosition;
7
+ readonly title?: string;
8
+ readonly groups: readonly PickGroup[];
9
+ readonly active?: string;
10
+ accept(id: string): void;
11
+ attempt?(id: string): Promise<string | null>;
12
+ preview?(id: string, signal: AbortSignal): Promise<PickPreview | null>;
13
+ }
14
+ export declare class PickFeature {
15
+ private readonly language;
16
+ private readonly timers;
17
+ private spec;
18
+ private ids;
19
+ private active;
20
+ private previewTimer;
21
+ private controller;
22
+ private refusals;
23
+ private attempting;
24
+ constructor(language: EditorLanguage, timers?: Timers);
25
+ get isOpen(): boolean;
26
+ open(spec: PickSpec): void;
27
+ close(): void;
28
+ move(delta: number): void;
29
+ choose(id: string): void;
30
+ private activate;
31
+ private key;
32
+ private schedulePreview;
33
+ private publish;
34
+ }
package/dist/pick.js ADDED
@@ -0,0 +1,166 @@
1
+ import { realTimers } from './timers.js';
2
+ const PREVIEW_DELAY_MS = 120;
3
+ /*
4
+ * The one list an editor has open under a character, for code actions, the places a name is defined and
5
+ * the like. The editor keeps the focus: arrows move through the rows, Enter takes the active one and
6
+ * Escape, typing, a click elsewhere or a change of the text close it.
7
+ */
8
+ export class PickFeature {
9
+ language;
10
+ timers;
11
+ spec = null;
12
+ ids = [];
13
+ active = '';
14
+ previewTimer;
15
+ controller = null;
16
+ /* The rows that said why they cannot be taken, by id. */
17
+ refusals = new Map();
18
+ attempting = false;
19
+ constructor(language, timers = realTimers) {
20
+ this.language = language;
21
+ this.timers = timers;
22
+ const { editor } = language;
23
+ const offs = [
24
+ editor.onKeyDown((event) => this.key(event)),
25
+ editor.onTextChange(() => this.close()),
26
+ editor.onCaret(() => this.close()),
27
+ editor.onBlur(() => this.close())
28
+ ];
29
+ language.onDispose(() => {
30
+ for (const off of offs) {
31
+ off();
32
+ }
33
+ this.close();
34
+ });
35
+ }
36
+ get isOpen() {
37
+ return this.spec !== null;
38
+ }
39
+ open(spec) {
40
+ this.close();
41
+ const ids = spec.groups.flatMap((group) => group.rows.map((row) => row.id));
42
+ if (ids.length === 0) {
43
+ return;
44
+ }
45
+ this.spec = spec;
46
+ this.refusals = new Map();
47
+ this.ids = ids;
48
+ this.active = spec.active !== undefined && ids.includes(spec.active) ? spec.active : ids[0];
49
+ this.publish(null);
50
+ this.schedulePreview();
51
+ }
52
+ close() {
53
+ if (this.spec === null) {
54
+ return;
55
+ }
56
+ this.spec = null;
57
+ this.timers.clear(this.previewTimer);
58
+ this.controller?.abort();
59
+ this.controller = null;
60
+ this.language.popups.setState({ pick: null });
61
+ }
62
+ move(delta) {
63
+ const index = this.ids.indexOf(this.active);
64
+ this.activate(this.ids[Math.max(0, Math.min(this.ids.length - 1, index + delta))]);
65
+ }
66
+ /* Takes a row, by a click or Enter. */
67
+ choose(id) {
68
+ const spec = this.spec;
69
+ if (spec === null || !this.ids.includes(id) || this.attempting) {
70
+ return;
71
+ }
72
+ if (spec.attempt === undefined) {
73
+ this.close();
74
+ spec.accept(id);
75
+ return;
76
+ }
77
+ this.attempting = true;
78
+ void spec
79
+ .attempt(id)
80
+ .catch(() => null)
81
+ .then((reason) => {
82
+ this.attempting = false;
83
+ if (this.spec !== spec) {
84
+ return;
85
+ }
86
+ if (reason === null) {
87
+ this.close();
88
+ spec.accept(id);
89
+ return;
90
+ }
91
+ this.refusals.set(id, reason);
92
+ this.publish(null);
93
+ });
94
+ }
95
+ activate(id) {
96
+ if (id === this.active) {
97
+ return;
98
+ }
99
+ this.active = id;
100
+ this.controller?.abort();
101
+ this.publish(null);
102
+ this.schedulePreview();
103
+ }
104
+ key(event) {
105
+ if (this.spec === null || event.metaKey || event.ctrlKey || event.altKey) {
106
+ return false;
107
+ }
108
+ switch (event.key) {
109
+ case 'ArrowDown':
110
+ this.move(1);
111
+ return true;
112
+ case 'ArrowUp':
113
+ this.move(-1);
114
+ return true;
115
+ case 'PageDown':
116
+ this.move(8);
117
+ return true;
118
+ case 'PageUp':
119
+ this.move(-8);
120
+ return true;
121
+ case 'Home':
122
+ this.activate(this.ids[0]);
123
+ return true;
124
+ case 'End':
125
+ this.activate(this.ids[this.ids.length - 1]);
126
+ return true;
127
+ case 'Enter':
128
+ case 'Tab':
129
+ this.choose(this.active);
130
+ return true;
131
+ case 'Escape':
132
+ this.close();
133
+ return true;
134
+ default:
135
+ return false;
136
+ }
137
+ }
138
+ schedulePreview() {
139
+ const spec = this.spec;
140
+ this.timers.clear(this.previewTimer);
141
+ if (spec?.preview === undefined) {
142
+ return;
143
+ }
144
+ const id = this.active;
145
+ this.previewTimer = this.timers.set(() => {
146
+ const controller = new AbortController();
147
+ this.controller = controller;
148
+ spec.preview(id, controller.signal)
149
+ .then((preview) => {
150
+ if (this.spec === spec && this.active === id && !controller.signal.aborted) {
151
+ this.publish(preview);
152
+ }
153
+ })
154
+ .catch(() => undefined);
155
+ }, PREVIEW_DELAY_MS);
156
+ }
157
+ publish(preview) {
158
+ const spec = this.spec;
159
+ if (spec === null) {
160
+ return;
161
+ }
162
+ const refusal = this.refusals.get(this.active);
163
+ const shown = refusal === undefined ? preview : { removed: [], added: [], note: null, refusal };
164
+ this.language.popups.setState({ pick: { anchor: spec.anchor, groups: spec.groups, active: this.active, preview: shown, title: spec.title ?? null } });
165
+ }
166
+ }
@@ -0,0 +1,23 @@
1
+ import type { EditorRect } from '@adecore/editor';
2
+ export interface PopupSize {
3
+ readonly width: number;
4
+ readonly height: number;
5
+ }
6
+ export interface PopupPlacement {
7
+ readonly left: number;
8
+ readonly top: number;
9
+ readonly maxHeight: number;
10
+ readonly side: 'above' | 'below';
11
+ }
12
+ export interface PlaceOptions {
13
+ gap?: number;
14
+ margin?: number;
15
+ prefer?: 'above' | 'below';
16
+ }
17
+ export declare function placePopup(anchor: EditorRect, size: PopupSize, window: PopupSize, options?: PlaceOptions): PopupPlacement;
18
+ export declare function placeBeside(first: {
19
+ left: number;
20
+ top: number;
21
+ width: number;
22
+ height: number;
23
+ }, size: PopupSize, window: PopupSize, margin?: number, gap?: number): PopupPlacement;
@@ -0,0 +1,34 @@
1
+ /*
2
+ * Where a popup goes next to a character: under it when it fits there, over it when it does not and
3
+ * there is room, and otherwise on the side with more room, shortened to fit. Aligned with the
4
+ * character's left edge, and pushed back in when that would leave the window. The anchor and the window
5
+ * are the page's own pixels, so a canvas's scale never enters.
6
+ */
7
+ export function placePopup(anchor, size, window, options = {}) {
8
+ const gap = options.gap ?? 4;
9
+ const margin = options.margin ?? 8;
10
+ const roomBelow = window.height - margin - (anchor.bottom + gap);
11
+ const roomAbove = anchor.top - gap - margin;
12
+ const fitsBelow = size.height <= roomBelow;
13
+ const fitsAbove = size.height <= roomAbove;
14
+ const below = options.prefer === 'above' ? !fitsAbove && (fitsBelow || roomBelow >= roomAbove) : fitsBelow || (!fitsAbove && roomBelow >= roomAbove);
15
+ const maxHeight = Math.max(0, below ? roomBelow : roomAbove);
16
+ const height = Math.min(size.height, maxHeight);
17
+ const left = Math.max(margin, Math.min(anchor.left, window.width - size.width - margin));
18
+ return { left, top: below ? anchor.bottom + gap : anchor.top - gap - height, maxHeight, side: below ? 'below' : 'above' };
19
+ }
20
+ /*
21
+ * A second box beside a first, such as the documentation of the highlighted suggestion: to the right
22
+ * of it when it fits, to the left when only that fits, and under it, at its left edge, when neither does.
23
+ */
24
+ export function placeBeside(first, size, window, margin = 8, gap = 4) {
25
+ const fitsRight = first.left + first.width + gap + size.width <= window.width - margin;
26
+ const fitsLeft = first.left - gap - size.width >= margin;
27
+ if (fitsRight || fitsLeft) {
28
+ const top = Math.max(margin, Math.min(first.top, window.height - size.height - margin));
29
+ return { left: fitsRight ? first.left + first.width + gap : first.left - gap - size.width, top, maxHeight: window.height - margin * 2, side: 'below' };
30
+ }
31
+ const top = first.top + first.height + gap;
32
+ const maxHeight = Math.max(0, window.height - margin - top);
33
+ return { left: Math.max(margin, Math.min(first.left, window.width - size.width - margin)), top, maxHeight, side: 'below' };
34
+ }