@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.
- 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/src/FindReplace.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { Button } from '@adecore/ui';
|
|
4
4
|
import { formatNumber } from '@adecore/ui/format';
|
|
5
|
-
import type
|
|
5
|
+
import { MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS, type Editor, type EditorFindState } from '@adecore/editor';
|
|
6
6
|
import { compileFind, EMPTY_FIND_QUERY, type FindQuery } from './find-query.ts';
|
|
7
7
|
|
|
8
8
|
export function FindReplace({ editor, className, onClose }: { editor: Editor; className?: string; onClose?(): void }) {
|
|
@@ -11,6 +11,7 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
|
|
|
11
11
|
const [replacement, setReplacement] = useState('');
|
|
12
12
|
const [preserveCase, setPreserveCase] = useState(false);
|
|
13
13
|
const [state, setState] = useState<EditorFindState>({ count: 0, current: null });
|
|
14
|
+
const root = useRef<HTMLDivElement>(null);
|
|
14
15
|
const compiled = useMemo(() => compileFind(query), [query]);
|
|
15
16
|
useEffect(() => editor.onFind(setState), [editor]);
|
|
16
17
|
useEffect(() => {
|
|
@@ -26,14 +27,56 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
|
|
|
26
27
|
},
|
|
27
28
|
[editor]
|
|
28
29
|
);
|
|
30
|
+
// The host closes the widget by unmounting it, with the keyboard in it or not; only a keyboard in it is handed back.
|
|
31
|
+
useLayoutEffect(
|
|
32
|
+
() => () => {
|
|
33
|
+
if (root.current?.contains(root.current.ownerDocument.activeElement) === true) {
|
|
34
|
+
editor.focus();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[editor]
|
|
38
|
+
);
|
|
39
|
+
const failure = state.failed ?? state.captureFailed;
|
|
29
40
|
const label =
|
|
30
41
|
compiled.kind === 'invalid'
|
|
31
42
|
? t('find.invalid')
|
|
32
|
-
: state.
|
|
33
|
-
? t(
|
|
34
|
-
|
|
43
|
+
: state.replaceFailed !== undefined
|
|
44
|
+
? t(`find.replace.failed.${state.replaceFailed}`, {
|
|
45
|
+
edits: formatNumber(MAX_REPLACEMENT_PAGE),
|
|
46
|
+
characters: formatNumber(MAX_REPLACEMENT_CHARACTERS)
|
|
47
|
+
})
|
|
48
|
+
: failure !== undefined
|
|
49
|
+
? t(`find.failed.${failure}`)
|
|
50
|
+
: state.searching
|
|
51
|
+
? t('find.searching')
|
|
52
|
+
: state.large
|
|
53
|
+
? t('find.large')
|
|
54
|
+
: state.noSelection
|
|
55
|
+
? t('find.noSelection')
|
|
56
|
+
: state.replaced !== undefined
|
|
57
|
+
? t('find.replaced', { count: formatNumber(state.replaced) })
|
|
58
|
+
: state.limited && state.current === null
|
|
59
|
+
? t('find.countMore', { total: formatNumber(state.count) })
|
|
60
|
+
: t(state.limited ? 'find.countLimited' : 'find.count', {
|
|
61
|
+
total: formatNumber(state.count),
|
|
62
|
+
current: state.current === null ? '0' : formatNumber(state.current + 1)
|
|
63
|
+
});
|
|
35
64
|
return (
|
|
36
|
-
<div
|
|
65
|
+
<div
|
|
66
|
+
ref={root}
|
|
67
|
+
className={`adecore-editor-find ${className ?? ''}`}
|
|
68
|
+
role="search"
|
|
69
|
+
aria-label={t('find.label')}
|
|
70
|
+
onKeyDown={(event) => {
|
|
71
|
+
if (event.key === 'Escape' && !event.nativeEvent.isComposing) {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
editor.endFind();
|
|
74
|
+
editor.setReplacePreview(null);
|
|
75
|
+
onClose?.();
|
|
76
|
+
editor.focus();
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
37
80
|
<div className="adecore-editor-find-row">
|
|
38
81
|
<input
|
|
39
82
|
type="text"
|
|
@@ -48,11 +91,6 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
|
|
|
48
91
|
event.preventDefault();
|
|
49
92
|
editor.findStep(event.shiftKey ? -1 : 1);
|
|
50
93
|
}
|
|
51
|
-
if (event.key === 'Escape') {
|
|
52
|
-
event.preventDefault();
|
|
53
|
-
onClose?.();
|
|
54
|
-
editor.focus();
|
|
55
|
-
}
|
|
56
94
|
}}
|
|
57
95
|
/>
|
|
58
96
|
<output aria-live="polite">{label}</output>
|
|
@@ -77,10 +115,10 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
|
|
|
77
115
|
<input type="checkbox" checked={preserveCase} onChange={(event) => setPreserveCase(event.target.checked)} />
|
|
78
116
|
{t('find.replace.preserveCase')}
|
|
79
117
|
</label>
|
|
80
|
-
<Button size="xs" onClick={() => editor.replace(replacement, { preserveCase })}>
|
|
118
|
+
<Button size="xs" disabled={state.searching === true} onClick={() => editor.replace(replacement, { preserveCase })}>
|
|
81
119
|
{t('find.replace.one')}
|
|
82
120
|
</Button>
|
|
83
|
-
<Button size="xs" onClick={() => editor.
|
|
121
|
+
<Button size="xs" disabled={state.searching === true} onClick={() => void editor.replaceAllAsync(replacement, { preserveCase })}>
|
|
84
122
|
{t('find.replace.allLabel')}
|
|
85
123
|
</Button>
|
|
86
124
|
</div>
|
package/src/HoverCard.tsx
CHANGED
|
@@ -108,28 +108,30 @@ export function HoverCard({
|
|
|
108
108
|
void language.navigation.goToName(name, anchor, name === (declared ?? info.word) ? info.definition : null);
|
|
109
109
|
}}
|
|
110
110
|
/>
|
|
111
|
-
|
|
112
|
-
<div className="flex
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
{info.plain !== true && (
|
|
112
|
+
<div className="flex flex-col gap-0.5 px-3 py-1.5 text-xs">
|
|
113
|
+
<div className="flex items-center gap-3 whitespace-nowrap">
|
|
114
|
+
{info.definition !== null && (
|
|
115
|
+
<button type="button" className="text-accent hover:underline" onClick={() => language.goTo(info.definition!)}>
|
|
116
|
+
{t('language.hover.definition')}
|
|
117
|
+
</button>
|
|
118
|
+
)}
|
|
119
|
+
{info.references !== null && info.references > 0 && (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
className="text-accent hover:underline"
|
|
123
|
+
onClick={() => {
|
|
124
|
+
language.hover.hide();
|
|
125
|
+
void language.peek.open(position);
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{t('language.hover.references', { count: info.references, formatted: formatNumber(info.references) })}
|
|
129
|
+
</button>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
{info.definition !== null && <DefinitionPlace definition={info.definition} uri={language.uri} folder={language.project.folder} />}
|
|
130
133
|
</div>
|
|
131
|
-
|
|
132
|
-
</div>
|
|
134
|
+
)}
|
|
133
135
|
</>
|
|
134
136
|
)}
|
|
135
137
|
{problems.map((problem, index) => (
|
package/src/LanguagePopups.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useEffect, useReducer, type ReactNode } from 'react';
|
|
|
2
2
|
import { useStore } from 'zustand';
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { AnchoredPopup } from './AnchoredPopup.tsx';
|
|
5
|
+
import { CallHierarchyPanel } from './CallHierarchyPanel.tsx';
|
|
5
6
|
import { CodeAuthorsCard } from './CodeAuthorsCard.tsx';
|
|
6
7
|
import { CompletionPopup } from './CompletionPopup.tsx';
|
|
7
8
|
import { EditorContextMenu } from './EditorContextMenu.tsx';
|
|
@@ -9,6 +10,7 @@ import { HoverCard } from './HoverCard.tsx';
|
|
|
9
10
|
import { PeekPanel } from './PeekPanel.tsx';
|
|
10
11
|
import { PickPopup } from './PickPopup.tsx';
|
|
11
12
|
import { RenameCard } from './RenameCard.tsx';
|
|
13
|
+
import { RenderedDocs } from './RenderedDocs.tsx';
|
|
12
14
|
import { SymbolPicker } from './SymbolPicker.tsx';
|
|
13
15
|
import { SignatureCard } from './SignatureCard.tsx';
|
|
14
16
|
|
|
@@ -25,6 +27,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
|
|
|
25
27
|
const pick = useStore(language.popups, (state) => state.pick);
|
|
26
28
|
const rename = useStore(language.popups, (state) => state.rename);
|
|
27
29
|
const peek = useStore(language.popups, (state) => state.peek);
|
|
30
|
+
const callHierarchy = useStore(language.popups, (state) => state.callHierarchy ?? null);
|
|
28
31
|
const symbols = useStore(language.popups, (state) => state.symbols);
|
|
29
32
|
const menu = useStore(language.popups, (state) => state.menu);
|
|
30
33
|
const authors = useStore(language.popups, (state) => state.authors);
|
|
@@ -39,6 +42,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
|
|
|
39
42
|
const renameEnd = rename === null ? null : language.editor.rectAt(rename.range.end);
|
|
40
43
|
return (
|
|
41
44
|
<>
|
|
45
|
+
<RenderedDocs language={language} />
|
|
42
46
|
{hover !== null && hoverRect !== null && (
|
|
43
47
|
<AnchoredPopup rect={hoverRect} onPointerEnter={() => language.hover.holdCard(true)} onPointerLeave={() => language.hover.holdCard(false)}>
|
|
44
48
|
<HoverCard language={language} problems={hover.problems} info={hover.info} anchor={hover.anchor} position={hover.position} />
|
|
@@ -58,6 +62,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
|
|
|
58
62
|
)}
|
|
59
63
|
{symbols !== null && <SymbolPicker language={language} view={symbols} />}
|
|
60
64
|
{peek !== null && <PeekPanel language={language} view={peek} />}
|
|
65
|
+
{callHierarchy !== null && <CallHierarchyPanel language={language} view={callHierarchy} />}
|
|
61
66
|
{authors !== null && <CodeAuthorsCard language={language} view={authors} />}
|
|
62
67
|
{rename !== null && renameRect !== null && <RenameCard language={language} view={rename} rect={renameRect} endRect={renameEnd} />}
|
|
63
68
|
</>
|
package/src/Markdown.tsx
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import ReactMarkdown from 'react-markdown';
|
|
1
|
+
import ReactMarkdown, { type Components } from 'react-markdown';
|
|
2
2
|
import remarkGfm from 'remark-gfm';
|
|
3
3
|
import remarkBreaks from 'remark-breaks';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/* An image becomes its alt text, so a comment cannot make the editor fetch a page of its choosing. */
|
|
6
|
+
const WITHOUT_IMAGES: Components = {
|
|
7
|
+
img: ({ alt }) => <>{alt ?? ''}</>
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function Markdown({ text, images = true }: { text: string; images?: boolean }) {
|
|
6
11
|
return (
|
|
7
12
|
<div className="adecore-editor-markdown">
|
|
8
|
-
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}
|
|
13
|
+
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} components={images ? undefined : WITHOUT_IMAGES}>
|
|
14
|
+
{text}
|
|
15
|
+
</ReactMarkdown>
|
|
9
16
|
</div>
|
|
10
17
|
);
|
|
11
18
|
}
|
package/src/PeekPanel.tsx
CHANGED
|
@@ -19,10 +19,9 @@ const CODE =
|
|
|
19
19
|
'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';
|
|
20
20
|
|
|
21
21
|
/* The code around the active reference, drawn like the editor's own rows: its gutter, the line the reference is on and the name marked. */
|
|
22
|
-
function Preview({
|
|
22
|
+
export function Preview({ preview }: { preview: PeekView['preview'] }) {
|
|
23
23
|
const { theme, highlight } = useEditorRendering();
|
|
24
24
|
const scroller = useRef<HTMLDivElement>(null);
|
|
25
|
-
const { preview } = view;
|
|
26
25
|
const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
|
|
27
26
|
const path = preview === null ? null : fileUriToPath(preview.uri);
|
|
28
27
|
const key = preview === null ? '' : `${theme}\0${path}\0${preview.text}`;
|
|
@@ -204,7 +203,7 @@ export function PeekPanel({ language, view }: { language: EditorLanguage; view:
|
|
|
204
203
|
</div>
|
|
205
204
|
</div>
|
|
206
205
|
<div className="flex h-[180px] shrink-0">
|
|
207
|
-
<Preview
|
|
206
|
+
<Preview preview={view.preview} />
|
|
208
207
|
<Places language={language} view={view} />
|
|
209
208
|
</div>
|
|
210
209
|
<div className="peek-hatch h-[14px]" />
|
package/src/PickPopup.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
1
|
+
import { useId, useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import type { EditorRect } from '@adecore/editor';
|
|
4
4
|
import type { EditorLanguage } from './editor-language.ts';
|
|
@@ -39,6 +39,7 @@ function PreviewLines({ preview }: { preview: PickPreview }) {
|
|
|
39
39
|
* the focus from the editor, so the keys keep going to it while the list is open.
|
|
40
40
|
*/
|
|
41
41
|
export function PickPopup({ language, view, rect }: { language: EditorLanguage; view: PickView; rect: EditorRect }) {
|
|
42
|
+
const listId = useId();
|
|
42
43
|
const card = useRef<HTMLDivElement>(null);
|
|
43
44
|
const activeRow = useRef<HTMLButtonElement>(null);
|
|
44
45
|
|
|
@@ -64,16 +65,31 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
|
|
|
64
65
|
activeRow.current?.scrollIntoView({ block: 'nearest' });
|
|
65
66
|
}, [view.active]);
|
|
66
67
|
|
|
68
|
+
const optionIds = new Map<string, string>();
|
|
69
|
+
for (const group of view.groups) {
|
|
70
|
+
for (const row of group.rows) {
|
|
71
|
+
optionIds.set(row.id, `${listId}-option-${optionIds.size}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const activeId = optionIds.get(view.active);
|
|
75
|
+
|
|
76
|
+
// The focus stays in the text, so the input is what names the list and its active option.
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
language.editor.setInputPopup({ controls: listId, activeDescendant: activeId });
|
|
79
|
+
return () => language.editor.setInputPopup(null);
|
|
80
|
+
}, [language, listId, activeId]);
|
|
81
|
+
|
|
67
82
|
return createPortal(
|
|
68
83
|
<div ref={card} className={CARD} style={{ visibility: 'hidden' }} onPointerDown={(event) => event.preventDefault()}>
|
|
69
84
|
{view.title !== null && <div className="border-b border-border px-3 py-1.5 text-xs text-text-muted">{view.title}</div>}
|
|
70
|
-
<div role="listbox" className="min-h-0 flex-1 overflow-y-auto p-1">
|
|
85
|
+
<div id={listId} role="listbox" className="min-h-0 flex-1 overflow-y-auto p-1">
|
|
71
86
|
{view.groups.map((group, groupIndex) => (
|
|
72
87
|
<div key={group.title ?? groupIndex} role="group" className="flex flex-col">
|
|
73
88
|
{group.title !== null && <div className="px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint">{group.title}</div>}
|
|
74
89
|
{group.rows.map((row) => (
|
|
75
90
|
<button
|
|
76
91
|
key={row.id}
|
|
92
|
+
id={optionIds.get(row.id)}
|
|
77
93
|
ref={row.id === view.active ? activeRow : undefined}
|
|
78
94
|
type="button"
|
|
79
95
|
role="option"
|
package/src/RenameCard.tsx
CHANGED
|
@@ -86,6 +86,11 @@ function Hints({ view, active, onPick }: { view: RenameView; active: number; onP
|
|
|
86
86
|
function Preview({ language, view }: { language: EditorLanguage; view: RenameView }) {
|
|
87
87
|
const { t } = useTranslation('editor');
|
|
88
88
|
const folder = language.project.folder;
|
|
89
|
+
const operations = view.operations ?? [];
|
|
90
|
+
const shownPath = (uri: string): string => {
|
|
91
|
+
const path = fileUriToPath(uri);
|
|
92
|
+
return path === null ? uri : relativeTo(path, folder);
|
|
93
|
+
};
|
|
89
94
|
return (
|
|
90
95
|
<div className="flex w-[520px] max-w-full flex-col" onPointerDown={(event) => event.preventDefault()}>
|
|
91
96
|
<div className="max-h-[320px] overflow-y-auto">
|
|
@@ -115,6 +120,19 @@ function Preview({ language, view }: { language: EditorLanguage; view: RenameVie
|
|
|
115
120
|
</div>
|
|
116
121
|
);
|
|
117
122
|
})}
|
|
123
|
+
{operations.length > 0 && (
|
|
124
|
+
<div className="flex flex-col gap-px border-b border-border px-3 py-1.5 text-xs last:border-b-0">
|
|
125
|
+
{operations.map((operation, index) => (
|
|
126
|
+
<div key={`${operation.kind}:${operation.uri}:${index}`} className="flex items-center gap-2">
|
|
127
|
+
<span className="shrink-0 font-medium">{t(`language.rename.operations.${operation.kind}`)}</span>
|
|
128
|
+
<span className="min-w-0 truncate text-text-faint">
|
|
129
|
+
{shownPath(operation.uri)}
|
|
130
|
+
{operation.newUri === undefined ? '' : ` \u2192 ${shownPath(operation.newUri)}`}
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
118
136
|
</div>
|
|
119
137
|
<div className="flex items-center justify-end gap-2 border-t border-border p-2">
|
|
120
138
|
<Button size="xs" variant="ghost" onClick={() => language.rename.cancel()}>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createPortal } from 'react-dom';
|
|
2
|
+
import { useSyncExternalStore, type MouseEvent } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
5
|
+
import { DOC_LINK_PREFIX, sectionsOf, type DocTagEntry } from './doc-comment-model.ts';
|
|
6
|
+
import { markdownParts } from './hover-content.ts';
|
|
7
|
+
import { Signature } from './HoverSections.tsx';
|
|
8
|
+
import { shikiLanguageOf } from './language-ids.ts';
|
|
9
|
+
import { Markdown } from './Markdown.tsx';
|
|
10
|
+
import type { RenderedDocEntry } from './rendered-docs.ts';
|
|
11
|
+
|
|
12
|
+
const LINK = /\[([^\]]*)\]\(#doc-link:[^)]*\)/g;
|
|
13
|
+
const KNOWN = new Set([
|
|
14
|
+
'deprecated',
|
|
15
|
+
'template',
|
|
16
|
+
'param',
|
|
17
|
+
'property',
|
|
18
|
+
'type',
|
|
19
|
+
'var',
|
|
20
|
+
'return',
|
|
21
|
+
'yield',
|
|
22
|
+
'throws',
|
|
23
|
+
'see',
|
|
24
|
+
'example',
|
|
25
|
+
'since',
|
|
26
|
+
'version',
|
|
27
|
+
'author'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/* Code in the editor's face, with a type fainter than a name. */
|
|
31
|
+
function Code({ children, faint = false }: { children: string; faint?: boolean }) {
|
|
32
|
+
return <code className={`font-mono text-code break-words ${faint ? 'text-text-faint' : 'text-text'}`}>{children}</code>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Markdown prose and fenced code as the hover draws them: the blocks in the code face, the rest muted. */
|
|
36
|
+
function Prose({ text, language, linkable, compact = false }: { text: string; language: string; linkable: boolean; compact?: boolean }) {
|
|
37
|
+
const plain = linkable ? text : text.replace(LINK, '$1');
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
{markdownParts(plain).map((part, index) =>
|
|
41
|
+
part.kind === 'code' ? (
|
|
42
|
+
<div key={index} className="rounded-md bg-surface-sunken px-2 py-1.5">
|
|
43
|
+
<Signature code={part.block.code} language={part.block.language === 'text' ? language : part.block.language} />
|
|
44
|
+
</div>
|
|
45
|
+
) : (
|
|
46
|
+
<div key={index} className={`[&_.adecore-editor-markdown]:text-xs ${compact ? '[&_p]:my-0' : '[&_p]:my-1'}`}>
|
|
47
|
+
<Markdown text={part.text} images={false} />
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
)}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* A value of a tag, in the second column of the table; `spaced` opens a new section with more room above. */
|
|
56
|
+
function TagRow({ tag, language, linkable, spaced }: { tag: DocTagEntry; language: string; linkable: boolean; spaced: boolean }) {
|
|
57
|
+
const target = tag.name === 'see' && tag.subject !== null ? tag.subject : null;
|
|
58
|
+
const link =
|
|
59
|
+
target === null ? null : /^https?:\/\//i.test(target) ? `[${target}](${target})` : `[\`${target}\`](${DOC_LINK_PREFIX}${encodeURIComponent(target)})`;
|
|
60
|
+
return (
|
|
61
|
+
<dd className={`col-start-2 m-0 flex min-w-0 flex-wrap items-baseline gap-x-2 ${spaced ? 'mt-1' : ''}`}>
|
|
62
|
+
{tag.name === 'example' ? (
|
|
63
|
+
<div className="w-full rounded-md bg-surface-sunken px-2 py-1.5">
|
|
64
|
+
<Signature code={tag.text} language={language} />
|
|
65
|
+
</div>
|
|
66
|
+
) : (
|
|
67
|
+
<>
|
|
68
|
+
{tag.subject !== null && target === null && <Code>{tag.subject}</Code>}
|
|
69
|
+
{tag.type !== null && <Code faint>{tag.type}</Code>}
|
|
70
|
+
{(link !== null || tag.text !== '') && (
|
|
71
|
+
<div className="min-w-0 grow basis-48 text-text">
|
|
72
|
+
<Prose
|
|
73
|
+
text={[link, tag.text].filter((part) => part !== null && part !== '').join(' ')}
|
|
74
|
+
language={language}
|
|
75
|
+
linkable={linkable}
|
|
76
|
+
compact
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</>
|
|
81
|
+
)}
|
|
82
|
+
</dd>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function RenderedDocBlock({ language, entry }: { language: EditorLanguage; entry: RenderedDocEntry }) {
|
|
87
|
+
const { t } = useTranslation('editor');
|
|
88
|
+
const feature = language.renderedDocs;
|
|
89
|
+
const { description, tags } = entry.parsed;
|
|
90
|
+
const highlighter = shikiLanguageOf(language.languageId);
|
|
91
|
+
const linkable = feature.canFollow;
|
|
92
|
+
const sections = sectionsOf(tags);
|
|
93
|
+
const deprecated = sections.find((section) => section.name === 'deprecated');
|
|
94
|
+
const shown = sections.filter((section) => section.name !== 'deprecated');
|
|
95
|
+
|
|
96
|
+
// One handler for every link, since the links are made by the Markdown renderer and not by us.
|
|
97
|
+
const follow = (event: MouseEvent<HTMLDivElement>): void => {
|
|
98
|
+
const anchor = (event.target as HTMLElement).closest<HTMLAnchorElement>('a');
|
|
99
|
+
if (anchor === null) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
const mod = language.apple ? event.metaKey : event.ctrlKey;
|
|
104
|
+
const href = anchor.getAttribute('href') ?? '';
|
|
105
|
+
if (!mod) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (href.startsWith(DOC_LINK_PREFIX)) {
|
|
109
|
+
feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), entry.comment);
|
|
110
|
+
} else if (/^(https?:|mailto:)/i.test(href)) {
|
|
111
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<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}>
|
|
117
|
+
{deprecated !== undefined && (
|
|
118
|
+
<div className="border-l-2 border-status-error pl-2 text-text-muted">
|
|
119
|
+
<span className="font-medium text-status-error">{t('language.docs.deprecated')}</span>
|
|
120
|
+
{deprecated.tags.some((tag) => tag.text !== '') && (
|
|
121
|
+
<Prose text={deprecated.tags.map((tag) => tag.text).join(' ')} language={highlighter} linkable={linkable} />
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
{description !== '' && (
|
|
126
|
+
<div className="text-text">
|
|
127
|
+
<Prose text={description} language={highlighter} linkable={linkable} />
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
{shown.length > 0 && (
|
|
131
|
+
<dl className="m-0 grid grid-cols-[max-content_minmax(0,1fr)] items-baseline gap-x-3 gap-y-0.5">
|
|
132
|
+
{shown.map((section, row) => [
|
|
133
|
+
<dt key={`${section.name}:label`} className={`col-start-1 font-medium text-text-muted ${row > 0 ? 'mt-1' : ''}`}>
|
|
134
|
+
{KNOWN.has(section.name) ? t(`language.docs.tags.${section.name}`) : `${section.name}:`}
|
|
135
|
+
</dt>,
|
|
136
|
+
...section.tags.map((tag, index) => (
|
|
137
|
+
<TagRow key={`${section.name}:${index}`} tag={tag} language={highlighter} linkable={linkable} spaced={row > 0 && index === 0} />
|
|
138
|
+
))
|
|
139
|
+
])}
|
|
140
|
+
</dl>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* The documentation comments the editor drew as rows, filled with their formatted text. It portals into
|
|
148
|
+
* the element the editor made for each row, so the editor decides what is in view; a comment is parsed
|
|
149
|
+
* only once its row is. Links in the text are followed with Cmd (Ctrl elsewhere) held, as definitions are.
|
|
150
|
+
*/
|
|
151
|
+
export function RenderedDocs({ language }: { language: EditorLanguage }) {
|
|
152
|
+
const feature = language.renderedDocs;
|
|
153
|
+
useSyncExternalStore(feature.subscribe, feature.getVersion, feature.getVersion);
|
|
154
|
+
return <>{feature.entries().map((entry) => createPortal(<RenderedDocBlock language={language} entry={entry} />, entry.container, entry.id))}</>;
|
|
155
|
+
}
|