@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,463 @@
1
+ import { StaleResultError } from '@adecore/lsp';
2
+ import { comparePositions, shiftPosition } from './diagnostics-model.js';
3
+ import { completionDocsOf, identifierPrefix, insertionOf, isIdentifierCharacter, itemsOf, matchedCharacters, mirroredInsertions, prefixFor, qualifiersOf, rankCompletions } from './completion-model.js';
4
+ import { PARAMETER_HINTS_COMMAND, isCallItem, planCall, withParentheses } from './completion-call.js';
5
+ import { RecentChoices, recentChoicesOf } from './recent-choices.js';
6
+ import { shikiLanguageOf } from './language-ids.js';
7
+ import { isShortcut } from './shortcut-keys.js';
8
+ import { realTimers } from './timers.js';
9
+ const METHOD = 'textDocument/completion';
10
+ const START_DELAY_MS = 80;
11
+ /* After typing stops, the list is asked for again at the new text: a server only resolves an item for the text it was made for. */
12
+ const REFRESH_DELAY_MS = 150;
13
+ const RESOLVE_DELAY_MS = 120;
14
+ const PAGE = 8;
15
+ const LINES_BACK = 60;
16
+ const TOAST_ID = 'language-completion';
17
+ function isEmpty(range) {
18
+ return comparePositions(range.start, range.end) === 0;
19
+ }
20
+ /*
21
+ * The suggestions that open while a word is typed. A single typed identifier character or a trigger
22
+ * character of the server asks for the list; every character after that only filters the list that
23
+ * is already there, unless the server called it incomplete. Enter inserts, Tab replaces, Escape closes.
24
+ */
25
+ export class CompletionFeature {
26
+ language;
27
+ timers;
28
+ pool = [];
29
+ incomplete = false;
30
+ ranked = [];
31
+ active = 0;
32
+ /* Whether the person moved in the list; until then the active row is the best match, whatever the typing does to the list. */
33
+ touched = false;
34
+ activeKey = null;
35
+ /* Asked for with the keyboard, which is as good as moving in the list when a commit character is typed. */
36
+ explicit = false;
37
+ detailsOpen = true;
38
+ start = null;
39
+ server = '';
40
+ request = 0;
41
+ controller = null;
42
+ requestTimer;
43
+ refreshTimer;
44
+ resolveTimer;
45
+ resolved = new Map();
46
+ constructor(language, timers = realTimers) {
47
+ this.language = language;
48
+ this.timers = timers;
49
+ const { editor } = language;
50
+ const offs = [
51
+ editor.onKeyDown((event) => this.key(event)),
52
+ editor.onTextChange((change) => this.edited(change)),
53
+ editor.onCaret((position) => this.caretMoved(position)),
54
+ editor.onBlur(() => this.close())
55
+ ];
56
+ language.onDispose(() => {
57
+ for (const off of offs) {
58
+ off();
59
+ }
60
+ this.close();
61
+ });
62
+ }
63
+ get isOpen() {
64
+ return this.language.popups.getState().completion !== null;
65
+ }
66
+ /* Asks for suggestions at the caret now, as Ctrl+Space does. */
67
+ invoke() {
68
+ this.explicit = true;
69
+ this.beginRequest({ triggerKind: 1 }, 0);
70
+ }
71
+ move(delta) {
72
+ if (this.ranked.length === 0) {
73
+ return;
74
+ }
75
+ const last = this.ranked.length - 1;
76
+ // One step past either end goes round, a page does not.
77
+ this.active = Math.abs(delta) === 1 ? (this.active + delta + last + 1) % (last + 1) : Math.max(0, Math.min(last, this.active + delta));
78
+ this.touched = true;
79
+ this.publish();
80
+ this.scheduleResolve();
81
+ }
82
+ /* Picks a row by a click or a key. */
83
+ async accept(replace, index = this.active, commit) {
84
+ const entry = this.ranked[index];
85
+ if (entry === undefined) {
86
+ return;
87
+ }
88
+ const { editor, project, uri } = this.language;
89
+ recentChoicesOf(this.language.languageId).record(entry.item);
90
+ let item = this.resolved.get(entry.item) ?? entry.item;
91
+ const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
92
+ this.close();
93
+ if (needsResolve) {
94
+ // Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
95
+ item = await this.resolveNow(entry.item);
96
+ }
97
+ const caret = editor.getCaret();
98
+ const lineBefore = editor.textInRange({ start: { line: caret.line, character: 0 }, end: caret });
99
+ const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
100
+ // A character that commits is typed after the name, so a call is not added before it.
101
+ const planned = insertionOf(item, caret, lineBefore, replace, commit === undefined ? lineAfter : '(');
102
+ const { before, rest } = this.surroundings(planned, caret, lineAfter);
103
+ const plan = commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
104
+ const main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
105
+ const extras = (item.additionalTextEdits ?? []).map((edit) => ({ range: edit.range, text: edit.newText }));
106
+ // An import above the insertion moves it, and the tab stops are measured from where it ends up.
107
+ const start = extras
108
+ .filter((extra) => comparePositions(extra.range.end, main.range.start) <= 0)
109
+ .sort((left, right) => comparePositions(right.range.start, left.range.start))
110
+ .reduce((place, extra) => shiftPosition(place, extra), main.range.start);
111
+ // A name typed at several carets is completed at all of them, and the other carets end behind what was put in.
112
+ const carets = editor.getSelections();
113
+ const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, (range) => editor.textInRange(range), main);
114
+ editor.applyEdits([{ range: main.range, text: main.text }, ...mirrored, ...extras]);
115
+ const landed = editor.getSelections();
116
+ if (commit === undefined) {
117
+ this.language.snippets.begin(start, main.text, main.stops);
118
+ if (plan.action === 'add' && plan.inside && mirrored.length > 0) {
119
+ this.placeMirroredCarets(carets, mirrored, landed);
120
+ }
121
+ if (plan.action === 'enter') {
122
+ const end = editor.getCaret();
123
+ editor.setCaret({ line: end.line, character: end.character + plan.shift });
124
+ }
125
+ }
126
+ else {
127
+ const end = editor.getCaret();
128
+ editor.applyEdits([{ range: { start: end, end }, text: commit }]);
129
+ if (this.triggerCharacters().includes(commit)) {
130
+ this.beginRequest({ triggerKind: 2, triggerCharacter: commit }, 0);
131
+ }
132
+ }
133
+ this.afterInsert(item, commit, lineBefore);
134
+ editor.focus();
135
+ }
136
+ /* The carets that got the same `()` go inside it like the primary one, which the snippet already placed. */
137
+ placeMirroredCarets(before, mirrored, landed) {
138
+ const { editor } = this.language;
139
+ const primary = before.length - 1;
140
+ const selections = landed.map((range, index) => {
141
+ const caret = before[index].start;
142
+ const got = mirrored.some((edit) => edit.range.start.line === caret.line && edit.range.start.character <= caret.character && caret.character <= edit.range.end.character);
143
+ if (index === primary || !got) {
144
+ return range;
145
+ }
146
+ const stop = { line: range.end.line, character: range.end.character - 1 };
147
+ return { start: stop, end: stop };
148
+ });
149
+ selections[primary] = editor.getSelection();
150
+ editor.setSelections(selections);
151
+ }
152
+ /* The lines above the insertion down to where it starts, and the rest of the line past what it replaces. */
153
+ surroundings(insertion, caret, lineAfter) {
154
+ const { editor } = this.language;
155
+ const { start, end } = insertion.range;
156
+ const before = editor.textInRange({ start: { line: Math.max(0, start.line - LINES_BACK), character: 0 }, end: start });
157
+ const rest = end.line === caret.line ? lineAfter.slice(Math.max(0, end.character - caret.character)) : '';
158
+ return { before, rest };
159
+ }
160
+ /* Parameter info for a call that was opened (or that the server asks for), then the item's own command. */
161
+ afterInsert(item, commit, lineBefore) {
162
+ const { editor, project, uri, signature } = this.language;
163
+ const { start } = editor.getSelection();
164
+ const insideCall = start.character > 0 && editor.textInRange({ start: { line: start.line, character: start.character - 1 }, end: start }) === '(';
165
+ if (commit === '(' || (insideCall && isCallItem(item, lineBefore))) {
166
+ signature.trigger();
167
+ }
168
+ const { command } = item;
169
+ if (command === undefined) {
170
+ return;
171
+ }
172
+ if (command.command === PARAMETER_HINTS_COMMAND) {
173
+ signature.trigger();
174
+ }
175
+ else {
176
+ project.service.executeCommand(uri, command).catch(() => undefined);
177
+ }
178
+ }
179
+ /*
180
+ * Resolving gives the item its documentation and the import it needs, but only for the text it was
181
+ * made for. When the person typed on since, the list is asked for again at the new text and the same
182
+ * suggestion is resolved in that one; an item that cannot be resolved is still inserted as it is.
183
+ */
184
+ async resolveNow(item) {
185
+ const { editor, project, uri } = this.language;
186
+ try {
187
+ return await project.service.resolveCompletion(uri, item);
188
+ }
189
+ catch (error) {
190
+ if (!(error instanceof StaleResultError)) {
191
+ return item;
192
+ }
193
+ }
194
+ try {
195
+ const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 })).items.find((candidate) => candidate.label === item.label && candidate.kind === item.kind);
196
+ return fresh === undefined ? item : await project.service.resolveCompletion(uri, fresh).catch(() => fresh);
197
+ }
198
+ catch {
199
+ return item;
200
+ }
201
+ }
202
+ close() {
203
+ this.request++;
204
+ this.controller?.abort();
205
+ this.timers.clear(this.requestTimer);
206
+ this.timers.clear(this.refreshTimer);
207
+ this.timers.clear(this.resolveTimer);
208
+ this.pool = [];
209
+ this.ranked = [];
210
+ this.touched = false;
211
+ this.explicit = false;
212
+ this.activeKey = null;
213
+ this.start = null;
214
+ this.resolved.clear();
215
+ if (this.language.popups.getState().completion !== null) {
216
+ this.language.popups.setState({ completion: null });
217
+ }
218
+ }
219
+ key(event) {
220
+ if (isShortcut(this.language.shortcuts.triggerCompletion, event)) {
221
+ if (this.isOpen) {
222
+ this.detailsOpen = !this.detailsOpen;
223
+ this.publish();
224
+ }
225
+ else {
226
+ this.invoke();
227
+ }
228
+ return true;
229
+ }
230
+ if (!this.isOpen || event.ctrlKey || event.metaKey || event.altKey) {
231
+ return false;
232
+ }
233
+ if (this.commits(event)) {
234
+ void this.accept(false, this.active, event.key);
235
+ return true;
236
+ }
237
+ switch (event.key) {
238
+ case 'ArrowDown':
239
+ this.move(1);
240
+ return true;
241
+ case 'ArrowUp':
242
+ this.move(-1);
243
+ return true;
244
+ case 'PageDown':
245
+ this.move(PAGE);
246
+ return true;
247
+ case 'PageUp':
248
+ this.move(-PAGE);
249
+ return true;
250
+ case 'Enter':
251
+ case 'Tab':
252
+ if (event.shiftKey) {
253
+ return false;
254
+ }
255
+ void this.accept(event.key === 'Tab');
256
+ return true;
257
+ case 'Escape':
258
+ this.close();
259
+ return true;
260
+ default:
261
+ return false;
262
+ }
263
+ }
264
+ /* A character the active item commits on, typed once the person has moved in the list or asked for it, which is when Enter would not be the way they chose. */
265
+ commits(event) {
266
+ if ((!this.touched && !this.explicit) || event.key.length !== 1 || event.isComposing) {
267
+ return false;
268
+ }
269
+ return this.ranked[this.active]?.item.commitCharacters?.includes(event.key) === true;
270
+ }
271
+ edited(change) {
272
+ if (change.source !== 'input' || change.changes.length !== 1) {
273
+ this.close();
274
+ return;
275
+ }
276
+ const { range, text } = change.changes[0];
277
+ const triggers = this.triggerCharacters();
278
+ const last = Array.from(text).at(-1) ?? '';
279
+ const typedIdentifier = text !== '' && Array.from(text).every(isIdentifierCharacter);
280
+ if (text.length > 0 && triggers.includes(last) && text.length === 1) {
281
+ this.close();
282
+ this.beginRequest({ triggerKind: 2, triggerCharacter: last }, 0);
283
+ }
284
+ else if (this.isOpen) {
285
+ if (text === '' || typedIdentifier) {
286
+ this.refilter();
287
+ this.scheduleRefresh();
288
+ }
289
+ else {
290
+ this.close();
291
+ }
292
+ }
293
+ else if (text.length === 1 && typedIdentifier && isEmpty(range)) {
294
+ this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
295
+ }
296
+ }
297
+ caretMoved(position) {
298
+ if (this.start === null) {
299
+ return;
300
+ }
301
+ if (position.line !== this.start.line || position.character < this.start.character) {
302
+ this.close();
303
+ }
304
+ else {
305
+ this.refilter();
306
+ }
307
+ }
308
+ triggerCharacters() {
309
+ const { project, uri } = this.language;
310
+ return project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
311
+ }
312
+ scheduleRefresh() {
313
+ const { project, uri } = this.language;
314
+ this.timers.clear(this.refreshTimer);
315
+ if (this.start === null || !project.service.supports(METHOD, uri)) {
316
+ return;
317
+ }
318
+ const request = this.request;
319
+ this.refreshTimer = this.timers.set(() => {
320
+ if (request === this.request && this.start !== null) {
321
+ void this.ask({ triggerKind: 1 }, request, true);
322
+ }
323
+ }, REFRESH_DELAY_MS);
324
+ }
325
+ beginRequest(context, delay) {
326
+ const { project, uri } = this.language;
327
+ if (!project.service.supports(METHOD, uri)) {
328
+ return;
329
+ }
330
+ this.timers.clear(this.requestTimer);
331
+ const request = ++this.request;
332
+ this.requestTimer = this.timers.set(() => void this.ask(context, request), delay);
333
+ }
334
+ async ask(context, request, refresh = false) {
335
+ const { editor, project, uri } = this.language;
336
+ const position = editor.getCaret();
337
+ this.controller?.abort();
338
+ const controller = new AbortController();
339
+ this.controller = controller;
340
+ let result;
341
+ try {
342
+ result = await project.service.completion(uri, position, context, { signal: controller.signal });
343
+ }
344
+ catch {
345
+ return;
346
+ }
347
+ if (request !== this.request) {
348
+ return;
349
+ }
350
+ const { items, incomplete } = itemsOf(result);
351
+ this.pool = items;
352
+ this.incomplete = incomplete;
353
+ this.resolved.clear();
354
+ this.server = project.host.serverNames?.(uri)[0] ?? '';
355
+ if (!refresh) {
356
+ const caret = editor.getCaret();
357
+ const lineBefore = editor.textInRange({ start: { line: caret.line, character: 0 }, end: caret });
358
+ const prefix = context.triggerKind === 2 ? '' : identifierPrefix(lineBefore);
359
+ this.start = { line: caret.line, character: caret.character - prefix.length };
360
+ this.active = 0;
361
+ this.touched = false;
362
+ }
363
+ else if (this.touched && this.ranked[this.active] !== undefined) {
364
+ // The list is made again, so the row that was chosen is found again by what it says.
365
+ this.activeKey = RecentChoices.keyOf(this.ranked[this.active].item);
366
+ }
367
+ const asked = this.explicit && !refresh;
368
+ this.refilter();
369
+ if (asked && !this.isOpen && !this.incomplete) {
370
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.none') });
371
+ }
372
+ else if (asked && this.ranked.length === 1 && !this.incomplete) {
373
+ // One answer to a list that was asked for is the one wanted.
374
+ void this.accept(false, 0);
375
+ }
376
+ }
377
+ refilter() {
378
+ const { editor } = this.language;
379
+ const caret = editor.getCaret();
380
+ if (this.start === null || caret.line !== this.start.line || caret.character < this.start.character) {
381
+ this.close();
382
+ return;
383
+ }
384
+ const lineBefore = editor.textInRange({ start: { line: caret.line, character: 0 }, end: caret });
385
+ // Most items of a list replace the same range, so the text under it is read once and not once per item.
386
+ const typed = new Map();
387
+ const textBetween = (range) => {
388
+ const key = `${range.start.line}:${range.start.character}:${range.end.line}:${range.end.character}`;
389
+ let text = typed.get(key);
390
+ if (text === undefined) {
391
+ text = editor.textInRange(range);
392
+ typed.set(key, text);
393
+ }
394
+ return text;
395
+ };
396
+ const before = this.ranked[this.active]?.item;
397
+ const recent = recentChoicesOf(this.language.languageId);
398
+ this.ranked = rankCompletions(this.pool, (item) => prefixFor(item, caret, lineBefore, textBetween), (item) => recent.recency(item));
399
+ if (this.ranked.length === 0) {
400
+ if (this.incomplete && this.pool.length > 0) {
401
+ this.beginRequest({ triggerKind: 3 }, START_DELAY_MS);
402
+ }
403
+ this.close();
404
+ return;
405
+ }
406
+ const key = this.activeKey ?? (this.touched && before !== undefined ? RecentChoices.keyOf(before) : null);
407
+ this.activeKey = null;
408
+ const kept = key === null ? -1 : this.ranked.findIndex((entry) => RecentChoices.keyOf(entry.item) === key);
409
+ this.active = kept >= 0 ? kept : 0;
410
+ this.touched = this.touched && kept >= 0;
411
+ this.publish();
412
+ this.scheduleResolve();
413
+ if (this.incomplete) {
414
+ this.beginRequest({ triggerKind: 3 }, START_DELAY_MS);
415
+ }
416
+ }
417
+ scheduleResolve() {
418
+ const { project, uri } = this.language;
419
+ this.timers.clear(this.resolveTimer);
420
+ const entry = this.ranked[this.active];
421
+ if (entry === undefined || this.resolved.has(entry.item) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
422
+ return;
423
+ }
424
+ this.resolveTimer = this.timers.set(() => {
425
+ void project.service
426
+ .resolveCompletion(uri, entry.item)
427
+ .then((item) => {
428
+ this.resolved.set(entry.item, item);
429
+ if (this.ranked[this.active]?.item === entry.item) {
430
+ this.publish();
431
+ }
432
+ })
433
+ .catch(() => undefined);
434
+ }, RESOLVE_DELAY_MS);
435
+ }
436
+ publish() {
437
+ const entry = this.ranked[this.active];
438
+ if (entry === undefined || this.start === null) {
439
+ return;
440
+ }
441
+ const item = this.resolved.get(entry.item) ?? entry.item;
442
+ const descriptions = qualifiersOf(this.ranked.map((ranked) => ranked.item));
443
+ const rows = this.ranked.map(({ item: row, prefix }, index) => ({
444
+ label: row.label,
445
+ kind: row.kind,
446
+ detail: row.labelDetails?.detail ?? '',
447
+ description: descriptions[index],
448
+ matches: matchedCharacters(row.label, prefix),
449
+ deprecated: row.deprecated === true || row.tags?.includes(1) === true
450
+ }));
451
+ const highlightLanguage = shikiLanguageOf(this.language.languageId);
452
+ const view = {
453
+ anchor: this.start,
454
+ rows,
455
+ active: this.active,
456
+ detailsOpen: this.detailsOpen,
457
+ docs: completionDocsOf(item, highlightLanguage),
458
+ server: this.server,
459
+ highlightLanguage
460
+ };
461
+ this.language.popups.setState({ completion: view });
462
+ }
463
+ }
@@ -0,0 +1,15 @@
1
+ import type { LineReplacement } from './line-edits.ts';
2
+ export interface ConflictStretch {
3
+ base: readonly string[];
4
+ ours: readonly string[];
5
+ theirs: readonly string[];
6
+ from: number;
7
+ to: number;
8
+ theirsStart: number;
9
+ theirsEnd: number;
10
+ }
11
+ export interface ConflictPlan {
12
+ merges: LineReplacement[];
13
+ conflicts: ConflictStretch[];
14
+ }
15
+ export declare function planConflict(base: string, ours: string, theirs: string): ConflictPlan;
@@ -0,0 +1,36 @@
1
+ import { splitBlocks, splitLines } from '@adecore/merge';
2
+ /*
3
+ * Where two versions of a file that grew from one disagree. The stretches only the incoming text changed
4
+ * are ours to take without asking; only a stretch both sides changed needs a person.
5
+ */
6
+ export function planConflict(base, ours, theirs) {
7
+ const blocks = splitBlocks(splitLines(base), splitLines(ours), splitLines(theirs));
8
+ const merges = [];
9
+ const conflicts = [];
10
+ let oursAt = 0;
11
+ let theirsAt = 0;
12
+ let mergedAt = 0;
13
+ for (const block of blocks) {
14
+ if (block.kind === 'theirs') {
15
+ merges.push({ from: oursAt, to: oursAt + block.ours.length, lines: block.theirs });
16
+ mergedAt += block.theirs.length;
17
+ }
18
+ else {
19
+ if (block.kind === 'conflict') {
20
+ conflicts.push({
21
+ base: block.base,
22
+ ours: block.ours,
23
+ theirs: block.theirs,
24
+ from: mergedAt,
25
+ to: mergedAt + block.ours.length,
26
+ theirsStart: theirsAt + 1,
27
+ theirsEnd: theirsAt + block.theirs.length
28
+ });
29
+ }
30
+ mergedAt += block.ours.length;
31
+ }
32
+ oursAt += block.ours.length;
33
+ theirsAt += block.theirs.length;
34
+ }
35
+ return { merges, conflicts };
36
+ }
@@ -0,0 +1,10 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ export type ClipboardAction = 'cut' | 'copy' | 'paste';
3
+ export declare class ContextMenuFeature {
4
+ private readonly language;
5
+ private request;
6
+ constructor(language: EditorLanguage);
7
+ close(): void;
8
+ clipboard(action: ClipboardAction): void;
9
+ private open;
10
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ * The editor's context menu. The editor only says where it was asked for; the menu is drawn by the page
3
+ * (`EditorContextMenu`) from what the language servers support at the caret, and the refactors it lists
4
+ * are asked for while it opens, so it never waits for them.
5
+ */
6
+ export class ContextMenuFeature {
7
+ language;
8
+ request = 0;
9
+ constructor(language) {
10
+ this.language = language;
11
+ const off = language.editor.onContextMenu((menu) => {
12
+ // A right click outside the selection moves the caret there first, as every code editor does, so the menu is about what was clicked.
13
+ if (!menu.inSelection) {
14
+ language.editor.setCaret(menu.position);
15
+ }
16
+ this.open(menu.x, menu.y);
17
+ });
18
+ language.onDispose(() => {
19
+ off();
20
+ this.close();
21
+ });
22
+ }
23
+ close() {
24
+ this.request++;
25
+ if (this.language.popups.getState().menu !== null) {
26
+ this.language.popups.setState({ menu: null });
27
+ }
28
+ }
29
+ /* Cut, copy and paste go through the browser's own commands on the editor's input, which is what keeps the clipboard and the undo history one thing. */
30
+ clipboard(action) {
31
+ const { editor } = this.language;
32
+ editor.focus();
33
+ const done = document.execCommand(action);
34
+ if (!done && action === 'paste') {
35
+ void navigator.clipboard
36
+ ?.readText()
37
+ .then((text) => {
38
+ const range = editor.getSelection();
39
+ editor.applyEdits([{ range, text }]);
40
+ })
41
+ .catch(() => undefined);
42
+ }
43
+ }
44
+ open(x, y) {
45
+ const request = ++this.request;
46
+ const { popups, codeActions } = this.language;
47
+ popups.setState({ menu: { x, y, refactors: [] } });
48
+ void codeActions.list(['refactor']).then((entries) => {
49
+ const shown = popups.getState().menu;
50
+ if (request === this.request && shown !== null && entries !== null) {
51
+ popups.setState({ menu: { ...shown, refactors: entries } });
52
+ }
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,14 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ export declare class DefinitionLinkFeature {
3
+ private readonly language;
4
+ private held;
5
+ private pointer;
6
+ private shownKey;
7
+ private drawn;
8
+ private request;
9
+ private lookup;
10
+ constructor(language: EditorLanguage);
11
+ setModHeld(held: boolean): void;
12
+ private clear;
13
+ private update;
14
+ }