@adecore/editor-react 0.22.0 → 0.23.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 (196) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +88 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PeekPanel.d.ts +3 -0
  15. package/dist/PeekPanel.js +2 -3
  16. package/dist/PickPopup.js +15 -2
  17. package/dist/RenameCard.js +10 -5
  18. package/dist/RenderedDocs.d.ts +9 -0
  19. package/dist/RenderedDocs.js +84 -0
  20. package/dist/SplitDiffView.d.ts +9 -0
  21. package/dist/SplitDiffView.js +276 -0
  22. package/dist/SymbolPicker.d.ts +3 -0
  23. package/dist/SymbolPicker.js +4 -3
  24. package/dist/call-hierarchy-model.d.ts +52 -0
  25. package/dist/call-hierarchy-model.js +99 -0
  26. package/dist/call-hierarchy.d.ts +42 -0
  27. package/dist/call-hierarchy.js +399 -0
  28. package/dist/code-actions-model.d.ts +2 -0
  29. package/dist/code-actions-model.js +12 -10
  30. package/dist/code-actions.js +3 -1
  31. package/dist/completion-model.d.ts +14 -4
  32. package/dist/completion-model.js +40 -33
  33. package/dist/completion.d.ts +4 -0
  34. package/dist/completion.js +257 -42
  35. package/dist/diagnostics.js +8 -6
  36. package/dist/diff-expansion.d.ts +9 -0
  37. package/dist/diff-expansion.js +41 -0
  38. package/dist/diff-overlays.d.ts +15 -0
  39. package/dist/diff-overlays.js +28 -0
  40. package/dist/diff-syntax.d.ts +16 -0
  41. package/dist/diff-syntax.js +49 -0
  42. package/dist/diff-wordmarks.d.ts +5 -0
  43. package/dist/diff-wordmarks.js +26 -0
  44. package/dist/doc-comment-model.d.ts +23 -0
  45. package/dist/doc-comment-model.js +291 -0
  46. package/dist/editor-language.d.ts +13 -1
  47. package/dist/editor-language.js +14 -4
  48. package/dist/editor-react.css +79 -0
  49. package/dist/format-on-type.d.ts +14 -0
  50. package/dist/format-on-type.js +96 -0
  51. package/dist/host-types.d.ts +17 -0
  52. package/dist/hover.d.ts +4 -0
  53. package/dist/hover.js +15 -2
  54. package/dist/index.d.ts +19 -5
  55. package/dist/index.js +9 -1
  56. package/dist/inlay-hints.d.ts +47 -2
  57. package/dist/inlay-hints.js +353 -16
  58. package/dist/language-ids.js +4 -2
  59. package/dist/locales/en.json +114 -3
  60. package/dist/locales/nl.json +114 -3
  61. package/dist/models.d.ts +6 -2
  62. package/dist/models.js +5 -1
  63. package/dist/patch-decorations.d.ts +27 -0
  64. package/dist/patch-decorations.js +182 -0
  65. package/dist/patch-layout.d.ts +40 -0
  66. package/dist/patch-layout.js +304 -0
  67. package/dist/peek.js +1 -0
  68. package/dist/pick.d.ts +1 -0
  69. package/dist/pick.js +11 -1
  70. package/dist/popups.d.ts +17 -0
  71. package/dist/popups.js +1 -0
  72. package/dist/project-language.d.ts +8 -0
  73. package/dist/project-language.js +34 -2
  74. package/dist/refresher.d.ts +1 -0
  75. package/dist/refresher.js +5 -0
  76. package/dist/rename-model.d.ts +1 -1
  77. package/dist/rename-model.js +3 -7
  78. package/dist/rename.js +5 -8
  79. package/dist/rendered-docs.d.ts +62 -0
  80. package/dist/rendered-docs.js +368 -0
  81. package/dist/row-host.d.ts +2 -0
  82. package/dist/row-host.js +11 -3
  83. package/dist/semantic-delta.d.ts +2 -0
  84. package/dist/semantic-delta.js +27 -0
  85. package/dist/semantic-model.js +16 -0
  86. package/dist/semantic-tokens.d.ts +26 -1
  87. package/dist/semantic-tokens.js +174 -14
  88. package/dist/shortcut-keys.d.ts +2 -0
  89. package/dist/shortcut-keys.js +6 -2
  90. package/dist/signature.js +1 -1
  91. package/dist/snippet-field-transforms.d.ts +30 -0
  92. package/dist/snippet-field-transforms.js +116 -0
  93. package/dist/snippet-indent.d.ts +2 -0
  94. package/dist/snippet-indent.js +49 -0
  95. package/dist/snippet-range-order.d.ts +3 -0
  96. package/dist/snippet-range-order.js +19 -0
  97. package/dist/snippet-session.d.ts +42 -6
  98. package/dist/snippet-session.js +650 -53
  99. package/dist/snippet-transform-client.d.ts +19 -0
  100. package/dist/snippet-transform-client.js +66 -0
  101. package/dist/snippet-transform-format.d.ts +15 -0
  102. package/dist/snippet-transform-format.js +302 -0
  103. package/dist/snippet-transform-protocol.d.ts +45 -0
  104. package/dist/snippet-transform-protocol.js +8 -0
  105. package/dist/snippet-transform-queue.d.ts +32 -0
  106. package/dist/snippet-transform-queue.js +173 -0
  107. package/dist/snippet-transform-worker.d.ts +10 -0
  108. package/dist/snippet-transform-worker.js +41 -0
  109. package/dist/snippet-variable-names.d.ts +1 -0
  110. package/dist/snippet-variable-names.js +36 -0
  111. package/dist/snippet-variables.d.ts +7 -0
  112. package/dist/snippet-variables.js +66 -0
  113. package/dist/snippet.d.ts +30 -3
  114. package/dist/snippet.js +402 -87
  115. package/dist/split-alignment.d.ts +6 -0
  116. package/dist/split-alignment.js +38 -0
  117. package/dist/split-scroll.d.ts +14 -0
  118. package/dist/split-scroll.js +45 -0
  119. package/dist/symbols.d.ts +2 -1
  120. package/dist/symbols.js +39 -7
  121. package/dist/testing.d.ts +5 -1
  122. package/dist/testing.js +12 -0
  123. package/dist/workspace-edit.d.ts +25 -3
  124. package/dist/workspace-edit.js +282 -143
  125. package/package.json +10 -5
  126. package/src/CallHierarchyPanel.tsx +238 -0
  127. package/src/CompletionPopup.tsx +12 -1
  128. package/src/DiffView.tsx +262 -0
  129. package/src/EditorContextMenu.tsx +1 -0
  130. package/src/EditorView.tsx +21 -2
  131. package/src/FindReplace.tsx +51 -13
  132. package/src/HoverCard.tsx +23 -21
  133. package/src/LanguagePopups.tsx +5 -0
  134. package/src/Markdown.tsx +10 -3
  135. package/src/PeekPanel.tsx +2 -3
  136. package/src/PickPopup.tsx +18 -2
  137. package/src/RenameCard.tsx +18 -0
  138. package/src/RenderedDocs.tsx +155 -0
  139. package/src/SplitDiffView.tsx +337 -0
  140. package/src/SymbolPicker.tsx +4 -3
  141. package/src/call-hierarchy-model.ts +158 -0
  142. package/src/call-hierarchy.ts +441 -0
  143. package/src/code-actions-model.ts +17 -11
  144. package/src/code-actions.ts +3 -1
  145. package/src/completion-model.ts +61 -35
  146. package/src/completion.ts +302 -42
  147. package/src/diagnostics.ts +8 -6
  148. package/src/diff-expansion.ts +49 -0
  149. package/src/diff-overlays.tsx +69 -0
  150. package/src/diff-syntax.ts +67 -0
  151. package/src/diff-wordmarks.ts +31 -0
  152. package/src/doc-comment-model.ts +336 -0
  153. package/src/editor-language.ts +21 -5
  154. package/src/editor-react.css +79 -0
  155. package/src/format-on-type.ts +111 -0
  156. package/src/host-types.ts +19 -0
  157. package/src/hover.ts +18 -3
  158. package/src/index.ts +27 -4
  159. package/src/inlay-hints.ts +394 -18
  160. package/src/language-ids.ts +4 -2
  161. package/src/locales/en.json +114 -3
  162. package/src/locales/nl.json +114 -3
  163. package/src/models.ts +40 -1
  164. package/src/patch-decorations.ts +224 -0
  165. package/src/patch-layout.ts +388 -0
  166. package/src/peek.ts +1 -0
  167. package/src/pick.ts +12 -1
  168. package/src/popups.ts +24 -0
  169. package/src/project-language.ts +43 -2
  170. package/src/refresher.ts +6 -0
  171. package/src/rename-model.ts +4 -8
  172. package/src/rename.ts +5 -8
  173. package/src/rendered-docs.ts +422 -0
  174. package/src/row-host.ts +12 -3
  175. package/src/semantic-delta.ts +29 -0
  176. package/src/semantic-model.ts +17 -0
  177. package/src/semantic-tokens.ts +202 -19
  178. package/src/shortcut-keys.ts +6 -2
  179. package/src/signature.ts +1 -1
  180. package/src/snippet-field-transforms.ts +144 -0
  181. package/src/snippet-indent.ts +53 -0
  182. package/src/snippet-range-order.ts +19 -0
  183. package/src/snippet-session.ts +797 -56
  184. package/src/snippet-transform-client.ts +84 -0
  185. package/src/snippet-transform-format.ts +325 -0
  186. package/src/snippet-transform-protocol.ts +24 -0
  187. package/src/snippet-transform-queue.ts +199 -0
  188. package/src/snippet-transform-worker.ts +58 -0
  189. package/src/snippet-variable-names.ts +36 -0
  190. package/src/snippet-variables.ts +74 -0
  191. package/src/snippet.ts +452 -89
  192. package/src/split-alignment.ts +45 -0
  193. package/src/split-scroll.ts +54 -0
  194. package/src/symbols.ts +43 -7
  195. package/src/testing.ts +25 -0
  196. package/src/workspace-edit.ts +347 -169
package/dist/PickPopup.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useLayoutEffect, useRef } from 'react';
2
+ import { useId, useLayoutEffect, useRef } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { PathText } from './PathText.js';
5
5
  import { placePopup } from './popup-placement.js';
@@ -12,6 +12,7 @@ function PreviewLines({ preview }) {
12
12
  * the focus from the editor, so the keys keep going to it while the list is open.
13
13
  */
14
14
  export function PickPopup({ language, view, rect }) {
15
+ const listId = useId();
15
16
  const card = useRef(null);
16
17
  const activeRow = useRef(null);
17
18
  // Every render: the character moves with the scroll, and the list changes size with its preview.
@@ -29,5 +30,17 @@ export function PickPopup({ language, view, rect }) {
29
30
  useLayoutEffect(() => {
30
31
  activeRow.current?.scrollIntoView({ block: 'nearest' });
31
32
  }, [view.active]);
32
- return createPortal(_jsxs("div", { ref: card, className: CARD, style: { visibility: 'hidden' }, onPointerDown: (event) => event.preventDefault(), children: [view.title !== null && _jsx("div", { className: "border-b border-border px-3 py-1.5 text-xs text-text-muted", children: view.title }), _jsx("div", { role: "listbox", className: "min-h-0 flex-1 overflow-y-auto p-1", children: view.groups.map((group, groupIndex) => (_jsxs("div", { role: "group", className: "flex flex-col", children: [group.title !== null && _jsx("div", { className: "px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint", children: group.title }), group.rows.map((row) => (_jsxs("button", { ref: row.id === view.active ? activeRow : undefined, type: "button", role: "option", "aria-selected": row.id === view.active, "data-active": row.id === view.active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => language.pick.choose(row.id), children: [_jsx("span", { className: row.path ? 'shrink-0' : 'min-w-0 truncate', children: row.label }), row.path ? (_jsx(PathText, { path: row.detail, className: "ml-auto font-mono text-text-faint" })) : (_jsx("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: row.id === view.active && row.detail === '' ? '↵' : row.detail }))] }, row.id)))] }, group.title ?? groupIndex))) }), view.preview !== null && _jsx(PreviewLines, { preview: view.preview })] }), document.body);
33
+ const optionIds = new Map();
34
+ for (const group of view.groups) {
35
+ for (const row of group.rows) {
36
+ optionIds.set(row.id, `${listId}-option-${optionIds.size}`);
37
+ }
38
+ }
39
+ const activeId = optionIds.get(view.active);
40
+ // The focus stays in the text, so the input is what names the list and its active option.
41
+ useLayoutEffect(() => {
42
+ language.editor.setInputPopup({ controls: listId, activeDescendant: activeId });
43
+ return () => language.editor.setInputPopup(null);
44
+ }, [language, listId, activeId]);
45
+ return createPortal(_jsxs("div", { ref: card, className: CARD, style: { visibility: 'hidden' }, onPointerDown: (event) => event.preventDefault(), children: [view.title !== null && _jsx("div", { className: "border-b border-border px-3 py-1.5 text-xs text-text-muted", children: view.title }), _jsx("div", { id: listId, role: "listbox", className: "min-h-0 flex-1 overflow-y-auto p-1", children: view.groups.map((group, groupIndex) => (_jsxs("div", { role: "group", className: "flex flex-col", children: [group.title !== null && _jsx("div", { className: "px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint", children: group.title }), group.rows.map((row) => (_jsxs("button", { id: optionIds.get(row.id), ref: row.id === view.active ? activeRow : undefined, type: "button", role: "option", "aria-selected": row.id === view.active, "data-active": row.id === view.active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => language.pick.choose(row.id), children: [_jsx("span", { className: row.path ? 'shrink-0' : 'min-w-0 truncate', children: row.label }), row.path ? (_jsx(PathText, { path: row.detail, className: "ml-auto font-mono text-text-faint" })) : (_jsx("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: row.id === view.active && row.detail === '' ? '↵' : row.detail }))] }, row.id)))] }, group.title ?? groupIndex))) }), view.preview !== null && _jsx(PreviewLines, { preview: view.preview })] }), document.body);
33
46
  }
@@ -35,11 +35,16 @@ function Hints({ view, active, onPick }) {
35
35
  function Preview({ language, view }) {
36
36
  const { t } = useTranslation('editor');
37
37
  const folder = language.project.folder;
38
- return (_jsxs("div", { className: "flex w-[520px] max-w-full flex-col", onPointerDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "max-h-[320px] overflow-y-auto", children: view.files.map((file) => {
39
- const path = fileUriToPath(file.uri);
40
- const shown = path === null ? file.uri : relativeTo(path, folder);
41
- return (_jsxs("div", { className: "border-b border-border last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 text-xs", children: [_jsx("span", { className: "font-medium", children: path === null ? shown : basenameOf(path) }), _jsx("span", { className: "min-w-0 truncate text-text-faint", children: shown })] }), _jsx("div", { className: "flex flex-col gap-px px-1 pb-1.5 font-mono text-code", children: file.rows.map((row) => (_jsxs("div", { className: "flex flex-col gap-px", children: [_jsxs("div", { className: "flex gap-2 rounded-sm bg-status-error/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0 text-right text-text-faint", children: row.line }), _jsx("span", { className: "min-w-0 truncate", children: row.before })] }), _jsxs("div", { className: "flex gap-2 rounded-sm bg-status-idle/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.after })] })] }, row.line))) })] }, file.uri));
42
- }) }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [_jsx(Button, { size: "xs", variant: "ghost", onClick: () => language.rename.cancel(), children: t('language.rename.cancel') }), _jsx(Button, { size: "xs", variant: "primary", onClick: () => void language.rename.confirm(), children: t('language.rename.apply') })] })] }));
38
+ const operations = view.operations ?? [];
39
+ const shownPath = (uri) => {
40
+ const path = fileUriToPath(uri);
41
+ return path === null ? uri : relativeTo(path, folder);
42
+ };
43
+ return (_jsxs("div", { className: "flex w-[520px] max-w-full flex-col", onPointerDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "max-h-[320px] overflow-y-auto", children: [view.files.map((file) => {
44
+ const path = fileUriToPath(file.uri);
45
+ const shown = path === null ? file.uri : relativeTo(path, folder);
46
+ return (_jsxs("div", { className: "border-b border-border last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 text-xs", children: [_jsx("span", { className: "font-medium", children: path === null ? shown : basenameOf(path) }), _jsx("span", { className: "min-w-0 truncate text-text-faint", children: shown })] }), _jsx("div", { className: "flex flex-col gap-px px-1 pb-1.5 font-mono text-code", children: file.rows.map((row) => (_jsxs("div", { className: "flex flex-col gap-px", children: [_jsxs("div", { className: "flex gap-2 rounded-sm bg-status-error/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0 text-right text-text-faint", children: row.line }), _jsx("span", { className: "min-w-0 truncate", children: row.before })] }), _jsxs("div", { className: "flex gap-2 rounded-sm bg-status-idle/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.after })] })] }, row.line))) })] }, file.uri));
47
+ }), operations.length > 0 && (_jsx("div", { className: "flex flex-col gap-px border-b border-border px-3 py-1.5 text-xs last:border-b-0", children: operations.map((operation, index) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "shrink-0 font-medium", children: t(`language.rename.operations.${operation.kind}`) }), _jsxs("span", { className: "min-w-0 truncate text-text-faint", children: [shownPath(operation.uri), operation.newUri === undefined ? '' : ` \u2192 ${shownPath(operation.newUri)}`] })] }, `${operation.kind}:${operation.uri}:${index}`))) }))] }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [_jsx(Button, { size: "xs", variant: "ghost", onClick: () => language.rename.cancel(), children: t('language.rename.cancel') }), _jsx(Button, { size: "xs", variant: "primary", onClick: () => void language.rename.confirm(), children: t('language.rename.apply') })] })] }));
43
48
  }
44
49
  /*
45
50
  * The rename of a symbol: an input over the symbol itself, and under it either the count of its places with
@@ -0,0 +1,9 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ import type { RenderedDocEntry } from './rendered-docs.ts';
3
+ export declare function RenderedDocBlock({ language, entry }: {
4
+ language: EditorLanguage;
5
+ entry: RenderedDocEntry;
6
+ }): import("react").JSX.Element;
7
+ export declare function RenderedDocs({ language }: {
8
+ language: EditorLanguage;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,84 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createPortal } from 'react-dom';
3
+ import { useSyncExternalStore } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { DOC_LINK_PREFIX, sectionsOf } from './doc-comment-model.js';
6
+ import { markdownParts } from './hover-content.js';
7
+ import { Signature } from './HoverSections.js';
8
+ import { shikiLanguageOf } from './language-ids.js';
9
+ import { Markdown } from './Markdown.js';
10
+ const LINK = /\[([^\]]*)\]\(#doc-link:[^)]*\)/g;
11
+ const KNOWN = new Set([
12
+ 'deprecated',
13
+ 'template',
14
+ 'param',
15
+ 'property',
16
+ 'type',
17
+ 'var',
18
+ 'return',
19
+ 'yield',
20
+ 'throws',
21
+ 'see',
22
+ 'example',
23
+ 'since',
24
+ 'version',
25
+ 'author'
26
+ ]);
27
+ /* Code in the editor's face, with a type fainter than a name. */
28
+ function Code({ children, faint = false }) {
29
+ return _jsx("code", { className: `font-mono text-code break-words ${faint ? 'text-text-faint' : 'text-text'}`, children: children });
30
+ }
31
+ /* Markdown prose and fenced code as the hover draws them: the blocks in the code face, the rest muted. */
32
+ function Prose({ text, language, linkable, compact = false }) {
33
+ const plain = linkable ? text : text.replace(LINK, '$1');
34
+ return (_jsx(_Fragment, { children: markdownParts(plain).map((part, index) => part.kind === 'code' ? (_jsx("div", { className: "rounded-md bg-surface-sunken px-2 py-1.5", children: _jsx(Signature, { code: part.block.code, language: part.block.language === 'text' ? language : part.block.language }) }, index)) : (_jsx("div", { className: `[&_.adecore-editor-markdown]:text-xs ${compact ? '[&_p]:my-0' : '[&_p]:my-1'}`, children: _jsx(Markdown, { text: part.text, images: false }) }, index))) }));
35
+ }
36
+ /* A value of a tag, in the second column of the table; `spaced` opens a new section with more room above. */
37
+ function TagRow({ tag, language, linkable, spaced }) {
38
+ const target = tag.name === 'see' && tag.subject !== null ? tag.subject : null;
39
+ const link = target === null ? null : /^https?:\/\//i.test(target) ? `[${target}](${target})` : `[\`${target}\`](${DOC_LINK_PREFIX}${encodeURIComponent(target)})`;
40
+ return (_jsx("dd", { className: `col-start-2 m-0 flex min-w-0 flex-wrap items-baseline gap-x-2 ${spaced ? 'mt-1' : ''}`, children: tag.name === 'example' ? (_jsx("div", { className: "w-full rounded-md bg-surface-sunken px-2 py-1.5", children: _jsx(Signature, { code: tag.text, language: language }) })) : (_jsxs(_Fragment, { children: [tag.subject !== null && target === null && _jsx(Code, { children: tag.subject }), tag.type !== null && _jsx(Code, { faint: true, children: tag.type }), (link !== null || tag.text !== '') && (_jsx("div", { className: "min-w-0 grow basis-48 text-text", children: _jsx(Prose, { text: [link, tag.text].filter((part) => part !== null && part !== '').join(' '), language: language, linkable: linkable, compact: true }) }))] })) }));
41
+ }
42
+ export function RenderedDocBlock({ language, entry }) {
43
+ const { t } = useTranslation('editor');
44
+ const feature = language.renderedDocs;
45
+ const { description, tags } = entry.parsed;
46
+ const highlighter = shikiLanguageOf(language.languageId);
47
+ const linkable = feature.canFollow;
48
+ const sections = sectionsOf(tags);
49
+ const deprecated = sections.find((section) => section.name === 'deprecated');
50
+ const shown = sections.filter((section) => section.name !== 'deprecated');
51
+ // One handler for every link, since the links are made by the Markdown renderer and not by us.
52
+ const follow = (event) => {
53
+ const anchor = event.target.closest('a');
54
+ if (anchor === null) {
55
+ return;
56
+ }
57
+ event.preventDefault();
58
+ const mod = language.apple ? event.metaKey : event.ctrlKey;
59
+ const href = anchor.getAttribute('href') ?? '';
60
+ if (!mod) {
61
+ return;
62
+ }
63
+ if (href.startsWith(DOC_LINK_PREFIX)) {
64
+ feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), entry.comment);
65
+ }
66
+ else if (/^(https?:|mailto:)/i.test(href)) {
67
+ window.open(href, '_blank', 'noopener,noreferrer');
68
+ }
69
+ };
70
+ return (_jsxs("div", { className: "adecore-rendered-doc flex max-w-[520px] flex-col gap-1.5 py-1 font-sans text-xs/[18px] text-text select-text", onClick: follow, children: [deprecated !== undefined && (_jsxs("div", { className: "border-l-2 border-status-error pl-2 text-text-muted", children: [_jsx("span", { className: "font-medium text-status-error", children: t('language.docs.deprecated') }), deprecated.tags.some((tag) => tag.text !== '') && (_jsx(Prose, { text: deprecated.tags.map((tag) => tag.text).join(' '), language: highlighter, linkable: linkable }))] })), description !== '' && (_jsx("div", { className: "text-text", children: _jsx(Prose, { text: description, language: highlighter, linkable: linkable }) })), shown.length > 0 && (_jsx("dl", { className: "m-0 grid grid-cols-[max-content_minmax(0,1fr)] items-baseline gap-x-3 gap-y-0.5", children: shown.map((section, row) => [
71
+ _jsx("dt", { className: `col-start-1 font-medium text-text-muted ${row > 0 ? 'mt-1' : ''}`, children: KNOWN.has(section.name) ? t(`language.docs.tags.${section.name}`) : `${section.name}:` }, `${section.name}:label`),
72
+ ...section.tags.map((tag, index) => (_jsx(TagRow, { tag: tag, language: highlighter, linkable: linkable, spaced: row > 0 && index === 0 }, `${section.name}:${index}`)))
73
+ ]) }))] }));
74
+ }
75
+ /*
76
+ * The documentation comments the editor drew as rows, filled with their formatted text. It portals into
77
+ * the element the editor made for each row, so the editor decides what is in view; a comment is parsed
78
+ * only once its row is. Links in the text are followed with Cmd (Ctrl elsewhere) held, as definitions are.
79
+ */
80
+ export function RenderedDocs({ language }) {
81
+ const feature = language.renderedDocs;
82
+ useSyncExternalStore(feature.subscribe, feature.getVersion, feature.getVersion);
83
+ return _jsx(_Fragment, { children: feature.entries().map((entry) => createPortal(_jsx(RenderedDocBlock, { language: language, entry: entry }), entry.container, entry.id)) });
84
+ }
@@ -0,0 +1,9 @@
1
+ import type { DiffSplit } from '@adecore/merge';
2
+ import type { DiffViewProps } from './DiffView.tsx';
3
+ interface SplitViewProps extends DiffViewProps {
4
+ display: DiffSplit;
5
+ unfold(id: number): void;
6
+ blocked: ReadonlySet<number>;
7
+ }
8
+ export declare function SplitDiffView({ engine, document, display, unfold, blocked, theme, tokens, wrap, fill, maxHeight, label, className, ref, onMount }: SplitViewProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,276 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { parseChord } from '@adecore/editor/keymap';
5
+ import { DiffOverlays } from './diff-overlays.js';
6
+ import { wordMarks } from './diff-wordmarks.js';
7
+ import { RowHost, LineActionHost } from './row-host.js';
8
+ import { splitAlignment } from './split-alignment.js';
9
+ import { SplitScroll } from './split-scroll.js';
10
+ const SIDES = ['old', 'new'];
11
+ export function SplitDiffView({ engine, document, display, unfold, blocked, theme, tokens, wrap = true, fill = false, maxHeight, label, className, ref, onMount }) {
12
+ const { t } = useTranslation('editor');
13
+ const emphasis = useMemo(() => wordMarks(document), [document]);
14
+ const oldElement = useRef(null);
15
+ const newElement = useRef(null);
16
+ const current = useRef({ display, tokens, theme, wrap, label });
17
+ const applied = useRef({ theme, wrap, label });
18
+ const [failure, setFailure] = useState(null);
19
+ const [mounted, setMounted] = useState(null);
20
+ const [height, setHeight] = useState(0);
21
+ useLayoutEffect(() => {
22
+ current.current = { display, tokens, theme, wrap, label };
23
+ });
24
+ useLayoutEffect(() => {
25
+ const oldHost = oldElement.current;
26
+ const newHost = newElement.current;
27
+ if (oldHost === null || newHost === null) {
28
+ return;
29
+ }
30
+ const initial = current.current;
31
+ applied.current = { theme: initial.theme, wrap: initial.wrap, label: initial.label };
32
+ const mount = (side, host) => {
33
+ const initial = current.current;
34
+ const editor = engine.mount(host, {
35
+ text: initial.display[side].text,
36
+ theme: initial.theme,
37
+ wrap: initial.wrap,
38
+ label: initial.label,
39
+ readOnly: true,
40
+ guides: false,
41
+ folding: false,
42
+ foldOutline: 'off',
43
+ handBack: [parseChord('Mod+Enter')]
44
+ });
45
+ editor.setBlocks([]);
46
+ editor.setLineTokens((line) => {
47
+ const row = current.current.display[side].rows[line];
48
+ return row === undefined ? null : current.current.tokens?.get(side, row[side].index);
49
+ });
50
+ return { engine, editor, rows: new RowHost(editor, 'diff-alignment'), actions: new LineActionHost(editor, 'diff-status') };
51
+ };
52
+ const old = mount('old', oldHost);
53
+ let fresh;
54
+ try {
55
+ fresh = mount('new', newHost);
56
+ }
57
+ catch (error) {
58
+ old.editor.dispose();
59
+ throw error;
60
+ }
61
+ const sides = { old, new: fresh };
62
+ let stopped = false;
63
+ let pending = false;
64
+ let failed = false;
65
+ const scroll = new SplitScroll({ old: old.editor, new: fresh.editor });
66
+ let aligning = false;
67
+ let updating = false;
68
+ const fail = (error) => {
69
+ failed = true;
70
+ setFailure({ error });
71
+ };
72
+ const measure = () => setHeight(Math.max(sides.old.editor.getContentHeight(), sides.new.editor.getContentHeight()));
73
+ const align = () => {
74
+ if (pending || stopped || failed) {
75
+ return;
76
+ }
77
+ pending = true;
78
+ // Intrinsic measurements arrive after each render; align both editors before the next paint.
79
+ queueMicrotask(() => {
80
+ pending = false;
81
+ if (stopped || failed) {
82
+ return;
83
+ }
84
+ aligning = true;
85
+ try {
86
+ const rows = splitAlignment(current.current.display.aligned, (line) => sides.old.editor.getLineHeight(line), (line) => sides.new.editor.getLineHeight(line));
87
+ sides.old.rows.set(rows.old);
88
+ sides.new.rows.set(rows.new);
89
+ }
90
+ catch (error) {
91
+ fail(error);
92
+ return;
93
+ }
94
+ finally {
95
+ aligning = false;
96
+ }
97
+ try {
98
+ measure();
99
+ scroll.sync();
100
+ }
101
+ catch (error) {
102
+ fail(error);
103
+ }
104
+ });
105
+ };
106
+ const update = (run) => {
107
+ updating = true;
108
+ try {
109
+ run();
110
+ }
111
+ finally {
112
+ updating = false;
113
+ }
114
+ align();
115
+ };
116
+ const stops = SIDES.flatMap((side) => [
117
+ sides[side].editor.onLineHeights(align),
118
+ sides[side].editor.onViewChange(() => {
119
+ if (stopped || failed || aligning || updating) {
120
+ return;
121
+ }
122
+ try {
123
+ measure();
124
+ scroll.changed(side);
125
+ }
126
+ catch (error) {
127
+ fail(error);
128
+ }
129
+ })
130
+ ]);
131
+ setMounted({ old: sides.old, new: sides.new, align, update, drive: (side) => scroll.drive(side) });
132
+ measure();
133
+ return () => {
134
+ stopped = true;
135
+ for (const stop of stops) {
136
+ stop();
137
+ }
138
+ for (const side of SIDES) {
139
+ sides[side].editor.dispose();
140
+ }
141
+ };
142
+ }, [engine]);
143
+ useLayoutEffect(() => {
144
+ if (mounted === null || mounted.old.engine !== engine) {
145
+ return;
146
+ }
147
+ mounted.update(() => {
148
+ for (const side of SIDES) {
149
+ const { editor, actions } = mounted[side];
150
+ editor.setText(display[side].text);
151
+ editor.setLineNumbers((line) => {
152
+ const row = current.current.display[side].rows[line];
153
+ return row === undefined ? null : String(row[side].number);
154
+ });
155
+ const highlights = [];
156
+ for (const [index, row] of display[side].rows.entries()) {
157
+ if (row.change === 'context') {
158
+ continue;
159
+ }
160
+ highlights.push({
161
+ startLine: index + 1,
162
+ endLine: index + 1,
163
+ color: side === 'old' ? '--editor-deleted' : '--editor-added',
164
+ bar: side === 'old' ? 'hatched' : 'solid',
165
+ emphasis: [emphasis[side].get(row[side].index) ?? []]
166
+ });
167
+ }
168
+ editor.setLineHighlights(highlights);
169
+ editor.setChangeMarks(highlights.map((highlight) => ({ startLine: highlight.startLine, endLine: highlight.endLine, kind: side === 'old' ? 'deleted' : 'added' })));
170
+ actions.set(display[side].rows.flatMap((row, line) => (row.noNewline === undefined ? [] : [{ id: `no-newline-${line}`, line }])));
171
+ }
172
+ });
173
+ }, [mounted, engine, display, emphasis]);
174
+ useLayoutEffect(() => {
175
+ if (mounted === null || mounted.old.engine !== engine) {
176
+ return;
177
+ }
178
+ for (const side of SIDES) {
179
+ mounted[side].editor.setLineLabels((line) => {
180
+ const row = display[side].rows[line];
181
+ if (row === undefined || row.change === 'context') {
182
+ return null;
183
+ }
184
+ return t(side === 'old' ? 'diff.removed' : 'diff.added', { number: row[side].number });
185
+ });
186
+ }
187
+ }, [mounted, engine, display, t]);
188
+ useLayoutEffect(() => {
189
+ if (mounted === null || mounted.old.engine !== engine) {
190
+ return;
191
+ }
192
+ mounted.update(() => {
193
+ for (const side of SIDES) {
194
+ const previous = applied.current;
195
+ if (previous.theme !== theme) {
196
+ mounted[side].editor.setTheme(theme);
197
+ }
198
+ if (previous.wrap !== wrap) {
199
+ mounted[side].editor.setWrap(wrap);
200
+ }
201
+ if (previous.label !== label) {
202
+ mounted[side].editor.setLabel(label);
203
+ }
204
+ }
205
+ });
206
+ applied.current = { theme, wrap, label };
207
+ }, [mounted, engine, theme, wrap, label]);
208
+ useLayoutEffect(() => {
209
+ if (mounted === null || mounted.old.engine !== engine) {
210
+ return;
211
+ }
212
+ const refresh = () => {
213
+ for (const side of SIDES) {
214
+ mounted[side].editor.setLineTokens((line) => {
215
+ const row = current.current.display[side].rows[line];
216
+ return row === undefined ? null : current.current.tokens?.get(side, row[side].index);
217
+ });
218
+ }
219
+ };
220
+ refresh();
221
+ const ownerWindow = oldElement.current?.ownerDocument.defaultView;
222
+ let frame;
223
+ let pending = false;
224
+ let stopped = false;
225
+ const schedule = () => {
226
+ if (pending) {
227
+ return;
228
+ }
229
+ pending = true;
230
+ const paint = () => {
231
+ pending = false;
232
+ if (!stopped) {
233
+ refresh();
234
+ }
235
+ };
236
+ if (ownerWindow?.requestAnimationFrame !== undefined) {
237
+ frame = ownerWindow.requestAnimationFrame(paint);
238
+ }
239
+ else {
240
+ queueMicrotask(paint);
241
+ }
242
+ };
243
+ const stop = tokens?.subscribe(schedule);
244
+ return () => {
245
+ stopped = true;
246
+ stop?.();
247
+ if (frame !== undefined) {
248
+ ownerWindow?.cancelAnimationFrame?.(frame);
249
+ }
250
+ };
251
+ }, [mounted, engine, display, tokens]);
252
+ useLayoutEffect(() => {
253
+ if (mounted === null || mounted.old.engine !== engine) {
254
+ return;
255
+ }
256
+ const stops = SIDES.map((side) => onMount?.(mounted[side].editor, side));
257
+ return () => {
258
+ for (const stop of stops) {
259
+ stop?.();
260
+ }
261
+ };
262
+ }, [mounted, engine, onMount]);
263
+ const limit = maxHeight !== undefined && Number.isFinite(maxHeight) && maxHeight > 0 ? maxHeight : Infinity;
264
+ if (failure !== null) {
265
+ throw failure.error;
266
+ }
267
+ return (_jsx("div", { className: `adecore-editor-diff adecore-editor-diff-split ${className ?? ''}`, ref: ref, role: "group", "aria-label": label, "data-scroll": fill || height > limit, style: { height: fill ? '100%' : Math.min(height, limit) }, children: SIDES.map((side) => (_jsxs("div", { className: "adecore-editor-diff-side", "data-side": side, role: "group", "aria-label": t(`diff.${side}Side`), onWheelCapture: () => {
268
+ mounted?.drive(side);
269
+ }, onFocusCapture: () => {
270
+ mounted?.drive(side);
271
+ }, onPointerDownCapture: () => {
272
+ mounted?.drive(side);
273
+ }, onKeyDownCapture: () => {
274
+ mounted?.drive(side);
275
+ }, children: [_jsx("div", { ref: side === 'old' ? oldElement : newElement, className: "adecore-editor-diff-surface" }), mounted !== null && mounted[side].engine === engine && (_jsx(DiffOverlays, { mounted: mounted[side], display: display[side], unfold: unfold, blocked: blocked }))] }, side))) }));
276
+ }
@@ -1,5 +1,8 @@
1
1
  import type { EditorLanguage } from './editor-language.ts';
2
2
  import type { SymbolsView } from './popups.ts';
3
+ export declare function SymbolBadge({ kind }: {
4
+ kind: number;
5
+ }): import("react").JSX.Element;
3
6
  export declare function SymbolPicker({ language, view }: {
4
7
  language: EditorLanguage;
5
8
  view: SymbolsView;
@@ -15,7 +15,8 @@ const TONES = {
15
15
  other: 'bg-surface-hover text-text-muted'
16
16
  };
17
17
  const SEARCH_DELAY_MS = 200;
18
- function Badge({ kind }) {
18
+ /* The letter badge of a symbol kind, tinted by what the symbol is. */
19
+ export function SymbolBadge({ kind }) {
19
20
  return (_jsx("span", { className: `grid size-4 shrink-0 place-items-center rounded-sm font-mono text-xs font-semibold ${TONES[toneOfKind(kind)]}`, children: letterOfKind(kind) }));
20
21
  }
21
22
  /*
@@ -94,14 +95,14 @@ export function SymbolPicker({ language, view }) {
94
95
  }, onKeyDown: onKeyDown, onBlur: () => language.symbolPicker.close() }), _jsx("span", { className: "shrink-0 text-xs text-text-faint", children: language.shortcuts.goToSymbol === null ? '' : formatShortcut(language.shortcuts.goToSymbol, language.apple) })] }), _jsxs("div", { role: "listbox", className: "max-h-[320px] overflow-y-auto p-1", children: [mode.mode === 'line' && (_jsx("div", { className: "px-2 py-1.5 text-xs text-text-muted", children: mode.line === null ? t('language.symbols.lineHint') : t('language.symbols.goToLine', { line: formatNumber(mode.line) }) })), mode.mode === 'workspace' &&
95
96
  places.map((place, index) => {
96
97
  const path = fileUriToPath(place.uri);
97
- return (_jsxs("button", { ref: index === active ? activeRow : undefined, type: "button", role: "option", "aria-selected": index === active, "data-active": index === active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => choose(index), children: [_jsx(Badge, { kind: place.kind }), _jsx("span", { className: "shrink-0", children: place.name }), _jsx("span", { className: "min-w-0 truncate font-mono text-text-faint", children: place.container }), _jsxs("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: [path === null ? '' : basenameOf(path), ":", formatNumber(place.line + 1)] })] }, place.id));
98
+ return (_jsxs("button", { ref: index === active ? activeRow : undefined, type: "button", role: "option", "aria-selected": index === active, "data-active": index === active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => choose(index), children: [_jsx(SymbolBadge, { kind: place.kind }), _jsx("span", { className: "shrink-0", children: place.name }), _jsx("span", { className: "min-w-0 truncate font-mono text-text-faint", children: place.container }), _jsxs("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: [path === null ? '' : basenameOf(path), ":", formatNumber(place.line + 1)] })] }, place.id));
98
99
  }), mode.mode === 'symbol' &&
99
100
  groups.map((group) => {
100
101
  const start = offset;
101
102
  offset += group.entries.length;
102
103
  return (_jsxs("div", { role: "group", children: [_jsx("div", { className: "px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint", children: t(`language.symbols.groups.${group.group}`) }), group.entries.map((entry, index) => {
103
104
  const position = start + index;
104
- return (_jsxs("button", { ref: position === active ? activeRow : undefined, type: "button", role: "option", "aria-selected": position === active, "data-active": position === active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => choose(position), children: [_jsx(Badge, { kind: entry.kind }), _jsx("span", { className: "shrink-0", children: entry.name }), _jsx("span", { className: "min-w-0 truncate font-mono text-text-faint", children: entry.detail }), entry.container !== '' && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: entry.container }), _jsx("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: formatNumber(entry.line + 1) })] }, entry.id));
105
+ return (_jsxs("button", { ref: position === active ? activeRow : undefined, type: "button", role: "option", "aria-selected": position === active, "data-active": position === active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => choose(position), children: [_jsx(SymbolBadge, { kind: entry.kind }), _jsx("span", { className: "shrink-0", children: entry.name }), _jsx("span", { className: "min-w-0 truncate font-mono text-text-faint", children: entry.detail }), entry.container !== '' && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: entry.container }), _jsx("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: formatNumber(entry.line + 1) })] }, entry.id));
105
106
  })] }, group.group));
106
107
  }), mode.mode === 'symbol' && flat.length === 0 && (_jsx("div", { className: "px-2 py-3 text-center text-xs text-text-muted", children: t('language.symbols.none') })), mode.mode === 'workspace' && places.length === 0 && (_jsx("div", { className: "px-2 py-3 text-center text-xs text-text-muted", children: mode.text === '' ? t('language.symbols.workspaceHint') : t('language.symbols.none') }))] }), _jsxs("div", { className: "flex items-center gap-3 border-t border-border px-3 py-1.5 text-xs whitespace-nowrap text-text-faint", children: [_jsxs("span", { children: ["\u2191\u2193 ", t('language.symbols.navigate')] }), _jsxs("span", { children: ["\u21B5 ", t('language.symbols.open')] }), _jsxs("button", { type: "button", className: "ml-auto hover:text-text", onClick: () => setInput('#'), children: ["# ", t('language.symbols.workspace')] }), _jsxs("button", { type: "button", className: "hover:text-text", onClick: () => setInput(':'), children: [": ", t('language.symbols.line')] })] })] }), document.body);
107
108
  }
@@ -0,0 +1,52 @@
1
+ import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Range } from '@adecore/lsp';
2
+ export type CallDirection = 'incoming' | 'outgoing';
3
+ export declare const CALL_LEVEL_LIMIT = 50;
4
+ export type CallLoad = 'idle' | 'loading' | 'loaded' | 'failed';
5
+ export interface CallNode {
6
+ readonly id: string;
7
+ readonly parent: string | null;
8
+ readonly level: number;
9
+ readonly item: CallHierarchyItem;
10
+ readonly sites: readonly Range[];
11
+ readonly siteUri: string | null;
12
+ readonly expanded: boolean;
13
+ readonly load: CallLoad;
14
+ readonly error: string | null;
15
+ readonly children: readonly string[];
16
+ readonly shown: number;
17
+ }
18
+ export type CallRow = {
19
+ readonly kind: 'node';
20
+ readonly id: string;
21
+ readonly level: number;
22
+ readonly item: CallHierarchyItem;
23
+ readonly expanded: boolean;
24
+ readonly load: CallLoad;
25
+ readonly sites: number;
26
+ readonly leaf: boolean;
27
+ readonly position: number;
28
+ readonly size: number;
29
+ } | {
30
+ readonly kind: 'status';
31
+ readonly id: string;
32
+ readonly parent: string;
33
+ readonly level: number;
34
+ readonly status: 'loading' | 'empty' | 'failed';
35
+ readonly message: string | null;
36
+ } | {
37
+ readonly kind: 'more';
38
+ readonly id: string;
39
+ readonly level: number;
40
+ readonly parent: string;
41
+ readonly hidden: number;
42
+ };
43
+ export declare function statusIdOf(parent: string): string;
44
+ export declare function moreIdOf(parent: string): string;
45
+ export declare function rootsOf(items: readonly CallHierarchyItem[]): CallNode[];
46
+ export declare function childrenOf(direction: CallDirection, parent: CallNode, calls: readonly (CallHierarchyIncomingCall | CallHierarchyOutgoingCall)[]): CallNode[];
47
+ export declare function rowsOf(nodes: ReadonlyMap<string, CallNode>, roots: readonly string[]): CallRow[];
48
+ export declare function stepsOf(rows: readonly CallRow[]): CallRow[];
49
+ export declare function targetOf(node: CallNode, site?: number): {
50
+ uri: string;
51
+ range: Range;
52
+ };
@@ -0,0 +1,99 @@
1
+ /* The most calls of one level that are drawn at once; the rest wait behind a row that shows more. */
2
+ export const CALL_LEVEL_LIMIT = 50;
3
+ /* The id of the status line under a node. */
4
+ export function statusIdOf(parent) {
5
+ return `${parent}:status`;
6
+ }
7
+ /* The id of the line that shows more calls under a node. */
8
+ export function moreIdOf(parent) {
9
+ return `${parent}:more`;
10
+ }
11
+ /* The tree's roots, each one a symbol the server prepared. */
12
+ export function rootsOf(items) {
13
+ return items.map((item, index) => ({
14
+ id: `r${index}`,
15
+ parent: null,
16
+ level: 1,
17
+ item,
18
+ sites: [],
19
+ siteUri: null,
20
+ expanded: true,
21
+ load: 'idle',
22
+ error: null,
23
+ children: [],
24
+ shown: 0
25
+ }));
26
+ }
27
+ function compareItems(left, right) {
28
+ return left.name.localeCompare(right.name) || left.uri.localeCompare(right.uri) || left.range.start.line - right.range.start.line;
29
+ }
30
+ /*
31
+ * The children of a node made from the calls a server returned, listed by name the way the symbols of a
32
+ * file are. The call sites of a caller are in its own file; those of a callee are in the file of the node
33
+ * that was asked about.
34
+ */
35
+ export function childrenOf(direction, parent, calls) {
36
+ const entries = calls.map((call) => ({ item: 'from' in call ? call.from : call.to, sites: call.fromRanges }));
37
+ return entries
38
+ .sort((left, right) => compareItems(left.item, right.item))
39
+ .map((entry, index) => ({
40
+ id: `${parent.id}.${index}`,
41
+ parent: parent.id,
42
+ level: parent.level + 1,
43
+ item: entry.item,
44
+ sites: entry.sites,
45
+ siteUri: direction === 'incoming' ? entry.item.uri : parent.item.uri,
46
+ expanded: false,
47
+ load: 'idle',
48
+ error: null,
49
+ children: [],
50
+ shown: 0
51
+ }));
52
+ }
53
+ /* The lines the tree draws: every root, and under an expanded node its status, its calls and the row that shows more. */
54
+ export function rowsOf(nodes, roots) {
55
+ const rows = [];
56
+ const visit = (id, position, size) => {
57
+ const node = nodes.get(id);
58
+ rows.push({
59
+ kind: 'node',
60
+ id,
61
+ level: node.level,
62
+ item: node.item,
63
+ expanded: node.expanded,
64
+ load: node.load,
65
+ sites: node.sites.length,
66
+ leaf: node.load === 'loaded' && node.children.length === 0,
67
+ position,
68
+ size
69
+ });
70
+ if (!node.expanded) {
71
+ return;
72
+ }
73
+ if (node.load === 'loading') {
74
+ rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'loading', message: null });
75
+ }
76
+ else if (node.load === 'failed') {
77
+ rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'failed', message: node.error });
78
+ }
79
+ else if (node.load === 'loaded' && node.children.length === 0) {
80
+ rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'empty', message: null });
81
+ }
82
+ const shown = node.children.slice(0, node.shown);
83
+ shown.forEach((child, index) => visit(child, index + 1, node.children.length));
84
+ if (node.children.length > node.shown) {
85
+ rows.push({ kind: 'more', id: moreIdOf(id), level: node.level + 1, parent: id, hidden: node.children.length - node.shown });
86
+ }
87
+ };
88
+ roots.forEach((root, index) => visit(root, index + 1, roots.length));
89
+ return rows;
90
+ }
91
+ /* The rows a key can land on: symbols, and the row that shows more. */
92
+ export function stepsOf(rows) {
93
+ return rows.filter((row) => row.kind !== 'status');
94
+ }
95
+ /* The place a node goes to: its first call site, or the symbol itself for a root. */
96
+ export function targetOf(node, site = 0) {
97
+ const range = node.sites[site];
98
+ return range === undefined || node.siteUri === null ? { uri: node.item.uri, range: node.item.selectionRange } : { uri: node.siteUri, range };
99
+ }