@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
package/src/hover.ts ADDED
@@ -0,0 +1,280 @@
1
+ import type { EditorHover, EditorPosition, EditorRange } from '@adecore/editor';
2
+ import type { EditorLanguage } from './editor-language.ts';
3
+ import { rangeHolds, type Problem } from './diagnostics-model.ts';
4
+ import { hoverTextOf, isEmptyHover, locationsOf } from './hover-content.ts';
5
+ import { wordRangeAt } from './rename-model.ts';
6
+ import type { HoverInfo } from './popups.ts';
7
+ import { isShortcut } from './shortcut-keys.ts';
8
+ import { realTimers, type Timers } from './timers.ts';
9
+
10
+ const SHOW_DELAY_MS = 300;
11
+ const HIDE_DELAY_MS = 250;
12
+ /* How long a card has to stay up before the references of its symbol are counted, which can be a request over the whole project. */
13
+ const COUNT_DELAY_MS = 500;
14
+ const TOAST_ID = 'language-hover';
15
+
16
+ /*
17
+ * The card under a character the pointer rests on: the problems there, and what the language servers
18
+ * say about the symbol. It opens after the pointer has stayed a moment, stays while the pointer is on
19
+ * the word or on the card itself, and goes when the text, the scroll or the focus moves.
20
+ */
21
+ export class HoverFeature {
22
+ private readonly language: EditorLanguage;
23
+ private readonly timers: Timers;
24
+ private showTimer: unknown;
25
+ private countTimer: unknown;
26
+ /* The counts asked for since the text last changed, by the symbol's position, so moving back to a name does not ask again. */
27
+ private readonly counts = new Map<string, number>();
28
+ private hideTimer: unknown;
29
+ private inCard = false;
30
+ /* Counts every request, so an answer that arrives after the pointer moved on is dropped. */
31
+ private request = 0;
32
+ private lookup: AbortController | null = null;
33
+ /* A card the keyboard opened: scrolling does not take it away, since it was never under a pointer. Moving the caret does. */
34
+ private pinned = false;
35
+ /* The card is being read: an explanation runs in it or is shown, so the pointer going elsewhere neither closes nor replaces it. */
36
+ private held = false;
37
+
38
+ constructor(language: EditorLanguage, timers: Timers = realTimers) {
39
+ this.language = language;
40
+ this.timers = timers;
41
+ const { editor } = language;
42
+ const offs = [
43
+ editor.onHover((hover) => this.moved(hover)),
44
+ editor.onTextChange(() => {
45
+ this.counts.clear();
46
+ this.hide();
47
+ }),
48
+ editor.onClick(() => {
49
+ if (this.held) {
50
+ this.hide();
51
+ }
52
+ return false;
53
+ }),
54
+ editor.onViewChange(() => {
55
+ if (!this.pinned) {
56
+ this.hide();
57
+ }
58
+ }),
59
+ editor.onCaret(() => {
60
+ if (this.pinned) {
61
+ this.hide();
62
+ }
63
+ }),
64
+ editor.onKeyDown((event) => {
65
+ if (isShortcut(this.language.shortcuts.quickInfo, event)) {
66
+ this.quickInfo();
67
+ return true;
68
+ }
69
+ if (event.key === 'Escape' && this.language.popups.getState().hover !== null) {
70
+ this.hide();
71
+ return true;
72
+ }
73
+ return false;
74
+ })
75
+ ];
76
+ language.onDispose(() => {
77
+ for (const off of offs) {
78
+ off();
79
+ }
80
+ this.hide();
81
+ });
82
+ }
83
+
84
+ /* The pointer entered or left the card, which is where a link in it is clicked. */
85
+ holdCard(inside: boolean): void {
86
+ this.inCard = inside;
87
+ if (inside) {
88
+ this.timers.clear(this.hideTimer);
89
+ } else {
90
+ this.scheduleHide();
91
+ }
92
+ }
93
+
94
+ /* The card for the caret, as a pointer resting there would open it. */
95
+ quickInfo(): void {
96
+ const request = ++this.request;
97
+ this.timers.clear(this.showTimer);
98
+ void this.show(this.language.editor.getCaret(), request, true);
99
+ }
100
+
101
+ /* The problems at a position as a card, at once; what the servers say about the symbol is left for the pointer and Quick Info. */
102
+ showProblemsAt(position: EditorPosition): boolean {
103
+ const problems = this.language.diagnostics.at(position);
104
+ if (problems.length === 0) {
105
+ return false;
106
+ }
107
+ this.hide();
108
+ this.pinned = true;
109
+ this.present(position, problems, null, problems[0]!.diagnostic.range);
110
+ return true;
111
+ }
112
+
113
+ /* Keeps the card up until Escape, a press in the text, an edit or a move of the caret. */
114
+ keep(): void {
115
+ this.pinned = true;
116
+ this.held = true;
117
+ this.timers.clear(this.hideTimer);
118
+ }
119
+
120
+ /* A card of its own for a range, such as the explanation of a selection, which stays like the card Quick Info opens. */
121
+ showRange(range: EditorRange): void {
122
+ this.hide();
123
+ this.keep();
124
+ this.present(range.start, [], null, range);
125
+ }
126
+
127
+ hide(): void {
128
+ this.pinned = false;
129
+ this.held = false;
130
+ this.request++;
131
+ this.lookup?.abort();
132
+ this.timers.clear(this.showTimer);
133
+ this.timers.clear(this.hideTimer);
134
+ this.timers.clear(this.countTimer);
135
+ this.inCard = false;
136
+ if (this.language.popups.getState().hover !== null) {
137
+ this.language.popups.setState({ hover: null });
138
+ }
139
+ }
140
+
141
+ private moved(hover: EditorHover | null): void {
142
+ if (this.held) {
143
+ return;
144
+ }
145
+ this.timers.clear(this.showTimer);
146
+ if (hover === null) {
147
+ this.request++;
148
+ this.scheduleHide();
149
+ return;
150
+ }
151
+ const shown = this.language.popups.getState().hover;
152
+ if (shown !== null && rangeHolds(shown.subject, hover.position)) {
153
+ this.timers.clear(this.hideTimer);
154
+ return;
155
+ }
156
+ this.scheduleHide();
157
+ const request = ++this.request;
158
+ this.showTimer = this.timers.set(() => void this.show(hover.position, request, false), SHOW_DELAY_MS);
159
+ }
160
+
161
+ private scheduleHide(): void {
162
+ this.timers.clear(this.hideTimer);
163
+ if (this.language.popups.getState().hover === null || this.inCard || this.held) {
164
+ return;
165
+ }
166
+ this.hideTimer = this.timers.set(() => {
167
+ if (!this.inCard) {
168
+ this.hide();
169
+ }
170
+ }, HIDE_DELAY_MS);
171
+ }
172
+
173
+ /* What the servers say at a position: the hover text and the definition, each of which may be missing without the other. */
174
+ private async ask(position: EditorHover['position'], signal: AbortSignal): Promise<{ info: HoverInfo | null; range: EditorRange | null }> {
175
+ const { service } = this.language.project;
176
+ const { uri } = this.language;
177
+ if (!service.supports('textDocument/hover', uri)) {
178
+ return { info: null, range: null };
179
+ }
180
+ const [hover, definition] = await Promise.all([
181
+ service.hover(uri, position, { signal }).catch(() => null),
182
+ service.supports('textDocument/definition', uri) ? service.definition(uri, position, { signal }).catch(() => null) : null
183
+ ]);
184
+ const text = hover === null ? null : hoverTextOf(hover);
185
+ if (hover === null || text === null || isEmptyHover(text)) {
186
+ return { info: null, range: null };
187
+ }
188
+ const { editor } = this.language;
189
+ const line = editor.textInRange({ start: { line: position.line, character: 0 }, end: { line: position.line, character: Number.MAX_SAFE_INTEGER } });
190
+ const word = wordRangeAt(line, position);
191
+ return {
192
+ info: {
193
+ text,
194
+ definition: locationsOf(definition)[0] ?? null,
195
+ word: word === null ? '' : line.slice(word.start.character, word.end.character),
196
+ references: null
197
+ },
198
+ range: hover.range ?? null
199
+ };
200
+ }
201
+
202
+ private async show(position: EditorPosition, request: number, pinned: boolean): Promise<void> {
203
+ const problems = this.language.diagnostics.at(position);
204
+ this.lookup?.abort();
205
+ const controller = new AbortController();
206
+ this.lookup = controller;
207
+ const { info, range } = await this.ask(position, controller.signal);
208
+ if (request !== this.request) {
209
+ return;
210
+ }
211
+ if (problems.length === 0 && info === null) {
212
+ this.hide();
213
+ if (pinned) {
214
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.hover.none') });
215
+ }
216
+ return;
217
+ }
218
+ this.pinned = pinned;
219
+ this.present(position, problems, info, problems[0]?.diagnostic.range ?? range);
220
+ if (info !== null) {
221
+ this.scheduleCount(position, request, controller.signal);
222
+ }
223
+ }
224
+
225
+ private present(position: EditorPosition, problems: readonly Problem[], info: HoverInfo | null, range: EditorRange | null): void {
226
+ const subject = range ?? { start: position, end: position };
227
+ this.timers.clear(this.hideTimer);
228
+ this.language.popups.setState({
229
+ hover: {
230
+ anchor: subject.start.line === position.line ? subject.start : position,
231
+ position,
232
+ subject,
233
+ problems,
234
+ info
235
+ }
236
+ });
237
+ }
238
+
239
+ /* The references of the symbol are counted once the card has stayed up a while, or at once when the text has not changed since they were. */
240
+ private scheduleCount(position: EditorPosition, request: number, signal: AbortSignal): void {
241
+ const key = `${position.line}:${this.wordStart(position)}`;
242
+ const known = this.counts.get(key);
243
+ if (known !== undefined) {
244
+ this.showCount(known, request);
245
+ return;
246
+ }
247
+ this.timers.clear(this.countTimer);
248
+ this.countTimer = this.timers.set(() => void this.count(position, key, request, signal), COUNT_DELAY_MS);
249
+ }
250
+
251
+ /* How many places use the symbol, which can take a while in a large project and so arrives after the card does. */
252
+ private async count(position: EditorPosition, key: string, request: number, signal: AbortSignal): Promise<void> {
253
+ const { service } = this.language.project;
254
+ if (request !== this.request || !service.supports('textDocument/references', this.language.uri)) {
255
+ return;
256
+ }
257
+ const found = await service.references(this.language.uri, position, false, { signal }).catch(() => null);
258
+ if (found === null) {
259
+ return;
260
+ }
261
+ this.counts.set(key, found.length);
262
+ this.showCount(found.length, request);
263
+ }
264
+
265
+ private showCount(count: number, request: number): void {
266
+ const shown = this.language.popups.getState().hover;
267
+ if (request === this.request && shown?.info != null) {
268
+ this.language.popups.setState({ hover: { ...shown, info: { ...shown.info, references: count } } });
269
+ }
270
+ }
271
+
272
+ /* Where the word at a position starts, which names the symbol for the cache. */
273
+ wordStart(position: EditorPosition): number {
274
+ const line = this.language.editor.textInRange({
275
+ start: { line: position.line, character: 0 },
276
+ end: { line: position.line, character: Number.MAX_SAFE_INTEGER }
277
+ });
278
+ return wordRangeAt(line, position)?.start.character ?? position.character;
279
+ }
280
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ export { EditorLanguage } from './editor-language.ts';
2
+ export { ProjectLanguage } from './project-language.ts';
3
+ export type { LanguageDocumentHandle } from './project-language.ts';
4
+ export type { LanguageHost, DiskText, GitBlameCommit, GitBlameResult, EditorNotification, RenameSuggestionsRequest } from './host-types.ts';
5
+ export { EditorView } from './EditorView.tsx';
6
+ export { createHolder } from './holder.ts';
7
+ export type { EditorViewProps } from './EditorView.tsx';
8
+ export { LanguagePopups } from './LanguagePopups.tsx';
9
+ export { AnchoredPopup } from './AnchoredPopup.tsx';
10
+ export { HoverCard } from './HoverCard.tsx';
11
+ export { CompletionPopup } from './CompletionPopup.tsx';
12
+ export { SignatureCard } from './SignatureCard.tsx';
13
+ export { RenameCard } from './RenameCard.tsx';
14
+ export { PickPopup } from './PickPopup.tsx';
15
+ export { PeekPanel } from './PeekPanel.tsx';
16
+ export { SymbolPicker } from './SymbolPicker.tsx';
17
+ export { CodeAuthorsCard } from './CodeAuthorsCard.tsx';
18
+ export { EditorContextMenu } from './EditorContextMenu.tsx';
19
+ export { EditorRenderingProvider } from './rendering.tsx';
20
+ export type { EditorRendering } from './rendering-context.ts';
21
+ export { FindReplace } from './FindReplace.tsx';
22
+ export { ChangeReview } from './ChangeReview.tsx';
23
+ export type { ChangeReviewProps } from './ChangeReview.tsx';
24
+ export { ProjectProblems, countsOf, compareRows } from './project-problems.ts';
25
+ export type { ProblemFile, ProblemRow } from './project-problems.ts';
26
+ export { applyWorkspaceEdit } from './workspace-edit.ts';
27
+ export type { ProjectFiles, StagedFile, WorkspaceEditHost } from './workspace-edit.ts';
28
+ export { useProblemCounts } from './use-problem-counts.ts';
29
+ export type { ProblemCounts } from './diagnostics.ts';
30
+ export type { BlameSource, CodeVisionSettings } from './code-vision.ts';
31
+ export type { Problem } from './diagnostics-model.ts';
32
+ export type {
33
+ PopupState,
34
+ PopupStore,
35
+ HoverInfo,
36
+ HoverView,
37
+ CompletionView,
38
+ CompletionDocs,
39
+ CompletionRow,
40
+ SignatureView,
41
+ RenameView,
42
+ RenameFileView,
43
+ PeekView,
44
+ PickView,
45
+ PickGroup,
46
+ PickRow,
47
+ PickPreview,
48
+ SymbolsView,
49
+ MenuView,
50
+ AuthorsView
51
+ } from './popups.ts';
52
+ export { RowHost, LineActionHost } from './row-host.ts';
53
+ export type { HostedRow, HostedAction } from './row-host.ts';
54
+ export { HighlightLayers, highlightLayers } from './highlight-layers.ts';
55
+ export { NavigationHistory } from './navigation-history.ts';
56
+ export type { Place } from './navigation-history.ts';
57
+ export { openingPlace, viewStates, followViewStates, viewStateKey, forgetMovesFrom } from './view-state.ts';
58
+ export type { ViewState, RevealLineRequest } from './view-state.ts';
59
+ export { lspLanguageIdOf, shikiLanguageOf, shikiLanguageOfPath } from './language-ids.ts';
60
+ export { AttributionCard } from './AttributionCard.tsx';
61
+ export type { AttributionCardProps } from './AttributionCard.tsx';
62
+ export { ReviewGroup, joinReviewGroup } from './review-group.ts';
63
+ export type { ReviewMember } from './review-group.ts';
@@ -0,0 +1,85 @@
1
+ import type { EditorInlayHint, EditorPosition, EditorRange } from '@adecore/editor';
2
+ import type { InlayHint } from '@adecore/lsp';
3
+ import type { EditorLanguage } from './editor-language.ts';
4
+ import { Refresher } from './refresher.ts';
5
+ import { realTimers, type Timers } from './timers.ts';
6
+
7
+ const METHOD = 'textDocument/inlayHint';
8
+ const PAUSE_MS = 300;
9
+ /* Lines asked for above and below what is in view, so a little scrolling stays inside what is already drawn. */
10
+ const MARGIN_LINES = 60;
11
+
12
+ /* A hint's label is a string or a list of parts, which are read as one text. */
13
+ export function hintLabelOf(hint: InlayHint): string {
14
+ return typeof hint.label === 'string' ? hint.label : hint.label.map((part) => part.value).join('');
15
+ }
16
+
17
+ export function editorHintsOf(hints: readonly InlayHint[]): EditorInlayHint[] {
18
+ return hints
19
+ .map((hint) => ({ position: hint.position, label: hintLabelOf(hint) }))
20
+ .filter((hint) => hint.label !== '')
21
+ .sort((left, right) => left.position.line - right.position.line || left.position.character - right.position.character);
22
+ }
23
+
24
+ /* The lines in view with a margin of lines around them, from the start of the first to the end of the last. */
25
+ export function withMargin(visible: EditorRange, endOfDocument: EditorPosition, endOfLine: (line: number) => EditorPosition): EditorRange {
26
+ const first = Math.max(0, visible.start.line - MARGIN_LINES);
27
+ const last = Math.min(endOfDocument.line, visible.end.line + MARGIN_LINES);
28
+ return { start: { line: first, character: 0 }, end: last === endOfDocument.line ? endOfDocument : endOfLine(last) };
29
+ }
30
+
31
+ /* Whether what was asked for already holds every line in view. */
32
+ export function covers(asked: EditorRange, visible: EditorRange): boolean {
33
+ return asked.start.line <= visible.start.line && asked.end.line >= visible.end.line;
34
+ }
35
+
36
+ /*
37
+ * Types and parameter names the servers infer, drawn in the text as soft pills. They are asked for the
38
+ * lines in view and a margin around them after a pause in typing or scrolling; until the answer comes the
39
+ * last ones stay where their text went, so nothing blinks while typing. Scrolling asks again only once the
40
+ * view leaves the lines that were asked for.
41
+ */
42
+ export class InlayHintsFeature {
43
+ private readonly refresher: Refresher;
44
+ /* The lines the hints on screen were asked for. */
45
+ private asked: EditorRange | null = null;
46
+
47
+ constructor(language: EditorLanguage, timers: Timers = realTimers) {
48
+ const { editor, project, uri } = language;
49
+ this.refresher = new Refresher(
50
+ async (signal) => {
51
+ const { service } = project;
52
+ if (!service.supports(METHOD, uri)) {
53
+ return;
54
+ }
55
+ const range = withMargin(editor.getVisibleRange(), editor.positionAt(Number.MAX_SAFE_INTEGER), (line) => ({
56
+ line,
57
+ character: editor.textInRange({ start: { line, character: 0 }, end: { line, character: Number.MAX_SAFE_INTEGER } }).length
58
+ }));
59
+ this.asked = range;
60
+ const hints = await service.inlayHints(uri, range, { signal });
61
+ if (!signal.aborted) {
62
+ editor.setInlayHints(hints === null ? [] : editorHintsOf(hints));
63
+ }
64
+ },
65
+ PAUSE_MS,
66
+ timers
67
+ );
68
+ const edits = editor.onTextChange(() => this.refresher.later());
69
+ const scrolls = editor.onViewChange(() => {
70
+ if (this.asked !== null && !covers(this.asked, editor.getVisibleRange())) {
71
+ this.refresher.later();
72
+ }
73
+ });
74
+ const providers = language.onProvidersChanged(() => {
75
+ this.refresher.now();
76
+ });
77
+ language.onDispose(() => {
78
+ edits();
79
+ scrolls();
80
+ providers();
81
+ this.refresher.dispose();
82
+ editor.setInlayHints([]);
83
+ });
84
+ }
85
+ }
@@ -0,0 +1,54 @@
1
+ /* The highlighter ids `fs.read` answers with, by the id LSP names the same language. */
2
+ const LSP_IDS: Record<string, string> = {
3
+ typescript: 'typescript',
4
+ tsx: 'typescriptreact',
5
+ javascript: 'javascript',
6
+ jsx: 'javascriptreact',
7
+ vue: 'vue',
8
+ php: 'php',
9
+ css: 'css',
10
+ scss: 'scss',
11
+ less: 'less',
12
+ html: 'html',
13
+ json: 'json',
14
+ jsonc: 'jsonc',
15
+ yaml: 'yaml',
16
+ python: 'python',
17
+ shellscript: 'shellscript',
18
+ docker: 'dockerfile'
19
+ };
20
+
21
+ /* The LSP language id of a file, or null when no language server here knows its language. */
22
+ export function lspLanguageIdOf(language: string | undefined): string | null {
23
+ return language === undefined ? null : (LSP_IDS[language] ?? null);
24
+ }
25
+
26
+ const SHIKI_IDS: Record<string, string> = { typescriptreact: 'tsx', javascriptreact: 'jsx' };
27
+
28
+ /* The highlighter id of an LSP language id, for code a server sends back to be drawn. */
29
+ export function shikiLanguageOf(lspLanguageId: string): string {
30
+ return SHIKI_IDS[lspLanguageId] ?? lspLanguageId;
31
+ }
32
+
33
+ const PATH_IDS: Record<string, string> = {
34
+ ts: 'typescript',
35
+ mts: 'typescript',
36
+ cts: 'typescript',
37
+ tsx: 'tsx',
38
+ js: 'javascript',
39
+ mjs: 'javascript',
40
+ cjs: 'javascript',
41
+ jsx: 'jsx',
42
+ vue: 'vue',
43
+ php: 'php',
44
+ json: 'json',
45
+ css: 'css',
46
+ scss: 'scss',
47
+ html: 'html',
48
+ md: 'markdown'
49
+ };
50
+
51
+ /* The highlighter id for the code of a file we only know by its path, such as a place another file refers to. */
52
+ export function shikiLanguageOfPath(path: string): string {
53
+ return PATH_IDS[path.slice(path.lastIndexOf('.') + 1).toLowerCase()] ?? 'text';
54
+ }
@@ -0,0 +1,48 @@
1
+ import { shapeOf, splitLines } from '@adecore/merge';
2
+ import type { EditorContentChange } from '@adecore/editor';
3
+
4
+ /* A replacement of whole lines, zero-based with `to` exclusive, such as a block a review puts back. */
5
+ export interface LineReplacement {
6
+ readonly from: number;
7
+ readonly to: number;
8
+ readonly lines: readonly string[];
9
+ }
10
+
11
+ /*
12
+ * The edit that makes the lines `from` up to `to` of `text` into `lines`. A file that does not end on a
13
+ * newline has no line break after its last line, so the break before it goes along when the last lines
14
+ * leave and comes with the new ones when they are put at the end.
15
+ */
16
+ export function replaceLines(text: string, replacement: LineReplacement): EditorContentChange {
17
+ const { from, to, lines } = replacement;
18
+ const eol = shapeOf(text).eol;
19
+ const raw = text.split('\n');
20
+ const count = splitLines(text).length;
21
+ const body = lines.join(eol);
22
+ if (to < count || text === '' || text.endsWith('\n')) {
23
+ return { range: { start: { line: from, character: 0 }, end: { line: to, character: 0 } }, text: lines.length === 0 ? '' : `${body}${eol}` };
24
+ }
25
+ const end = { line: count - 1, character: raw[count - 1]!.length };
26
+ if (from === count) {
27
+ return { range: { start: end, end }, text: lines.length === 0 ? '' : `${eol}${body}` };
28
+ }
29
+ if (lines.length === 0 && from > 0) {
30
+ return { range: { start: { line: from - 1, character: raw[from - 1]!.length }, end }, text: '' };
31
+ }
32
+ return { range: { start: { line: from, character: 0 }, end }, text: body };
33
+ }
34
+
35
+ /* Several replacements of one text as the edits an editor applies at once; replacements that touch are one edit. */
36
+ export function replaceAllLines(text: string, replacements: readonly LineReplacement[]): EditorContentChange[] {
37
+ const ordered = [...replacements].sort((left, right) => left.from - right.from);
38
+ const merged: LineReplacement[] = [];
39
+ for (const next of ordered) {
40
+ const last = merged.at(-1);
41
+ if (last !== undefined && last.to >= next.from) {
42
+ merged[merged.length - 1] = { from: last.from, to: Math.max(last.to, next.to), lines: [...last.lines, ...next.lines] };
43
+ } else {
44
+ merged.push(next);
45
+ }
46
+ }
47
+ return merged.map((replacement) => replaceLines(text, replacement));
48
+ }