@adecore/editor-react 0.0.1 → 0.17.0-beta.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 (264) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +55 -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 +15 -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 +4 -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 +29 -0
  44. package/dist/code-actions.js +329 -0
  45. package/dist/code-authors.d.ts +17 -0
  46. package/dist/code-authors.js +80 -0
  47. package/dist/code-vision-model.d.ts +20 -0
  48. package/dist/code-vision-model.js +184 -0
  49. package/dist/code-vision.d.ts +48 -0
  50. package/dist/code-vision.js +234 -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 +464 -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 +64 -0
  68. package/dist/editor-language.js +148 -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 +97 -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 +47 -0
  80. package/dist/holder.d.ts +5 -0
  81. package/dist/holder.js +19 -0
  82. package/dist/host-types.d.ts +43 -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 +261 -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 +73 -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 +22 -0
  103. package/dist/navigation.js +156 -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 +29 -0
  109. package/dist/peek.js +216 -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 +34 -0
  117. package/dist/project-language.js +187 -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 +20 -0
  131. package/dist/rename.js +258 -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 +40 -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 +77 -0
  162. package/dist/symbols.d.ts +11 -0
  163. package/dist/symbols.js +97 -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 +45 -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 +61 -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 +387 -0
  198. package/src/code-authors.ts +104 -0
  199. package/src/code-vision-model.ts +230 -0
  200. package/src/code-vision.ts +267 -0
  201. package/src/completion-call.ts +139 -0
  202. package/src/completion-model.ts +440 -0
  203. package/src/completion.ts +501 -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 +163 -0
  210. package/src/editor-react.css +43 -0
  211. package/src/find-query.ts +74 -0
  212. package/src/folding.ts +112 -0
  213. package/src/highlight-layers.ts +37 -0
  214. package/src/highlights.ts +123 -0
  215. package/src/history.ts +57 -0
  216. package/src/holder.ts +19 -0
  217. package/src/host-types.ts +49 -0
  218. package/src/hover-content.ts +264 -0
  219. package/src/hover.ts +281 -0
  220. package/src/index.ts +63 -0
  221. package/src/inlay-hints.ts +87 -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 +169 -0
  229. package/src/paths.ts +3 -0
  230. package/src/peek-model.ts +147 -0
  231. package/src/peek.ts +248 -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 +220 -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 +291 -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 +48 -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 +86 -0
  258. package/src/symbols.ts +112 -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
package/src/rename.ts ADDED
@@ -0,0 +1,291 @@
1
+ import i18next from 'i18next';
2
+ import { StaleResultError, type Location, type PrepareRenameResult, type WorkspaceEdit } from '@adecore/lsp';
3
+ import type { EditorRange } from '@adecore/editor';
4
+ import type { EditorLanguage } from './editor-language.ts';
5
+ import type { RenameView } from './popups.ts';
6
+ import { occurrencesOf, renamePreviewOf, renameTargetOf, wordRangeAt, type RenameTarget } from './rename-model.ts';
7
+ import { isShortcut } from './shortcut-keys.ts';
8
+
9
+ const METHOD = 'textDocument/rename';
10
+ const TOAST_ID = 'language-rename';
11
+ /* How many names are offered, and how many other lines and files are read to say how the symbol is used. */
12
+ const SAMPLE_LOCATIONS = 8;
13
+ const SAMPLE_OTHER_FILES = 3;
14
+ const CONTEXT_LINES = 3;
15
+
16
+ function say(key: string, options?: Record<string, unknown>): string {
17
+ return i18next.t(`editor:language.rename.${key}`, options);
18
+ }
19
+
20
+ interface Session {
21
+ readonly target: RenameTarget;
22
+ readonly original: string;
23
+ /* Set once the preview asked for the edit, so Enter applies exactly what was shown. */
24
+ edit: WorkspaceEdit | null;
25
+ /* The request for names, which ends with the rename. */
26
+ readonly names: AbortController;
27
+ }
28
+
29
+ /*
30
+ * Renaming a symbol. The input sits on the symbol itself, every place the name has lights up while the
31
+ * new one is typed, Enter applies the server's edit at once and Shift+Enter lists what it would change first.
32
+ * Edits to other files land as unsaved drafts (`applyWorkspaceEdit`), so nothing is written to disk here.
33
+ */
34
+ export class RenameFeature {
35
+ private tell(kind: 'success' | 'error', title: string): void {
36
+ this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
37
+ }
38
+
39
+ private readonly language: EditorLanguage;
40
+ private session: Session | null = null;
41
+
42
+ constructor(language: EditorLanguage) {
43
+ this.language = language;
44
+ const { editor } = language;
45
+ const offs = [
46
+ editor.onKeyDown((event) => {
47
+ if (!isShortcut(this.language.shortcuts.rename, event)) {
48
+ return false;
49
+ }
50
+ void this.start();
51
+ return true;
52
+ }),
53
+ editor.onTextChange(() => this.cancel())
54
+ ];
55
+ language.onDispose(() => {
56
+ for (const off of offs) {
57
+ off();
58
+ }
59
+ this.cancel();
60
+ });
61
+ }
62
+
63
+ get isOpen(): boolean {
64
+ return this.session !== null;
65
+ }
66
+
67
+ async start(): Promise<void> {
68
+ const { editor, project, uri } = this.language;
69
+ if (!project.service.supports(METHOD, uri)) {
70
+ this.tell('error', say('unavailable'));
71
+ return;
72
+ }
73
+ this.cancel();
74
+ const caret = editor.getCaret();
75
+ const prepare = project.service.providerOptions(METHOD, uri)?.prepareProvider === true;
76
+ let answer: PrepareRenameResult;
77
+ try {
78
+ answer = prepare ? await project.service.prepareRename(uri, caret) : { defaultBehavior: true };
79
+ } catch (error) {
80
+ if (!(error instanceof StaleResultError)) {
81
+ this.tell('error', say('nothing'));
82
+ }
83
+ return;
84
+ }
85
+ const target = renameTargetOf(
86
+ answer,
87
+ (range) => editor.textInRange(range),
88
+ () =>
89
+ wordRangeAt(
90
+ editor.textInRange({ start: { line: caret.line, character: 0 }, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } }),
91
+ caret
92
+ )
93
+ );
94
+ if (target === null) {
95
+ this.tell('error', say('nothing'));
96
+ return;
97
+ }
98
+ const session: Session = { target, original: editor.textInRange(target.range), edit: null, names: new AbortController() };
99
+ this.session = session;
100
+ this.publish({ phase: 'input', busy: false, name: target.placeholder, files: [], occurrences: null, suggestions: [] });
101
+ void this.light(session).then((locations) => this.suggest(session, locations));
102
+ }
103
+
104
+ /* The name was typed: Enter applies it, Shift+Enter lists what it would change first. */
105
+ async submit(name: string, preview: boolean): Promise<void> {
106
+ const session = this.session;
107
+ const next = name.trim();
108
+ if (session === null) {
109
+ return;
110
+ }
111
+ if (next === '' || next === session.original) {
112
+ this.finish();
113
+ return;
114
+ }
115
+ this.publish({ phase: 'input', busy: true, name: next, error: null });
116
+ const { project, uri } = this.language;
117
+ try {
118
+ const edit = await project.service.rename(uri, session.target.range.start, next);
119
+ if (this.session !== session) {
120
+ return;
121
+ }
122
+ if (edit === null) {
123
+ this.fail(say('refused'));
124
+ return;
125
+ }
126
+ session.edit = edit;
127
+ if (!preview) {
128
+ await this.apply(session);
129
+ return;
130
+ }
131
+ const files = await renamePreviewOf(edit, (target) => project.readText(target));
132
+ if (this.session !== session) {
133
+ return;
134
+ }
135
+ if (files === null) {
136
+ // An edit that renames files too has no list of lines to show, so it is applied or refused as a whole.
137
+ await this.apply(session);
138
+ return;
139
+ }
140
+ this.publish({ phase: 'preview', busy: false, name: next, files });
141
+ } catch (error) {
142
+ if (this.session === session) {
143
+ this.fail(error instanceof StaleResultError ? say('stale') : error instanceof Error ? error.message : String(error));
144
+ }
145
+ }
146
+ }
147
+
148
+ /* Applies the edit the preview showed. */
149
+ async confirm(): Promise<void> {
150
+ const session = this.session;
151
+ if (session?.edit) {
152
+ await this.apply(session);
153
+ }
154
+ }
155
+
156
+ cancel(): void {
157
+ if (this.session === null) {
158
+ return;
159
+ }
160
+ this.finish();
161
+ }
162
+
163
+ private async apply(session: Session): Promise<void> {
164
+ const result = await this.language.project.applyWorkspaceEdit(session.edit!);
165
+ if (this.session !== session) {
166
+ return;
167
+ }
168
+ if (result.applied) {
169
+ this.finish();
170
+ } else {
171
+ this.fail(result.failureReason ?? say('refused'));
172
+ }
173
+ }
174
+
175
+ /* The input stays, with the reason under it, so the name can be changed and tried again. */
176
+ private fail(message: string): void {
177
+ if (this.session !== null) {
178
+ this.session.edit = null;
179
+ this.publish({ phase: 'input', busy: false, files: [], error: say('failed', { message }) });
180
+ }
181
+ }
182
+
183
+ /* The name was changed, which answers the failure under it. */
184
+ edited(): void {
185
+ if (this.language.popups.getState().rename?.error != null) {
186
+ this.publish({ error: null });
187
+ }
188
+ }
189
+
190
+ private finish(): void {
191
+ this.session?.names.abort();
192
+ this.session = null;
193
+ const { editor, popups } = this.language;
194
+ popups.setState({ rename: null });
195
+ editor.setHighlights([]);
196
+ editor.focus();
197
+ }
198
+
199
+ /* Lights up the places the name has in this file, and counts them all; what the servers listed, or null when they did not. */
200
+ private async light(session: Session): Promise<readonly Location[] | null> {
201
+ const { editor, project, uri } = this.language;
202
+ if (!project.service.supports('textDocument/references', uri)) {
203
+ return null;
204
+ }
205
+ try {
206
+ const locations = await project.service.references(uri, session.target.range.start, true);
207
+ if (this.session !== session || locations === null) {
208
+ return null;
209
+ }
210
+ const found = occurrencesOf(locations, uri);
211
+ editor.setHighlights(found.inFile.map((range: EditorRange) => ({ range, kind: 'write' as const })));
212
+ this.publish({ occurrences: { count: found.count, files: found.files } });
213
+ return locations;
214
+ } catch {
215
+ // The count is a courtesy; renaming does not wait for it.
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /*
221
+ * Names from the host suggestion provider, using the lines around the symbol and a sample of the lines that
222
+ * use it. They only appear under the input: the name that is typed stays the one that is applied.
223
+ */
224
+ private async suggest(session: Session, locations: readonly Location[] | null): Promise<void> {
225
+ const suggestNames = this.language.project.host.suggestNames;
226
+ if (suggestNames === undefined || this.session !== session) {
227
+ return;
228
+ }
229
+ const { editor, languageId, uri } = this.language;
230
+ try {
231
+ const line = session.target.range.start.line;
232
+ const context = editor.textInRange({
233
+ start: { line: Math.max(0, line - CONTEXT_LINES), character: 0 },
234
+ end: { line: line + CONTEXT_LINES, character: Number.MAX_SAFE_INTEGER }
235
+ });
236
+ const uses = await this.linesOf(locations ?? []);
237
+ const names = await suggestNames({ uri, languageId, name: session.original, context, uses }, session.names.signal);
238
+ if (this.session === session && names.length > 0) {
239
+ this.publish({ suggestions: names });
240
+ }
241
+ } catch {
242
+ // Suggestions do not prevent a manual rename.
243
+ }
244
+ }
245
+
246
+ /* The text of the first lines that name the symbol, in the order the servers listed them, reading at most a few other files. */
247
+ private async linesOf(locations: readonly Location[]): Promise<string[]> {
248
+ const { project } = this.language;
249
+ const texts = new Map<string, string[] | null>();
250
+ const lines: string[] = [];
251
+ let others = 0;
252
+ for (const location of locations.slice(0, SAMPLE_LOCATIONS)) {
253
+ if (!texts.has(location.uri)) {
254
+ if (location.uri !== this.language.uri && others++ >= SAMPLE_OTHER_FILES) {
255
+ continue;
256
+ }
257
+ texts.set(location.uri, (await project.readText(location.uri))?.split('\n') ?? null);
258
+ }
259
+ const text = texts.get(location.uri)?.[location.range.start.line];
260
+ if (text !== undefined && text.trim() !== '' && !lines.includes(text)) {
261
+ lines.push(text);
262
+ }
263
+ }
264
+ return lines;
265
+ }
266
+
267
+ private publish(patch: Partial<RenameView>): void {
268
+ const session = this.session;
269
+ if (session === null) {
270
+ return;
271
+ }
272
+ const { popups } = this.language;
273
+ const current = popups.getState().rename;
274
+ popups.setState({
275
+ rename: {
276
+ phase: 'input',
277
+ range: session.target.range,
278
+ original: session.original,
279
+ placeholder: session.target.placeholder,
280
+ occurrences: null,
281
+ busy: false,
282
+ error: null,
283
+ name: session.target.placeholder,
284
+ files: [],
285
+ suggestions: [],
286
+ ...current,
287
+ ...patch
288
+ }
289
+ });
290
+ }
291
+ }
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export interface EditorRendering {
4
+ readonly theme: string;
5
+ highlight?(code: string, language: string, theme: string): Promise<string>;
6
+ }
7
+
8
+ export const RenderingContext = createContext<EditorRendering>({ theme: 'light' });
9
+
10
+ export function useEditorRendering(): EditorRendering {
11
+ return useContext(RenderingContext);
12
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ import { RenderingContext, type EditorRendering } from './rendering-context.ts';
3
+
4
+ export function EditorRenderingProvider({ value, children }: { value: EditorRendering; children: ReactNode }) {
5
+ return <RenderingContext value={value}>{children}</RenderingContext>;
6
+ }
@@ -0,0 +1,70 @@
1
+ /* What the review of one editor does for the other editors that show the same file. */
2
+ export interface ReviewMember {
3
+ /* Optimistically hides runs until the host answers. */
4
+ hide(runIds: readonly string[]): void;
5
+ /* Restores runs after a refused operation. */
6
+ show(runIds: readonly string[]): void;
7
+ /* Refreshes runs after an operation. */
8
+ refresh(): void;
9
+ }
10
+
11
+ /*
12
+ * The editors of one file in this window. An answer in one of them is an answer for all: the rows of
13
+ * the others go with it at once and every one of them reads the runs again, so no editor keeps a
14
+ * change in front of a person that the other already took.
15
+ */
16
+ export class ReviewGroup {
17
+ private readonly members = new Set<ReviewMember>();
18
+
19
+ join(member: ReviewMember): () => void {
20
+ this.members.add(member);
21
+ return () => {
22
+ this.members.delete(member);
23
+ };
24
+ }
25
+
26
+ hide(runIds: readonly string[]): void {
27
+ for (const member of [...this.members]) {
28
+ member.hide(runIds);
29
+ }
30
+ }
31
+
32
+ show(runIds: readonly string[]): void {
33
+ for (const member of [...this.members]) {
34
+ member.show(runIds);
35
+ }
36
+ }
37
+
38
+ refresh(): void {
39
+ for (const member of [...this.members]) {
40
+ member.refresh();
41
+ }
42
+ }
43
+ }
44
+
45
+ const groups = new Map<string, { group: ReviewGroup; holders: number }>();
46
+
47
+ /* The host supplies a shared file key; the group is released with its last editor. */
48
+ export function joinReviewGroup(key: string): { group: ReviewGroup; leave(): void } {
49
+ let entry = groups.get(key);
50
+ if (entry === undefined) {
51
+ entry = { group: new ReviewGroup(), holders: 0 };
52
+ groups.set(key, entry);
53
+ }
54
+ entry.holders++;
55
+ const held = entry;
56
+ let left = false;
57
+ return {
58
+ group: held.group,
59
+ leave: () => {
60
+ if (left) {
61
+ return;
62
+ }
63
+ left = true;
64
+ held.holders--;
65
+ if (held.holders === 0 && groups.get(key) === held) {
66
+ groups.delete(key);
67
+ }
68
+ }
69
+ };
70
+ }
@@ -0,0 +1,158 @@
1
+ import type { Editor } from '@adecore/editor';
2
+
3
+ export interface HostedRow {
4
+ id: string;
5
+ /* Zero-based. */
6
+ line: number;
7
+ placement: 'above' | 'below';
8
+ height: number;
9
+ }
10
+
11
+ /*
12
+ * The rows of one owner in an editor, and the elements the editor draws them in, so React can portal
13
+ * into them. The editor makes an element again whenever its row scrolls back into view, which is why a
14
+ * row's content never keeps state of its own.
15
+ */
16
+ export class RowHost {
17
+ private readonly editor: Editor;
18
+ private readonly owner: string;
19
+ private readonly containers = new Map<string, HTMLElement>();
20
+ private readonly listeners = new Set<() => void>();
21
+ private version = 0;
22
+ private signature = '';
23
+
24
+ constructor(editor: Editor, owner: string) {
25
+ this.editor = editor;
26
+ this.owner = owner;
27
+ }
28
+
29
+ getVersion = (): number => this.version;
30
+
31
+ subscribe = (listener: () => void): (() => void) => {
32
+ this.listeners.add(listener);
33
+ return () => {
34
+ this.listeners.delete(listener);
35
+ };
36
+ };
37
+
38
+ container(id: string): HTMLElement | undefined {
39
+ return this.containers.get(id);
40
+ }
41
+
42
+ /* Sets the owner's rows; the same rows again are left alone, so an editor does not measure them twice. */
43
+ set(rows: readonly HostedRow[]): void {
44
+ const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
45
+ if (signature === this.signature) {
46
+ return;
47
+ }
48
+ this.signature = signature;
49
+ const kept = new Set(rows.map((row) => row.id));
50
+ for (const id of [...this.containers.keys()]) {
51
+ if (!kept.has(id)) {
52
+ this.containers.delete(id);
53
+ }
54
+ }
55
+ this.editor.setWidgets(
56
+ rows.map((row) => ({
57
+ id: row.id,
58
+ line: row.line,
59
+ placement: row.placement,
60
+ height: row.height,
61
+ render: (container: HTMLElement) => this.mounted(row.id, container)
62
+ })),
63
+ this.owner
64
+ );
65
+ this.bump();
66
+ }
67
+
68
+ clear(): void {
69
+ this.set([]);
70
+ }
71
+
72
+ private mounted(id: string, container: HTMLElement): void {
73
+ this.containers.set(id, container);
74
+ this.bump();
75
+ }
76
+
77
+ private bump(): void {
78
+ this.version++;
79
+ for (const listener of [...this.listeners]) {
80
+ listener();
81
+ }
82
+ }
83
+ }
84
+
85
+ export interface HostedAction {
86
+ id: string;
87
+ /* Zero-based. */
88
+ line: number;
89
+ }
90
+
91
+ /*
92
+ * The actions of one owner after the end of lines in an editor, and the elements the editor makes for
93
+ * them. An element lives as long as its action does, unlike a row's, so what React draws into it keeps
94
+ * its state.
95
+ */
96
+ export class LineActionHost {
97
+ private readonly editor: Editor;
98
+ private readonly owner: string;
99
+ private readonly containers = new Map<string, HTMLElement>();
100
+ private readonly listeners = new Set<() => void>();
101
+ private version = 0;
102
+ private signature = '';
103
+
104
+ constructor(editor: Editor, owner: string) {
105
+ this.editor = editor;
106
+ this.owner = owner;
107
+ }
108
+
109
+ getVersion = (): number => this.version;
110
+
111
+ subscribe = (listener: () => void): (() => void) => {
112
+ this.listeners.add(listener);
113
+ return () => {
114
+ this.listeners.delete(listener);
115
+ };
116
+ };
117
+
118
+ container(id: string): HTMLElement | undefined {
119
+ return this.containers.get(id);
120
+ }
121
+
122
+ set(actions: readonly HostedAction[]): void {
123
+ const signature = actions.map((action) => `${action.id}@${action.line}`).join('|');
124
+ if (signature === this.signature) {
125
+ return;
126
+ }
127
+ this.signature = signature;
128
+ const kept = new Set(actions.map((action) => action.id));
129
+ for (const id of [...this.containers.keys()]) {
130
+ if (!kept.has(id)) {
131
+ this.containers.delete(id);
132
+ }
133
+ }
134
+ this.editor.setLineActions(
135
+ actions.map((action) => ({
136
+ id: action.id,
137
+ line: action.line,
138
+ render: (container: HTMLElement) => {
139
+ this.containers.set(action.id, container);
140
+ this.bump();
141
+ }
142
+ })),
143
+ this.owner
144
+ );
145
+ this.bump();
146
+ }
147
+
148
+ clear(): void {
149
+ this.set([]);
150
+ }
151
+
152
+ private bump(): void {
153
+ this.version++;
154
+ for (const listener of [...this.listeners]) {
155
+ listener();
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,32 @@
1
+ import type { EditorRange } from '@adecore/editor';
2
+ import type { SelectionRange } from '@adecore/lsp';
3
+ import type { EditorLanguage } from './editor-language.ts';
4
+
5
+ const METHOD = 'textDocument/selectionRange';
6
+
7
+ /* The links of one answer, the smallest range first. */
8
+ export function selectionChainOf(selection: SelectionRange): EditorRange[] {
9
+ const chain: EditorRange[] = [];
10
+ for (let link: SelectionRange | undefined = selection; link; link = link.parent) {
11
+ chain.push(link.range);
12
+ }
13
+ return chain;
14
+ }
15
+
16
+ /*
17
+ * Extend and Shrink Selection grow through the ranges the language server knows around the caret, which
18
+ * follow the syntax of the file. Where no server offers them the editor grows by its own rule.
19
+ */
20
+ export class SelectionRangesFeature {
21
+ constructor(language: EditorLanguage) {
22
+ const { editor, project, uri } = language;
23
+ editor.setSelectionRanges(async (positions) => {
24
+ if (!project.service.supports(METHOD, uri)) {
25
+ return null;
26
+ }
27
+ const chains = await project.service.selectionRanges(uri, positions);
28
+ return chains === null || chains.length !== positions.length ? null : chains.map(selectionChainOf);
29
+ });
30
+ language.onDispose(() => editor.setSelectionRanges(null));
31
+ }
32
+ }
@@ -0,0 +1,80 @@
1
+ import type { EditorSemanticToken } from '@adecore/editor';
2
+ import type { SemanticTokens, SemanticTokensLegend } from '@adecore/lsp';
3
+
4
+ /*
5
+ * The TextMate scopes a classification stands for, so the editor's theme colors it the way its own
6
+ * grammar colors the same thing. Null where the grammar already did better: a keyword, a string, a
7
+ * local variable. A function being called, a method, a static member and a declaration are scopes of
8
+ * their own, as the platform's color scheme tells them apart. `scoped` says the server marks what is
9
+ * local to a function, so a variable it leaves unmarked lives at the top of a file or comes from a
10
+ * library, which the scheme draws as a global.
11
+ */
12
+ export function scopesOf(type: string, modifiers: ReadonlySet<string>, scoped = false): string[] | null {
13
+ switch (type) {
14
+ case 'namespace':
15
+ return ['entity.name.namespace'];
16
+ case 'class':
17
+ return ['entity.name.type.class'];
18
+ case 'enum':
19
+ return ['entity.name.type.enum'];
20
+ case 'interface':
21
+ return ['entity.name.type.interface'];
22
+ case 'struct':
23
+ return ['entity.name.type.struct'];
24
+ case 'type':
25
+ return ['entity.name.type'];
26
+ case 'typeParameter':
27
+ return ['entity.name.type.parameter'];
28
+ case 'parameter':
29
+ return ['variable.parameter'];
30
+ case 'property':
31
+ return [modifiers.has('static') ? 'variable.other.property.static' : 'variable.other.property'];
32
+ case 'enumMember':
33
+ return ['variable.other.enummember'];
34
+ case 'function':
35
+ case 'method':
36
+ if (modifiers.has('static')) {
37
+ return ['entity.name.function.static'];
38
+ }
39
+ if (modifiers.has('declaration')) {
40
+ return ['entity.name.function'];
41
+ }
42
+ return [type === 'method' ? 'entity.name.function.method' : 'entity.name.function.call'];
43
+ case 'decorator':
44
+ return ['meta.decorator'];
45
+ case 'macro':
46
+ return ['entity.name.function.macro'];
47
+ case 'variable':
48
+ if (scoped) {
49
+ return modifiers.has('local') ? null : ['variable.other.constant'];
50
+ }
51
+ return modifiers.has('readonly') ? ['variable.other.constant'] : null;
52
+ default:
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /* The encoded data of `textDocument/semanticTokens` as one token per classified range, the ones the theme has nothing to say about left out. */
58
+ export function decodeSemanticTokens(tokens: SemanticTokens, legend: SemanticTokensLegend): EditorSemanticToken[] {
59
+ const result: EditorSemanticToken[] = [];
60
+ const { data } = tokens;
61
+ const scoped = legend.tokenModifiers.includes('local');
62
+ let line = 0;
63
+ let character = 0;
64
+ for (let at = 0; at + 4 < data.length; at += 5) {
65
+ const deltaLine = data[at]!;
66
+ line += deltaLine;
67
+ character = deltaLine === 0 ? character + data[at + 1]! : data[at + 1]!;
68
+ const type = legend.tokenTypes[data[at + 3]!];
69
+ if (type === undefined) {
70
+ continue;
71
+ }
72
+ const bits = data[at + 4]!;
73
+ const modifiers = new Set(legend.tokenModifiers.filter((_, index) => (bits & (1 << index)) !== 0));
74
+ const scopes = scopesOf(type, modifiers, scoped);
75
+ if (scopes !== null) {
76
+ result.push({ line, character, length: data[at + 2]!, scopes });
77
+ }
78
+ }
79
+ return result;
80
+ }