@adecore/editor-react 0.42.0 → 0.44.0

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 (150) hide show
  1. package/dist/CallHierarchyPanel.js +177 -181
  2. package/dist/EditorContextMenu.js +118 -105
  3. package/dist/LanguagePopups.js +7 -0
  4. package/dist/PickPopup.js +245 -104
  5. package/dist/WrapCard.d.ts +8 -0
  6. package/dist/WrapCard.js +172 -0
  7. package/dist/call-hierarchy-model.d.ts +4 -2
  8. package/dist/call-hierarchy-model.js +20 -0
  9. package/dist/call-hierarchy.d.ts +3 -0
  10. package/dist/call-hierarchy.js +54 -22
  11. package/dist/clipboard-history.d.ts +31 -0
  12. package/dist/clipboard-history.js +70 -0
  13. package/dist/code-actions.d.ts +3 -1
  14. package/dist/code-actions.js +50 -10
  15. package/dist/code-vision.js +2 -3
  16. package/dist/completion.d.ts +28 -0
  17. package/dist/completion.js +83 -27
  18. package/dist/context-menu-rows.d.ts +1 -1
  19. package/dist/context-menu-rows.js +4 -0
  20. package/dist/definition-link.js +15 -5
  21. package/dist/document-colors.d.ts +24 -0
  22. package/dist/document-colors.js +194 -0
  23. package/dist/document-links.d.ts +22 -0
  24. package/dist/document-links.js +133 -0
  25. package/dist/document-query.d.ts +15 -0
  26. package/dist/document-query.js +54 -0
  27. package/dist/edit-locations.d.ts +20 -0
  28. package/dist/edit-locations.js +136 -0
  29. package/dist/editor-language.d.ts +23 -1
  30. package/dist/editor-language.js +83 -2
  31. package/dist/emmet.d.ts +13 -0
  32. package/dist/emmet.js +62 -0
  33. package/dist/folding.js +2 -3
  34. package/dist/format-on-type.js +3 -5
  35. package/dist/highlights.js +2 -3
  36. package/dist/history.d.ts +7 -0
  37. package/dist/history.js +79 -1
  38. package/dist/host-types.d.ts +10 -0
  39. package/dist/index.d.ts +13 -2
  40. package/dist/index.js +10 -0
  41. package/dist/inlay-hints.js +2 -2
  42. package/dist/inline-completion.d.ts +39 -0
  43. package/dist/inline-completion.js +221 -0
  44. package/dist/key-priority.d.ts +9 -5
  45. package/dist/key-priority.js +11 -6
  46. package/dist/linked-editing.d.ts +26 -0
  47. package/dist/linked-editing.js +215 -0
  48. package/dist/live-templates.d.ts +16 -0
  49. package/dist/live-templates.js +289 -0
  50. package/dist/locales/en.json +61 -2
  51. package/dist/locales/nl.json +61 -2
  52. package/dist/markup-context.d.ts +55 -0
  53. package/dist/markup-context.js +408 -0
  54. package/dist/markup.d.ts +39 -0
  55. package/dist/markup.js +465 -0
  56. package/dist/navigation.js +4 -6
  57. package/dist/paste-history.d.ts +7 -0
  58. package/dist/paste-history.js +104 -0
  59. package/dist/peek.js +3 -4
  60. package/dist/pick.d.ts +21 -2
  61. package/dist/pick.js +152 -19
  62. package/dist/popups.d.ts +10 -0
  63. package/dist/popups.js +2 -1
  64. package/dist/postfix.d.ts +21 -0
  65. package/dist/postfix.js +212 -0
  66. package/dist/project-language.d.ts +20 -1
  67. package/dist/project-language.js +132 -18
  68. package/dist/rename.js +8 -11
  69. package/dist/rendered-docs.js +1 -2
  70. package/dist/selection-ranges.js +2 -3
  71. package/dist/shortcut-keys.d.ts +24 -0
  72. package/dist/signature.js +3 -6
  73. package/dist/snippet-insert.d.ts +10 -0
  74. package/dist/snippet-insert.js +217 -0
  75. package/dist/snippet-session.d.ts +2 -2
  76. package/dist/snippet-session.js +3 -3
  77. package/dist/snippet.d.ts +7 -1
  78. package/dist/snippet.js +65 -8
  79. package/dist/surround-constructs.d.ts +10 -0
  80. package/dist/surround-constructs.js +100 -0
  81. package/dist/surround.d.ts +18 -0
  82. package/dist/surround.js +399 -0
  83. package/dist/symbol-picker.js +4 -6
  84. package/dist/symbols.js +22 -3
  85. package/dist/template-builtins.d.ts +3 -0
  86. package/dist/template-builtins.js +46 -0
  87. package/dist/template-context.d.ts +12 -0
  88. package/dist/template-context.js +56 -0
  89. package/dist/templates.d.ts +59 -0
  90. package/dist/templates.js +103 -0
  91. package/dist/testing.d.ts +23 -1
  92. package/dist/testing.js +114 -0
  93. package/package.json +7 -6
  94. package/src/CallHierarchyPanel.tsx +26 -11
  95. package/src/EditorContextMenu.tsx +7 -0
  96. package/src/LanguagePopups.tsx +4 -0
  97. package/src/PickPopup.tsx +47 -5
  98. package/src/WrapCard.tsx +64 -0
  99. package/src/call-hierarchy-model.ts +25 -3
  100. package/src/call-hierarchy.ts +62 -20
  101. package/src/clipboard-history.ts +102 -0
  102. package/src/code-actions.ts +48 -11
  103. package/src/code-vision.ts +3 -3
  104. package/src/completion.ts +123 -25
  105. package/src/context-menu-rows.ts +3 -2
  106. package/src/definition-link.ts +17 -5
  107. package/src/document-colors.ts +189 -0
  108. package/src/document-links.ts +129 -0
  109. package/src/document-query.ts +71 -0
  110. package/src/edit-locations.ts +162 -0
  111. package/src/editor-language.ts +86 -3
  112. package/src/emmet-module.d.ts +44 -0
  113. package/src/emmet.ts +73 -0
  114. package/src/folding.ts +3 -3
  115. package/src/format-on-type.ts +5 -5
  116. package/src/highlights.ts +3 -3
  117. package/src/history.ts +81 -1
  118. package/src/host-types.ts +17 -0
  119. package/src/index.ts +28 -3
  120. package/src/inlay-hints.ts +2 -2
  121. package/src/inline-completion.ts +221 -0
  122. package/src/key-priority.ts +11 -6
  123. package/src/linked-editing.ts +199 -0
  124. package/src/live-templates.ts +315 -0
  125. package/src/locales/en.json +61 -2
  126. package/src/locales/nl.json +61 -2
  127. package/src/markup-context.ts +387 -0
  128. package/src/markup.ts +377 -0
  129. package/src/navigation.ts +6 -6
  130. package/src/paste-history.ts +111 -0
  131. package/src/peek.ts +4 -4
  132. package/src/pick.ts +218 -20
  133. package/src/popups.ts +21 -1
  134. package/src/postfix.ts +224 -0
  135. package/src/project-language.ts +139 -19
  136. package/src/rename.ts +11 -11
  137. package/src/rendered-docs.ts +2 -2
  138. package/src/selection-ranges.ts +3 -3
  139. package/src/signature.ts +6 -6
  140. package/src/snippet-insert.ts +258 -0
  141. package/src/snippet-session.ts +9 -4
  142. package/src/snippet.ts +82 -7
  143. package/src/surround-constructs.ts +85 -0
  144. package/src/surround.ts +430 -0
  145. package/src/symbol-picker.ts +6 -6
  146. package/src/symbols.ts +30 -4
  147. package/src/template-builtins.ts +94 -0
  148. package/src/template-context.ts +65 -0
  149. package/src/templates.ts +197 -0
  150. package/src/testing.ts +126 -1
@@ -1,11 +1,15 @@
1
1
  import { disposeSnippetTransforms } from './snippet-transform-queue.js';
2
2
  import i18next from 'i18next';
3
- import { fileUriToPath, pathToFileUri } from '@adecore/lsp';
3
+ import { fileUriToPath, pathToFileUri, unservedLanguageService } from '@adecore/lsp';
4
+ import { ClipboardHistory } from './clipboard-history.js';
5
+ import { TemplateRegistry } from './templates.js';
6
+ import { EditLocations } from './edit-locations.js';
4
7
  import { NavigationHistory } from './navigation-history.js';
5
8
  import { ProjectProblems, serviceProblems } from './project-problems.js';
6
9
  import { applyWorkspaceEdit } from './workspace-edit.js';
7
10
  import { relativeTo } from './paths.js';
8
11
  import { PANEL_BODY_HEIGHT } from './panel-size.js';
12
+ const UNSERVED = unservedLanguageService();
9
13
  /* Sends one editor's edits to the document, holding them until the document is open. */
10
14
  class DocumentSync {
11
15
  ready;
@@ -86,6 +90,9 @@ export class ProjectLanguage {
86
90
  folder;
87
91
  problems;
88
92
  history = new NavigationHistory();
93
+ edits;
94
+ clipboard;
95
+ templates;
89
96
  holders = new Map();
90
97
  files;
91
98
  /* The body height of the peek and call hierarchy panels, shared by the editors of the project. */
@@ -94,14 +101,23 @@ export class ProjectLanguage {
94
101
  pendingCaret = null;
95
102
  lastPlace = null;
96
103
  disposed = false;
104
+ subscriptions;
105
+ /* The result id the server gave each document in its last workspace report, which the next asks with. */
106
+ workspaceResults = new Map();
97
107
  constructor(service, host = {}) {
98
108
  this.service = service;
99
109
  this.host = host;
100
110
  this.i18n = host.i18n ?? i18next;
101
111
  this.folder = host.folder ?? '';
102
112
  this.files = host.files ?? null;
113
+ this.edits = new EditLocations(host.editHistoryLimit);
114
+ this.clipboard = host.clipboard ?? new ClipboardHistory();
115
+ this.templates = host.templates ?? new TemplateRegistry();
103
116
  this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
104
117
  this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, uri => this.pathOfLocation(uri) ?? uri));
118
+ const shows = service.onShowDocument?.(params => this.showDocument(params));
119
+ const messages = host.onServerMessage === undefined ? undefined : service.onServerMessage?.(message => host.onServerMessage(message));
120
+ this.subscriptions = [shows, messages].flatMap(subscription => subscription === undefined ? [] : [subscription]);
105
121
  }
106
122
  /* Open panels of other editors of the project follow a resize. */
107
123
  onPanelBodyChange(listener) {
@@ -153,16 +169,86 @@ export class ProjectLanguage {
153
169
  }
154
170
  /* Tells the servers that files were deleted, once they are. */
155
171
  didDeleteFiles(targets) {
156
- return this.service.didDeleteFiles?.(filesOf(targets)) ?? Promise.resolve();
172
+ const files = filesOf(targets);
173
+ this.edits.forget(uri => files.some(file => file.directory ? uri.startsWith(`${file.uri}/`) : uri === file.uri));
174
+ return this.service.didDeleteFiles?.(files) ?? Promise.resolve();
157
175
  }
158
- /* 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
+ /*
177
+ * 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
178
+ * text file. A document of a scheme a server owns, such as a decompiled class, is read from that server.
179
+ */
159
180
  async readText(uri) {
160
- const open = this.holders.get(uri)?.sync.text;
181
+ const open = this.holders.get(uri)?.editors[0]?.getText();
161
182
  if (open !== undefined) {
162
183
  return open;
163
184
  }
164
185
  const path = fileUriToPath(uri);
165
- return path === null || this.files === null ? null : (await this.files.read(path))?.text ?? null;
186
+ if (path === null) {
187
+ return (await this.service.textDocumentContent?.(uri).catch(() => null)) ?? null;
188
+ }
189
+ return this.files === null ? null : (await this.files.read(path))?.text ?? null;
190
+ }
191
+ /*
192
+ * Asks the server of `uri` for the problems of every file it knows and lists them in `problems`, for a server that
193
+ * offers workspace diagnostics. A file the server reports as unchanged keeps what it had. False when no server offers them.
194
+ */
195
+ async pullWorkspaceProblems(uri, options = {}) {
196
+ const {
197
+ service
198
+ } = this;
199
+ if (service.workspaceDiagnostics === undefined || !service.supports('workspace/diagnostic', uri)) {
200
+ return false;
201
+ }
202
+ const previous = [...this.workspaceResults].map(([key, value]) => ({
203
+ uri: key,
204
+ value
205
+ }));
206
+ const report = await service.workspaceDiagnostics(uri, previous, options);
207
+ const server = this.host.serverNames?.(uri)[0] ?? 'workspace';
208
+ for (const item of report.items) {
209
+ if (item.resultId !== undefined) {
210
+ this.workspaceResults.set(item.uri, item.resultId);
211
+ }
212
+ if (item.kind === 'full') {
213
+ this.problems.report(this.pathOfLocation(item.uri) ?? item.uri, server, item.items);
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+ /* Shows what a server asks for: a web address outside the editor, anything else as a place of the host. */
219
+ async showDocument(params) {
220
+ const web = /^(https?|mailto):/i.test(params.uri);
221
+ if (params.external === true || web) {
222
+ if (this.host.openExternal !== undefined) {
223
+ this.host.openExternal(params.uri);
224
+ return {
225
+ success: true
226
+ };
227
+ }
228
+ if (typeof window === 'undefined') {
229
+ return {
230
+ success: false
231
+ };
232
+ }
233
+ return {
234
+ success: window.open(params.uri, '_blank', 'noopener,noreferrer') !== null
235
+ };
236
+ }
237
+ if (this.host.openPlace === undefined) {
238
+ return {
239
+ success: false
240
+ };
241
+ }
242
+ this.openPlace({
243
+ uri: params.uri,
244
+ position: params.selection?.start ?? {
245
+ line: 0,
246
+ character: 0
247
+ }
248
+ });
249
+ return {
250
+ success: true
251
+ };
166
252
  }
167
253
  /*
168
254
  * Opens the file of a place on its line. A file opens on a line only, so the column is kept here until the editor
@@ -192,27 +278,40 @@ export class ProjectLanguage {
192
278
  this.pendingCaret = null;
193
279
  return pending.position;
194
280
  }
281
+ /* The editors that hold a document, one per uri in the order the documents were first opened. */
282
+ openEditors() {
283
+ return [...this.holders].flatMap(([uri, holder]) => holder.editors[0] === undefined ? [] : [{
284
+ uri,
285
+ editor: holder.editors[0]
286
+ }]);
287
+ }
195
288
  /* Opens `uri` for the editor, or joins the editors that already hold it. */
196
- acquire(uri, languageId, editor) {
289
+ acquire(uri, languageId, editor, options = {}) {
197
290
  if (this.disposed) {
198
291
  throw new Error('The project language is disposed');
199
292
  }
293
+ const serve = options.serve ?? true;
200
294
  let holder = this.holders.get(uri);
201
295
  if (holder === undefined) {
202
296
  holder = {
203
297
  editors: [],
204
- sync: new DocumentSync(this.service, uri, languageId, editor),
298
+ served: [],
299
+ sync: null,
205
300
  languageId
206
301
  };
207
302
  this.holders.set(uri, holder);
208
303
  }
209
304
  holder.editors.push(editor);
305
+ if (serve) {
306
+ holder.served.push(editor);
307
+ holder.sync ??= new DocumentSync(this.service, uri, languageId, editor);
308
+ }
210
309
  let released = false;
211
310
  return {
212
311
  uri,
213
- service: this.service,
312
+ service: serve ? this.service : UNSERVED,
214
313
  get ready() {
215
- return holder.sync.ready;
314
+ return serve ? holder.sync?.ready ?? Promise.resolve() : Promise.resolve();
216
315
  },
217
316
  release: () => {
218
317
  if (!released) {
@@ -228,20 +327,29 @@ export class ProjectLanguage {
228
327
  return;
229
328
  }
230
329
  holder.editors.splice(holder.editors.indexOf(editor), 1);
231
- const next = holder.editors[0];
232
- if (next === undefined) {
233
- holder.sync.dispose();
330
+ if (holder.served.includes(editor)) {
331
+ holder.served.splice(holder.served.indexOf(editor), 1);
332
+ }
333
+ const {
334
+ sync
335
+ } = holder;
336
+ if (holder.editors.length === 0) {
234
337
  this.holders.delete(uri);
235
- void holder.sync.ready.catch(() => undefined).then(() => {
236
- if (this.holders.has(uri)) {
338
+ }
339
+ if (sync === null || sync.editor !== editor) {
340
+ return;
341
+ }
342
+ sync.dispose();
343
+ const next = holder.served[0];
344
+ // The editor that sent the changes is gone; the next served one takes over and opens the document with its own text.
345
+ holder.sync = next === undefined ? null : new DocumentSync(this.service, uri, holder.languageId, next);
346
+ if (holder.sync === null) {
347
+ void sync.ready.catch(() => undefined).then(() => {
348
+ if (this.holders.get(uri)?.sync) {
237
349
  return undefined;
238
350
  }
239
351
  return this.service.closeDocument(uri);
240
352
  }).catch(() => undefined);
241
- } else if (holder.sync.editor === editor) {
242
- // The editor that sent the changes is gone; the next one takes over and opens the document with its own text.
243
- holder.sync.dispose();
244
- holder.sync = new DocumentSync(this.service, uri, holder.languageId, next);
245
353
  }
246
354
  }
247
355
  dispose() {
@@ -249,8 +357,14 @@ export class ProjectLanguage {
249
357
  return;
250
358
  }
251
359
  this.disposed = true;
360
+ for (const subscription of this.subscriptions) {
361
+ subscription.dispose();
362
+ }
252
363
  disposeSnippetTransforms(this);
253
364
  for (const [uri, holder] of this.holders) {
365
+ if (holder.sync === null) {
366
+ continue;
367
+ }
254
368
  holder.sync.dispose();
255
369
  void holder.sync.ready.catch(() => undefined).then(() => this.service.closeDocument(uri)).catch(() => undefined);
256
370
  }
package/dist/rename.js CHANGED
@@ -44,19 +44,18 @@ export class RenameFeature {
44
44
  async start() {
45
45
  const {
46
46
  editor,
47
- project,
48
47
  uri
49
48
  } = this.language;
50
- if (!project.service.supports(METHOD, uri)) {
49
+ if (!this.language.service.supports(METHOD, uri)) {
51
50
  this.tell('error', this.say('unavailable'));
52
51
  return;
53
52
  }
54
53
  this.cancel();
55
54
  const caret = editor.getCaret();
56
- const prepare = project.service.providerOptions(METHOD, uri)?.prepareProvider === true;
55
+ const prepare = this.language.service.providerOptions(METHOD, uri)?.prepareProvider === true;
57
56
  let answer;
58
57
  try {
59
- answer = prepare ? await project.service.prepareRename(uri, caret) : {
58
+ answer = prepare ? await this.language.service.prepareRename(uri, caret) : {
60
59
  defaultBehavior: true
61
60
  };
62
61
  } catch (error) {
@@ -110,7 +109,7 @@ export class RenameFeature {
110
109
  uri
111
110
  } = this.language;
112
111
  try {
113
- const edit = await project.service.rename(uri, session.target.range.start, next);
112
+ const edit = await this.language.service.rename(uri, session.target.range.start, next);
114
113
  if (this.session !== session) {
115
114
  return;
116
115
  }
@@ -204,14 +203,13 @@ export class RenameFeature {
204
203
  async light(session) {
205
204
  const {
206
205
  editor,
207
- project,
208
206
  uri
209
207
  } = this.language;
210
- if (!project.service.supports('textDocument/references', uri)) {
208
+ if (!this.language.service.supports('textDocument/references', uri)) {
211
209
  return null;
212
210
  }
213
211
  try {
214
- const locations = await project.service.references(uri, session.target.range.start, true);
212
+ const locations = await this.language.service.references(uri, session.target.range.start, true);
215
213
  if (this.session !== session || locations === null) {
216
214
  return null;
217
215
  }
@@ -282,14 +280,13 @@ export class RenameFeature {
282
280
  /* So a suggester knows a function from a variable; a rename on a use finds the kind through the declaration's place in this file. */
283
281
  async kindOf(session, locations) {
284
282
  const {
285
- project,
286
283
  uri
287
284
  } = this.language;
288
- if (!project.service.supports('textDocument/documentSymbol', uri)) {
285
+ if (!this.language.service.supports('textDocument/documentSymbol', uri)) {
289
286
  return undefined;
290
287
  }
291
288
  try {
292
- const symbols = await project.service.documentSymbols(uri, {
289
+ const symbols = await this.language.service.documentSymbols(uri, {
293
290
  signal: session.names.signal,
294
291
  parallel: true
295
292
  });
@@ -192,10 +192,9 @@ export class RenderedDocsFeature {
192
192
  /* Whether names in `{@link}` and `@see` can be followed, which takes a server that finds symbols by name. */
193
193
  get canFollow() {
194
194
  const {
195
- project,
196
195
  uri
197
196
  } = this.language;
198
- return project.service.supports('workspace/symbol', uri);
197
+ return this.language.service.supports('workspace/symbol', uri);
199
198
  }
200
199
  /* Goes to what a link in a comment names. */
201
200
  follow(name, comment) {
@@ -15,14 +15,13 @@ export class SelectionRangesFeature {
15
15
  constructor(language) {
16
16
  const {
17
17
  editor,
18
- project,
19
18
  uri
20
19
  } = language;
21
20
  editor.setSelectionRanges(async positions => {
22
- if (!project.service.supports(METHOD, uri)) {
21
+ if (!language.service.supports(METHOD, uri)) {
23
22
  return null;
24
23
  }
25
- const chains = await project.service.selectionRanges(uri, positions);
24
+ const chains = await language.service.selectionRanges(uri, positions);
26
25
  return chains === null || chains.length !== positions.length ? null : chains.map(selectionChainOf);
27
26
  });
28
27
  language.onDispose(() => editor.setSelectionRanges(null));
@@ -12,10 +12,15 @@ export declare function shortcutsOf(keymap: Keymap, apple: boolean): {
12
12
  autoIndentLines: LanguageShortcut | null;
13
13
  callHierarchy: LanguageShortcut | null;
14
14
  codeActions: LanguageShortcut | null;
15
+ codeBlockEnd: LanguageShortcut | null;
16
+ codeBlockEndWithSelection: LanguageShortcut | null;
17
+ codeBlockStart: LanguageShortcut | null;
18
+ codeBlockStartWithSelection: LanguageShortcut | null;
15
19
  collapse: LanguageShortcut | null;
16
20
  collapseAll: LanguageShortcut | null;
17
21
  collapseDocComments: LanguageShortcut | null;
18
22
  collapseRecursively: LanguageShortcut | null;
23
+ completeCurrentStatement: LanguageShortcut | null;
19
24
  convertIndentsToSpaces: LanguageShortcut | null;
20
25
  convertIndentsToTabs: LanguageShortcut | null;
21
26
  decreaseFontSize: LanguageShortcut | null;
@@ -41,20 +46,33 @@ export declare function shortcutsOf(keymap: Keymap, apple: boolean): {
41
46
  goToImplementation: LanguageShortcut | null;
42
47
  goToSymbol: LanguageShortcut | null;
43
48
  goToTypeDefinition: LanguageShortcut | null;
49
+ hippieBackwardCompletion: LanguageShortcut | null;
50
+ hippieCompletion: LanguageShortcut | null;
44
51
  historyBack: LanguageShortcut | null;
45
52
  historyForward: LanguageShortcut | null;
46
53
  increaseFontSize: LanguageShortcut | null;
47
54
  inlineEdit: LanguageShortcut | null;
55
+ insertLiveTemplate: LanguageShortcut | null;
48
56
  joinLines: LanguageShortcut | null;
57
+ lastEditLocation: LanguageShortcut | null;
49
58
  matchBrace: LanguageShortcut | null;
59
+ methodDown: LanguageShortcut | null;
60
+ methodUp: LanguageShortcut | null;
50
61
  moveLineDown: LanguageShortcut | null;
51
62
  moveLineUp: LanguageShortcut | null;
63
+ moveStatementDown: LanguageShortcut | null;
64
+ moveStatementUp: LanguageShortcut | null;
65
+ nextChange: LanguageShortcut | null;
66
+ nextEditLocation: LanguageShortcut | null;
52
67
  nextHighlight: LanguageShortcut | null;
53
68
  nextProblem: LanguageShortcut | null;
54
69
  organizeImports: LanguageShortcut | null;
55
70
  parameterInfo: LanguageShortcut | null;
71
+ pasteAsPlainText: LanguageShortcut | null;
72
+ pasteFromHistory: LanguageShortcut | null;
56
73
  peekDefinition: LanguageShortcut | null;
57
74
  peekReferences: LanguageShortcut | null;
75
+ previousChange: LanguageShortcut | null;
58
76
  previousHighlight: LanguageShortcut | null;
59
77
  previousProblem: LanguageShortcut | null;
60
78
  quickInfo: LanguageShortcut | null;
@@ -66,6 +84,7 @@ export declare function shortcutsOf(keymap: Keymap, apple: boolean): {
66
84
  resetFontSize: LanguageShortcut | null;
67
85
  reverseLines: LanguageShortcut | null;
68
86
  save: LanguageShortcut | null;
87
+ saveAsLiveTemplate: LanguageShortcut | null;
69
88
  selectAll: LanguageShortcut | null;
70
89
  selectAllOccurrences: LanguageShortcut | null;
71
90
  selectNextOccurrence: LanguageShortcut | null;
@@ -84,6 +103,8 @@ export declare function shortcutsOf(keymap: Keymap, apple: boolean): {
84
103
  startNewLine: LanguageShortcut | null;
85
104
  startNewLineBefore: LanguageShortcut | null;
86
105
  suggestInline: LanguageShortcut | null;
106
+ surroundWith: LanguageShortcut | null;
107
+ surroundWithLiveTemplate: LanguageShortcut | null;
87
108
  textEnd: LanguageShortcut | null;
88
109
  textStart: LanguageShortcut | null;
89
110
  toggleBlockComment: LanguageShortcut | null;
@@ -92,10 +113,13 @@ export declare function shortcutsOf(keymap: Keymap, apple: boolean): {
92
113
  toggleLineComment: LanguageShortcut | null;
93
114
  toggleRenderedDocs: LanguageShortcut | null;
94
115
  triggerCompletion: LanguageShortcut | null;
116
+ typeHierarchy: LanguageShortcut | null;
95
117
  undo: LanguageShortcut | null;
96
118
  uniqueLines: LanguageShortcut | null;
97
119
  unselectOccurrence: LanguageShortcut | null;
120
+ unwrap: LanguageShortcut | null;
98
121
  wordLeft: LanguageShortcut | null;
99
122
  wordRight: LanguageShortcut | null;
123
+ wrapWithAbbreviation: LanguageShortcut | null;
100
124
  };
101
125
  export declare function isShortcut(target: LanguageShortcut | null, event: KeyboardEvent): boolean;
package/dist/signature.js CHANGED
@@ -102,10 +102,9 @@ export class SignatureFeature {
102
102
  }
103
103
  options() {
104
104
  const {
105
- project,
106
105
  uri
107
106
  } = this.language;
108
- const options = project.service.providerOptions(METHOD, uri);
107
+ const options = this.language.service.providerOptions(METHOD, uri);
109
108
  return options === undefined ? null : {
110
109
  triggers: options.triggerCharacters ?? [],
111
110
  retriggers: options.retriggerCharacters ?? []
@@ -145,10 +144,9 @@ export class SignatureFeature {
145
144
  }
146
145
  ask(context) {
147
146
  const {
148
- project,
149
147
  uri
150
148
  } = this.language;
151
- if (!project.service.supports(METHOD, uri)) {
149
+ if (!this.language.service.supports(METHOD, uri)) {
152
150
  return;
153
151
  }
154
152
  this.timers.clear(this.timer);
@@ -164,7 +162,6 @@ export class SignatureFeature {
164
162
  async fetch(context, request) {
165
163
  const {
166
164
  editor,
167
- project,
168
165
  uri
169
166
  } = this.language;
170
167
  const caret = editor.getCaret();
@@ -173,7 +170,7 @@ export class SignatureFeature {
173
170
  this.controller = controller;
174
171
  let help;
175
172
  try {
176
- help = await project.service.signatureHelp(uri, caret, context, {
173
+ help = await this.language.service.signatureHelp(uri, caret, context, {
177
174
  signal: controller.signal
178
175
  });
179
176
  } catch {
@@ -0,0 +1,10 @@
1
+ import type { Editor, EditorRange } from '@adecore/editor';
2
+ import type { EditorLanguage } from './editor-language.ts';
3
+ export interface SnippetPlacement {
4
+ readonly range: EditorRange;
5
+ readonly selectedText?: string;
6
+ }
7
+ export declare function bodyWithIndentUnit(body: string, unit: string): string;
8
+ export declare function indentUnitOf(editor: Editor): string;
9
+ export declare function dedentLines(text: string, base?: string): string;
10
+ export declare function insertSnippet(language: EditorLanguage, source: string, placements: readonly SnippetPlacement[], extra?: (name: string) => string | undefined): Promise<boolean>;
@@ -0,0 +1,217 @@
1
+ import { SnippetTransformQueueError } from './snippet-transform-queue.js';
2
+ import { comparePositions, shiftPosition } from './diagnostics-model.js';
3
+ import { lineTextOf, textBeforeOf } from './line-text.js';
4
+ import { wordRangeAt } from './rename-model.js';
5
+ import { jobTable } from './snippet-field-transforms.js';
6
+ import { indentSnippet } from './snippet-indent.js';
7
+ import { positionInInsertion } from './snippet-session.js';
8
+ import { SnippetTransformError } from './snippet-transform-client.js';
9
+ import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK } from './snippet-transform-protocol.js';
10
+ import { compileSnippet, tabOrder } from './snippet.js';
11
+ import { createSnippetResolver } from './snippet-variables.js';
12
+ import { fileUriToPath } from '@adecore/lsp';
13
+ /* The same limits as an inserted completion item. */
14
+ const MAX_CHARACTERS = 2_000_000;
15
+ const MAX_RANGES = 1_000;
16
+ const TOAST_ID = 'language-completion';
17
+ class LimitError extends Error {}
18
+ /* Each line's leading tabs become the editor's own indentation unit. */
19
+ export function bodyWithIndentUnit(body, unit) {
20
+ return unit === '\t' ? body : body.replace(/^\t+/gm, tabs => unit.repeat(tabs.length));
21
+ }
22
+ /* One level of the editor's indentation: spaces for a tab size, or a tab. */
23
+ export function indentUnitOf(editor) {
24
+ const {
25
+ tabSize,
26
+ insertSpaces
27
+ } = editor.getIndentation();
28
+ return insertSpaces ? ' '.repeat(tabSize) : '\t';
29
+ }
30
+ /* The lines after the first without `base` in front of them, or without the shortest indentation among them. */
31
+ export function dedentLines(text, base) {
32
+ const lines = text.split(/\r\n|\r|\n/).slice(1);
33
+ const indents = lines.filter(line => line.trim() !== '').map(line => /^[\t ]*/.exec(line)[0].length);
34
+ const removed = base?.length ?? (indents.length === 0 ? 0 : Math.min(...indents));
35
+ return removed === 0 ? text : text.replace(/(\r\n|\r|\n)([^\r\n]*)/g, (_match, lineBreak, line) => lineBreak + line.slice(Math.min(removed, /^[\t ]*/.exec(line)[0].length)));
36
+ }
37
+ function sizeOf(parsed) {
38
+ return (parsed.transforms?.length ?? 0) + (parsed.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? parsed.stops.length);
39
+ }
40
+ /*
41
+ * Inserts a snippet at every placement, the last of them the primary one, and starts a session on its tab stops. The
42
+ * variables are resolved for each placement (`extra` first, then the host's and the built-in ones), the lines are indented
43
+ * to the text before the placement, and a running session takes the insertion as a nested one. False when nothing was inserted.
44
+ */
45
+ export async function insertSnippet(language, source, placements, extra) {
46
+ const {
47
+ editor,
48
+ project,
49
+ uri,
50
+ snippets
51
+ } = language;
52
+ if (placements.length === 0 || editor.isReadOnly?.() === true) {
53
+ return false;
54
+ }
55
+ const template = compileSnippet(source);
56
+ const now = new Date();
57
+ const token = snippets.token;
58
+ const continuing = snippets.isActive;
59
+ let stale = false;
60
+ const off = editor.onTextChange(() => {
61
+ stale = true;
62
+ });
63
+ const current = () => !stale && snippets.token === token;
64
+ try {
65
+ let characters = 0;
66
+ let ranges = 0;
67
+ const drafts = placements.map((placement, index) => {
68
+ const line = lineTextOf(editor, placement.range.start.line);
69
+ const word = wordRangeAt(line, placement.range.start);
70
+ const resolver = createSnippetResolver({
71
+ uri,
72
+ languageId: language.languageId,
73
+ position: placement.range.start,
74
+ selection: placement.range,
75
+ selectedText: placement.selectedText ?? '',
76
+ currentLine: line,
77
+ currentWord: word === null ? '' : editor.textInRange(word),
78
+ cursorIndex: index,
79
+ cursorCount: placements.length,
80
+ folder: project.host.folder,
81
+ path: fileUriToPath(uri) ?? project.host.pathOfUri?.(uri) ?? undefined
82
+ }, project.host.resolveSnippetVariable?.bind(project.host), {
83
+ now
84
+ });
85
+ const resolve = name => extra?.(name) ?? resolver(name);
86
+ const raw = template.render(resolve, undefined, {
87
+ indentVariables: true
88
+ });
89
+ return {
90
+ placement,
91
+ resolve,
92
+ raw,
93
+ parsed: indentSnippet(raw, textBeforeOf(editor, placement.range.start))
94
+ };
95
+ });
96
+ for (const {
97
+ parsed
98
+ } of drafts) {
99
+ characters += parsed.text.length;
100
+ ranges += sizeOf(parsed);
101
+ if (parsed.limited === true || characters > MAX_CHARACTERS || ranges > MAX_RANGES) {
102
+ throw new LimitError();
103
+ }
104
+ }
105
+ await transformVariables(language, template, drafts, current);
106
+ if (!current()) {
107
+ return false;
108
+ }
109
+ const changes = drafts.map(draft => ({
110
+ range: draft.placement.range,
111
+ text: draft.parsed.text
112
+ }));
113
+ if (changes.reduce((total, change) => total + change.text.length, 0) > MAX_CHARACTERS) {
114
+ throw new LimitError();
115
+ }
116
+ // Insertions before one another move it, so each start is mapped through the changes in front of it.
117
+ const starts = changes.map(change => changes.filter(other => other !== change && comparePositions(other.range.end, change.range.start) <= 0).sort((left, right) => comparePositions(right.range.start, left.range.start)).reduce((place, other) => shiftPosition(place, other), change.range.start));
118
+ if (!(continuing ? snippets.applyEdits(changes, token) : editor.applyEdits(changes))) {
119
+ return false;
120
+ }
121
+ off();
122
+ place(language, drafts, starts, continuing && snippets.isActive && snippets.token === token);
123
+ editor.focus();
124
+ return true;
125
+ } catch (error) {
126
+ if (error instanceof LimitError || error instanceof SnippetTransformError && error.kind === 'range') {
127
+ project.host.notify?.({
128
+ id: TOAST_ID,
129
+ kind: 'error',
130
+ title: project.i18n.t('editor:language.completion.tooLarge')
131
+ });
132
+ return false;
133
+ }
134
+ if (error instanceof SnippetTransformQueueError) {
135
+ project.host.notify?.({
136
+ id: TOAST_ID,
137
+ kind: 'error',
138
+ title: project.i18n.t('editor:language.completion.transformBusy')
139
+ });
140
+ return false;
141
+ }
142
+ if (error instanceof SnippetTransformError) {
143
+ project.host.notify?.({
144
+ id: TOAST_ID,
145
+ kind: 'error',
146
+ title: project.i18n.t('editor:language.completion.transformFailed')
147
+ });
148
+ return false;
149
+ }
150
+ throw error;
151
+ } finally {
152
+ off();
153
+ }
154
+ }
155
+ /* Runs the transforms of variables through the worker, once for all placements, and renders them again with the answers. */
156
+ async function transformVariables(language, template, drafts, current) {
157
+ const {
158
+ editor,
159
+ project,
160
+ snippets
161
+ } = language;
162
+ const {
163
+ jobs,
164
+ slotOf
165
+ } = jobTable();
166
+ const slots = drafts.map(draft => (draft.raw.transforms ?? []).map(range => slotOf({
167
+ ...range.transform,
168
+ value: draft.raw.text.slice(range.start, range.end)
169
+ })));
170
+ if (jobs.length === 0 || project.host.snippetTransformWorker === undefined) {
171
+ return;
172
+ }
173
+ if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
174
+ throw new LimitError();
175
+ }
176
+ const values = await snippets.transformVariables(jobs, current);
177
+ if (values === null || !current()) {
178
+ return;
179
+ }
180
+ drafts.forEach((draft, index) => {
181
+ const raw = template.render(draft.resolve, slots[index].map(slot => values[slot]), {
182
+ indentVariables: true
183
+ });
184
+ const parsed = indentSnippet(raw, textBeforeOf(editor, draft.placement.range.start));
185
+ if (parsed.limited === true) {
186
+ throw new LimitError();
187
+ }
188
+ draft.raw = raw;
189
+ draft.parsed = parsed;
190
+ });
191
+ }
192
+ /* Starts the session on the tab stops, or puts the caret after the primary insertion when there are none. */
193
+ function place(language, drafts, starts, nested) {
194
+ const {
195
+ editor,
196
+ snippets
197
+ } = language;
198
+ const primary = drafts.at(-1).parsed;
199
+ if (primary.stops.length > 0) {
200
+ snippets.beginInstances(drafts.map((draft, index) => ({
201
+ start: starts[index],
202
+ text: draft.parsed.text,
203
+ stops: tabOrder(draft.parsed),
204
+ fields: draft.parsed.fields
205
+ })), {
206
+ nested
207
+ });
208
+ return;
209
+ }
210
+ editor.setSelections(drafts.map((draft, index) => {
211
+ const end = positionInInsertion(starts[index], draft.parsed.text, draft.parsed.text.length);
212
+ return {
213
+ start: end,
214
+ end
215
+ };
216
+ }));
217
+ }