@adecore/editor-react 0.39.0 → 0.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallHierarchyPanel.js +30 -55
- package/dist/CodeAuthorsCard.js +0 -1
- package/dist/CompletionPopup.js +150 -181
- package/dist/DiffView.d.ts +1 -1
- package/dist/DiffView.js +2 -29
- package/dist/PickPopup.js +54 -80
- package/dist/SplitDiffView.js +27 -57
- package/dist/SymbolPicker.js +2 -8
- package/dist/code-actions-model.js +2 -9
- package/dist/code-actions.js +10 -11
- package/dist/code-vision.js +2 -1
- package/dist/completion-model.js +2 -2
- package/dist/completion.js +13 -34
- package/dist/definition-link.d.ts +1 -0
- package/dist/definition-link.js +22 -18
- package/dist/diagnostics-model.d.ts +1 -0
- package/dist/diagnostics-model.js +3 -1
- package/dist/diagnostics.js +1 -2
- package/dist/diff-syntax.d.ts +1 -0
- package/dist/diff-syntax.js +31 -0
- package/dist/doc-comment-model.d.ts +1 -0
- package/dist/doc-comment-model.js +1 -1
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +6 -10
- package/dist/hover-content.d.ts +0 -2
- package/dist/hover-content.js +1 -9
- package/dist/inlay-hints.d.ts +0 -1
- package/dist/inlay-hints.js +11 -41
- package/dist/navigation.js +4 -14
- package/dist/peek.js +2 -12
- package/dist/popup-focus.d.ts +3 -1
- package/dist/popup-focus.js +33 -0
- package/dist/recent-choices.d.ts +0 -1
- package/dist/recent-choices.js +0 -4
- package/dist/rendered-docs.d.ts +1 -0
- package/dist/rendered-docs.js +15 -28
- package/dist/snippet-field-transforms.d.ts +5 -0
- package/dist/snippet-field-transforms.js +27 -16
- package/dist/snippet-session.js +8 -20
- package/dist/symbol-links.js +3 -10
- package/dist/symbol-picker-model.d.ts +1 -0
- package/dist/symbol-picker-model.js +7 -0
- package/dist/unique.d.ts +10 -0
- package/dist/unique.js +15 -0
- package/package.json +9 -9
- package/src/AnchoredPopup.tsx +2 -0
- package/src/BaselineStatus.tsx +1 -0
- package/src/CallHierarchyPanel.tsx +16 -6
- package/src/ChangeReview.tsx +7 -0
- package/src/CodeAuthorsCard.tsx +2 -5
- package/src/CompletionPopup.tsx +21 -15
- package/src/DiffView.tsx +53 -30
- package/src/EditorContextMenu.tsx +7 -0
- package/src/EditorView.tsx +28 -0
- package/src/FindReplace.tsx +8 -0
- package/src/HoverCard.tsx +8 -0
- package/src/HoverSections.tsx +11 -0
- package/src/LanguagePopups.tsx +1 -0
- package/src/PanelResizeHandle.tsx +8 -0
- package/src/PathText.tsx +1 -0
- package/src/PeekPanel.tsx +13 -0
- package/src/PickPopup.tsx +7 -6
- package/src/RenameCard.tsx +13 -0
- package/src/RenderedDocs.tsx +11 -0
- package/src/SplitDiffView.tsx +91 -56
- package/src/SymbolPicker.tsx +10 -18
- package/src/call-hierarchy-model.ts +10 -0
- package/src/call-hierarchy.ts +82 -0
- package/src/code-actions-model.ts +21 -9
- package/src/code-actions.ts +83 -7
- package/src/code-authors.ts +20 -0
- package/src/code-vision-model.ts +37 -0
- package/src/code-vision.ts +47 -3
- package/src/completion-call.ts +19 -0
- package/src/completion-model.ts +58 -2
- package/src/completion.ts +156 -30
- package/src/conflict-model.ts +4 -0
- package/src/context-menu-rows.ts +1 -0
- package/src/context-menu.ts +7 -0
- package/src/definition-link.ts +42 -18
- package/src/diagnostics-model.ts +19 -1
- package/src/diagnostics.ts +31 -2
- package/src/diff-expansion.ts +9 -0
- package/src/diff-overlays.tsx +8 -0
- package/src/diff-syntax.ts +54 -0
- package/src/diff-wordmarks.ts +8 -0
- package/src/doc-comment-model.ts +51 -1
- package/src/editor-language.ts +11 -0
- package/src/find-query.ts +7 -0
- package/src/folding.ts +10 -3
- package/src/format-on-type.ts +23 -10
- package/src/highlight-layers.ts +3 -0
- package/src/highlights.ts +18 -0
- package/src/history.ts +11 -0
- package/src/holder.ts +3 -0
- package/src/hover-content.ts +34 -17
- package/src/hover.ts +57 -0
- package/src/index.ts +4 -0
- package/src/inlay-hints.ts +86 -47
- package/src/key-priority.ts +2 -0
- package/src/line-edits.ts +8 -0
- package/src/models.ts +6 -0
- package/src/navigation-history.ts +5 -0
- package/src/navigation.ts +28 -13
- package/src/panel-size.ts +7 -0
- package/src/patch-decorations.ts +39 -0
- package/src/patch-layout.ts +47 -0
- package/src/peek-model.ts +21 -0
- package/src/peek.ts +49 -13
- package/src/pick.ts +35 -0
- package/src/popup-focus.ts +11 -1
- package/src/popup-placement.ts +5 -0
- package/src/popups.ts +2 -6
- package/src/project-language.ts +33 -0
- package/src/project-problems.ts +11 -0
- package/src/proposal-model.ts +40 -0
- package/src/recent-choices.ts +3 -5
- package/src/refresher.ts +3 -0
- package/src/rename-model.ts +28 -0
- package/src/rename.ts +56 -0
- package/src/rendered-docs.ts +89 -19
- package/src/review-group.ts +4 -0
- package/src/row-host.ts +12 -0
- package/src/selection-ranges.ts +6 -0
- package/src/semantic-delta.ts +7 -0
- package/src/semantic-model.ts +11 -0
- package/src/semantic-tokens.ts +37 -0
- package/src/shortcut-keys.ts +3 -0
- package/src/signature-model.ts +7 -0
- package/src/signature.ts +27 -0
- package/src/snippet-field-transforms.ts +48 -13
- package/src/snippet-indent.ts +10 -0
- package/src/snippet-range-order.ts +5 -0
- package/src/snippet-session.ts +173 -31
- package/src/snippet-transform-client.ts +14 -0
- package/src/snippet-transform-format.ts +98 -0
- package/src/snippet-transform-protocol.ts +5 -0
- package/src/snippet-transform-queue.ts +53 -0
- package/src/snippet-transform-worker.ts +14 -0
- package/src/snippet-variables.ts +9 -0
- package/src/snippet.ts +107 -0
- package/src/split-alignment.ts +10 -0
- package/src/split-scroll.ts +8 -0
- package/src/symbol-links.ts +9 -12
- package/src/symbol-picker-model.ts +25 -0
- package/src/symbol-picker.ts +9 -0
- package/src/symbols.ts +21 -0
- package/src/testing.ts +7 -0
- package/src/timers.ts +5 -0
- package/src/unique.ts +23 -0
- package/src/use-problem-counts.ts +4 -0
- package/src/view-state.ts +9 -0
- package/src/word-diff.ts +5 -0
- package/src/workspace-edit.ts +84 -0
package/src/PeekPanel.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import type { PeekView } from './popups.ts';
|
|
|
18
18
|
import { useEditorRendering } from './rendering-context.ts';
|
|
19
19
|
|
|
20
20
|
const TAB_SIZE = 4;
|
|
21
|
+
|
|
21
22
|
const CODE =
|
|
22
23
|
'font-mono text-(length:--code-font-size) [tab-size:4] [&_.line]:block [&_.line]:h-(--code-line-height) [&_.line]:leading-(--code-line-height) [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:p-0 [&_pre]:whitespace-pre [&_code]:font-mono';
|
|
23
24
|
|
|
@@ -33,10 +34,13 @@ export function Preview({ preview }: { preview: PeekView['preview'] }) {
|
|
|
33
34
|
if (preview === null || highlight === undefined) {
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
37
|
+
|
|
36
38
|
let alive = true;
|
|
39
|
+
|
|
37
40
|
highlight(preview.text, shikiLanguageOfPath(path ?? preview.uri), theme)
|
|
38
41
|
.then((result) => alive && setHtml({ key, html: result }))
|
|
39
42
|
.catch(() => undefined);
|
|
43
|
+
|
|
40
44
|
return () => {
|
|
41
45
|
alive = false;
|
|
42
46
|
};
|
|
@@ -49,8 +53,10 @@ export function Preview({ preview }: { preview: PeekView['preview'] }) {
|
|
|
49
53
|
if (preview === null) {
|
|
50
54
|
return <div className="min-w-0 flex-1" />;
|
|
51
55
|
}
|
|
56
|
+
|
|
52
57
|
const lines = preview.text.split('\n');
|
|
53
58
|
const name = preview.name === null ? null : columnsOf(lines[preview.active] ?? '', preview.name);
|
|
59
|
+
|
|
54
60
|
return (
|
|
55
61
|
<div ref={scroller} className="relative min-w-0 flex-1 overflow-auto pt-1">
|
|
56
62
|
<div className="relative flex min-w-max">
|
|
@@ -94,6 +100,7 @@ export function Preview({ preview }: { preview: PeekView['preview'] }) {
|
|
|
94
100
|
/* The name's stretch of a line in monospace columns, which is what the code is laid out in. */
|
|
95
101
|
function columnsOf(line: string, name: NameRange): NameRange {
|
|
96
102
|
const start = visualColumnOf(line, name.start, TAB_SIZE);
|
|
103
|
+
|
|
97
104
|
return { start, end: visualColumnOf(line, name.end, TAB_SIZE) };
|
|
98
105
|
}
|
|
99
106
|
|
|
@@ -106,10 +113,13 @@ function PlaceText({ place }: { place: PeekPlace }) {
|
|
|
106
113
|
if (place.text === '') {
|
|
107
114
|
return <span className="min-w-0 truncate text-text-muted">{`:${place.line + 1}`}</span>;
|
|
108
115
|
}
|
|
116
|
+
|
|
109
117
|
const { name } = place;
|
|
118
|
+
|
|
110
119
|
if (name === null) {
|
|
111
120
|
return <span className="min-w-0 truncate text-text-muted">{place.text}</span>;
|
|
112
121
|
}
|
|
122
|
+
|
|
113
123
|
return (
|
|
114
124
|
<span className="min-w-0 truncate text-text-muted">
|
|
115
125
|
{place.text.slice(0, name.start)}
|
|
@@ -134,6 +144,7 @@ function Places({ language, view }: { language: EditorLanguage; view: PeekView }
|
|
|
134
144
|
<div className="w-1/3 shrink-0 overflow-y-auto border-l border-border py-1">
|
|
135
145
|
{view.files.map((file, fileIndex) => {
|
|
136
146
|
const collapsed = view.collapsed.includes(file.uri);
|
|
147
|
+
|
|
137
148
|
return (
|
|
138
149
|
<div key={file.uri}>
|
|
139
150
|
<Tooltip label={paths[fileIndex]!} side="left">
|
|
@@ -183,9 +194,11 @@ export function PeekPanel({ language, view }: { language: EditorLanguage; view:
|
|
|
183
194
|
const folder = language.project.folder;
|
|
184
195
|
const shown = path === null ? '' : folderOf(relativeTo(path, folder));
|
|
185
196
|
const body = usePanelBody(language);
|
|
197
|
+
|
|
186
198
|
if (view.container === null) {
|
|
187
199
|
return null;
|
|
188
200
|
}
|
|
201
|
+
|
|
189
202
|
return createPortal(
|
|
190
203
|
<div
|
|
191
204
|
className="flex flex-col bg-surface pt-[2px] font-sans text-xs text-text"
|
package/src/PickPopup.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
|
|
|
3
3
|
import type { EditorRect } from '@adecore/editor';
|
|
4
4
|
import type { EditorLanguage } from './editor-language.ts';
|
|
5
5
|
import { PathText } from './PathText.tsx';
|
|
6
|
-
import { keepFocusOutOfList } from './popup-focus.ts';
|
|
6
|
+
import { keepFocusOutOfList, useInputPopup } from './popup-focus.ts';
|
|
7
7
|
import { placePopup } from './popup-placement.ts';
|
|
8
8
|
import type { PickPreview, PickView } from './popups.ts';
|
|
9
9
|
|
|
@@ -47,15 +47,18 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
|
|
|
47
47
|
// Every render: the character moves with the scroll, and the list changes size with its preview.
|
|
48
48
|
useLayoutEffect(() => {
|
|
49
49
|
const element = card.current;
|
|
50
|
+
|
|
50
51
|
if (element === null) {
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
54
|
+
|
|
53
55
|
const placed = placePopup(
|
|
54
56
|
rect,
|
|
55
57
|
{ width: element.offsetWidth, height: element.offsetHeight },
|
|
56
58
|
{ width: window.innerWidth, height: window.innerHeight },
|
|
57
59
|
{ gap: 2 }
|
|
58
60
|
);
|
|
61
|
+
|
|
59
62
|
element.style.left = `${Math.round(placed.left)}px`;
|
|
60
63
|
element.style.top = `${Math.round(placed.top)}px`;
|
|
61
64
|
element.style.maxHeight = `${Math.floor(placed.maxHeight)}px`;
|
|
@@ -67,18 +70,16 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
|
|
|
67
70
|
}, [view.active]);
|
|
68
71
|
|
|
69
72
|
const optionIds = new Map<string, string>();
|
|
73
|
+
|
|
70
74
|
for (const group of view.groups) {
|
|
71
75
|
for (const row of group.rows) {
|
|
72
76
|
optionIds.set(row.id, `${listId}-option-${optionIds.size}`);
|
|
73
77
|
}
|
|
74
78
|
}
|
|
79
|
+
|
|
75
80
|
const activeId = optionIds.get(view.active);
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
useLayoutEffect(() => {
|
|
79
|
-
language.editor.setInputPopup({ controls: listId, activeDescendant: activeId });
|
|
80
|
-
return () => language.editor.setInputPopup(null);
|
|
81
|
-
}, [language, listId, activeId]);
|
|
82
|
+
useInputPopup(language, listId, activeId);
|
|
82
83
|
|
|
83
84
|
return createPortal(
|
|
84
85
|
<div
|
package/src/RenameCard.tsx
CHANGED
|
@@ -25,6 +25,7 @@ function staysInside(target: EventTarget | null): boolean {
|
|
|
25
25
|
*/
|
|
26
26
|
function Suggestions({ names, active, onPick, onFocusBack }: { names: readonly string[]; active: number; onPick(name: string): void; onFocusBack(): void }) {
|
|
27
27
|
const { t } = useTranslation('editor');
|
|
28
|
+
|
|
28
29
|
return (
|
|
29
30
|
<div className="flex w-[250px] flex-col p-1.25" {...keepFocusOutOfList(onFocusBack)}>
|
|
30
31
|
<div className="flex items-center gap-1.5 px-2.25 pt-1.25 pb-1 text-xs font-semibold text-text-faint">
|
|
@@ -54,6 +55,7 @@ function Suggestions({ names, active, onPick, onFocusBack }: { names: readonly s
|
|
|
54
55
|
function Hints({ view, active, onPick, onFocusBack }: { view: RenameView; active: number; onPick(name: string): void; onFocusBack(): void }) {
|
|
55
56
|
const { t } = useTranslation('editor');
|
|
56
57
|
const { occurrences } = view;
|
|
58
|
+
|
|
57
59
|
return (
|
|
58
60
|
<div className="flex flex-col">
|
|
59
61
|
{view.error !== null && (
|
|
@@ -88,16 +90,20 @@ function Preview({ language, view }: { language: EditorLanguage; view: RenameVie
|
|
|
88
90
|
const { t } = useTranslation('editor');
|
|
89
91
|
const folder = language.project.folder;
|
|
90
92
|
const operations = view.operations ?? [];
|
|
93
|
+
|
|
91
94
|
const shownPath = (uri: string): string => {
|
|
92
95
|
const path = fileUriToPath(uri);
|
|
96
|
+
|
|
93
97
|
return path === null ? uri : relativeTo(path, folder);
|
|
94
98
|
};
|
|
99
|
+
|
|
95
100
|
return (
|
|
96
101
|
<div className="flex w-[520px] max-w-full flex-col" onPointerDown={(event) => event.preventDefault()}>
|
|
97
102
|
<div className="max-h-[320px] overflow-y-auto">
|
|
98
103
|
{view.files.map((file) => {
|
|
99
104
|
const path = fileUriToPath(file.uri);
|
|
100
105
|
const shown = path === null ? file.uri : relativeTo(path, folder);
|
|
106
|
+
|
|
101
107
|
return (
|
|
102
108
|
<div key={file.uri} className="border-b border-border last:border-b-0">
|
|
103
109
|
<div className="flex items-center gap-2 px-3 py-1.5 text-xs">
|
|
@@ -170,19 +176,24 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
|
|
|
170
176
|
|
|
171
177
|
function onKeyDown(event: KeyboardEvent<HTMLInputElement>): void {
|
|
172
178
|
const names = view.phase === 'input' ? view.suggestions : [];
|
|
179
|
+
|
|
173
180
|
if (names.length > 0 && !event.nativeEvent.isComposing && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
174
181
|
const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
|
|
182
|
+
|
|
175
183
|
if (step !== 0 || event.key === 'Tab') {
|
|
176
184
|
event.preventDefault();
|
|
177
185
|
event.stopPropagation();
|
|
186
|
+
|
|
178
187
|
if (event.key === 'Tab') {
|
|
179
188
|
fill(names[Math.min(active, names.length - 1)]!);
|
|
180
189
|
} else {
|
|
181
190
|
setActive((active + step + names.length) % names.length);
|
|
182
191
|
}
|
|
192
|
+
|
|
183
193
|
return;
|
|
184
194
|
}
|
|
185
195
|
}
|
|
196
|
+
|
|
186
197
|
if (event.key === 'Escape') {
|
|
187
198
|
event.preventDefault();
|
|
188
199
|
event.stopPropagation();
|
|
@@ -190,6 +201,7 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
|
|
|
190
201
|
} else if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
|
|
191
202
|
event.preventDefault();
|
|
192
203
|
event.stopPropagation();
|
|
204
|
+
|
|
193
205
|
if (view.phase === 'preview') {
|
|
194
206
|
void language.rename.confirm();
|
|
195
207
|
} else if (!view.busy) {
|
|
@@ -199,6 +211,7 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
|
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
const symbolWidth = endRect === null ? 0 : Math.max(0, endRect.left - rect.left);
|
|
214
|
+
|
|
202
215
|
return createPortal(
|
|
203
216
|
<>
|
|
204
217
|
<input
|
package/src/RenderedDocs.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { shikiLanguageOf } from './language-ids.ts';
|
|
|
10
10
|
import { Markdown } from './Markdown.tsx';
|
|
11
11
|
|
|
12
12
|
const LINK = /\[([^\]]*)\]\(#doc-link:[^)]*\)/g;
|
|
13
|
+
|
|
13
14
|
const KNOWN = new Set([
|
|
14
15
|
'deprecated',
|
|
15
16
|
'template',
|
|
@@ -35,6 +36,7 @@ function Code({ children, faint = false }: { children: string; faint?: boolean }
|
|
|
35
36
|
/* Markdown prose and fenced code as the hover draws them: the blocks in the code face, the rest muted. */
|
|
36
37
|
function Prose({ text, language, linkable, compact = false }: { text: string; language: string; linkable: boolean; compact?: boolean }) {
|
|
37
38
|
const plain = linkable ? text : text.replace(LINK, '$1');
|
|
39
|
+
|
|
38
40
|
return (
|
|
39
41
|
<>
|
|
40
42
|
{markdownParts(plain).map((part, index) =>
|
|
@@ -55,8 +57,10 @@ function Prose({ text, language, linkable, compact = false }: { text: string; la
|
|
|
55
57
|
/* A value of a tag, in the second column of the table; `spaced` opens a new section with more room above. */
|
|
56
58
|
function TagRow({ tag, language, linkable, spaced }: { tag: DocTagEntry; language: string; linkable: boolean; spaced: boolean }) {
|
|
57
59
|
const target = tag.name === 'see' && tag.subject !== null ? tag.subject : null;
|
|
60
|
+
|
|
58
61
|
const link =
|
|
59
62
|
target === null ? null : /^https?:\/\//i.test(target) ? `[${target}](${target})` : `[\`${target}\`](${DOC_LINK_PREFIX}${encodeURIComponent(target)})`;
|
|
63
|
+
|
|
60
64
|
return (
|
|
61
65
|
<dd className={`col-start-2 m-0 flex min-w-0 flex-wrap items-baseline gap-x-2 ${spaced ? 'mt-1' : ''}`}>
|
|
62
66
|
{tag.name === 'example' ? (
|
|
@@ -102,15 +106,19 @@ function DocBlock({ language, comment, parsed, linkable }: DocBlockProps) {
|
|
|
102
106
|
// One handler for every link, since the links are made by the Markdown renderer and not by us.
|
|
103
107
|
const follow = (event: MouseEvent<HTMLDivElement>): void => {
|
|
104
108
|
const anchor = (event.target as HTMLElement).closest<HTMLAnchorElement>('a');
|
|
109
|
+
|
|
105
110
|
if (anchor === null) {
|
|
106
111
|
return;
|
|
107
112
|
}
|
|
113
|
+
|
|
108
114
|
event.preventDefault();
|
|
109
115
|
const mod = language.apple ? event.metaKey : event.ctrlKey;
|
|
110
116
|
const href = anchor.getAttribute('href') ?? '';
|
|
117
|
+
|
|
111
118
|
if (!mod) {
|
|
112
119
|
return;
|
|
113
120
|
}
|
|
121
|
+
|
|
114
122
|
if (href.startsWith(DOC_LINK_PREFIX)) {
|
|
115
123
|
feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), comment);
|
|
116
124
|
} else if (/^(https?:|mailto:)/i.test(href)) {
|
|
@@ -159,9 +167,12 @@ export const RenderedDocBlock = memo(DocBlock);
|
|
|
159
167
|
*/
|
|
160
168
|
export function RenderedDocs({ language }: { language: EditorLanguage }) {
|
|
161
169
|
const feature = language.renderedDocs;
|
|
170
|
+
|
|
162
171
|
const getEntries = () => feature.getEntries();
|
|
172
|
+
|
|
163
173
|
const entries = useSyncExternalStore((listener) => feature.subscribe(listener), getEntries, getEntries);
|
|
164
174
|
const linkable = feature.canFollow;
|
|
175
|
+
|
|
165
176
|
return (
|
|
166
177
|
<>
|
|
167
178
|
{entries.map((entry) =>
|
package/src/SplitDiffView.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { parseChord } from '@adecore/editor/keymap';
|
|
|
4
4
|
import type { EditorLineHighlight } from '@adecore/editor';
|
|
5
5
|
import type { DiffSplit } from '@adecore/merge';
|
|
6
6
|
import type { DiffViewProps } from './DiffView.tsx';
|
|
7
|
-
import type
|
|
7
|
+
import { repaintOnTokens, type DiffSourceSide } from './diff-syntax.ts';
|
|
8
8
|
import { DiffOverlays, type MountedDiff } from './diff-overlays.tsx';
|
|
9
9
|
import { wordMarks } from './diff-wordmarks.ts';
|
|
10
10
|
import { RowHost, LineActionHost } from './row-host.ts';
|
|
@@ -52,19 +52,25 @@ export function SplitDiffView({
|
|
|
52
52
|
const [failure, setFailure] = useState<{ error: unknown } | null>(null);
|
|
53
53
|
const [mounted, setMounted] = useState<MountedSplit | null>(null);
|
|
54
54
|
const [height, setHeight] = useState(0);
|
|
55
|
+
|
|
55
56
|
useLayoutEffect(() => {
|
|
56
57
|
current.current = { display, tokens, theme, wrap, label };
|
|
57
58
|
});
|
|
59
|
+
|
|
58
60
|
useLayoutEffect(() => {
|
|
59
61
|
const oldHost = oldElement.current;
|
|
60
62
|
const newHost = newElement.current;
|
|
63
|
+
|
|
61
64
|
if (oldHost === null || newHost === null) {
|
|
62
65
|
return;
|
|
63
66
|
}
|
|
67
|
+
|
|
64
68
|
const initial = current.current;
|
|
65
69
|
applied.current = { theme: initial.theme, wrap: initial.wrap, label: initial.label };
|
|
70
|
+
|
|
66
71
|
const mount = (side: DiffSourceSide, host: HTMLElement): MountedDiff => {
|
|
67
72
|
const initial = current.current;
|
|
73
|
+
|
|
68
74
|
const editor = engine.mount(host, {
|
|
69
75
|
text: initial.display[side].text,
|
|
70
76
|
theme: initial.theme,
|
|
@@ -76,21 +82,29 @@ export function SplitDiffView({
|
|
|
76
82
|
foldOutline: 'off',
|
|
77
83
|
handBack: [parseChord('Mod+Enter')]
|
|
78
84
|
});
|
|
85
|
+
|
|
79
86
|
editor.setBlocks([]);
|
|
87
|
+
|
|
80
88
|
editor.setLineTokens((line) => {
|
|
81
89
|
const row = current.current.display[side].rows[line];
|
|
90
|
+
|
|
82
91
|
return row === undefined ? null : current.current.tokens?.get(side, row[side]!.index);
|
|
83
92
|
});
|
|
93
|
+
|
|
84
94
|
return { engine, editor, rows: new RowHost(editor, 'diff-alignment'), actions: new LineActionHost(editor, 'diff-status') };
|
|
85
95
|
};
|
|
96
|
+
|
|
86
97
|
const old = mount('old', oldHost);
|
|
87
98
|
let fresh: MountedDiff;
|
|
99
|
+
|
|
88
100
|
try {
|
|
89
101
|
fresh = mount('new', newHost);
|
|
90
102
|
} catch (error) {
|
|
91
103
|
old.editor.dispose();
|
|
104
|
+
|
|
92
105
|
throw error;
|
|
93
106
|
}
|
|
107
|
+
|
|
94
108
|
const sides = { old, new: fresh };
|
|
95
109
|
let stopped = false;
|
|
96
110
|
let pending = false;
|
|
@@ -98,37 +112,48 @@ export function SplitDiffView({
|
|
|
98
112
|
const scroll = new SplitScroll({ old: old.editor, new: fresh.editor });
|
|
99
113
|
let aligning = false;
|
|
100
114
|
let updating = false;
|
|
115
|
+
|
|
101
116
|
const fail = (error: unknown): void => {
|
|
102
117
|
failed = true;
|
|
103
118
|
setFailure({ error });
|
|
104
119
|
};
|
|
120
|
+
|
|
105
121
|
const measure = (): void => setHeight(Math.max(sides.old.editor.getContentHeight(), sides.new.editor.getContentHeight()));
|
|
122
|
+
|
|
106
123
|
const align = (): void => {
|
|
107
124
|
if (pending || stopped || failed) {
|
|
108
125
|
return;
|
|
109
126
|
}
|
|
127
|
+
|
|
110
128
|
pending = true;
|
|
129
|
+
|
|
111
130
|
// Intrinsic measurements arrive after each render; align both editors before the next paint.
|
|
112
131
|
queueMicrotask(() => {
|
|
113
132
|
pending = false;
|
|
133
|
+
|
|
114
134
|
if (stopped || failed) {
|
|
115
135
|
return;
|
|
116
136
|
}
|
|
137
|
+
|
|
117
138
|
aligning = true;
|
|
139
|
+
|
|
118
140
|
try {
|
|
119
141
|
const rows = splitAlignment(
|
|
120
142
|
current.current.display.aligned,
|
|
121
143
|
(line) => sides.old.editor.getLineHeight(line),
|
|
122
144
|
(line) => sides.new.editor.getLineHeight(line)
|
|
123
145
|
);
|
|
146
|
+
|
|
124
147
|
sides.old.rows.set(rows.old);
|
|
125
148
|
sides.new.rows.set(rows.new);
|
|
126
149
|
} catch (error) {
|
|
127
150
|
fail(error);
|
|
151
|
+
|
|
128
152
|
return;
|
|
129
153
|
} finally {
|
|
130
154
|
aligning = false;
|
|
131
155
|
}
|
|
156
|
+
|
|
132
157
|
try {
|
|
133
158
|
measure();
|
|
134
159
|
scroll.sync();
|
|
@@ -137,21 +162,26 @@ export function SplitDiffView({
|
|
|
137
162
|
}
|
|
138
163
|
});
|
|
139
164
|
};
|
|
165
|
+
|
|
140
166
|
const update = (run: () => void): void => {
|
|
141
167
|
updating = true;
|
|
168
|
+
|
|
142
169
|
try {
|
|
143
170
|
run();
|
|
144
171
|
} finally {
|
|
145
172
|
updating = false;
|
|
146
173
|
}
|
|
174
|
+
|
|
147
175
|
align();
|
|
148
176
|
};
|
|
177
|
+
|
|
149
178
|
const stops = SIDES.flatMap((side) => [
|
|
150
179
|
sides[side].editor.onLineHeights(align),
|
|
151
180
|
sides[side].editor.onViewChange(() => {
|
|
152
181
|
if (stopped || failed || aligning || updating) {
|
|
153
182
|
return;
|
|
154
183
|
}
|
|
184
|
+
|
|
155
185
|
try {
|
|
156
186
|
measure();
|
|
157
187
|
scroll.changed(side);
|
|
@@ -160,35 +190,46 @@ export function SplitDiffView({
|
|
|
160
190
|
}
|
|
161
191
|
})
|
|
162
192
|
]);
|
|
193
|
+
|
|
163
194
|
setMounted({ old: sides.old, new: sides.new, align, update, drive: (side) => scroll.drive(side) });
|
|
164
195
|
measure();
|
|
196
|
+
|
|
165
197
|
return () => {
|
|
166
198
|
stopped = true;
|
|
199
|
+
|
|
167
200
|
for (const stop of stops) {
|
|
168
201
|
stop();
|
|
169
202
|
}
|
|
203
|
+
|
|
170
204
|
for (const side of SIDES) {
|
|
171
205
|
sides[side].editor.dispose();
|
|
172
206
|
}
|
|
173
207
|
};
|
|
174
208
|
}, [engine]);
|
|
209
|
+
|
|
175
210
|
useLayoutEffect(() => {
|
|
176
211
|
if (mounted === null || mounted.old.engine !== engine) {
|
|
177
212
|
return;
|
|
178
213
|
}
|
|
214
|
+
|
|
179
215
|
mounted.update(() => {
|
|
180
216
|
for (const side of SIDES) {
|
|
181
217
|
const { editor, actions } = mounted[side];
|
|
182
218
|
editor.setText(display[side].text);
|
|
219
|
+
|
|
183
220
|
editor.setLineNumbers((line) => {
|
|
184
221
|
const row = current.current.display[side].rows[line];
|
|
222
|
+
|
|
185
223
|
return row === undefined ? null : String(row[side]!.number);
|
|
186
224
|
});
|
|
225
|
+
|
|
187
226
|
const highlights: EditorLineHighlight[] = [];
|
|
227
|
+
|
|
188
228
|
for (const [index, row] of display[side].rows.entries()) {
|
|
189
229
|
if (row.change === 'context') {
|
|
190
230
|
continue;
|
|
191
231
|
}
|
|
232
|
+
|
|
192
233
|
highlights.push({
|
|
193
234
|
startLine: index + 1,
|
|
194
235
|
endLine: index + 1,
|
|
@@ -197,106 +238,102 @@ export function SplitDiffView({
|
|
|
197
238
|
emphasis: [emphasis[side].get(row[side]!.index) ?? []]
|
|
198
239
|
});
|
|
199
240
|
}
|
|
241
|
+
|
|
200
242
|
editor.setLineHighlights(highlights);
|
|
243
|
+
|
|
201
244
|
editor.setChangeMarks(
|
|
202
245
|
highlights.map((highlight) => ({ startLine: highlight.startLine, endLine: highlight.endLine, kind: side === 'old' ? 'deleted' : 'added' }))
|
|
203
246
|
);
|
|
247
|
+
|
|
204
248
|
actions.set(display[side].rows.flatMap((row, line) => (row.noNewline === undefined ? [] : [{ id: `no-newline-${line}`, line }])));
|
|
205
249
|
}
|
|
206
250
|
});
|
|
207
251
|
}, [mounted, engine, display, emphasis]);
|
|
252
|
+
|
|
208
253
|
useLayoutEffect(() => {
|
|
209
254
|
if (mounted === null || mounted.old.engine !== engine) {
|
|
210
255
|
return;
|
|
211
256
|
}
|
|
257
|
+
|
|
212
258
|
for (const side of SIDES) {
|
|
213
259
|
mounted[side].editor.setLineLabels((line) => {
|
|
214
260
|
const row = display[side].rows[line];
|
|
261
|
+
|
|
215
262
|
if (row === undefined || row.change === 'context') {
|
|
216
263
|
return null;
|
|
217
264
|
}
|
|
265
|
+
|
|
218
266
|
return t(side === 'old' ? 'diff.removed' : 'diff.added', { number: row[side]!.number });
|
|
219
267
|
});
|
|
220
268
|
}
|
|
221
269
|
}, [mounted, engine, display, t]);
|
|
270
|
+
|
|
222
271
|
useLayoutEffect(() => {
|
|
223
272
|
if (mounted === null || mounted.old.engine !== engine) {
|
|
224
273
|
return;
|
|
225
274
|
}
|
|
275
|
+
|
|
226
276
|
mounted.update(() => {
|
|
227
277
|
for (const side of SIDES) {
|
|
228
278
|
const previous = applied.current;
|
|
279
|
+
|
|
229
280
|
if (previous.theme !== theme) {
|
|
230
281
|
mounted[side].editor.setTheme(theme);
|
|
231
282
|
}
|
|
283
|
+
|
|
232
284
|
if (previous.wrap !== wrap) {
|
|
233
285
|
mounted[side].editor.setWrap(wrap);
|
|
234
286
|
}
|
|
287
|
+
|
|
235
288
|
if (previous.label !== label) {
|
|
236
289
|
mounted[side].editor.setLabel(label);
|
|
237
290
|
}
|
|
238
291
|
}
|
|
239
292
|
});
|
|
293
|
+
|
|
240
294
|
applied.current = { theme, wrap, label };
|
|
241
295
|
}, [mounted, engine, theme, wrap, label]);
|
|
296
|
+
|
|
242
297
|
useLayoutEffect(() => {
|
|
243
298
|
if (mounted === null || mounted.old.engine !== engine) {
|
|
244
299
|
return;
|
|
245
300
|
}
|
|
301
|
+
|
|
246
302
|
const refresh = (): void => {
|
|
247
303
|
for (const side of SIDES) {
|
|
248
304
|
mounted[side].editor.setLineTokens((line) => {
|
|
249
305
|
const row = current.current.display[side].rows[line];
|
|
306
|
+
|
|
250
307
|
return row === undefined ? null : current.current.tokens?.get(side, row[side]!.index);
|
|
251
308
|
});
|
|
252
309
|
}
|
|
253
310
|
};
|
|
311
|
+
|
|
254
312
|
refresh();
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
let pending = false;
|
|
258
|
-
let stopped = false;
|
|
259
|
-
const schedule = (): void => {
|
|
260
|
-
if (pending) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
pending = true;
|
|
264
|
-
const paint = (): void => {
|
|
265
|
-
pending = false;
|
|
266
|
-
if (!stopped) {
|
|
267
|
-
refresh();
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
if (ownerWindow?.requestAnimationFrame !== undefined) {
|
|
271
|
-
frame = ownerWindow.requestAnimationFrame(paint);
|
|
272
|
-
} else {
|
|
273
|
-
queueMicrotask(paint);
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
const stop = tokens?.subscribe(schedule);
|
|
277
|
-
return () => {
|
|
278
|
-
stopped = true;
|
|
279
|
-
stop?.();
|
|
280
|
-
if (frame !== undefined) {
|
|
281
|
-
ownerWindow?.cancelAnimationFrame?.(frame);
|
|
282
|
-
}
|
|
283
|
-
};
|
|
313
|
+
|
|
314
|
+
return repaintOnTokens(oldElement.current?.ownerDocument.defaultView, tokens, refresh);
|
|
284
315
|
}, [mounted, engine, display, tokens]);
|
|
316
|
+
|
|
285
317
|
useLayoutEffect(() => {
|
|
286
318
|
if (mounted === null || mounted.old.engine !== engine) {
|
|
287
319
|
return;
|
|
288
320
|
}
|
|
321
|
+
|
|
289
322
|
const stops = SIDES.map((side) => onMount?.(mounted[side].editor, side));
|
|
323
|
+
|
|
290
324
|
return () => {
|
|
291
325
|
for (const stop of stops) {
|
|
292
326
|
stop?.();
|
|
293
327
|
}
|
|
294
328
|
};
|
|
295
329
|
}, [mounted, engine, onMount]);
|
|
330
|
+
|
|
296
331
|
const limit = maxHeight !== undefined && Number.isFinite(maxHeight) && maxHeight > 0 ? maxHeight : Infinity;
|
|
332
|
+
|
|
297
333
|
if (failure !== null) {
|
|
298
334
|
throw failure.error;
|
|
299
335
|
}
|
|
336
|
+
|
|
300
337
|
return (
|
|
301
338
|
<div
|
|
302
339
|
className={`adecore-editor-diff adecore-editor-diff-split ${className ?? ''}`}
|
|
@@ -306,32 +343,30 @@ export function SplitDiffView({
|
|
|
306
343
|
data-scroll={fill || height > limit}
|
|
307
344
|
style={{ height: fill ? '100%' : Math.min(height, limit) }}
|
|
308
345
|
>
|
|
309
|
-
{SIDES.map((side) =>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
</div>
|
|
334
|
-
))}
|
|
346
|
+
{SIDES.map((side) => {
|
|
347
|
+
const drive = (): void => {
|
|
348
|
+
mounted?.drive(side);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div
|
|
353
|
+
key={side}
|
|
354
|
+
className="adecore-editor-diff-side"
|
|
355
|
+
data-side={side}
|
|
356
|
+
role="group"
|
|
357
|
+
aria-label={t(`diff.${side}Side`)}
|
|
358
|
+
onWheelCapture={drive}
|
|
359
|
+
onFocusCapture={drive}
|
|
360
|
+
onPointerDownCapture={drive}
|
|
361
|
+
onKeyDownCapture={drive}
|
|
362
|
+
>
|
|
363
|
+
<div ref={side === 'old' ? oldElement : newElement} className="adecore-editor-diff-surface" />
|
|
364
|
+
{mounted !== null && mounted[side].engine === engine && (
|
|
365
|
+
<DiffOverlays mounted={mounted[side]} display={display[side]} unfold={unfold} blocked={blocked} />
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
})}
|
|
335
370
|
</div>
|
|
336
371
|
);
|
|
337
372
|
}
|