@adecore/editor-react 0.39.0 → 0.40.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 (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
package/src/peek.ts CHANGED
@@ -8,8 +8,10 @@ import type { PeekView } from './popups.ts';
8
8
  import { isShortcut } from './shortcut-keys.ts';
9
9
  import { messageOf } from './error-message.ts';
10
10
  import { realTimers, revealRow, type Timers } from './timers.ts';
11
+ import { placeKey, uniqueBy } from './unique.ts';
11
12
 
12
13
  const TOAST_ID = 'language-peek';
14
+
13
15
  /* The block as the panel draws it: a hatched band, the header, eight lines of code and a hatched band, until the editor has measured it. */
14
16
  export const PEEK_HEIGHT = 241;
15
17
 
@@ -20,10 +22,6 @@ const KINDS = {
20
22
  definitions: { method: 'textDocument/definition', unavailable: 'unavailableDefinition', none: 'noDefinition' }
21
23
  } as const;
22
24
 
23
- function placeKey(location: Location): string {
24
- return `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`;
25
- }
26
-
27
25
  /*
28
26
  * The references of the name at the caret, between the lines of the file under the line it is on. The
29
27
  * editor keeps the keyboard: arrows pick a reference, Enter goes to it and Escape closes the peek; typing
@@ -33,8 +31,10 @@ export class PeekFeature {
33
31
  private readonly language: EditorLanguage;
34
32
  private readonly timers: Timers;
35
33
  private kind: PeekKind = 'references';
34
+
36
35
  /* The whole declaration of each place of a definition peek, where the server gave it. */
37
36
  private targets = new Map<string, Range>();
37
+
38
38
  private texts = new Map<string, string>();
39
39
  private places = new Map<string, Location>();
40
40
  private order: string[] = [];
@@ -45,24 +45,31 @@ export class PeekFeature {
45
45
  this.language = language;
46
46
  this.timers = timers;
47
47
  const { editor } = language;
48
+
48
49
  const offs = [
49
50
  language.onKey((event) => {
50
51
  if (isShortcut(this.language.shortcuts.peekReferences, event)) {
51
52
  void this.open();
53
+
52
54
  return true;
53
55
  }
56
+
54
57
  if (isShortcut(this.language.shortcuts.peekDefinition, event)) {
55
58
  void this.openDefinition();
59
+
56
60
  return true;
57
61
  }
62
+
58
63
  return this.key(event);
59
64
  }, KEY_PRIORITY.peek),
60
65
  editor.onTextChange(() => this.close())
61
66
  ];
67
+
62
68
  language.onDispose(() => {
63
69
  for (const off of offs) {
64
70
  off();
65
71
  }
72
+
66
73
  this.close();
67
74
  });
68
75
  }
@@ -74,61 +81,69 @@ export class PeekFeature {
74
81
  async open(position: EditorPosition = this.language.editor.getCaret(), kind: PeekKind = 'references'): Promise<void> {
75
82
  const { editor, project, uri } = this.language;
76
83
  const method = KINDS[kind].method;
84
+
77
85
  if (!project.service.supports(method, uri)) {
78
86
  this.tell(this.say(KINDS[kind].unavailable));
87
+
79
88
  return;
80
89
  }
90
+
81
91
  const token = ++this.token;
82
92
  let found: { locations: Location[]; targets: Map<string, Range> };
93
+
83
94
  try {
84
95
  found = await this.fetch(kind, position);
85
96
  } catch (error) {
86
97
  if (!(error instanceof StaleResultError)) {
87
98
  this.tell(this.say('failed', { message: messageOf(error) }));
88
99
  }
100
+
89
101
  return;
90
102
  }
103
+
91
104
  if (token !== this.token) {
92
105
  return;
93
106
  }
107
+
94
108
  const { targets } = found;
95
- const seen = new Set<string>();
96
- const unique = found.locations.filter((location) => {
97
- const key = placeKey(location);
98
- if (seen.has(key)) {
99
- return false;
100
- }
101
- seen.add(key);
102
- return true;
103
- });
109
+ const unique = uniqueBy(found.locations, placeKey);
110
+
104
111
  if (unique.length === 0) {
105
112
  this.tell(this.say(KINDS[kind].none));
113
+
106
114
  return;
107
115
  }
116
+
108
117
  const uris = [...new Set(unique.map((location) => location.uri))].slice(0, PEEK_READ_FILES);
109
118
  const texts = new Map<string, string>();
119
+
110
120
  await Promise.all(
111
121
  uris.map(async (target) => {
112
122
  const text = await project.readText(target);
123
+
113
124
  if (text !== null) {
114
125
  texts.set(target, text);
115
126
  }
116
127
  })
117
128
  );
129
+
118
130
  if (token !== this.token) {
119
131
  return;
120
132
  }
133
+
121
134
  this.texts = texts;
122
135
  this.kind = kind;
123
136
  this.targets = targets;
124
137
  const files = peekFilesOf(unique, uri, (target) => texts.get(target) ?? null);
125
138
  this.places = new Map(files.flatMap((file: PeekFile) => file.places.map((place) => [place.id, place.location] as const)));
126
139
  this.order = files.flatMap((file) => file.places.map((place) => place.id));
140
+
127
141
  // The references open on the first place that is not the one the caret is on, which is what a person came to see.
128
142
  const first =
129
143
  kind === 'definitions'
130
144
  ? files[0]!.places[0]!
131
145
  : (files.flatMap((file) => file.places).find((place) => !(place.location.uri === uri && place.line === position.line)) ?? files[0]!.places[0]!);
146
+
132
147
  this.line = position.line;
133
148
  this.language.callHierarchy.close();
134
149
  this.language.popups.setState({ peek: { kind, container: null, files, active: first.id, collapsed: [], count: unique.length, preview: null } });
@@ -146,20 +161,25 @@ export class PeekFeature {
146
161
 
147
162
  private async fetch(kind: PeekKind, position: EditorPosition): Promise<{ locations: Location[]; targets: Map<string, Range> }> {
148
163
  const { project, uri } = this.language;
164
+
149
165
  if (kind === 'references') {
150
166
  // Without the declaration, so the list holds what the usages count above a declaration counts.
151
167
  return { locations: (await project.service.references(uri, position, false)) ?? [], targets: new Map() };
152
168
  }
169
+
153
170
  const places = uniquePlaces(await project.service.definition(uri, position));
154
171
  const targets = new Map(places.flatMap(({ location, target }) => (target === null ? [] : [[placeKey(location), target] as const])));
172
+
155
173
  return { locations: places.map(({ location }) => location), targets };
156
174
  }
157
175
 
158
176
  close(): void {
159
177
  this.token++;
178
+
160
179
  if (this.language.popups.getState().peek === null) {
161
180
  return;
162
181
  }
182
+
163
183
  this.language.popups.setState({ peek: null });
164
184
  this.language.editor.setWidgets([]);
165
185
  this.language.editor.focus();
@@ -169,23 +189,28 @@ export class PeekFeature {
169
189
  select(id: string): void {
170
190
  const view = this.language.popups.getState().peek;
171
191
  const location = this.places.get(id);
192
+
172
193
  if (view === null || location === undefined) {
173
194
  return;
174
195
  }
196
+
175
197
  const text = this.texts.get(location.uri);
176
198
  const line = location.range.start.line;
199
+
177
200
  const snippet =
178
201
  text === undefined
179
202
  ? null
180
203
  : this.kind === 'definitions'
181
204
  ? definitionSnippetOf(text, line, this.targets.get(placeKey(location)) ?? null, location.range)
182
205
  : snippetOf(text, line, location.range);
206
+
183
207
  this.language.popups.setState({ peek: { ...view, active: id, preview: snippet === null ? null : { ...snippet, uri: location.uri } } });
184
208
  }
185
209
 
186
210
  /* Folds the places of a file away from the list, or brings them back. */
187
211
  toggleFile(uri: string): void {
188
212
  const view = this.language.popups.getState().peek;
213
+
189
214
  if (view !== null) {
190
215
  const collapsed = view.collapsed.includes(uri) ? view.collapsed.filter((other) => other !== uri) : [...view.collapsed, uri];
191
216
  this.language.popups.setState({ peek: { ...view, collapsed } });
@@ -195,6 +220,7 @@ export class PeekFeature {
195
220
  /* Goes to a place and closes the peek. */
196
221
  go(id: string): void {
197
222
  const location = this.places.get(id);
223
+
198
224
  if (location !== undefined) {
199
225
  this.close();
200
226
  this.language.goTo(location);
@@ -203,6 +229,7 @@ export class PeekFeature {
203
229
 
204
230
  private mounted(container: HTMLElement): void {
205
231
  const view = this.language.popups.getState().peek;
232
+
206
233
  if (view !== null && view.container !== container) {
207
234
  this.language.popups.setState({ peek: { ...view, container } });
208
235
  revealRow(this.timers, container);
@@ -211,21 +238,27 @@ export class PeekFeature {
211
238
 
212
239
  private key(event: KeyboardEvent): boolean {
213
240
  const view = this.language.popups.getState().peek;
241
+
214
242
  if (view === null || event.metaKey || event.ctrlKey || event.altKey) {
215
243
  return false;
216
244
  }
245
+
217
246
  switch (event.key) {
218
247
  case 'ArrowDown':
219
248
  this.step(view, 1);
249
+
220
250
  return true;
221
251
  case 'ArrowUp':
222
252
  this.step(view, -1);
253
+
223
254
  return true;
224
255
  case 'Enter':
225
256
  this.go(view.active);
257
+
226
258
  return true;
227
259
  case 'Escape':
228
260
  this.close();
261
+
229
262
  return true;
230
263
  default:
231
264
  return false;
@@ -235,10 +268,13 @@ export class PeekFeature {
235
268
  /* The next place in the list a person can see, which a folded file has none of. */
236
269
  private step(view: PeekView, direction: 1 | -1): void {
237
270
  const index = this.order.indexOf(view.active);
271
+
238
272
  for (let next = index + direction; next >= 0 && next < this.order.length; next += direction) {
239
273
  const id = this.order[next]!;
274
+
240
275
  if (!view.collapsed.includes(this.places.get(id)!.uri)) {
241
276
  this.select(id);
277
+
242
278
  return;
243
279
  }
244
280
  }
package/src/pick.ts CHANGED
@@ -37,24 +37,29 @@ export class PickFeature {
37
37
  private active = '';
38
38
  private previewTimer: unknown;
39
39
  private controller: AbortController | null = null;
40
+
40
41
  /* The rows that said why they cannot be taken, by id. */
41
42
  private refusals = new Map<string, string>();
43
+
42
44
  private attempting = false;
43
45
 
44
46
  constructor(language: EditorLanguage, timers: Timers = realTimers) {
45
47
  this.language = language;
46
48
  this.timers = timers;
47
49
  const { editor } = language;
50
+
48
51
  const offs = [
49
52
  language.onKey((event) => this.key(event), KEY_PRIORITY.pick),
50
53
  editor.onTextChange(() => this.close()),
51
54
  editor.onCaret(() => this.close()),
52
55
  editor.onBlur(() => this.close())
53
56
  ];
57
+
54
58
  language.onDispose(() => {
55
59
  for (const off of offs) {
56
60
  off();
57
61
  }
62
+
58
63
  this.close();
59
64
  });
60
65
  }
@@ -66,9 +71,11 @@ export class PickFeature {
66
71
  open(spec: PickSpec): void {
67
72
  this.close();
68
73
  const ids = spec.groups.flatMap((group) => group.rows.map((row) => row.id));
74
+
69
75
  if (ids.length === 0) {
70
76
  return;
71
77
  }
78
+
72
79
  this.spec = spec;
73
80
  this.refusals = new Map();
74
81
  this.ids = ids;
@@ -81,6 +88,7 @@ export class PickFeature {
81
88
  if (this.spec === null) {
82
89
  return;
83
90
  }
91
+
84
92
  this.spec = null;
85
93
  this.timers.clear(this.previewTimer);
86
94
  this.controller?.abort();
@@ -96,28 +104,37 @@ export class PickFeature {
96
104
  /* Takes a row, by a click or Enter. */
97
105
  choose(id: string): void {
98
106
  const spec = this.spec;
107
+
99
108
  if (spec === null || !this.ids.includes(id) || this.attempting) {
100
109
  return;
101
110
  }
111
+
102
112
  if (spec.attempt === undefined) {
103
113
  this.close();
104
114
  spec.accept(id);
115
+
105
116
  return;
106
117
  }
118
+
107
119
  this.attempting = true;
120
+
108
121
  void spec
109
122
  .attempt(id)
110
123
  .catch(() => null)
111
124
  .then((reason) => {
112
125
  this.attempting = false;
126
+
113
127
  if (this.spec !== spec) {
114
128
  return;
115
129
  }
130
+
116
131
  if (reason === null) {
117
132
  this.close();
118
133
  spec.accept(id);
134
+
119
135
  return;
120
136
  }
137
+
121
138
  this.refusals.set(id, reason);
122
139
  this.publish(null);
123
140
  });
@@ -127,6 +144,7 @@ export class PickFeature {
127
144
  if (id === this.active) {
128
145
  return;
129
146
  }
147
+
130
148
  this.active = id;
131
149
  this.controller?.abort();
132
150
  this.publish(null);
@@ -137,40 +155,51 @@ export class PickFeature {
137
155
  if (this.spec === null || event.metaKey || event.ctrlKey || event.altKey) {
138
156
  return false;
139
157
  }
158
+
140
159
  switch (event.key) {
141
160
  case 'ArrowDown':
142
161
  this.move(1);
162
+
143
163
  return true;
144
164
  case 'ArrowUp':
145
165
  this.move(-1);
166
+
146
167
  return true;
147
168
  case 'PageDown':
148
169
  this.move(8);
170
+
149
171
  return true;
150
172
  case 'PageUp':
151
173
  this.move(-8);
174
+
152
175
  return true;
153
176
  case 'Home':
154
177
  this.activate(this.ids[0]!);
178
+
155
179
  return true;
156
180
  case 'End':
157
181
  this.activate(this.ids[this.ids.length - 1]!);
182
+
158
183
  return true;
159
184
  case 'Enter':
160
185
  this.choose(this.active);
186
+
161
187
  return true;
162
188
  case 'Tab': {
163
189
  const spec = this.spec;
190
+
164
191
  if (spec.onTab !== undefined) {
165
192
  this.close();
166
193
  spec.onTab(event.shiftKey);
167
194
  } else {
168
195
  this.choose(this.active);
169
196
  }
197
+
170
198
  return true;
171
199
  }
172
200
  case 'Escape':
173
201
  this.close();
202
+
174
203
  return true;
175
204
  default:
176
205
  return false;
@@ -180,13 +209,17 @@ export class PickFeature {
180
209
  private schedulePreview(): void {
181
210
  const spec = this.spec;
182
211
  this.timers.clear(this.previewTimer);
212
+
183
213
  if (spec?.preview === undefined) {
184
214
  return;
185
215
  }
216
+
186
217
  const id = this.active;
218
+
187
219
  this.previewTimer = this.timers.set(() => {
188
220
  const controller = new AbortController();
189
221
  this.controller = controller;
222
+
190
223
  spec.preview!(id, controller.signal)
191
224
  .then((preview) => {
192
225
  if (this.spec === spec && this.active === id && !controller.signal.aborted) {
@@ -199,9 +232,11 @@ export class PickFeature {
199
232
 
200
233
  private publish(preview: PickPreview | null): void {
201
234
  const spec = this.spec;
235
+
202
236
  if (spec === null) {
203
237
  return;
204
238
  }
239
+
205
240
  const refusal = this.refusals.get(this.active);
206
241
  const shown = refusal === undefined ? preview : { removed: [], added: [], note: null, refusal };
207
242
  this.language.popups.setState({ pick: { anchor: spec.anchor, groups: spec.groups, active: this.active, preview: shown, title: spec.title ?? null } });
@@ -1,4 +1,5 @@
1
- import type { FocusEvent, MouseEvent } from 'react';
1
+ import { useLayoutEffect, type FocusEvent, type MouseEvent } from 'react';
2
+ import type { EditorLanguage } from './editor-language.ts';
2
3
 
3
4
  /*
4
5
  * The handlers for a popup list whose rows the keyboard never enters: the text it belongs to keeps the
@@ -15,3 +16,12 @@ export function keepFocusOutOfList(focusBack: () => void): {
15
16
  onFocus: () => focusBack()
16
17
  };
17
18
  }
19
+
20
+ /* The focus stays in the text, so the input is what names the popup's list and its active option. */
21
+ export function useInputPopup(language: EditorLanguage, controls: string, activeDescendant: string | undefined): void {
22
+ useLayoutEffect(() => {
23
+ language.editor.setInputPopup({ controls, activeDescendant });
24
+
25
+ return () => language.editor.setInputPopup(null);
26
+ }, [language, controls, activeDescendant]);
27
+ }
@@ -38,6 +38,7 @@ export function placePopup(anchor: EditorRect, size: PopupSize, window: PopupSiz
38
38
  const maxHeight = Math.max(0, below ? roomBelow : roomAbove);
39
39
  const height = Math.min(size.height, maxHeight);
40
40
  const left = Math.max(margin, Math.min(anchor.left, window.width - size.width - margin));
41
+
41
42
  return { left, top: below ? anchor.bottom + gap : anchor.top - gap - height, maxHeight, side: below ? 'below' : 'above' };
42
43
  }
43
44
 
@@ -54,11 +55,15 @@ export function placeBeside(
54
55
  ): PopupPlacement {
55
56
  const fitsRight = first.left + first.width + gap + size.width <= window.width - margin;
56
57
  const fitsLeft = first.left - gap - size.width >= margin;
58
+
57
59
  if (fitsRight || fitsLeft) {
58
60
  const top = Math.max(margin, Math.min(first.top, window.height - size.height - margin));
61
+
59
62
  return { left: fitsRight ? first.left + first.width + gap : first.left - gap - size.width, top, maxHeight: window.height - margin * 2, side: 'below' };
60
63
  }
64
+
61
65
  const top = first.top + first.height + gap;
62
66
  const maxHeight = Math.max(0, window.height - margin - top);
67
+
63
68
  return { left: Math.max(margin, Math.min(first.left, window.width - size.width - margin)), top, maxHeight, side: 'below' };
64
69
  }
package/src/popups.ts CHANGED
@@ -20,7 +20,6 @@ export interface HoverView {
20
20
  /* While the pointer stays in this range the card stays, so moving along a word does not make it blink. */
21
21
  readonly subject: EditorRange;
22
22
  readonly problems: readonly Problem[];
23
- /* What the servers say about the symbol, and where it is defined. */
24
23
  readonly info: HoverInfo | null;
25
24
  }
26
25
 
@@ -29,13 +28,12 @@ export interface HoverInfo {
29
28
  readonly definition: Location | null;
30
29
  /* The name under the pointer, which a signature's own symbol is told by. */
31
30
  readonly word: string;
32
- /* How many other places use the symbol, once a server has counted them. */
31
+ /* How many other places use the symbol; null until a server has counted them. */
33
32
  readonly references: number | null;
34
- /* Only the text: no footer for the definition and the references, which a card made of someone else's words has none of. */
33
+ /* Only the text, without the footer for the definition and the references. */
35
34
  readonly plain?: boolean;
36
35
  }
37
36
 
38
- /* One line of the suggestions list. */
39
37
  export interface CompletionRow {
40
38
  readonly label: string;
41
39
  readonly kind: number | undefined;
@@ -56,7 +54,6 @@ export interface CompletionDocs {
56
54
  readonly text: HoverText;
57
55
  }
58
56
 
59
- /* The suggestions while a word is being typed. */
60
57
  export interface CompletionView {
61
58
  /* The start of the word, which the list is placed under. */
62
59
  readonly anchor: EditorPosition;
@@ -65,7 +62,6 @@ export interface CompletionView {
65
62
  /* Whether the documentation of the active row is shown beside the list. */
66
63
  readonly detailsOpen: boolean;
67
64
  readonly docs: CompletionDocs | null;
68
- /* The server the suggestions came from. */
69
65
  readonly server: string;
70
66
  /* The shiki id of the file's language, for the signature in the documentation. */
71
67
  readonly highlightLanguage: string;
@@ -32,12 +32,15 @@ class DocumentSync {
32
32
  this.uri = uri;
33
33
  this.editor = editor;
34
34
  this.stop = editor.onTextChange((change) => this.changed(change.changes));
35
+
35
36
  this.ready = service.openDocument({ uri, languageId, text: editor.getText() }).then(() => {
36
37
  this.opened = true;
38
+
37
39
  if (!this.disposed) {
38
40
  this.flush();
39
41
  }
40
42
  });
43
+
41
44
  // A failed open is also a document nothing can be asked of; the features' own requests say so.
42
45
  this.ready.catch(() => undefined);
43
46
  }
@@ -54,6 +57,7 @@ class DocumentSync {
54
57
 
55
58
  private changed(changes: readonly ContentChange[]): void {
56
59
  this.pending.push(...changes);
60
+
57
61
  if (this.opened) {
58
62
  this.flush();
59
63
  }
@@ -63,6 +67,7 @@ class DocumentSync {
63
67
  if (this.pending.length === 0) {
64
68
  return;
65
69
  }
70
+
66
71
  const changes = this.pending;
67
72
  this.pending = [];
68
73
  void this.service.changeDocument(this.uri, changes).catch(() => undefined);
@@ -82,6 +87,7 @@ function filesOf(targets: readonly FileOperationTarget[]): Array<{ uri: string;
82
87
  return targets.map((target) => {
83
88
  const { path, directory } =
84
89
  typeof target === 'string' ? { path: target, directory: false } : { path: target.path, directory: target.directory === true };
90
+
85
91
  return { uri: pathToFileUri(path), directory };
86
92
  });
87
93
  }
@@ -99,8 +105,10 @@ export class ProjectLanguage {
99
105
  readonly history = new NavigationHistory();
100
106
  private readonly holders = new Map<string, Holder>();
101
107
  private readonly files: ProjectFiles | null;
108
+
102
109
  /* The body height of the peek and call hierarchy panels, shared by the editors of the project. */
103
110
  panelBodyHeight: number;
111
+
104
112
  private readonly panelListeners = new Set<(height: number) => void>();
105
113
  private pendingCaret: Place | null = null;
106
114
  private lastPlace: Place | null = null;
@@ -119,6 +127,7 @@ export class ProjectLanguage {
119
127
  /* Open panels of other editors of the project follow a resize. */
120
128
  onPanelBodyChange(listener: (height: number) => void): () => void {
121
129
  this.panelListeners.add(listener);
130
+
122
131
  return () => this.panelListeners.delete(listener);
123
132
  }
124
133
 
@@ -126,6 +135,7 @@ export class ProjectLanguage {
126
135
  if (height === this.panelBodyHeight) {
127
136
  return;
128
137
  }
138
+
129
139
  this.panelBodyHeight = height;
130
140
  this.panelListeners.forEach((listener) => listener(height));
131
141
  this.host.onPanelResize?.(height);
@@ -135,13 +145,16 @@ export class ProjectLanguage {
135
145
  if (this.host.pathOfUri !== undefined) {
136
146
  return this.host.pathOfUri(uri);
137
147
  }
148
+
138
149
  const path = fileUriToPath(uri);
150
+
139
151
  return path !== null && this.folder !== '' ? relativeTo(path, this.folder) : path;
140
152
  }
141
153
 
142
154
  /* Makes an edit of a language server: an open document takes it as one undo step and any other file gets an unsaved draft. */
143
155
  applyWorkspaceEdit(edit: WorkspaceEdit): Promise<ApplyWorkspaceEditResult> {
144
156
  const { service } = this;
157
+
145
158
  return applyWorkspaceEdit(edit, {
146
159
  editorOf: (uri) => this.holders.get(uri)?.editors[0],
147
160
  files: this.files,
@@ -175,10 +188,13 @@ export class ProjectLanguage {
175
188
  /* The text of a file as it stands now: an open document's, else a draft's or the machine's; null when it is no text file. */
176
189
  async readText(uri: string): Promise<string | null> {
177
190
  const open = this.holders.get(uri)?.sync.text;
191
+
178
192
  if (open !== undefined) {
179
193
  return open;
180
194
  }
195
+
181
196
  const path = fileUriToPath(uri);
197
+
182
198
  return path === null || this.files === null ? null : ((await this.files.read(path))?.text ?? null);
183
199
  }
184
200
 
@@ -201,16 +217,20 @@ export class ProjectLanguage {
201
217
  if (this.lastPlace !== null) {
202
218
  this.history.record(this.lastPlace);
203
219
  }
220
+
204
221
  this.openPlace(place);
205
222
  }
206
223
 
207
224
  /* The column an opened place waits for, once the editor of its file has put its caret on the line; each place is asked for once. */
208
225
  takeCaret(uri: string, line: number): EditorPosition | null {
209
226
  const pending = this.pendingCaret;
227
+
210
228
  if (pending === null || pending.uri !== uri || pending.position.line !== line) {
211
229
  return null;
212
230
  }
231
+
213
232
  this.pendingCaret = null;
233
+
214
234
  return pending.position;
215
235
  }
216
236
 
@@ -219,13 +239,17 @@ export class ProjectLanguage {
219
239
  if (this.disposed) {
220
240
  throw new Error('The project language is disposed');
221
241
  }
242
+
222
243
  let holder = this.holders.get(uri);
244
+
223
245
  if (holder === undefined) {
224
246
  holder = { editors: [], sync: new DocumentSync(this.service, uri, languageId, editor), languageId };
225
247
  this.holders.set(uri, holder);
226
248
  }
249
+
227
250
  holder.editors.push(editor);
228
251
  let released = false;
252
+
229
253
  return {
230
254
  uri,
231
255
  service: this.service,
@@ -243,20 +267,25 @@ export class ProjectLanguage {
243
267
 
244
268
  private release(uri: string, editor: Editor): void {
245
269
  const holder = this.holders.get(uri);
270
+
246
271
  if (holder === undefined) {
247
272
  return;
248
273
  }
274
+
249
275
  holder.editors.splice(holder.editors.indexOf(editor), 1);
250
276
  const next = holder.editors[0];
277
+
251
278
  if (next === undefined) {
252
279
  holder.sync.dispose();
253
280
  this.holders.delete(uri);
281
+
254
282
  void holder.sync.ready
255
283
  .catch(() => undefined)
256
284
  .then(() => {
257
285
  if (this.holders.has(uri)) {
258
286
  return undefined;
259
287
  }
288
+
260
289
  return this.service.closeDocument(uri);
261
290
  })
262
291
  .catch(() => undefined);
@@ -271,15 +300,19 @@ export class ProjectLanguage {
271
300
  if (this.disposed) {
272
301
  return;
273
302
  }
303
+
274
304
  this.disposed = true;
275
305
  disposeSnippetTransforms(this);
306
+
276
307
  for (const [uri, holder] of this.holders) {
277
308
  holder.sync.dispose();
309
+
278
310
  void holder.sync.ready
279
311
  .catch(() => undefined)
280
312
  .then(() => this.service.closeDocument(uri))
281
313
  .catch(() => undefined);
282
314
  }
315
+
283
316
  this.holders.clear();
284
317
  this.problems.dispose();
285
318
  }