@adecore/editor-react 0.22.1 → 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.
- package/dist/CallHierarchyPanel.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +88 -0
- package/dist/CompletionPopup.js +11 -3
- package/dist/DiffView.d.ts +22 -0
- package/dist/DiffView.js +203 -0
- package/dist/EditorContextMenu.js +1 -0
- package/dist/EditorView.js +18 -1
- package/dist/FindReplace.d.ts +1 -1
- package/dist/FindReplace.js +41 -11
- package/dist/HoverCard.js +2 -2
- package/dist/LanguagePopups.js +4 -1
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.js +8 -4
- package/dist/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +2 -3
- package/dist/PickPopup.js +15 -2
- package/dist/RenameCard.js +10 -5
- package/dist/RenderedDocs.d.ts +9 -0
- package/dist/RenderedDocs.js +84 -0
- package/dist/SplitDiffView.d.ts +9 -0
- package/dist/SplitDiffView.js +276 -0
- package/dist/SymbolPicker.d.ts +3 -0
- package/dist/SymbolPicker.js +4 -3
- package/dist/call-hierarchy-model.d.ts +52 -0
- package/dist/call-hierarchy-model.js +99 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +399 -0
- package/dist/code-actions-model.d.ts +2 -0
- package/dist/code-actions-model.js +12 -10
- package/dist/code-actions.js +3 -1
- package/dist/completion-model.d.ts +14 -4
- package/dist/completion-model.js +40 -33
- package/dist/completion.d.ts +4 -0
- package/dist/completion.js +257 -42
- package/dist/diagnostics.js +8 -6
- package/dist/diff-expansion.d.ts +9 -0
- package/dist/diff-expansion.js +41 -0
- package/dist/diff-overlays.d.ts +15 -0
- package/dist/diff-overlays.js +28 -0
- package/dist/diff-syntax.d.ts +16 -0
- package/dist/diff-syntax.js +49 -0
- package/dist/diff-wordmarks.d.ts +5 -0
- package/dist/diff-wordmarks.js +26 -0
- package/dist/doc-comment-model.d.ts +23 -0
- package/dist/doc-comment-model.js +291 -0
- package/dist/editor-language.d.ts +13 -1
- package/dist/editor-language.js +14 -4
- package/dist/editor-react.css +79 -0
- package/dist/format-on-type.d.ts +14 -0
- package/dist/format-on-type.js +96 -0
- package/dist/host-types.d.ts +17 -0
- package/dist/hover.d.ts +4 -0
- package/dist/hover.js +15 -2
- package/dist/index.d.ts +19 -5
- package/dist/index.js +9 -1
- package/dist/inlay-hints.d.ts +47 -2
- package/dist/inlay-hints.js +353 -16
- package/dist/language-ids.js +4 -2
- package/dist/locales/en.json +114 -3
- package/dist/locales/nl.json +114 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- package/dist/patch-decorations.d.ts +27 -0
- package/dist/patch-decorations.js +182 -0
- package/dist/patch-layout.d.ts +40 -0
- package/dist/patch-layout.js +304 -0
- package/dist/peek.js +1 -0
- package/dist/pick.d.ts +1 -0
- package/dist/pick.js +11 -1
- package/dist/popups.d.ts +17 -0
- package/dist/popups.js +1 -0
- package/dist/project-language.d.ts +8 -0
- package/dist/project-language.js +34 -2
- package/dist/refresher.d.ts +1 -0
- package/dist/refresher.js +5 -0
- package/dist/rename-model.d.ts +1 -1
- package/dist/rename-model.js +3 -7
- package/dist/rename.js +5 -8
- package/dist/rendered-docs.d.ts +62 -0
- package/dist/rendered-docs.js +368 -0
- package/dist/row-host.d.ts +2 -0
- package/dist/row-host.js +11 -3
- package/dist/semantic-delta.d.ts +2 -0
- package/dist/semantic-delta.js +27 -0
- package/dist/semantic-model.js +16 -0
- package/dist/semantic-tokens.d.ts +26 -1
- package/dist/semantic-tokens.js +174 -14
- package/dist/shortcut-keys.d.ts +2 -0
- package/dist/shortcut-keys.js +6 -2
- package/dist/signature.js +1 -1
- package/dist/snippet-field-transforms.d.ts +30 -0
- package/dist/snippet-field-transforms.js +116 -0
- package/dist/snippet-indent.d.ts +2 -0
- package/dist/snippet-indent.js +49 -0
- package/dist/snippet-range-order.d.ts +3 -0
- package/dist/snippet-range-order.js +19 -0
- package/dist/snippet-session.d.ts +42 -6
- package/dist/snippet-session.js +650 -53
- package/dist/snippet-transform-client.d.ts +19 -0
- package/dist/snippet-transform-client.js +66 -0
- package/dist/snippet-transform-format.d.ts +15 -0
- package/dist/snippet-transform-format.js +302 -0
- package/dist/snippet-transform-protocol.d.ts +45 -0
- package/dist/snippet-transform-protocol.js +8 -0
- package/dist/snippet-transform-queue.d.ts +32 -0
- package/dist/snippet-transform-queue.js +173 -0
- package/dist/snippet-transform-worker.d.ts +10 -0
- package/dist/snippet-transform-worker.js +41 -0
- package/dist/snippet-variable-names.d.ts +1 -0
- package/dist/snippet-variable-names.js +36 -0
- package/dist/snippet-variables.d.ts +7 -0
- package/dist/snippet-variables.js +66 -0
- package/dist/snippet.d.ts +30 -3
- package/dist/snippet.js +402 -87
- package/dist/split-alignment.d.ts +6 -0
- package/dist/split-alignment.js +38 -0
- package/dist/split-scroll.d.ts +14 -0
- package/dist/split-scroll.js +45 -0
- package/dist/symbols.d.ts +2 -1
- package/dist/symbols.js +39 -7
- package/dist/testing.d.ts +5 -1
- package/dist/testing.js +12 -0
- package/dist/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +238 -0
- package/src/CompletionPopup.tsx +12 -1
- package/src/DiffView.tsx +262 -0
- package/src/EditorContextMenu.tsx +1 -0
- package/src/EditorView.tsx +21 -2
- package/src/FindReplace.tsx +51 -13
- package/src/HoverCard.tsx +23 -21
- package/src/LanguagePopups.tsx +5 -0
- package/src/Markdown.tsx +10 -3
- package/src/PeekPanel.tsx +2 -3
- package/src/PickPopup.tsx +18 -2
- package/src/RenameCard.tsx +18 -0
- package/src/RenderedDocs.tsx +155 -0
- package/src/SplitDiffView.tsx +337 -0
- package/src/SymbolPicker.tsx +4 -3
- package/src/call-hierarchy-model.ts +158 -0
- package/src/call-hierarchy.ts +441 -0
- package/src/code-actions-model.ts +17 -11
- package/src/code-actions.ts +3 -1
- package/src/completion-model.ts +61 -35
- package/src/completion.ts +302 -42
- package/src/diagnostics.ts +8 -6
- package/src/diff-expansion.ts +49 -0
- package/src/diff-overlays.tsx +69 -0
- package/src/diff-syntax.ts +67 -0
- package/src/diff-wordmarks.ts +31 -0
- package/src/doc-comment-model.ts +336 -0
- package/src/editor-language.ts +21 -5
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +19 -0
- package/src/hover.ts +18 -3
- package/src/index.ts +27 -4
- package/src/inlay-hints.ts +394 -18
- package/src/language-ids.ts +4 -2
- package/src/locales/en.json +114 -3
- package/src/locales/nl.json +114 -3
- package/src/models.ts +40 -1
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +1 -0
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +43 -2
- package/src/refresher.ts +6 -0
- package/src/rename-model.ts +4 -8
- package/src/rename.ts +5 -8
- package/src/rendered-docs.ts +422 -0
- package/src/row-host.ts +12 -3
- package/src/semantic-delta.ts +29 -0
- package/src/semantic-model.ts +17 -0
- package/src/semantic-tokens.ts +202 -19
- package/src/shortcut-keys.ts +6 -2
- package/src/signature.ts +1 -1
- package/src/snippet-field-transforms.ts +144 -0
- package/src/snippet-indent.ts +53 -0
- package/src/snippet-range-order.ts +19 -0
- package/src/snippet-session.ts +797 -56
- package/src/snippet-transform-client.ts +84 -0
- package/src/snippet-transform-format.ts +325 -0
- package/src/snippet-transform-protocol.ts +24 -0
- package/src/snippet-transform-queue.ts +199 -0
- package/src/snippet-transform-worker.ts +58 -0
- package/src/snippet-variable-names.ts +36 -0
- package/src/snippet-variables.ts +74 -0
- package/src/snippet.ts +452 -89
- package/src/split-alignment.ts +45 -0
- package/src/split-scroll.ts +54 -0
- package/src/symbols.ts +43 -7
- package/src/testing.ts +25 -0
- package/src/workspace-edit.ts +347 -169
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
"./locales/*.json": {
|
|
50
50
|
"source": "./src/locales/*.json",
|
|
51
51
|
"default": "./dist/locales/*.json"
|
|
52
|
+
},
|
|
53
|
+
"./snippet-transform-worker": {
|
|
54
|
+
"source": "./src/snippet-transform-worker.ts",
|
|
55
|
+
"types": "./dist/snippet-transform-worker.d.ts",
|
|
56
|
+
"default": "./dist/snippet-transform-worker.js"
|
|
52
57
|
}
|
|
53
58
|
},
|
|
54
59
|
"scripts": {
|
|
@@ -57,9 +62,9 @@
|
|
|
57
62
|
"test": "bun test --conditions=source"
|
|
58
63
|
},
|
|
59
64
|
"dependencies": {
|
|
60
|
-
"@adecore/editor": "0.
|
|
61
|
-
"@adecore/lsp": "0.
|
|
62
|
-
"@adecore/merge": "0.
|
|
65
|
+
"@adecore/editor": "0.23.0",
|
|
66
|
+
"@adecore/lsp": "0.23.0",
|
|
67
|
+
"@adecore/merge": "0.23.0",
|
|
63
68
|
"lucide-react": "^1.47.0",
|
|
64
69
|
"react-markdown": "^10.1.0",
|
|
65
70
|
"remark-breaks": "^4.0.0",
|
|
@@ -67,7 +72,7 @@
|
|
|
67
72
|
"zustand": "^5.0.15"
|
|
68
73
|
},
|
|
69
74
|
"peerDependencies": {
|
|
70
|
-
"@adecore/ui": "^0.
|
|
75
|
+
"@adecore/ui": "^0.23.0",
|
|
71
76
|
"i18next": "^26.4.2",
|
|
72
77
|
"react": "^19.3.0",
|
|
73
78
|
"react-dom": "^19.3.0",
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useEffect, useId, useLayoutEffect, useRef, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
|
5
|
+
import { fileUriToPath } from '@adecore/lsp';
|
|
6
|
+
import { Icon, IconButton, Segmented, Spinner, Tooltip } from '@adecore/ui';
|
|
7
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
8
|
+
import { CALL_HIERARCHY_HEIGHT } from './call-hierarchy.ts';
|
|
9
|
+
import { CALL_LEVEL_LIMIT, type CallDirection, type CallRow } from './call-hierarchy-model.ts';
|
|
10
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
11
|
+
import { basenameOf, relativeTo } from './paths.ts';
|
|
12
|
+
import { PathText } from './PathText.tsx';
|
|
13
|
+
import { Preview } from './PeekPanel.tsx';
|
|
14
|
+
import type { CallHierarchyView } from './popups.ts';
|
|
15
|
+
import { SymbolBadge } from './SymbolPicker.tsx';
|
|
16
|
+
|
|
17
|
+
const INDENT = 14;
|
|
18
|
+
const BASE_INDENT = 8;
|
|
19
|
+
|
|
20
|
+
type NodeRow = Extract<CallRow, { kind: 'node' }>;
|
|
21
|
+
|
|
22
|
+
function folderOf(path: string): string {
|
|
23
|
+
return path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface LineProps {
|
|
27
|
+
row: CallRow;
|
|
28
|
+
id: string;
|
|
29
|
+
active: boolean;
|
|
30
|
+
expanded?: boolean;
|
|
31
|
+
position?: number;
|
|
32
|
+
size?: number;
|
|
33
|
+
ref?: Ref<HTMLDivElement>;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
onClick(): void;
|
|
36
|
+
onDoubleClick?(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* One line of the tree. Status lines are treeitems too, so the level of a symbol's calls is read the same way, but nothing selects them. */
|
|
40
|
+
function Line({ row, id, active, expanded, position, size, ref, children, onClick, onDoubleClick }: LineProps) {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
ref={ref}
|
|
44
|
+
id={id}
|
|
45
|
+
role="treeitem"
|
|
46
|
+
aria-level={row.level}
|
|
47
|
+
aria-selected={active}
|
|
48
|
+
aria-expanded={expanded}
|
|
49
|
+
aria-posinset={position}
|
|
50
|
+
aria-setsize={size}
|
|
51
|
+
aria-disabled={row.kind === 'status' ? true : undefined}
|
|
52
|
+
data-active={active}
|
|
53
|
+
className="flex h-[22px] w-full cursor-default items-center gap-1.5 pr-3 text-left font-mono text-code hover:bg-surface-hover data-[active=true]:bg-accent-soft"
|
|
54
|
+
style={{ paddingLeft: BASE_INDENT + (row.level - 1) * INDENT }}
|
|
55
|
+
onClick={onClick}
|
|
56
|
+
onDoubleClick={onDoubleClick}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Symbol({ language, row, active, id, ref }: { language: EditorLanguage; row: NodeRow; active: boolean; id: string; ref?: Ref<HTMLDivElement> }) {
|
|
64
|
+
const { t } = useTranslation('editor');
|
|
65
|
+
const { item } = row;
|
|
66
|
+
const path = relativeTo(fileUriToPath(item.uri) ?? item.uri, language.project.folder);
|
|
67
|
+
const tool = language.callHierarchy;
|
|
68
|
+
return (
|
|
69
|
+
<Line
|
|
70
|
+
ref={ref}
|
|
71
|
+
row={row}
|
|
72
|
+
id={id}
|
|
73
|
+
active={active}
|
|
74
|
+
expanded={row.leaf ? undefined : row.expanded}
|
|
75
|
+
position={row.position}
|
|
76
|
+
size={row.size}
|
|
77
|
+
onClick={() => (active ? tool.go(row.id) : tool.select(row.id))}
|
|
78
|
+
onDoubleClick={() => tool.go(row.id)}
|
|
79
|
+
>
|
|
80
|
+
<span
|
|
81
|
+
aria-hidden
|
|
82
|
+
className="grid size-3.5 shrink-0 place-items-center text-text-muted"
|
|
83
|
+
onClick={(event) => {
|
|
84
|
+
event.stopPropagation();
|
|
85
|
+
tool.toggle(row.id);
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{row.leaf ? null : <Icon icon={row.expanded ? ChevronDown : ChevronRight} size={14} />}
|
|
89
|
+
</span>
|
|
90
|
+
<SymbolBadge kind={item.kind} />
|
|
91
|
+
<span className={`shrink-0 text-text ${item.tags?.includes(1) ? 'line-through' : ''}`}>{item.name}</span>
|
|
92
|
+
{item.detail !== undefined && item.detail !== '' && <span className="min-w-0 flex-1 truncate text-text-muted">{item.detail}</span>}
|
|
93
|
+
<span className="ml-auto flex shrink-0 items-center gap-2 pl-2 font-sans text-xs text-text-faint">
|
|
94
|
+
{row.sites > 1 && (
|
|
95
|
+
<Tooltip label={t('language.callHierarchy.sites', { count: row.sites, formatted: formatNumber(row.sites) })}>
|
|
96
|
+
<span className="text-text-muted">{`×${formatNumber(row.sites)}`}</span>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
)}
|
|
99
|
+
<Tooltip label={path}>
|
|
100
|
+
<span className="max-w-40 truncate">{`${basenameOf(path)}:${formatNumber(item.selectionRange.start.line + 1)}`}</span>
|
|
101
|
+
</Tooltip>
|
|
102
|
+
</span>
|
|
103
|
+
</Line>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function Tree({ language, view }: { language: EditorLanguage; view: CallHierarchyView }) {
|
|
108
|
+
const { t } = useTranslation('editor');
|
|
109
|
+
const treeId = useId();
|
|
110
|
+
const activeRow = useRef<HTMLDivElement>(null);
|
|
111
|
+
const idOf = (row: CallRow): string => `${treeId}-${row.id}`;
|
|
112
|
+
const activeId = view.rows.some((row) => row.id === view.active) ? `${treeId}-${view.active}` : undefined;
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
activeRow.current?.scrollIntoView({ block: 'nearest' });
|
|
116
|
+
}, [view.active]);
|
|
117
|
+
|
|
118
|
+
// The focus stays in the text, so the input is what names the tree and its active row.
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
language.editor.setInputPopup({ controls: treeId, activeDescendant: activeId });
|
|
121
|
+
return () => language.editor.setInputPopup(null);
|
|
122
|
+
}, [language, treeId, activeId]);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<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">
|
|
126
|
+
{view.rows.map((row) => {
|
|
127
|
+
const active = row.id === view.active;
|
|
128
|
+
const ref = active ? activeRow : undefined;
|
|
129
|
+
if (row.kind === 'node') {
|
|
130
|
+
return <Symbol key={row.id} ref={ref} language={language} row={row} active={active} id={idOf(row)} />;
|
|
131
|
+
}
|
|
132
|
+
if (row.kind === 'more') {
|
|
133
|
+
const next = Math.min(row.hidden, CALL_LEVEL_LIMIT);
|
|
134
|
+
return (
|
|
135
|
+
<Line key={row.id} ref={ref} row={row} id={idOf(row)} active={active} onClick={() => language.callHierarchy.showMore(row.parent)}>
|
|
136
|
+
<span className="font-sans text-xs text-accent">
|
|
137
|
+
{t('language.callHierarchy.more', { count: next, formatted: formatNumber(next), hidden: formatNumber(row.hidden) })}
|
|
138
|
+
</span>
|
|
139
|
+
</Line>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return (
|
|
143
|
+
<Line key={row.id} row={row} id={idOf(row)} active={false} onClick={() => language.callHierarchy.retry(row.parent)}>
|
|
144
|
+
<StatusText row={row} direction={view.direction} />
|
|
145
|
+
</Line>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function StatusText({ row, direction }: { row: Extract<CallRow, { kind: 'status' }>; direction: CallDirection }) {
|
|
153
|
+
const { t } = useTranslation('editor');
|
|
154
|
+
if (row.status === 'loading') {
|
|
155
|
+
return (
|
|
156
|
+
<span className="flex items-center gap-2 font-sans text-xs text-text-muted">
|
|
157
|
+
<Spinner size={12} />
|
|
158
|
+
{t('language.callHierarchy.loading')}
|
|
159
|
+
</span>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (row.status === 'failed') {
|
|
163
|
+
return (
|
|
164
|
+
<span className="min-w-0 truncate font-sans text-xs text-status-error">{t('language.callHierarchy.retry', { message: row.message ?? '' })}</span>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return (
|
|
168
|
+
<span className="font-sans text-xs text-text-muted">
|
|
169
|
+
{t(direction === 'incoming' ? 'language.callHierarchy.noIncoming' : 'language.callHierarchy.noOutgoing')}
|
|
170
|
+
</span>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* The call hierarchy of a symbol in the row the editor leaves under the line: the tree of callers or callees on
|
|
176
|
+
* the left and the code of the active call on the right. Like the peek it portals into that row, which the
|
|
177
|
+
* editor makes again when it scrolls back into view.
|
|
178
|
+
*/
|
|
179
|
+
export function CallHierarchyPanel({ language, view }: { language: EditorLanguage; view: CallHierarchyView }) {
|
|
180
|
+
const { t } = useTranslation('editor');
|
|
181
|
+
if (view.container === null) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const folder = language.project.folder;
|
|
185
|
+
const path = view.preview === null ? null : fileUriToPath(view.preview.uri);
|
|
186
|
+
const shown = path === null ? '' : folderOf(relativeTo(path, folder));
|
|
187
|
+
return createPortal(
|
|
188
|
+
<div
|
|
189
|
+
className="flex flex-col bg-surface pt-[2px] font-sans text-xs text-text"
|
|
190
|
+
style={{ height: CALL_HIERARCHY_HEIGHT }}
|
|
191
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
192
|
+
>
|
|
193
|
+
<div className="peek-hatch h-[14px]" />
|
|
194
|
+
<div className="flex h-[33px] shrink-0 items-center gap-2 border-b border-border pr-2 pl-3 text-sm">
|
|
195
|
+
<Segmented
|
|
196
|
+
value={view.direction}
|
|
197
|
+
onValueChange={(direction) => language.callHierarchy.setDirection(direction)}
|
|
198
|
+
label={t('language.callHierarchy.direction')}
|
|
199
|
+
options={[
|
|
200
|
+
{ id: 'incoming', label: t('language.callHierarchy.incoming') },
|
|
201
|
+
{ id: 'outgoing', label: t('language.callHierarchy.outgoing') }
|
|
202
|
+
]}
|
|
203
|
+
/>
|
|
204
|
+
<span className="shrink-0 font-medium">{view.name}</span>
|
|
205
|
+
<span className="shrink-0 font-medium text-text-muted">{path === null ? '' : basenameOf(path)}</span>
|
|
206
|
+
<PathText path={shown} className="text-text-muted" />
|
|
207
|
+
<div className="ml-auto flex shrink-0 items-center gap-1">
|
|
208
|
+
{view.sites > 1 && (
|
|
209
|
+
<>
|
|
210
|
+
<IconButton
|
|
211
|
+
icon={ChevronLeft}
|
|
212
|
+
size="xs"
|
|
213
|
+
label={t('language.callHierarchy.previousSite')}
|
|
214
|
+
onClick={() => language.callHierarchy.stepSite(-1)}
|
|
215
|
+
/>
|
|
216
|
+
<span className="text-text-muted">
|
|
217
|
+
{t('language.callHierarchy.site', { current: formatNumber(view.site + 1), total: formatNumber(view.sites) })}
|
|
218
|
+
</span>
|
|
219
|
+
<IconButton
|
|
220
|
+
icon={ChevronRight}
|
|
221
|
+
size="xs"
|
|
222
|
+
label={t('language.callHierarchy.nextSite')}
|
|
223
|
+
onClick={() => language.callHierarchy.stepSite(1)}
|
|
224
|
+
/>
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
227
|
+
<IconButton icon={X} size="xs" label={t('language.callHierarchy.close')} onClick={() => language.callHierarchy.close()} />
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="flex h-[180px] shrink-0">
|
|
231
|
+
<Tree language={language} view={view} />
|
|
232
|
+
<Preview preview={view.preview} />
|
|
233
|
+
</div>
|
|
234
|
+
<div className="peek-hatch h-[14px]" />
|
|
235
|
+
</div>,
|
|
236
|
+
view.container
|
|
237
|
+
);
|
|
238
|
+
}
|
package/src/CompletionPopup.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLayoutEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useId, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import type { EditorRect } from '@adecore/editor';
|
|
@@ -64,6 +64,7 @@ const ROW_CHROME = 72;
|
|
|
64
64
|
*/
|
|
65
65
|
export function CompletionPopup({ language, view, rect }: { language: EditorLanguage; view: CompletionView; rect: EditorRect }) {
|
|
66
66
|
const { t } = useTranslation('editor');
|
|
67
|
+
const listId = useId();
|
|
67
68
|
const list = useRef<HTMLDivElement>(null);
|
|
68
69
|
const docs = useRef<HTMLDivElement>(null);
|
|
69
70
|
const activeRow = useRef<HTMLButtonElement>(null);
|
|
@@ -114,6 +115,14 @@ export function CompletionPopup({ language, view, rect }: { language: EditorLang
|
|
|
114
115
|
|
|
115
116
|
const total = view.rows.length;
|
|
116
117
|
const { first, last } = rowWindow(listTop, view.active, total, ROW_HEIGHT, VISIBLE_ROWS, OVERSCAN_ROWS, LIST_PADDING);
|
|
118
|
+
const optionId = (index: number): string => `${listId}-option-${index}`;
|
|
119
|
+
const activeId = view.active >= first && view.active < last ? optionId(view.active) : undefined;
|
|
120
|
+
|
|
121
|
+
// The focus stays in the text, so the input is what names the list and its active option.
|
|
122
|
+
useLayoutEffect(() => {
|
|
123
|
+
language.editor.setInputPopup({ controls: listId, activeDescendant: activeId });
|
|
124
|
+
return () => language.editor.setInputPopup(null);
|
|
125
|
+
}, [language, listId, activeId]);
|
|
117
126
|
|
|
118
127
|
return createPortal(
|
|
119
128
|
<>
|
|
@@ -132,6 +141,7 @@ export function CompletionPopup({ language, view, rect }: { language: EditorLang
|
|
|
132
141
|
}}
|
|
133
142
|
/>
|
|
134
143
|
<div
|
|
144
|
+
id={listId}
|
|
135
145
|
role="listbox"
|
|
136
146
|
className="w-0 min-w-full overflow-y-auto p-1"
|
|
137
147
|
style={{ maxHeight: ROW_HEIGHT * VISIBLE_ROWS + 8 }}
|
|
@@ -143,6 +153,7 @@ export function CompletionPopup({ language, view, rect }: { language: EditorLang
|
|
|
143
153
|
return (
|
|
144
154
|
<button
|
|
145
155
|
key={`${index}:${row.label}`}
|
|
156
|
+
id={optionId(index)}
|
|
146
157
|
ref={index === view.active ? activeRow : undefined}
|
|
147
158
|
type="button"
|
|
148
159
|
role="option"
|
package/src/DiffView.tsx
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { useLayoutEffect, useMemo, useRef, useState, type Ref } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import type { Editor, EditorEngine, EditorLineHighlight, EditorTheme } from '@adecore/editor';
|
|
4
|
+
import { parseChord } from '@adecore/editor/keymap';
|
|
5
|
+
import { diffDisplay, diffSplit, type DiffDocument, type DiffDisplay } from '@adecore/merge';
|
|
6
|
+
import { wordMarks } from './diff-wordmarks.ts';
|
|
7
|
+
import { DiffOverlays, type MountedDiff } from './diff-overlays.tsx';
|
|
8
|
+
import { SplitDiffView } from './SplitDiffView.tsx';
|
|
9
|
+
import type { DiffSourceTokens } from './diff-syntax.ts';
|
|
10
|
+
import { LineActionHost, RowHost } from './row-host.ts';
|
|
11
|
+
import { DEFAULT_UNFOLD_LIMITS, expandDiff, type DiffLimits } from './diff-expansion.ts';
|
|
12
|
+
|
|
13
|
+
export interface DiffViewProps {
|
|
14
|
+
engine: EditorEngine;
|
|
15
|
+
document: DiffDocument;
|
|
16
|
+
theme: EditorTheme;
|
|
17
|
+
tokens?: DiffSourceTokens;
|
|
18
|
+
wrap?: boolean;
|
|
19
|
+
layout?: 'unified' | 'split';
|
|
20
|
+
/* Above this many displayed unified rows, split uses fixed line heights. Defaults to 2000. */
|
|
21
|
+
splitWrapLimit?: number;
|
|
22
|
+
/* A fixed-height host supplies the height; otherwise the view grows with its rows. */
|
|
23
|
+
fill?: boolean;
|
|
24
|
+
/* Positive pixels; taller content scrolls inside the editor. Ignored with fill. */
|
|
25
|
+
maxHeight?: number;
|
|
26
|
+
/* Total visible source-text budgets after unfolding; changed rows are always retained. */
|
|
27
|
+
unfoldLimits?: DiffLimits;
|
|
28
|
+
label: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
ref?: Ref<HTMLDivElement>;
|
|
31
|
+
onMount?(editor: Editor, side?: 'old' | 'new'): void | (() => void);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface UnifiedViewProps extends DiffViewProps {
|
|
35
|
+
display: DiffDisplay;
|
|
36
|
+
unfold(id: number): void;
|
|
37
|
+
blocked: ReadonlySet<number>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function UnifiedDiffView({
|
|
41
|
+
engine,
|
|
42
|
+
document,
|
|
43
|
+
display,
|
|
44
|
+
unfold,
|
|
45
|
+
blocked,
|
|
46
|
+
theme,
|
|
47
|
+
tokens,
|
|
48
|
+
wrap = true,
|
|
49
|
+
fill = false,
|
|
50
|
+
maxHeight,
|
|
51
|
+
label,
|
|
52
|
+
className,
|
|
53
|
+
ref,
|
|
54
|
+
onMount
|
|
55
|
+
}: UnifiedViewProps) {
|
|
56
|
+
const { t } = useTranslation('editor');
|
|
57
|
+
const emphasis = useMemo(() => wordMarks(document), [document]);
|
|
58
|
+
const element = useRef<HTMLDivElement>(null);
|
|
59
|
+
const current = useRef({ display, tokens, theme, wrap, label });
|
|
60
|
+
const applied = useRef({ theme, wrap, label });
|
|
61
|
+
const [mounted, setMounted] = useState<MountedDiff | null>(null);
|
|
62
|
+
const [height, setHeight] = useState(0);
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
current.current = { display, tokens, theme, wrap, label };
|
|
65
|
+
});
|
|
66
|
+
useLayoutEffect(() => {
|
|
67
|
+
const host = element.current;
|
|
68
|
+
if (host === null) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const initial = current.current;
|
|
72
|
+
const editor = engine.mount(host, {
|
|
73
|
+
text: initial.display.text,
|
|
74
|
+
theme: initial.theme,
|
|
75
|
+
wrap: initial.wrap,
|
|
76
|
+
label: initial.label,
|
|
77
|
+
readOnly: true,
|
|
78
|
+
guides: false,
|
|
79
|
+
foldOutline: 'off',
|
|
80
|
+
folding: false,
|
|
81
|
+
// Prompt cards use this chord for their primary action; a read-only review has no split-line action.
|
|
82
|
+
handBack: [parseChord('Mod+Enter')]
|
|
83
|
+
});
|
|
84
|
+
applied.current = { theme: initial.theme, wrap: initial.wrap, label: initial.label };
|
|
85
|
+
editor.setBlocks([]);
|
|
86
|
+
editor.setLineTokens((line) => {
|
|
87
|
+
const { display, tokens } = current.current;
|
|
88
|
+
const row = display.rows[line];
|
|
89
|
+
return row === undefined ? null : tokens?.get(row.new === undefined ? 'old' : 'new', (row.new ?? row.old)!.index);
|
|
90
|
+
});
|
|
91
|
+
setMounted({ engine, editor, rows: new RowHost(editor, 'diff-gaps'), actions: new LineActionHost(editor, 'diff-status') });
|
|
92
|
+
const measure = (): void => setHeight(editor.getContentHeight());
|
|
93
|
+
const stop = editor.onViewChange(measure);
|
|
94
|
+
measure();
|
|
95
|
+
return () => {
|
|
96
|
+
stop();
|
|
97
|
+
editor.dispose();
|
|
98
|
+
};
|
|
99
|
+
}, [engine]);
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
if (mounted === null || mounted.engine !== engine) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const { editor, rows, actions } = mounted;
|
|
105
|
+
editor.setText(display.text);
|
|
106
|
+
editor.setLineNumbers((line) => {
|
|
107
|
+
const row = current.current.display.rows[line];
|
|
108
|
+
return row === undefined ? null : String((row.new ?? row.old)!.number);
|
|
109
|
+
});
|
|
110
|
+
const highlights: EditorLineHighlight[] = [];
|
|
111
|
+
for (const [index, row] of display.rows.entries()) {
|
|
112
|
+
if (row.change !== 'delete' && row.change !== 'insert') {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const removed = row.change === 'delete';
|
|
116
|
+
highlights.push({
|
|
117
|
+
startLine: index + 1,
|
|
118
|
+
endLine: index + 1,
|
|
119
|
+
color: removed ? '--editor-deleted' : '--editor-added',
|
|
120
|
+
bar: removed ? 'hatched' : 'solid',
|
|
121
|
+
emphasis: [(removed ? emphasis.old.get(row.old!.index) : emphasis.new.get(row.new!.index)) ?? []]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
editor.setLineHighlights(highlights);
|
|
125
|
+
editor.setChangeMarks(
|
|
126
|
+
highlights.map((highlight) => ({
|
|
127
|
+
startLine: highlight.startLine,
|
|
128
|
+
endLine: highlight.endLine,
|
|
129
|
+
kind: highlight.bar === 'hatched' ? 'deleted' : 'added'
|
|
130
|
+
}))
|
|
131
|
+
);
|
|
132
|
+
rows.set(
|
|
133
|
+
display.gaps.map(({ before, row }) => ({
|
|
134
|
+
id: `gap-${row.id}`,
|
|
135
|
+
line: Math.min(before, Math.max(0, display.rows.length - 1)),
|
|
136
|
+
placement: before < display.rows.length ? 'above' : 'below',
|
|
137
|
+
height: 32
|
|
138
|
+
}))
|
|
139
|
+
);
|
|
140
|
+
actions.set(display.rows.flatMap((row, line) => (row.noNewline === undefined ? [] : [{ id: `no-newline-${line}`, line }])));
|
|
141
|
+
}, [mounted, engine, display, emphasis]);
|
|
142
|
+
useLayoutEffect(() => {
|
|
143
|
+
if (mounted === null || mounted.engine !== engine) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// The color of a changed line is not the only thing that says so: the gutter carries a sentence with the numbers it reads from.
|
|
147
|
+
mounted.editor.setLineLabels((line) => {
|
|
148
|
+
const row = display.rows[line];
|
|
149
|
+
if (row?.change === 'delete') {
|
|
150
|
+
return t('diff.removed', { number: row.old!.number });
|
|
151
|
+
}
|
|
152
|
+
if (row?.change === 'insert') {
|
|
153
|
+
return t('diff.added', { number: row.new!.number });
|
|
154
|
+
}
|
|
155
|
+
return row?.old !== undefined && row.new !== undefined ? t('diff.unchanged', { old: row.old.number, new: row.new.number }) : null;
|
|
156
|
+
});
|
|
157
|
+
}, [mounted, engine, display, t]);
|
|
158
|
+
useLayoutEffect(() => {
|
|
159
|
+
if (mounted === null || mounted.engine !== engine) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const previous = applied.current;
|
|
163
|
+
if (previous.theme !== theme) {
|
|
164
|
+
mounted.editor.setTheme(theme);
|
|
165
|
+
}
|
|
166
|
+
if (previous.wrap !== wrap) {
|
|
167
|
+
mounted.editor.setWrap(wrap);
|
|
168
|
+
}
|
|
169
|
+
if (previous.label !== label) {
|
|
170
|
+
mounted.editor.setLabel(label);
|
|
171
|
+
}
|
|
172
|
+
applied.current = { theme, wrap, label };
|
|
173
|
+
}, [mounted, engine, theme, wrap, label]);
|
|
174
|
+
useLayoutEffect(() => {
|
|
175
|
+
if (mounted === null || mounted.engine !== engine) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const refresh = (): void => {
|
|
179
|
+
mounted.editor.setLineTokens((line) => {
|
|
180
|
+
const row = current.current.display.rows[line];
|
|
181
|
+
return row === undefined ? null : current.current.tokens?.get(row.new === undefined ? 'old' : 'new', (row.new ?? row.old)!.index);
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
refresh();
|
|
185
|
+
const ownerWindow = element.current?.ownerDocument.defaultView;
|
|
186
|
+
let pending = false;
|
|
187
|
+
let stopped = false;
|
|
188
|
+
let frame: number | undefined;
|
|
189
|
+
const schedule = (): void => {
|
|
190
|
+
if (pending) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
pending = true;
|
|
194
|
+
const paint = (): void => {
|
|
195
|
+
pending = false;
|
|
196
|
+
if (!stopped) {
|
|
197
|
+
refresh();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
if (ownerWindow?.requestAnimationFrame !== undefined) {
|
|
201
|
+
frame = ownerWindow.requestAnimationFrame(paint);
|
|
202
|
+
} else {
|
|
203
|
+
queueMicrotask(paint);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const stop = tokens?.subscribe(schedule);
|
|
207
|
+
return () => {
|
|
208
|
+
stopped = true;
|
|
209
|
+
stop?.();
|
|
210
|
+
if (frame !== undefined) {
|
|
211
|
+
ownerWindow?.cancelAnimationFrame?.(frame);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}, [mounted, engine, tokens, display]);
|
|
215
|
+
useLayoutEffect(() => {
|
|
216
|
+
if (mounted !== null && mounted.engine === engine) {
|
|
217
|
+
return onMount?.(mounted.editor);
|
|
218
|
+
}
|
|
219
|
+
}, [mounted, engine, onMount]);
|
|
220
|
+
const limit = maxHeight !== undefined && Number.isFinite(maxHeight) && maxHeight > 0 ? maxHeight : Infinity;
|
|
221
|
+
return (
|
|
222
|
+
<div
|
|
223
|
+
className={`adecore-editor-diff ${className ?? ''}`}
|
|
224
|
+
ref={ref}
|
|
225
|
+
data-scroll={fill || height > limit}
|
|
226
|
+
style={{ position: 'relative', minHeight: 0, height: fill ? '100%' : Math.min(height, limit) }}
|
|
227
|
+
>
|
|
228
|
+
<div ref={element} style={{ position: 'absolute', inset: 0 }} />
|
|
229
|
+
{mounted !== null && mounted.engine === engine && <DiffOverlays mounted={mounted} display={display} unfold={unfold} blocked={blocked} />}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function DiffView(props: DiffViewProps) {
|
|
235
|
+
const { document, unfoldLimits = DEFAULT_UNFOLD_LIMITS } = props;
|
|
236
|
+
const [expanded, setExpanded] = useState<{ document: DiffDocument; gaps: ReadonlyMap<number, number>; blocked: ReadonlySet<number> }>(() => ({
|
|
237
|
+
document,
|
|
238
|
+
gaps: new Map(),
|
|
239
|
+
blocked: new Set()
|
|
240
|
+
}));
|
|
241
|
+
const gaps = expanded.document === document ? expanded.gaps : undefined;
|
|
242
|
+
const blocked = expanded.document === document ? expanded.blocked : new Set<number>();
|
|
243
|
+
const projection = useMemo(() => (props.layout === 'split' ? diffSplit(document, gaps) : diffDisplay(document, gaps)), [props.layout, document, gaps]);
|
|
244
|
+
const unfold = (id: number): void =>
|
|
245
|
+
setExpanded((previous) => {
|
|
246
|
+
const gaps = previous.document === document ? previous.gaps : new Map<number, number>();
|
|
247
|
+
const next = expandDiff(document, gaps, id, unfoldLimits);
|
|
248
|
+
return {
|
|
249
|
+
document,
|
|
250
|
+
gaps: next,
|
|
251
|
+
blocked: new Set([...(previous.document === document ? previous.blocked : []), ...(next === gaps ? [id] : [])])
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
if ('aligned' in projection && projection.old.lines.length > 0 && projection.new.lines.length > 0) {
|
|
255
|
+
const visibleRows = projection.aligned.reduce((count, row) => count + (row.kind === 'gap' ? 0 : row.row.change === 'pair' ? 2 : 1), 0);
|
|
256
|
+
const wrap = props.wrap !== false && visibleRows <= (props.splitWrapLimit ?? 2000);
|
|
257
|
+
return <SplitDiffView {...props} wrap={wrap} display={projection} unfold={unfold} blocked={blocked} />;
|
|
258
|
+
}
|
|
259
|
+
// An absent side has no selectable text; keep additions and deletions in the unified editor.
|
|
260
|
+
const display = 'aligned' in projection ? (projection.old.lines.length > 0 ? projection.old : projection.new) : projection;
|
|
261
|
+
return <UnifiedDiffView {...props} display={display} unfold={unfold} blocked={blocked} />;
|
|
262
|
+
}
|
|
@@ -17,6 +17,7 @@ export function EditorContextMenu({ language, view, children }: { language: Edit
|
|
|
17
17
|
{ id: 'goToTypeDefinition', method: 'textDocument/typeDefinition', run: () => void language.navigation.go('typeDefinition') },
|
|
18
18
|
{ id: 'goToImplementation', method: 'textDocument/implementation', run: () => void language.navigation.go('implementation') },
|
|
19
19
|
{ id: 'peekReferences', method: 'textDocument/references', run: () => void language.peek.open() },
|
|
20
|
+
{ id: 'callHierarchy', method: 'textDocument/prepareCallHierarchy', run: () => void language.callHierarchy.open() },
|
|
20
21
|
{ id: 'renameSymbol', method: 'textDocument/rename', run: () => void language.rename.start() },
|
|
21
22
|
{ id: 'codeActions', method: 'textDocument/codeAction', run: () => void language.codeActions.open() }
|
|
22
23
|
];
|
package/src/EditorView.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHolder } from './holder.ts';
|
|
2
2
|
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore, type ReactNode, type Ref } from 'react';
|
|
3
|
-
import
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { TFunction } from 'i18next';
|
|
5
|
+
import type { Editor, EditorEngine, EditorMessages, EditorOptions } from '@adecore/editor';
|
|
4
6
|
import { EditorLanguage } from './editor-language.ts';
|
|
5
7
|
import { LanguagePopups } from './LanguagePopups.tsx';
|
|
6
8
|
import type { ProjectLanguage } from './project-language.ts';
|
|
@@ -18,8 +20,17 @@ export interface EditorViewProps {
|
|
|
18
20
|
contextMenuItems?(language: EditorLanguage): ReactNode;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/* The names of the fold controls in the editor's `{start}` and `{end}` placeholders, which the editor fills in per control. */
|
|
24
|
+
function foldMessages(t: TFunction<'editor'>): Pick<EditorMessages, 'fold' | 'unfold'> {
|
|
25
|
+
const lines = { start: '{start}', end: '{end}' };
|
|
26
|
+
return { fold: t('language.fold.fold', lines), unfold: t('language.fold.unfold', lines) };
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export function EditorView({ engine, options, project, uri, languageId, className, ref, onMount, contextMenuItems }: EditorViewProps) {
|
|
30
|
+
const { t } = useTranslation('editor');
|
|
22
31
|
const element = useRef<HTMLDivElement>(null);
|
|
32
|
+
const mounted = useRef<Editor | null>(null);
|
|
33
|
+
const words = useRef(t);
|
|
23
34
|
const holder = useMemo(() => createHolder<EditorLanguage>(), []);
|
|
24
35
|
const language = useSyncExternalStore(holder.subscribe, holder.get, holder.get);
|
|
25
36
|
useLayoutEffect(() => {
|
|
@@ -27,17 +38,25 @@ export function EditorView({ engine, options, project, uri, languageId, classNam
|
|
|
27
38
|
if (host === null) {
|
|
28
39
|
return;
|
|
29
40
|
}
|
|
30
|
-
const editor = engine.mount(host, options);
|
|
41
|
+
const editor = engine.mount(host, { ...options, messages: { ...foldMessages(words.current), ...options.messages } });
|
|
42
|
+
mounted.current = editor;
|
|
31
43
|
const attached = project === undefined || uri === undefined || languageId === undefined ? null : new EditorLanguage(project, editor, uri, languageId);
|
|
32
44
|
holder.set(attached);
|
|
33
45
|
const stop = onMount?.(editor, attached);
|
|
34
46
|
return () => {
|
|
47
|
+
mounted.current = null;
|
|
35
48
|
holder.set(null);
|
|
36
49
|
stop?.();
|
|
37
50
|
attached?.dispose();
|
|
38
51
|
editor.dispose();
|
|
39
52
|
};
|
|
40
53
|
}, [engine, options, project, uri, languageId, holder, onMount]);
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
words.current = t;
|
|
56
|
+
const { fold, unfold } = options.messages ?? {};
|
|
57
|
+
const localized = foldMessages(t);
|
|
58
|
+
mounted.current?.setMessages({ fold: fold ?? localized.fold, unfold: unfold ?? localized.unfold });
|
|
59
|
+
}, [t, options.messages]);
|
|
41
60
|
return (
|
|
42
61
|
<div className={className} ref={ref} style={{ position: 'relative', minHeight: 0 }}>
|
|
43
62
|
<div ref={element} style={{ position: 'absolute', inset: 0 }} />
|