@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
@@ -0,0 +1,103 @@
1
+ import { BUILT_IN_LIVE_TEMPLATES, BUILT_IN_POSTFIX_TEMPLATES } from './template-builtins.js';
2
+ import { checkSnippet } from './snippet.js';
3
+ export { BUILT_IN_LIVE_TEMPLATES, BUILT_IN_POSTFIX_TEMPLATES };
4
+ export {} from './live-templates.js';
5
+ const MAX_KEY_LENGTH = 40;
6
+ const MAX_BODY_LENGTH = 20_000;
7
+ const POSTFIX_LANGUAGES = ['javascript', 'typescript', 'php'];
8
+ const LANGUAGES = ['javascript', 'typescript', 'php', 'css', 'html', 'xml', 'other'];
9
+ const PLACES = ['code', 'comment', 'string'];
10
+ /*
11
+ * The templates the editors of a project know: the built-in ones and the ones a host sets. A host that keeps
12
+ * templates feeds them through `set`, and the editors read the registry on every use.
13
+ */
14
+ export class TemplateRegistry {
15
+ builtins;
16
+ listeners = new Set();
17
+ own = {
18
+ live: [],
19
+ postfix: []
20
+ };
21
+ off = new Set();
22
+ current = {
23
+ liveInCompletion: true,
24
+ postfix: true,
25
+ postfixInCompletion: true
26
+ };
27
+ liveList;
28
+ postfixList;
29
+ constructor(options = {}) {
30
+ this.builtins = options.builtins !== false;
31
+ this.liveList = this.listed(BUILT_IN_LIVE_TEMPLATES, []);
32
+ this.postfixList = this.listed(BUILT_IN_POSTFIX_TEMPLATES, []);
33
+ }
34
+ /* The built-in templates that are not disabled, then the host's. */
35
+ get live() {
36
+ return this.liveList;
37
+ }
38
+ get postfix() {
39
+ return this.postfixList;
40
+ }
41
+ get settings() {
42
+ return this.current;
43
+ }
44
+ /* Replaces what the update names. A template that fails `checkTemplate` is dropped; this never throws. */
45
+ set(update) {
46
+ this.own = {
47
+ live: update.live === undefined ? this.own.live : update.live.filter(template => checkTemplate(template, 'live') === null),
48
+ postfix: update.postfix === undefined ? this.own.postfix : update.postfix.filter(template => checkTemplate(template, 'postfix') === null)
49
+ };
50
+ this.off = update.disabled === undefined ? this.off : new Set(update.disabled);
51
+ if (update.settings !== undefined) {
52
+ this.current = {
53
+ liveInCompletion: update.settings.liveInCompletion !== false,
54
+ postfix: update.settings.postfix !== false,
55
+ postfixInCompletion: update.settings.postfixInCompletion !== false
56
+ };
57
+ }
58
+ this.liveList = this.listed(BUILT_IN_LIVE_TEMPLATES, this.own.live);
59
+ this.postfixList = this.listed(BUILT_IN_POSTFIX_TEMPLATES, this.own.postfix);
60
+ for (const listener of [...this.listeners]) {
61
+ listener();
62
+ }
63
+ }
64
+ /* Calls `listener` after every `set`. Returns the function that stops it. */
65
+ onChange(listener) {
66
+ this.listeners.add(listener);
67
+ return () => {
68
+ this.listeners.delete(listener);
69
+ };
70
+ }
71
+ listed(builtins, own) {
72
+ return [...(this.builtins ? builtins.filter(template => !this.off.has(template.id)) : []), ...own];
73
+ }
74
+ }
75
+ /*
76
+ * Why a template cannot be used, in English for a host to show, or null. `kind` says which rules apply; without it a
77
+ * template with an `expression` or an `example` counts as a postfix one.
78
+ */
79
+ export function checkTemplate(template, kind) {
80
+ const postfix = kind === undefined ? 'expression' in template || 'example' in template : kind === 'postfix';
81
+ if (template.id === '') {
82
+ return 'The template has no id.';
83
+ }
84
+ if (template.key === '' || template.key.length > MAX_KEY_LENGTH) {
85
+ return `The key is one to ${MAX_KEY_LENGTH} characters.`;
86
+ }
87
+ if (postfix ? !/^[\p{L}\p{N}_$]+$/u.test(template.key) : /\s/.test(template.key)) {
88
+ return postfix ? 'The key holds letters, digits, underscores and dollar signs only.' : 'The key holds no whitespace.';
89
+ }
90
+ if (template.body.trim() === '') {
91
+ return 'The text is empty.';
92
+ }
93
+ if (template.body.length > MAX_BODY_LENGTH) {
94
+ return `The text is longer than ${MAX_BODY_LENGTH} characters.`;
95
+ }
96
+ if (template.languages.length === 0 || template.languages.some(language => !(postfix ? POSTFIX_LANGUAGES : LANGUAGES).includes(language))) {
97
+ return 'The languages are empty or not known.';
98
+ }
99
+ if (!postfix && template.places?.some(place => !PLACES.includes(place)) === true) {
100
+ return 'The places are not known.';
101
+ }
102
+ return checkSnippet(template.body);
103
+ }
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type LanguageService, type LanguageDocument, type LanguageRequestOptions, type DiagnosticsReport, type ContentChange, type Disposable, type ProviderOptions, type CallHierarchyIncomingCall, type CallHierarchyItem, type CallHierarchyOutgoingCall, type CodeAction, type CodeActionContext, type CodeLens, type Command, type CompletionContext, type CompletionItem, type CompletionResult, type DocumentHighlight, type DocumentSymbolResult, type FoldingRange, type FormattingOptions, type Hover, type InlayHint, type Location, type NavigationResult, type Position, type PrepareRenameResult, type Range, type SelectionRange, type SemanticTokens, type SemanticTokensDelta, type SignatureHelp, type SignatureHelpContext, type TextEdit, type WorkspaceEdit, type WorkspaceSymbolResult } from '@adecore/lsp';
1
+ import { type LanguageService, type LanguageDocument, type LanguageRequestOptions, type DiagnosticsReport, type ContentChange, type Disposable, type ProviderOptions, type CallHierarchyIncomingCall, type CallHierarchyItem, type CallHierarchyOutgoingCall, type CodeAction, type CodeActionContext, type CodeLens, type Command, type CompletionContext, type CompletionItem, type CompletionResult, type DocumentHighlight, type DocumentSymbolResult, type FoldingRange, type FormattingOptions, type Hover, type InlayHint, type Location, type NavigationResult, type Position, type PrepareRenameResult, type Range, type SelectionRange, type SemanticTokens, type SemanticTokensDelta, type SignatureHelp, type SignatureHelpContext, type TextEdit, type WorkspaceEdit, type WorkspaceSymbolResult, type Color, type ColorInformation, type ColorPresentation, type DocumentLink, type InlineCompletionContext, type InlineCompletionResult, type InlineValue, type InlineValueContext, type LinkedEditingRanges, type Moniker, type PreviousResultId, type ServerMessage, type ShowDocumentParams, type ShowDocumentResult, type TextDocumentSaveReason, type TypeHierarchyItem, type WorkspaceDiagnosticReport } from '@adecore/lsp';
2
2
  export { ManualTimers } from './timers.ts';
3
3
  export interface LanguageCall {
4
4
  method: string;
@@ -15,6 +15,8 @@ export declare class FakeLanguageService implements LanguageService {
15
15
  private readonly answers;
16
16
  private readonly diagnostics;
17
17
  private readonly changes;
18
+ private readonly messages;
19
+ private showDocument;
18
20
  respond(method: string, answer: (call: LanguageCall) => unknown, provider?: ProviderOptions): void;
19
21
  openDocument(document: LanguageDocument): Promise<void>;
20
22
  changeDocument(uri: string, changes: readonly ContentChange[]): Promise<void>;
@@ -58,4 +60,24 @@ export declare class FakeLanguageService implements LanguageService {
58
60
  outgoingCalls(uri: string, item: CallHierarchyItem, options?: LanguageRequestOptions): Promise<CallHierarchyOutgoingCall[] | null>;
59
61
  codeLenses(uri: string, options?: LanguageRequestOptions): Promise<CodeLens[] | null>;
60
62
  resolveCodeLens(uri: string, lens: CodeLens, options?: LanguageRequestOptions): Promise<CodeLens>;
63
+ documentLinks(uri: string, options?: LanguageRequestOptions): Promise<DocumentLink[] | null>;
64
+ resolveDocumentLink(uri: string, link: DocumentLink, options?: LanguageRequestOptions): Promise<DocumentLink>;
65
+ documentColors(uri: string, options?: LanguageRequestOptions): Promise<ColorInformation[] | null>;
66
+ colorPresentations(uri: string, color: Color, range: Range, options?: LanguageRequestOptions): Promise<ColorPresentation[] | null>;
67
+ linkedEditingRanges(uri: string, position: Position, options?: LanguageRequestOptions): Promise<LinkedEditingRanges | null>;
68
+ monikers(uri: string, position: Position, options?: LanguageRequestOptions): Promise<Moniker[] | null>;
69
+ prepareTypeHierarchy(uri: string, position: Position, options?: LanguageRequestOptions): Promise<TypeHierarchyItem[] | null>;
70
+ supertypes(uri: string, item: TypeHierarchyItem, options?: LanguageRequestOptions): Promise<TypeHierarchyItem[] | null>;
71
+ subtypes(uri: string, item: TypeHierarchyItem, options?: LanguageRequestOptions): Promise<TypeHierarchyItem[] | null>;
72
+ inlineValues(uri: string, range: Range, context: InlineValueContext, options?: LanguageRequestOptions): Promise<InlineValue[] | null>;
73
+ inlineCompletions(uri: string, position: Position, context: InlineCompletionContext, options?: LanguageRequestOptions): Promise<InlineCompletionResult>;
74
+ rangesFormatting(uri: string, ranges: readonly Range[], formatting: FormattingOptions, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
75
+ willSave(uri: string, reason?: TextDocumentSaveReason): Promise<void>;
76
+ willSaveWaitUntil(uri: string, reason?: TextDocumentSaveReason, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
77
+ workspaceDiagnostics(uri: string, previousResultIds?: readonly PreviousResultId[], options?: LanguageRequestOptions): Promise<WorkspaceDiagnosticReport>;
78
+ textDocumentContent(uri: string, options?: LanguageRequestOptions): Promise<string | null>;
79
+ onServerMessage(listener: (message: ServerMessage) => void): Disposable;
80
+ onShowDocument(handler: (params: ShowDocumentParams) => Promise<ShowDocumentResult>): Disposable;
81
+ message(message: ServerMessage): void;
82
+ show(params: ShowDocumentParams): Promise<boolean>;
61
83
  }
package/dist/testing.js CHANGED
@@ -7,6 +7,8 @@ export class FakeLanguageService {
7
7
  answers = new Map();
8
8
  diagnostics = new Set();
9
9
  changes = new Set();
10
+ messages = new Set();
11
+ showDocument = null;
10
12
  respond(method, answer, provider = {}) {
11
13
  this.answers.set(method, answer);
12
14
  this.providers.set(method, provider);
@@ -248,4 +250,116 @@ export class FakeLanguageService {
248
250
  resolveCodeLens(uri, lens, options = {}) {
249
251
  return this.request('codeLens/resolve', uri, lens, options);
250
252
  }
253
+ documentLinks(uri, options = {}) {
254
+ return this.request('textDocument/documentLink', uri, {}, options);
255
+ }
256
+ resolveDocumentLink(uri, link, options = {}) {
257
+ return this.request('documentLink/resolve', uri, link, options);
258
+ }
259
+ documentColors(uri, options = {}) {
260
+ return this.request('textDocument/documentColor', uri, {}, options);
261
+ }
262
+ colorPresentations(uri, color, range, options = {}) {
263
+ return this.request('textDocument/colorPresentation', uri, {
264
+ color,
265
+ range
266
+ }, options);
267
+ }
268
+ linkedEditingRanges(uri, position, options = {}) {
269
+ return this.request('textDocument/linkedEditingRange', uri, {
270
+ position
271
+ }, options);
272
+ }
273
+ monikers(uri, position, options = {}) {
274
+ return this.request('textDocument/moniker', uri, {
275
+ position
276
+ }, options);
277
+ }
278
+ prepareTypeHierarchy(uri, position, options = {}) {
279
+ return this.request('textDocument/prepareTypeHierarchy', uri, {
280
+ position
281
+ }, options);
282
+ }
283
+ supertypes(uri, item, options = {}) {
284
+ return this.request('typeHierarchy/supertypes', uri, {
285
+ item
286
+ }, options);
287
+ }
288
+ subtypes(uri, item, options = {}) {
289
+ return this.request('typeHierarchy/subtypes', uri, {
290
+ item
291
+ }, options);
292
+ }
293
+ inlineValues(uri, range, context, options = {}) {
294
+ return this.request('textDocument/inlineValue', uri, {
295
+ range,
296
+ context
297
+ }, options);
298
+ }
299
+ inlineCompletions(uri, position, context, options = {}) {
300
+ return this.request('textDocument/inlineCompletion', uri, {
301
+ position,
302
+ context
303
+ }, options);
304
+ }
305
+ rangesFormatting(uri, ranges, formatting, options = {}) {
306
+ return this.request('textDocument/rangesFormatting', uri, {
307
+ ranges,
308
+ options: formatting
309
+ }, options);
310
+ }
311
+ async willSave(uri, reason = 1) {
312
+ this.calls.push({
313
+ method: 'textDocument/willSave',
314
+ uri,
315
+ params: {
316
+ reason
317
+ },
318
+ options: {}
319
+ });
320
+ }
321
+ async willSaveWaitUntil(uri, reason = 1, options = {}) {
322
+ return this.supports('textDocument/willSaveWaitUntil', uri) ? this.request('textDocument/willSaveWaitUntil', uri, {
323
+ reason
324
+ }, options) : null;
325
+ }
326
+ workspaceDiagnostics(uri, previousResultIds = [], options = {}) {
327
+ return this.request('workspace/diagnostic', uri, {
328
+ previousResultIds
329
+ }, options);
330
+ }
331
+ async textDocumentContent(uri, options = {}) {
332
+ if (!this.supports('workspace/textDocumentContent', uri)) {
333
+ return null;
334
+ }
335
+ return (await this.request('workspace/textDocumentContent', uri, {
336
+ uri
337
+ }, options))?.text ?? null;
338
+ }
339
+ onServerMessage(listener) {
340
+ this.messages.add(listener);
341
+ return {
342
+ dispose: () => this.messages.delete(listener)
343
+ };
344
+ }
345
+ onShowDocument(handler) {
346
+ this.showDocument = handler;
347
+ return {
348
+ dispose: () => {
349
+ if (this.showDocument === handler) {
350
+ this.showDocument = null;
351
+ }
352
+ }
353
+ };
354
+ }
355
+ /* Sends a message as a server would. */
356
+ message(message) {
357
+ for (const listener of this.messages) {
358
+ listener(message);
359
+ }
360
+ }
361
+ /* Asks the host to show a document as a server would; false when nothing handles it. */
362
+ async show(params) {
363
+ return (await this.showDocument?.(params))?.success ?? false;
364
+ }
251
365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-react",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "type": "module",
5
5
  "description": "Language-enabled editors, feature coordination and React popups over injected language services.",
6
6
  "license": "FSL-1.1-MIT",
@@ -62,9 +62,10 @@
62
62
  "test": "bun test --conditions=source"
63
63
  },
64
64
  "dependencies": {
65
- "@adecore/editor": "0.42.0",
66
- "@adecore/lsp": "0.42.0",
67
- "@adecore/merge": "0.42.0",
65
+ "@adecore/editor": "0.44.0",
66
+ "@adecore/lsp": "0.44.0",
67
+ "@adecore/merge": "0.44.0",
68
+ "emmet": "^2.4.11",
68
69
  "lucide-react": "^1.61.0",
69
70
  "react-markdown": "^10.1.0",
70
71
  "remark-breaks": "^4.0.0",
@@ -72,14 +73,14 @@
72
73
  "zustand": "^5.0.16"
73
74
  },
74
75
  "peerDependencies": {
75
- "@adecore/ui": "^0.42.0",
76
+ "@adecore/ui": "^0.44.0",
76
77
  "i18next": "^26.4.2",
77
78
  "react": "^19.3.0",
78
79
  "react-dom": "^19.3.0",
79
80
  "react-i18next": "^17.0.15"
80
81
  },
81
82
  "devDependencies": {
82
- "@adecore/ui": "0.42.0",
83
+ "@adecore/ui": "0.44.0",
83
84
  "@types/bun": "^1.4.4",
84
85
  "@types/react": "^19.3.0",
85
86
  "@types/react-dom": "^19.3.0",
@@ -9,7 +9,7 @@ import { CALL_HIERARCHY_HEIGHT } from './call-hierarchy.ts';
9
9
  import { PanelResizeHandle } from './PanelResizeHandle.tsx';
10
10
  import { usePanelBody } from './use-panel-body.ts';
11
11
  import { PANEL_BODY_HEIGHT } from './panel-size.ts';
12
- import { CALL_LEVEL_LIMIT, type CallDirection, type CallRow } from './call-hierarchy-model.ts';
12
+ import { CALL_LEVEL_LIMIT, isTypeDirection, type CallDirection, type CallRow } from './call-hierarchy-model.ts';
13
13
  import type { EditorLanguage } from './editor-language.ts';
14
14
  import { basenameOf, relativeTo } from './paths.ts';
15
15
  import { PathText } from './PathText.tsx';
@@ -125,7 +125,12 @@ function Tree({ language, view }: { language: EditorLanguage; view: CallHierarch
125
125
  useInputPopup(language, treeId, activeId);
126
126
 
127
127
  return (
128
- <div id={treeId} role="tree" aria-label={t('language.callHierarchy.label')} className="w-1/2 shrink-0 overflow-y-auto border-r border-border py-1">
128
+ <div
129
+ id={treeId}
130
+ role="tree"
131
+ aria-label={t(isTypeDirection(view.direction) ? 'language.callHierarchy.typesLabel' : 'language.callHierarchy.label')}
132
+ className="w-1/2 shrink-0 overflow-y-auto border-r border-border py-1"
133
+ >
129
134
  {view.rows.map((row) => {
130
135
  const active = row.id === view.active;
131
136
  const ref = active ? activeRow : undefined;
@@ -174,11 +179,14 @@ function StatusText({ row, direction }: { row: Extract<CallRow, { kind: 'status'
174
179
  );
175
180
  }
176
181
 
177
- return (
178
- <span className="font-sans text-xs text-text-muted">
179
- {t(direction === 'incoming' ? 'language.callHierarchy.noIncoming' : 'language.callHierarchy.noOutgoing')}
180
- </span>
181
- );
182
+ const empty = {
183
+ incoming: 'noIncoming',
184
+ outgoing: 'noOutgoing',
185
+ supertypes: 'noSupertypes',
186
+ subtypes: 'noSubtypes'
187
+ } as const;
188
+
189
+ return <span className="font-sans text-xs text-text-muted">{t(`language.callHierarchy.${empty[direction]}`)}</span>;
182
190
  }
183
191
 
184
192
  /*
@@ -210,10 +218,17 @@ export function CallHierarchyPanel({ language, view }: { language: EditorLanguag
210
218
  value={view.direction}
211
219
  onValueChange={(direction) => language.callHierarchy.setDirection(direction)}
212
220
  label={t('language.callHierarchy.direction')}
213
- options={[
214
- { id: 'incoming', label: t('language.callHierarchy.incoming') },
215
- { id: 'outgoing', label: t('language.callHierarchy.outgoing') }
216
- ]}
221
+ options={
222
+ isTypeDirection(view.direction)
223
+ ? [
224
+ { id: 'supertypes', label: t('language.callHierarchy.supertypes') },
225
+ { id: 'subtypes', label: t('language.callHierarchy.subtypes') }
226
+ ]
227
+ : [
228
+ { id: 'incoming', label: t('language.callHierarchy.incoming') },
229
+ { id: 'outgoing', label: t('language.callHierarchy.outgoing') }
230
+ ]
231
+ }
217
232
  />
218
233
  <span className="shrink-0 font-medium">{view.name}</span>
219
234
  <span className="shrink-0 font-medium text-text-muted">{path === null ? '' : basenameOf(path)}</span>
@@ -28,6 +28,7 @@ export function EditorContextMenu({ language, view, children }: { language: Edit
28
28
  implementation: () => void language.navigation.go('implementation'),
29
29
  peekReferences: () => void language.peek.open(),
30
30
  callHierarchy: () => void language.callHierarchy.open(),
31
+ typeHierarchy: () => void language.callHierarchy.open(undefined, 'supertypes'),
31
32
  renameSymbol: () => void language.rename.start(),
32
33
  codeActions: () => void language.codeActions.open()
33
34
  };
@@ -100,6 +101,12 @@ export function EditorContextMenu({ language, view, children }: { language: Edit
100
101
  {t('commands.formatDocument')}
101
102
  {language.shortcuts.formatDocument !== null && <Kbd shortcut={language.shortcuts.formatDocument} />}
102
103
  </ContextMenu.Item>
104
+ {language.codeActions.formatsSelection &&
105
+ language.editor
106
+ .getSelections()
107
+ .some((range) => range.start.line !== range.end.line || range.start.character !== range.end.character) && (
108
+ <ContextMenu.Item onClick={() => void language.codeActions.formatSelection()}>{t('commands.formatSelection')}</ContextMenu.Item>
109
+ )}
103
110
  </>
104
111
  )}
105
112
  </ContextMenu.Popup>
@@ -13,6 +13,7 @@ import { RenameCard } from './RenameCard.tsx';
13
13
  import { RenderedDocs } from './RenderedDocs.tsx';
14
14
  import { SymbolPicker } from './SymbolPicker.tsx';
15
15
  import { SignatureCard } from './SignatureCard.tsx';
16
+ import { WrapCard } from './WrapCard.tsx';
16
17
  import type { HoverView } from './popups.ts';
17
18
 
18
19
  export interface LanguagePopupsProps<Language extends EditorLanguage = EditorLanguage> {
@@ -44,6 +45,7 @@ export function LanguagePopups<Language extends EditorLanguage>({ language, cont
44
45
  const symbols = useStore(language.popups, (state) => state.symbols);
45
46
  const menu = useStore(language.popups, (state) => state.menu);
46
47
  const authors = useStore(language.popups, (state) => state.authors);
48
+ const wrap = useStore(language.popups, (state) => state.wrap);
47
49
 
48
50
  useEffect(() => language.editor.onViewChange(redraw), [language]);
49
51
 
@@ -53,6 +55,7 @@ export function LanguagePopups<Language extends EditorLanguage>({ language, cont
53
55
  const pickRect = pick === null ? null : language.editor.rectAt(pick.anchor);
54
56
  const renameRect = rename === null ? null : language.editor.rectAt(rename.range.start);
55
57
  const renameEnd = rename === null ? null : language.editor.rectAt(rename.range.end);
58
+ const wrapRect = wrap === null ? null : language.editor.rectAt(wrap.anchor);
56
59
 
57
60
  return (
58
61
  <>
@@ -85,6 +88,7 @@ export function LanguagePopups<Language extends EditorLanguage>({ language, cont
85
88
  {peek !== null && <PeekPanel language={language} view={peek} />}
86
89
  {callHierarchy !== null && <CallHierarchyPanel language={language} view={callHierarchy} />}
87
90
  {authors !== null && <CodeAuthorsCard language={language} view={authors} />}
91
+ {wrap !== null && wrapRect !== null && <WrapCard language={language} view={wrap} rect={wrapRect} />}
88
92
  {children}
89
93
  {rename !== null && renameRect !== null && <RenameCard language={language} view={rename} rect={renameRect} endRect={renameEnd} />}
90
94
  </>
package/src/PickPopup.tsx CHANGED
@@ -10,7 +10,34 @@ import type { PickPreview, PickView } from './popups.ts';
10
10
  const CARD =
11
11
  'fixed top-0 left-0 z-(--z-popup) flex w-[420px] flex-col overflow-hidden rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow)';
12
12
 
13
+ /* The label with the part a query matched marked, whatever the case. */
14
+ function MatchedLabel({ label, query }: { label: string; query: string | null }) {
15
+ const start = query === null || query === '' ? -1 : label.toLowerCase().indexOf(query.toLowerCase());
16
+
17
+ if (query === null || start < 0) {
18
+ return label;
19
+ }
20
+
21
+ return (
22
+ <>
23
+ {label.slice(0, start)}
24
+ <mark className="bg-transparent font-semibold text-accent">{label.slice(start, start + query.length)}</mark>
25
+ {label.slice(start + query.length)}
26
+ </>
27
+ );
28
+ }
29
+
13
30
  function PreviewLines({ preview }: { preview: PickPreview }) {
31
+ if (preview.kind === 'text') {
32
+ return (
33
+ <div className="max-h-48 overflow-auto border-t border-border p-2 font-mono text-code whitespace-pre">
34
+ {preview.added.map((line, index) => (
35
+ <div key={index}>{line === '' ? ' ' : line}</div>
36
+ ))}
37
+ </div>
38
+ );
39
+ }
40
+
14
41
  return (
15
42
  <div className="flex flex-col gap-px border-t border-border p-1 font-mono text-code">
16
43
  {preview.removed.map((line, index) => (
@@ -78,6 +105,7 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
78
105
  }
79
106
 
80
107
  const activeId = optionIds.get(view.active);
108
+ const mnemonics = view.groups.some((group) => group.rows.some((row) => row.mnemonic !== undefined));
81
109
 
82
110
  useInputPopup(language, listId, activeId);
83
111
 
@@ -89,7 +117,12 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
89
117
  onPointerDown={(event) => event.preventDefault()}
90
118
  {...keepFocusOutOfList(() => language.editor.focus())}
91
119
  >
92
- {view.title !== null && <div className="border-b border-border px-3 py-1.5 text-xs text-text-muted">{view.title}</div>}
120
+ {(view.title !== null || view.query !== null) && (
121
+ <div className="flex items-baseline gap-2 border-b border-border px-3 py-1.5 text-xs text-text-muted">
122
+ <span className="min-w-0 truncate">{view.title}</span>
123
+ {view.query !== null && view.query !== '' && <span className="ml-auto shrink-0 font-mono text-text">{view.query}</span>}
124
+ </div>
125
+ )}
93
126
  <div id={listId} role="listbox" className="min-h-0 flex-1 overflow-y-auto p-1">
94
127
  {view.groups.map((group, groupIndex) => (
95
128
  <div key={group.title ?? groupIndex} role="group" className="flex flex-col">
@@ -102,12 +135,21 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
102
135
  type="button"
103
136
  role="option"
104
137
  tabIndex={-1}
105
- aria-selected={row.id === view.active}
138
+ aria-selected={row.id === view.active || view.chosen.includes(row.id)}
139
+ aria-keyshortcuts={row.mnemonic}
106
140
  data-active={row.id === view.active}
107
- className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row"
108
- onClick={() => language.pick.choose(row.id)}
141
+ data-chosen={view.chosen.includes(row.id)}
142
+ className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row data-[chosen=true]:bg-accent/15"
143
+ onClick={(event) => language.pick.choose(row.id, { toggle: event.metaKey || event.ctrlKey, range: event.shiftKey })}
109
144
  >
110
- <span className={row.path ? 'shrink-0' : 'min-w-0 truncate'}>{row.label}</span>
145
+ {mnemonics && (
146
+ <span aria-hidden="true" className="w-3 shrink-0 text-center font-mono text-text-faint">
147
+ {row.mnemonic}
148
+ </span>
149
+ )}
150
+ <span className={row.path ? 'shrink-0' : 'min-w-0 truncate'}>
151
+ <MatchedLabel label={row.label} query={view.query} />
152
+ </span>
111
153
  {row.path ? (
112
154
  <PathText path={row.detail} className="ml-auto font-mono text-text-faint" />
113
155
  ) : (
@@ -0,0 +1,64 @@
1
+ import { useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { EditorRect } from '@adecore/editor';
4
+ import { Input } from '@adecore/ui';
5
+ import { AnchoredPopup } from './AnchoredPopup.tsx';
6
+ import type { EditorLanguage } from './editor-language.ts';
7
+ import type { WrapView } from './popups.ts';
8
+
9
+ /* The field for the abbreviation a selection is wrapped in: Enter wraps, Escape or leaving the field cancels. */
10
+ export function WrapCard({ language, view, rect }: { language: EditorLanguage; view: WrapView; rect: EditorRect }) {
11
+ const { t } = useTranslation('editor');
12
+ const input = useRef<HTMLInputElement>(null);
13
+ const [value, setValue] = useState('');
14
+
15
+ useLayoutEffect(() => {
16
+ input.current?.focus();
17
+ }, []);
18
+
19
+ function onKeyDown(event: KeyboardEvent<HTMLInputElement>): void {
20
+ if (event.nativeEvent.isComposing) {
21
+ return;
22
+ }
23
+ if (event.key === 'Escape') {
24
+ event.preventDefault();
25
+ event.stopPropagation();
26
+ language.markup.cancelWrap();
27
+ language.editor.focus();
28
+ } else if (event.key === 'Enter') {
29
+ event.preventDefault();
30
+ event.stopPropagation();
31
+ if (value.trim() !== '') {
32
+ language.markup.submitWrap(value);
33
+ }
34
+ }
35
+ }
36
+
37
+ return (
38
+ <AnchoredPopup rect={rect} placement={{ prefer: 'above' }}>
39
+ <div className="flex w-[280px] flex-col gap-1.5 p-2">
40
+ <Input
41
+ ref={input}
42
+ size="sm"
43
+ mono
44
+ value={value}
45
+ spellCheck={false}
46
+ autoComplete="off"
47
+ aria-label={t('language.markup.wrap')}
48
+ aria-invalid={view.error !== null}
49
+ placeholder={t('language.markup.placeholder')}
50
+ onChange={(event) => setValue(event.target.value)}
51
+ onKeyDown={onKeyDown}
52
+ onBlur={() => language.markup.cancelWrap()}
53
+ />
54
+ {view.error !== null ? (
55
+ <div role="alert" className="text-xs text-status-error">
56
+ {view.error}
57
+ </div>
58
+ ) : (
59
+ <div className="text-xs text-text-muted">↵ {t('language.markup.apply')}</div>
60
+ )}
61
+ </div>
62
+ </AnchoredPopup>
63
+ );
64
+ }
@@ -1,7 +1,12 @@
1
- import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Range } from '@adecore/lsp';
1
+ import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Range, TypeHierarchyItem } from '@adecore/lsp';
2
2
 
3
- /* Callers of a symbol, or the symbols it calls. */
4
- export type CallDirection = 'incoming' | 'outgoing';
3
+ /* Callers of a symbol or the symbols it calls; or the super and sub types of a type, which the same tree draws. */
4
+ export type CallDirection = 'incoming' | 'outgoing' | 'supertypes' | 'subtypes';
5
+
6
+ /* Whether a direction walks a type hierarchy and not the calls. */
7
+ export function isTypeDirection(direction: CallDirection): direction is 'supertypes' | 'subtypes' {
8
+ return direction === 'supertypes' || direction === 'subtypes';
9
+ }
5
10
 
6
11
  /* The most calls of one level that are drawn at once; the rest wait behind a row that shows more. */
7
12
  export const CALL_LEVEL_LIMIT = 50;
@@ -103,6 +108,23 @@ export function childrenOf(direction: CallDirection, parent: CallNode, calls: re
103
108
  }));
104
109
  }
105
110
 
111
+ /* The children of a node made from the types a server returned; a type has no call sites, so a row goes to the type itself. */
112
+ export function typeChildrenOf(parent: CallNode, items: readonly TypeHierarchyItem[]): CallNode[] {
113
+ return items.map((item, index) => ({
114
+ id: `${parent.id}.${index}`,
115
+ parent: parent.id,
116
+ level: parent.level + 1,
117
+ item,
118
+ sites: [],
119
+ siteUri: null,
120
+ expanded: false,
121
+ load: 'idle',
122
+ error: null,
123
+ children: [],
124
+ shown: 0
125
+ }));
126
+ }
127
+
106
128
  /* The lines the tree draws: every root, and under an expanded node its status, its calls and the row that shows more. */
107
129
  export function rowsOf(nodes: ReadonlyMap<string, CallNode>, roots: readonly string[]): CallRow[] {
108
130
  const rows: CallRow[] = [];