@adecore/editor-react 0.0.1 → 0.17.0-beta.2
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/LICENSE +110 -0
- package/README.md +60 -0
- package/dist/AnchoredPopup.d.ts +12 -0
- package/dist/AnchoredPopup.js +27 -0
- package/dist/AttributionCard.d.ts +14 -0
- package/dist/AttributionCard.js +5 -0
- package/dist/BaselineStatus.d.ts +4 -0
- package/dist/BaselineStatus.js +13 -0
- package/dist/ChangeReview.d.ts +12 -0
- package/dist/ChangeReview.js +27 -0
- package/dist/CodeAuthorsCard.d.ts +6 -0
- package/dist/CodeAuthorsCard.js +22 -0
- package/dist/CompletionPopup.d.ts +8 -0
- package/dist/CompletionPopup.js +103 -0
- package/dist/EditorContextMenu.d.ts +8 -0
- package/dist/EditorContextMenu.js +23 -0
- package/dist/EditorView.d.ts +16 -0
- package/dist/EditorView.js +27 -0
- package/dist/FindReplace.d.ts +6 -0
- package/dist/FindReplace.js +44 -0
- package/dist/HoverCard.d.ts +11 -0
- package/dist/HoverCard.js +38 -0
- package/dist/HoverSections.d.ts +21 -0
- package/dist/HoverSections.js +82 -0
- package/dist/LanguagePopups.d.ts +6 -0
- package/dist/LanguagePopups.js +38 -0
- package/dist/Markdown.d.ts +3 -0
- package/dist/Markdown.js +7 -0
- package/dist/PathText.d.ts +4 -0
- package/dist/PathText.js +12 -0
- package/dist/PeekPanel.d.ts +6 -0
- package/dist/PeekPanel.js +111 -0
- package/dist/PickPopup.d.ts +8 -0
- package/dist/PickPopup.js +33 -0
- package/dist/RenameCard.d.ts +9 -0
- package/dist/RenameCard.js +108 -0
- package/dist/SignatureCard.d.ts +4 -0
- package/dist/SignatureCard.js +8 -0
- package/dist/SymbolPicker.d.ts +6 -0
- package/dist/SymbolPicker.js +108 -0
- package/dist/code-actions-model.d.ts +23 -0
- package/dist/code-actions-model.js +114 -0
- package/dist/code-actions.d.ts +30 -0
- package/dist/code-actions.js +326 -0
- package/dist/code-authors.d.ts +18 -0
- package/dist/code-authors.js +79 -0
- package/dist/code-vision-model.d.ts +21 -0
- package/dist/code-vision-model.js +183 -0
- package/dist/code-vision.d.ts +48 -0
- package/dist/code-vision.js +236 -0
- package/dist/completion-call.d.ts +24 -0
- package/dist/completion-call.js +110 -0
- package/dist/completion-model.d.ts +51 -0
- package/dist/completion-model.js +353 -0
- package/dist/completion.d.ts +43 -0
- package/dist/completion.js +463 -0
- package/dist/conflict-model.d.ts +15 -0
- package/dist/conflict-model.js +36 -0
- package/dist/context-menu.d.ts +10 -0
- package/dist/context-menu.js +55 -0
- package/dist/definition-link.d.ts +14 -0
- package/dist/definition-link.js +99 -0
- package/dist/diagnostics-model.d.ts +15 -0
- package/dist/diagnostics-model.js +83 -0
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +95 -0
- package/dist/editor-language.d.ts +65 -0
- package/dist/editor-language.js +170 -0
- package/dist/editor-react.css +43 -0
- package/dist/find-query.d.ts +26 -0
- package/dist/find-query.js +51 -0
- package/dist/folding.d.ts +16 -0
- package/dist/folding.js +95 -0
- package/dist/highlight-layers.d.ts +10 -0
- package/dist/highlight-layers.js +30 -0
- package/dist/highlights.d.ts +16 -0
- package/dist/highlights.js +102 -0
- package/dist/history.d.ts +8 -0
- package/dist/history.js +46 -0
- package/dist/holder.d.ts +5 -0
- package/dist/holder.js +19 -0
- package/dist/host-types.d.ts +45 -0
- package/dist/host-types.js +1 -0
- package/dist/hover-content.d.ts +45 -0
- package/dist/hover-content.js +198 -0
- package/dist/hover.d.ts +32 -0
- package/dist/hover.js +260 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +28 -0
- package/dist/inlay-hints.d.ts +13 -0
- package/dist/inlay-hints.js +71 -0
- package/dist/language-ids.d.ts +3 -0
- package/dist/language-ids.js +49 -0
- package/dist/line-edits.d.ts +8 -0
- package/dist/line-edits.js +39 -0
- package/dist/locales/en.json +171 -0
- package/dist/locales/nl.json +171 -0
- package/dist/models.d.ts +20 -0
- package/dist/models.js +20 -0
- package/dist/navigation-history.d.ts +16 -0
- package/dist/navigation-history.js +53 -0
- package/dist/navigation.d.ts +23 -0
- package/dist/navigation.js +155 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +3 -0
- package/dist/peek-model.d.ts +28 -0
- package/dist/peek-model.js +104 -0
- package/dist/peek.d.ts +30 -0
- package/dist/peek.js +215 -0
- package/dist/pick.d.ts +34 -0
- package/dist/pick.js +166 -0
- package/dist/popup-placement.d.ts +23 -0
- package/dist/popup-placement.js +34 -0
- package/dist/popups.d.ts +133 -0
- package/dist/popups.js +14 -0
- package/dist/project-language.d.ts +36 -0
- package/dist/project-language.js +190 -0
- package/dist/project-problems.d.ts +24 -0
- package/dist/project-problems.js +63 -0
- package/dist/proposal-model.d.ts +36 -0
- package/dist/proposal-model.js +197 -0
- package/dist/recent-choices.d.ts +10 -0
- package/dist/recent-choices.js +39 -0
- package/dist/refresher.d.ts +13 -0
- package/dist/refresher.js +50 -0
- package/dist/relative-time.d.ts +1 -0
- package/dist/relative-time.js +9 -0
- package/dist/rename-model.d.ts +25 -0
- package/dist/rename-model.js +93 -0
- package/dist/rename.d.ts +21 -0
- package/dist/rename.js +257 -0
- package/dist/rendering-context.d.ts +6 -0
- package/dist/rendering-context.js +5 -0
- package/dist/rendering.d.ts +6 -0
- package/dist/rendering.js +5 -0
- package/dist/review-group.d.ts +16 -0
- package/dist/review-group.js +54 -0
- package/dist/row-host.d.ts +42 -0
- package/dist/row-host.js +120 -0
- package/dist/selection-ranges.d.ts +7 -0
- package/dist/selection-ranges.js +26 -0
- package/dist/semantic-model.d.ts +4 -0
- package/dist/semantic-model.js +76 -0
- package/dist/semantic-tokens.d.ts +6 -0
- package/dist/semantic-tokens.js +38 -0
- package/dist/shortcut-keys.d.ts +91 -0
- package/dist/shortcut-keys.js +12 -0
- package/dist/signature-model.d.ts +18 -0
- package/dist/signature-model.js +33 -0
- package/dist/signature.d.ts +22 -0
- package/dist/signature.js +150 -0
- package/dist/snippet-session.d.ts +17 -0
- package/dist/snippet-session.js +110 -0
- package/dist/snippet.d.ts +11 -0
- package/dist/snippet.js +134 -0
- package/dist/symbol-links.d.ts +6 -0
- package/dist/symbol-links.js +50 -0
- package/dist/symbol-picker-model.d.ts +44 -0
- package/dist/symbol-picker-model.js +146 -0
- package/dist/symbol-picker.d.ts +12 -0
- package/dist/symbol-picker.js +76 -0
- package/dist/symbols.d.ts +11 -0
- package/dist/symbols.js +95 -0
- package/dist/testing.d.ts +57 -0
- package/dist/testing.js +156 -0
- package/dist/timers.d.ts +14 -0
- package/dist/timers.js +34 -0
- package/dist/use-problem-counts.d.ts +3 -0
- package/dist/use-problem-counts.js +20 -0
- package/dist/view-state.d.ts +21 -0
- package/dist/view-state.js +63 -0
- package/dist/word-diff.d.ts +1 -0
- package/dist/word-diff.js +32 -0
- package/dist/workspace-edit.d.ts +21 -0
- package/dist/workspace-edit.js +147 -0
- package/package.json +74 -2
- package/src/AnchoredPopup.tsx +51 -0
- package/src/AttributionCard.tsx +36 -0
- package/src/BaselineStatus.tsx +20 -0
- package/src/ChangeReview.tsx +43 -0
- package/src/CodeAuthorsCard.tsx +81 -0
- package/src/CompletionPopup.tsx +196 -0
- package/src/EditorContextMenu.tsx +67 -0
- package/src/EditorView.tsx +47 -0
- package/src/FindReplace.tsx +89 -0
- package/src/HoverCard.tsx +121 -0
- package/src/HoverSections.tsx +146 -0
- package/src/LanguagePopups.tsx +65 -0
- package/src/Markdown.tsx +11 -0
- package/src/PathText.tsx +18 -0
- package/src/PeekPanel.tsx +222 -0
- package/src/PickPopup.tsx +102 -0
- package/src/RenameCard.tsx +219 -0
- package/src/SignatureCard.tsx +42 -0
- package/src/SymbolPicker.tsx +219 -0
- package/src/code-actions-model.ts +146 -0
- package/src/code-actions.ts +384 -0
- package/src/code-authors.ts +104 -0
- package/src/code-vision-model.ts +230 -0
- package/src/code-vision.ts +269 -0
- package/src/completion-call.ts +139 -0
- package/src/completion-model.ts +440 -0
- package/src/completion.ts +500 -0
- package/src/conflict-model.ts +56 -0
- package/src/context-menu.ts +63 -0
- package/src/definition-link.ts +106 -0
- package/src/diagnostics-model.ts +102 -0
- package/src/diagnostics.ts +117 -0
- package/src/editor-language.ts +189 -0
- package/src/editor-react.css +43 -0
- package/src/find-query.ts +74 -0
- package/src/folding.ts +110 -0
- package/src/highlight-layers.ts +37 -0
- package/src/highlights.ts +123 -0
- package/src/history.ts +56 -0
- package/src/holder.ts +19 -0
- package/src/host-types.ts +52 -0
- package/src/hover-content.ts +264 -0
- package/src/hover.ts +280 -0
- package/src/index.ts +63 -0
- package/src/inlay-hints.ts +85 -0
- package/src/language-ids.ts +54 -0
- package/src/line-edits.ts +48 -0
- package/src/locales/en.json +171 -0
- package/src/locales/nl.json +171 -0
- package/src/models.ts +157 -0
- package/src/navigation-history.ts +69 -0
- package/src/navigation.ts +168 -0
- package/src/paths.ts +3 -0
- package/src/peek-model.ts +147 -0
- package/src/peek.ts +247 -0
- package/src/pick.ts +197 -0
- package/src/popup-placement.ts +64 -0
- package/src/popups.ts +204 -0
- package/src/project-language.ts +223 -0
- package/src/project-problems.ts +87 -0
- package/src/proposal-model.ts +259 -0
- package/src/recent-choices.ts +48 -0
- package/src/refresher.ts +56 -0
- package/src/relative-time.ts +10 -0
- package/src/rename-model.ts +130 -0
- package/src/rename.ts +290 -0
- package/src/rendering-context.ts +12 -0
- package/src/rendering.tsx +6 -0
- package/src/review-group.ts +70 -0
- package/src/row-host.ts +158 -0
- package/src/selection-ranges.ts +32 -0
- package/src/semantic-model.ts +80 -0
- package/src/semantic-tokens.ts +46 -0
- package/src/shortcut-keys.ts +21 -0
- package/src/signature-model.ts +49 -0
- package/src/signature.ts +164 -0
- package/src/snippet-session.ts +122 -0
- package/src/snippet.ts +147 -0
- package/src/symbol-links.ts +72 -0
- package/src/symbol-picker-model.ts +198 -0
- package/src/symbol-picker.ts +85 -0
- package/src/symbols.ts +110 -0
- package/src/testing.ts +240 -0
- package/src/timers.ts +45 -0
- package/src/use-problem-counts.ts +25 -0
- package/src/view-state.ts +88 -0
- package/src/word-diff.ts +35 -0
- package/src/workspace-edit.ts +194 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { EditorRect } from '@adecore/editor';
|
|
5
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
6
|
+
import { kindLetterOf, kindToneOf, rowWindow, type KindTone } from './completion-model.ts';
|
|
7
|
+
import { SourceLine, SymbolSections } from './HoverSections.tsx';
|
|
8
|
+
import { PathText } from './PathText.tsx';
|
|
9
|
+
import { placeBeside, placePopup } from './popup-placement.ts';
|
|
10
|
+
import type { CompletionView } from './popups.ts';
|
|
11
|
+
|
|
12
|
+
const TONES: Record<KindTone, string> = {
|
|
13
|
+
callable: 'bg-accent-soft text-accent',
|
|
14
|
+
value: 'bg-status-running/15 text-status-running',
|
|
15
|
+
type: 'bg-status-idle/15 text-status-idle',
|
|
16
|
+
other: 'bg-surface-hover text-text-muted'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/* The label with the characters that match what was typed set apart. */
|
|
20
|
+
function MarkedLabel({ label, matches }: { label: string; matches: readonly number[] }) {
|
|
21
|
+
if (matches.length === 0) {
|
|
22
|
+
return <>{label}</>;
|
|
23
|
+
}
|
|
24
|
+
const marked = new Set(matches);
|
|
25
|
+
const runs: { text: string; marked: boolean }[] = [];
|
|
26
|
+
for (let at = 0; at < label.length; at++) {
|
|
27
|
+
const isMarked = marked.has(at);
|
|
28
|
+
const last = runs[runs.length - 1];
|
|
29
|
+
if (last !== undefined && last.marked === isMarked) {
|
|
30
|
+
last.text += label[at];
|
|
31
|
+
} else {
|
|
32
|
+
runs.push({ text: label[at]!, marked: isMarked });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
{runs.map((run, index) =>
|
|
38
|
+
run.marked ? (
|
|
39
|
+
<span key={index} className="font-semibold text-accent">
|
|
40
|
+
{run.text}
|
|
41
|
+
</span>
|
|
42
|
+
) : (
|
|
43
|
+
run.text
|
|
44
|
+
)
|
|
45
|
+
)}
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CARD = 'fixed top-0 left-0 z-(--z-popup) overflow-hidden rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow)';
|
|
51
|
+
const ROW_HEIGHT = 24;
|
|
52
|
+
const VISIBLE_ROWS = 8;
|
|
53
|
+
/* Rows drawn past the visible ones on each side, so a wheel turn lands on rows that exist. */
|
|
54
|
+
const OVERSCAN_ROWS = 6;
|
|
55
|
+
/* The padding the listbox has around its rows. */
|
|
56
|
+
const LIST_PADDING = 4;
|
|
57
|
+
/* What a row takes besides its name and namespace: padding, the kind badge and the gaps. */
|
|
58
|
+
const ROW_CHROME = 72;
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* The suggestions under the word being typed: a narrow list with the parameters of each, and the
|
|
62
|
+
* documentation of the active one beside it, or under it when the window has no room at its side. A
|
|
63
|
+
* press on a row never takes the focus from the editor, so typing goes on while the list is open.
|
|
64
|
+
*/
|
|
65
|
+
export function CompletionPopup({ language, view, rect }: { language: EditorLanguage; view: CompletionView; rect: EditorRect }) {
|
|
66
|
+
const { t } = useTranslation('editor');
|
|
67
|
+
const list = useRef<HTMLDivElement>(null);
|
|
68
|
+
const docs = useRef<HTMLDivElement>(null);
|
|
69
|
+
const activeRow = useRef<HTMLButtonElement>(null);
|
|
70
|
+
// The widest row so far in characters, which only grows, so the list never narrows or jumps while it is typed into.
|
|
71
|
+
const [widest, setWidest] = useState(0);
|
|
72
|
+
// Only the rows near the visible ones are drawn: a list of 150 is mostly out of sight, and every keystroke makes it again.
|
|
73
|
+
const [listTop, setListTop] = useState(0);
|
|
74
|
+
const need = Math.max(0, ...view.rows.slice(0, VISIBLE_ROWS * 2).map((row) => row.label.length + 2 + row.description.length));
|
|
75
|
+
if (need > widest) {
|
|
76
|
+
setWidest(need);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Every render: the word moves when the editor scrolls, and the list and the documentation change size with what they hold.
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
const card = list.current;
|
|
82
|
+
if (card === null) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const viewport = {
|
|
86
|
+
width: window.innerWidth,
|
|
87
|
+
height: window.innerHeight
|
|
88
|
+
};
|
|
89
|
+
const side = docs.current;
|
|
90
|
+
// Every size is read before the first write, so the page is laid out once and not after each of them.
|
|
91
|
+
const size = { width: card.offsetWidth, height: card.offsetHeight };
|
|
92
|
+
const sideSize = side === null ? null : { width: side.offsetWidth, height: side.offsetHeight };
|
|
93
|
+
const placed = placePopup(rect, size, viewport, { gap: 2 });
|
|
94
|
+
card.style.left = `${Math.round(placed.left)}px`;
|
|
95
|
+
card.style.top = `${Math.round(placed.top)}px`;
|
|
96
|
+
card.style.visibility = 'visible';
|
|
97
|
+
if (side !== null && sideSize !== null) {
|
|
98
|
+
const box = {
|
|
99
|
+
left: Math.round(placed.left),
|
|
100
|
+
top: Math.round(placed.top),
|
|
101
|
+
...size
|
|
102
|
+
};
|
|
103
|
+
const beside = placeBeside(box, sideSize, viewport);
|
|
104
|
+
side.style.left = `${Math.round(beside.left)}px`;
|
|
105
|
+
side.style.top = `${Math.round(beside.top)}px`;
|
|
106
|
+
side.style.maxHeight = `${Math.floor(beside.maxHeight)}px`;
|
|
107
|
+
side.style.visibility = 'visible';
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
useLayoutEffect(() => {
|
|
112
|
+
activeRow.current?.scrollIntoView({ block: 'nearest' });
|
|
113
|
+
}, [view.active, view.rows]);
|
|
114
|
+
|
|
115
|
+
const total = view.rows.length;
|
|
116
|
+
const { first, last } = rowWindow(listTop, view.active, total, ROW_HEIGHT, VISIBLE_ROWS, OVERSCAN_ROWS, LIST_PADDING);
|
|
117
|
+
|
|
118
|
+
return createPortal(
|
|
119
|
+
<>
|
|
120
|
+
<div
|
|
121
|
+
ref={list}
|
|
122
|
+
className={`${CARD} flex w-max max-w-[min(560px,calc(100vw-16px))] min-w-[340px] flex-col`}
|
|
123
|
+
style={{ visibility: 'hidden' }}
|
|
124
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
125
|
+
>
|
|
126
|
+
{/* Sizes the list to the widest rows in the face the rows use; the rows themselves fill that width and truncate inside it. */}
|
|
127
|
+
<div
|
|
128
|
+
aria-hidden
|
|
129
|
+
className="h-0 overflow-hidden font-mono text-code"
|
|
130
|
+
style={{
|
|
131
|
+
width: `calc(${Math.max(widest, need)}ch + ${ROW_CHROME}px)`
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
<div
|
|
135
|
+
role="listbox"
|
|
136
|
+
className="w-0 min-w-full overflow-y-auto p-1"
|
|
137
|
+
style={{ maxHeight: ROW_HEIGHT * VISIBLE_ROWS + 8 }}
|
|
138
|
+
onScroll={(event) => setListTop(event.currentTarget.scrollTop)}
|
|
139
|
+
>
|
|
140
|
+
<div aria-hidden style={{ height: first * ROW_HEIGHT }} />
|
|
141
|
+
{view.rows.slice(first, last).map((row, offset) => {
|
|
142
|
+
const index = first + offset;
|
|
143
|
+
return (
|
|
144
|
+
<button
|
|
145
|
+
key={`${index}:${row.label}`}
|
|
146
|
+
ref={index === view.active ? activeRow : undefined}
|
|
147
|
+
type="button"
|
|
148
|
+
role="option"
|
|
149
|
+
aria-selected={index === view.active}
|
|
150
|
+
aria-setsize={total}
|
|
151
|
+
aria-posinset={index + 1}
|
|
152
|
+
data-active={index === view.active}
|
|
153
|
+
className="flex h-6 w-full items-center gap-2 overflow-hidden rounded-md px-1.5 text-left text-xs cursor-row"
|
|
154
|
+
onClick={() => void language.completion.accept(false, index)}
|
|
155
|
+
>
|
|
156
|
+
<span
|
|
157
|
+
className={`grid size-4 shrink-0 place-items-center rounded-sm font-mono text-xs leading-none font-semibold ${TONES[kindToneOf(row.kind)]}`}
|
|
158
|
+
>
|
|
159
|
+
{kindLetterOf(row.kind)}
|
|
160
|
+
</span>
|
|
161
|
+
<span className={`max-w-full shrink-0 truncate font-mono text-code ${row.deprecated ? 'line-through' : ''}`}>
|
|
162
|
+
<MarkedLabel label={row.label} matches={row.matches} />
|
|
163
|
+
</span>
|
|
164
|
+
<span className="min-w-0 grow truncate font-mono text-text-faint">{row.detail}</span>
|
|
165
|
+
<PathText path={row.description} className="max-w-[60%] font-mono text-text-faint" />
|
|
166
|
+
</button>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
<div aria-hidden style={{ height: (total - last) * ROW_HEIGHT }} />
|
|
170
|
+
</div>
|
|
171
|
+
<div className="flex items-center gap-3 border-t border-border px-2 py-1 text-xs whitespace-nowrap text-text-faint">
|
|
172
|
+
<span>↵ {t('language.completion.insert')}</span>
|
|
173
|
+
<span>⇥ {t('language.completion.replace')}</span>
|
|
174
|
+
<span>^Space {t('language.completion.details')}</span>
|
|
175
|
+
<span className="ml-auto">{view.server}</span>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
{view.detailsOpen && view.docs !== null && (
|
|
179
|
+
<div
|
|
180
|
+
ref={docs}
|
|
181
|
+
className={`${CARD} flex w-[360px] flex-col divide-y divide-border overflow-y-auto select-text`}
|
|
182
|
+
style={{ visibility: 'hidden' }}
|
|
183
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
184
|
+
>
|
|
185
|
+
{view.docs.source !== '' && (
|
|
186
|
+
<div className="px-3 py-2">
|
|
187
|
+
<SourceLine text={view.docs.source} language={view.highlightLanguage} />
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
<SymbolSections text={view.docs.text} />
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</>,
|
|
194
|
+
document.body
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Fragment, useEffect, useRef, type ReactNode } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ContextMenu, Kbd } from '@adecore/ui';
|
|
4
|
+
import type { KeymapId } from '@adecore/editor/keymap';
|
|
5
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
6
|
+
import type { MenuView } from './popups.ts';
|
|
7
|
+
|
|
8
|
+
export function EditorContextMenu({ language, view, children }: { language: EditorLanguage; view: MenuView; children?: ReactNode }) {
|
|
9
|
+
const { t } = useTranslation('editor');
|
|
10
|
+
const trigger = useRef<HTMLDivElement>(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
trigger.current?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: view.x, clientY: view.y }));
|
|
13
|
+
}, [view.x, view.y]);
|
|
14
|
+
const { service } = language.project;
|
|
15
|
+
const rows: { id: KeymapId; method: string; run(): void }[] = [
|
|
16
|
+
{ id: 'goToDefinition', method: 'textDocument/definition', run: () => void language.navigation.go('definition') },
|
|
17
|
+
{ id: 'goToTypeDefinition', method: 'textDocument/typeDefinition', run: () => void language.navigation.go('typeDefinition') },
|
|
18
|
+
{ id: 'goToImplementation', method: 'textDocument/implementation', run: () => void language.navigation.go('implementation') },
|
|
19
|
+
{ id: 'peekReferences', method: 'textDocument/references', run: () => void language.peek.open() },
|
|
20
|
+
{ id: 'renameSymbol', method: 'textDocument/rename', run: () => void language.rename.start() },
|
|
21
|
+
{ id: 'codeActions', method: 'textDocument/codeAction', run: () => void language.codeActions.open() }
|
|
22
|
+
];
|
|
23
|
+
return (
|
|
24
|
+
<ContextMenu.Root onOpenChange={(open) => !open && language.contextMenu.close()}>
|
|
25
|
+
<ContextMenu.Trigger ref={trigger} className="fixed top-0 left-0 h-0 w-0" aria-hidden />
|
|
26
|
+
<ContextMenu.Popup>
|
|
27
|
+
{rows
|
|
28
|
+
.filter((row) => service.supports(row.method, language.uri))
|
|
29
|
+
.map((row) => (
|
|
30
|
+
<ContextMenu.Item key={row.id} onClick={row.run}>
|
|
31
|
+
{t(`commands.${row.id}`)}
|
|
32
|
+
{language.shortcuts[row.id] !== null && <Kbd shortcut={language.shortcuts[row.id]!} />}
|
|
33
|
+
</ContextMenu.Item>
|
|
34
|
+
))}
|
|
35
|
+
{view.refactors.length > 0 && (
|
|
36
|
+
<ContextMenu.SubmenuRoot>
|
|
37
|
+
<ContextMenu.SubmenuTrigger>{t('language.context.refactor')}</ContextMenu.SubmenuTrigger>
|
|
38
|
+
<ContextMenu.Popup>
|
|
39
|
+
{view.refactors.map((entry, index) => (
|
|
40
|
+
<Fragment key={entry.id}>
|
|
41
|
+
{index > 0 && view.refactors[index - 1]!.group !== entry.group && <ContextMenu.Separator />}
|
|
42
|
+
<ContextMenu.Item onClick={() => void language.codeActions.apply(entry)}>{entry.action.title}</ContextMenu.Item>
|
|
43
|
+
</Fragment>
|
|
44
|
+
))}
|
|
45
|
+
</ContextMenu.Popup>
|
|
46
|
+
</ContextMenu.SubmenuRoot>
|
|
47
|
+
)}
|
|
48
|
+
{children}
|
|
49
|
+
<ContextMenu.Separator />
|
|
50
|
+
{(['cut', 'copy', 'paste'] as const).map((action) => (
|
|
51
|
+
<ContextMenu.Item key={action} onClick={() => language.contextMenu.clipboard(action)}>
|
|
52
|
+
{t(`language.context.${action}`)}
|
|
53
|
+
</ContextMenu.Item>
|
|
54
|
+
))}
|
|
55
|
+
{service.supports('textDocument/formatting', language.uri) && (
|
|
56
|
+
<>
|
|
57
|
+
<ContextMenu.Separator />
|
|
58
|
+
<ContextMenu.Item onClick={() => void language.codeActions.formatDocument()}>
|
|
59
|
+
{t('commands.formatDocument')}
|
|
60
|
+
{language.shortcuts.formatDocument !== null && <Kbd shortcut={language.shortcuts.formatDocument} />}
|
|
61
|
+
</ContextMenu.Item>
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</ContextMenu.Popup>
|
|
65
|
+
</ContextMenu.Root>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHolder } from './holder.ts';
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore, type ReactNode, type Ref } from 'react';
|
|
3
|
+
import type { Editor, EditorEngine, EditorOptions } from '@adecore/editor';
|
|
4
|
+
import { EditorLanguage } from './editor-language.ts';
|
|
5
|
+
import { LanguagePopups } from './LanguagePopups.tsx';
|
|
6
|
+
import type { ProjectLanguage } from './project-language.ts';
|
|
7
|
+
|
|
8
|
+
export interface EditorViewProps {
|
|
9
|
+
engine: EditorEngine;
|
|
10
|
+
options: EditorOptions;
|
|
11
|
+
project?: ProjectLanguage;
|
|
12
|
+
uri?: string;
|
|
13
|
+
languageId?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
ref?: Ref<HTMLDivElement>;
|
|
16
|
+
onMount?(editor: Editor, language: EditorLanguage | null): void | (() => void);
|
|
17
|
+
/* Rows of the app in the context menu, between the refactorings and the clipboard rows. */
|
|
18
|
+
contextMenuItems?(language: EditorLanguage): ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function EditorView({ engine, options, project, uri, languageId, className, ref, onMount, contextMenuItems }: EditorViewProps) {
|
|
22
|
+
const element = useRef<HTMLDivElement>(null);
|
|
23
|
+
const holder = useMemo(() => createHolder<EditorLanguage>(), []);
|
|
24
|
+
const language = useSyncExternalStore(holder.subscribe, holder.get, holder.get);
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
const host = element.current;
|
|
27
|
+
if (host === null) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const editor = engine.mount(host, options);
|
|
31
|
+
const attached = project === undefined || uri === undefined || languageId === undefined ? null : new EditorLanguage(project, editor, uri, languageId);
|
|
32
|
+
holder.set(attached);
|
|
33
|
+
const stop = onMount?.(editor, attached);
|
|
34
|
+
return () => {
|
|
35
|
+
holder.set(null);
|
|
36
|
+
stop?.();
|
|
37
|
+
attached?.dispose();
|
|
38
|
+
editor.dispose();
|
|
39
|
+
};
|
|
40
|
+
}, [engine, options, project, uri, languageId, holder, onMount]);
|
|
41
|
+
return (
|
|
42
|
+
<div className={className} ref={ref} style={{ position: 'relative', minHeight: 0 }}>
|
|
43
|
+
<div ref={element} style={{ position: 'absolute', inset: 0 }} />
|
|
44
|
+
{language !== null && <LanguagePopups language={language} contextMenuItems={contextMenuItems} />}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '@adecore/ui';
|
|
4
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
5
|
+
import type { Editor, EditorFindState } from '@adecore/editor';
|
|
6
|
+
import { compileFind, EMPTY_FIND_QUERY, type FindQuery } from './find-query.ts';
|
|
7
|
+
|
|
8
|
+
export function FindReplace({ editor, className, onClose }: { editor: Editor; className?: string; onClose?(): void }) {
|
|
9
|
+
const { t } = useTranslation('editor');
|
|
10
|
+
const [query, setQuery] = useState<FindQuery>(EMPTY_FIND_QUERY);
|
|
11
|
+
const [replacement, setReplacement] = useState('');
|
|
12
|
+
const [preserveCase, setPreserveCase] = useState(false);
|
|
13
|
+
const [state, setState] = useState<EditorFindState>({ count: 0, current: null });
|
|
14
|
+
const compiled = useMemo(() => compileFind(query), [query]);
|
|
15
|
+
useEffect(() => editor.onFind(setState), [editor]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
editor.find(compiled.kind === 'pattern' ? query : null);
|
|
18
|
+
}, [editor, query, compiled]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
editor.setReplacePreview(compiled.kind === 'pattern' ? replacement : null, { preserveCase });
|
|
21
|
+
}, [editor, compiled, replacement, preserveCase]);
|
|
22
|
+
useEffect(
|
|
23
|
+
() => () => {
|
|
24
|
+
editor.endFind();
|
|
25
|
+
editor.setReplacePreview(null);
|
|
26
|
+
},
|
|
27
|
+
[editor]
|
|
28
|
+
);
|
|
29
|
+
const label =
|
|
30
|
+
compiled.kind === 'invalid'
|
|
31
|
+
? t('find.invalid')
|
|
32
|
+
: state.noSelection
|
|
33
|
+
? t('find.noSelection')
|
|
34
|
+
: t('find.count', { total: formatNumber(state.count), current: state.current === null ? '0' : formatNumber(state.current + 1) });
|
|
35
|
+
return (
|
|
36
|
+
<div className={`adecore-editor-find ${className ?? ''}`} role="search" aria-label={t('find.label')}>
|
|
37
|
+
<div className="adecore-editor-find-row">
|
|
38
|
+
<input
|
|
39
|
+
type="text"
|
|
40
|
+
aria-label={t('find.label')}
|
|
41
|
+
value={query.text}
|
|
42
|
+
onChange={(event) => setQuery({ ...query, text: event.target.value })}
|
|
43
|
+
onKeyDown={(event) => {
|
|
44
|
+
if (event.nativeEvent.isComposing) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (event.key === 'Enter') {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
editor.findStep(event.shiftKey ? -1 : 1);
|
|
50
|
+
}
|
|
51
|
+
if (event.key === 'Escape') {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
onClose?.();
|
|
54
|
+
editor.focus();
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
<output aria-live="polite">{label}</output>
|
|
59
|
+
<Button size="xs" onClick={() => editor.findStep(-1)}>
|
|
60
|
+
{t('find.previous')}
|
|
61
|
+
</Button>
|
|
62
|
+
<Button size="xs" onClick={() => editor.findStep(1)}>
|
|
63
|
+
{t('find.next')}
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="adecore-editor-find-row">
|
|
67
|
+
{(['caseSensitive', 'wholeWord', 'regex', 'inSelection'] as const).map((option) => (
|
|
68
|
+
<label key={option}>
|
|
69
|
+
<input type="checkbox" checked={query[option] === true} onChange={(event) => setQuery({ ...query, [option]: event.target.checked })} />
|
|
70
|
+
{t(`find.${option}`)}
|
|
71
|
+
</label>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
<div className="adecore-editor-find-row">
|
|
75
|
+
<input type="text" aria-label={t('find.replace.label')} value={replacement} onChange={(event) => setReplacement(event.target.value)} />
|
|
76
|
+
<label>
|
|
77
|
+
<input type="checkbox" checked={preserveCase} onChange={(event) => setPreserveCase(event.target.checked)} />
|
|
78
|
+
{t('find.replace.preserveCase')}
|
|
79
|
+
</label>
|
|
80
|
+
<Button size="xs" onClick={() => editor.replace(replacement, { preserveCase })}>
|
|
81
|
+
{t('find.replace.one')}
|
|
82
|
+
</Button>
|
|
83
|
+
<Button size="xs" onClick={() => editor.replaceAll(replacement, { preserveCase })}>
|
|
84
|
+
{t('find.replace.allLabel')}
|
|
85
|
+
</Button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { CircleX, Info, TriangleAlert } from 'lucide-react';
|
|
3
|
+
import { fileUriToPath } from '@adecore/lsp';
|
|
4
|
+
import { Button, Icon, Tooltip } from '@adecore/ui';
|
|
5
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
6
|
+
import type { EditorPosition } from '@adecore/editor';
|
|
7
|
+
import { basenameOf } from './paths.ts';
|
|
8
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
9
|
+
import { codeLabelOf, severityOf, type Problem } from './diagnostics-model.ts';
|
|
10
|
+
import type { HoverInfo } from './popups.ts';
|
|
11
|
+
import { SymbolSections } from './HoverSections.tsx';
|
|
12
|
+
|
|
13
|
+
const SEVERITY_ICONS = { error: CircleX, warning: TriangleAlert, info: Info, hint: Info } as const;
|
|
14
|
+
const SEVERITY_COLORS = { error: 'text-status-error', warning: 'text-status-needs-you', info: 'text-status-running', hint: 'text-text-muted' } as const;
|
|
15
|
+
|
|
16
|
+
function ProblemSection({ problem, language }: { problem: Problem; language: EditorLanguage }) {
|
|
17
|
+
const { t } = useTranslation('editor');
|
|
18
|
+
const { diagnostic } = problem;
|
|
19
|
+
const severity = severityOf(diagnostic);
|
|
20
|
+
const label =
|
|
21
|
+
diagnostic.source === undefined ? [codeLabelOf(diagnostic), problem.server].filter((part) => part !== '').join(' · ') : codeLabelOf(diagnostic);
|
|
22
|
+
const supported = language.codeActions.supported;
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col gap-1.5 px-3 py-2.5">
|
|
25
|
+
<div className="flex items-start gap-2">
|
|
26
|
+
<Icon icon={SEVERITY_ICONS[severity]} size={14} className={`mt-px shrink-0 ${SEVERITY_COLORS[severity]}`} />
|
|
27
|
+
<div className="min-w-0 text-xs/[18px] break-words whitespace-pre-wrap select-text">{diagnostic.message}</div>
|
|
28
|
+
</div>
|
|
29
|
+
{label !== '' && <div className="pl-[22px] text-xs text-text-muted select-text">{label}</div>}
|
|
30
|
+
{diagnostic.relatedInformation?.map((related, index) => {
|
|
31
|
+
const path = fileUriToPath(related.location.uri);
|
|
32
|
+
return (
|
|
33
|
+
<div key={index} className="pl-[22px] text-xs text-text-muted">
|
|
34
|
+
{path !== null && (
|
|
35
|
+
<button type="button" className="font-mono text-accent hover:underline" onClick={() => language.goTo(related.location)}>
|
|
36
|
+
{basenameOf(path)}:{formatNumber(related.location.range.start.line + 1)}
|
|
37
|
+
</button>
|
|
38
|
+
)}{' '}
|
|
39
|
+
{related.message}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
<div className="pt-1 pl-[22px]">
|
|
44
|
+
<Tooltip label={t(supported ? 'language.hover.quickFix' : 'language.hover.quickFixNone')}>
|
|
45
|
+
<Button
|
|
46
|
+
size="xs"
|
|
47
|
+
variant="secondary"
|
|
48
|
+
aria-disabled={!supported}
|
|
49
|
+
onClick={() => {
|
|
50
|
+
if (supported) {
|
|
51
|
+
language.hover.hide();
|
|
52
|
+
void language.codeActions.quickFixFor(problem);
|
|
53
|
+
}
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{t('language.hover.quickFix')}
|
|
57
|
+
</Button>
|
|
58
|
+
</Tooltip>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function HoverCard({
|
|
65
|
+
language,
|
|
66
|
+
problems,
|
|
67
|
+
info,
|
|
68
|
+
anchor,
|
|
69
|
+
position
|
|
70
|
+
}: {
|
|
71
|
+
language: EditorLanguage;
|
|
72
|
+
problems: readonly Problem[];
|
|
73
|
+
info: HoverInfo | null;
|
|
74
|
+
anchor: EditorPosition;
|
|
75
|
+
position: EditorPosition;
|
|
76
|
+
}) {
|
|
77
|
+
const { t } = useTranslation('editor');
|
|
78
|
+
const place = info?.definition === null || info === null ? null : fileUriToPath(info.definition.uri);
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex w-max min-w-[280px] max-w-[min(520px,calc(100vw-16px))] flex-col divide-y divide-border">
|
|
81
|
+
{info !== null && (
|
|
82
|
+
<>
|
|
83
|
+
<SymbolSections
|
|
84
|
+
text={info.text}
|
|
85
|
+
onName={(name, declared) => {
|
|
86
|
+
language.hover.hide();
|
|
87
|
+
void language.navigation.goToName(name, anchor, name === (declared ?? info.word) ? info.definition : null);
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
<div className="flex items-center gap-3 px-3 py-1.5 text-xs">
|
|
91
|
+
{info.definition !== null && (
|
|
92
|
+
<button type="button" className="text-accent hover:underline" onClick={() => language.goTo(info.definition!)}>
|
|
93
|
+
{t('language.hover.definition')}
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
{info.references !== null && info.references > 0 && (
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
className="text-accent hover:underline"
|
|
100
|
+
onClick={() => {
|
|
101
|
+
language.hover.hide();
|
|
102
|
+
void language.peek.open(position);
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{t('language.hover.references', { count: info.references, formatted: formatNumber(info.references) })}
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
{place !== null && info.definition !== null && (
|
|
109
|
+
<span className="ml-auto font-mono text-text-faint">
|
|
110
|
+
{basenameOf(place)}:{formatNumber(info.definition.range.start.line + 1)}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
{problems.map((problem, index) => (
|
|
117
|
+
<ProblemSection key={index} problem={problem} language={language} />
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useEditorRendering } from './rendering-context.ts';
|
|
2
|
+
import { useEffect, useState, type MouseEvent } from 'react';
|
|
3
|
+
import { Markdown } from './Markdown.tsx';
|
|
4
|
+
import { Tooltip } from '@adecore/ui';
|
|
5
|
+
import { hoverSectionsOf, markdownParts, type DocTag, type HoverSection, type HoverText } from './hover-content.ts';
|
|
6
|
+
import { BaselineStatus } from './BaselineStatus.tsx';
|
|
7
|
+
import { declaredNameOf, linkTypeNames } from './symbol-links.ts';
|
|
8
|
+
|
|
9
|
+
/* Code as the viewer colors it, plain until the grammar is in so the card never changes size under the pointer by much. */
|
|
10
|
+
export function Signature({ code, language, onName }: { code: string; language: string; onName?: (name: string) => void }) {
|
|
11
|
+
const { theme, highlight } = useEditorRendering();
|
|
12
|
+
const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
|
|
13
|
+
const key = `${language}\0${theme}\0${code}\0${onName !== undefined}`;
|
|
14
|
+
// Only whether there are links goes into the highlighting; the handler is read when a name is pressed.
|
|
15
|
+
const linked = onName !== undefined;
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (highlight === undefined) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let alive = true;
|
|
22
|
+
highlight(code, language, theme)
|
|
23
|
+
.then((result) => alive && setHtml({ key, html: linked ? linkTypeNames(result) : result }))
|
|
24
|
+
.catch(() => undefined);
|
|
25
|
+
return () => {
|
|
26
|
+
alive = false;
|
|
27
|
+
};
|
|
28
|
+
}, [code, language, theme, key, linked, highlight]);
|
|
29
|
+
|
|
30
|
+
const className =
|
|
31
|
+
'font-mono text-code break-words whitespace-pre-wrap [&_.line]:block [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:whitespace-pre-wrap [&_code]:font-mono';
|
|
32
|
+
// One handler for every name, since the names are part of the highlighted HTML and not elements of ours.
|
|
33
|
+
const follow = (event: MouseEvent<HTMLDivElement>): void => {
|
|
34
|
+
const name = (event.target as HTMLElement).closest<HTMLElement>('[data-symbol]')?.dataset.symbol;
|
|
35
|
+
if (name !== undefined && onName !== undefined) {
|
|
36
|
+
onName(name);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return html?.key === key ? (
|
|
40
|
+
<div className={className} onClick={follow} dangerouslySetInnerHTML={{ __html: html.html }} />
|
|
41
|
+
) : (
|
|
42
|
+
<div className={className}>{code}</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* A docblock's tags as rows: the tag once beside each run of the same tag, so a list of parameters reads as one. */
|
|
47
|
+
export function DocTags({ tags }: { tags: readonly DocTag[] }) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-0.5 text-xs/[18px] select-text">
|
|
50
|
+
{tags.map((tag, index) => (
|
|
51
|
+
<div key={index} className="contents">
|
|
52
|
+
<span className="font-mono text-text-faint">{tags[index - 1]?.name === tag.name ? '' : `@${tag.name}`}</span>
|
|
53
|
+
<div className="min-w-0 break-words text-text-muted [&_.adecore-editor-markdown]:text-xs [&_code]:bg-transparent! [&_code]:p-0! [&_p]:m-0">
|
|
54
|
+
<Markdown text={tag.markdown} />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* One symbol: its qualified name muted, its signature, the prose and examples as compact blocks, and its tags as rows. Names in the signature are links only where `onName` says what a press does. */
|
|
63
|
+
export function SymbolSection({ section, onName }: { section: HoverSection; onName?: (name: string, declared: string | null) => void }) {
|
|
64
|
+
const declared = declaredNameOf(section.signatures[0]?.code ?? '');
|
|
65
|
+
const follow = onName === undefined ? undefined : (name: string): void => onName(name, declared);
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex flex-col gap-1.5 px-3 py-2">
|
|
68
|
+
{section.title !== null && <div className="truncate font-mono text-xs text-text-faint select-text">{section.title}</div>}
|
|
69
|
+
{section.signatures.map((block, index) => (
|
|
70
|
+
<Signature key={index} code={block.code} language={block.language} onName={follow} />
|
|
71
|
+
))}
|
|
72
|
+
{section.baseline !== null && <BaselineStatus baseline={section.baseline} />}
|
|
73
|
+
{markdownParts(section.markdown).map((part, index) =>
|
|
74
|
+
part.kind === 'code' ? (
|
|
75
|
+
<div key={index} className="rounded-md bg-surface-sunken px-2 py-1.5">
|
|
76
|
+
<Signature code={part.block.code} language={part.block.language} onName={follow} />
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
<div key={index} className="text-text-muted select-text [&_.adecore-editor-markdown]:text-xs [&_p]:my-1">
|
|
80
|
+
<Markdown text={part.text} />
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
)}
|
|
84
|
+
{section.tags.length > 0 && <DocTags tags={section.tags} />}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* What a server says about a symbol as the sections of a hover card, shared by the hover and the documentation beside the suggestions. */
|
|
90
|
+
export function SymbolSections({ text, onName }: { text: HoverText; onName?: (name: string, declared: string | null) => void }) {
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
{hoverSectionsOf(text).map((section, index) => (
|
|
94
|
+
<SymbolSection key={index} section={section} onName={onName} />
|
|
95
|
+
))}
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* PHP highlights only after its opening tag, which is put in front for the grammar and taken off again. */
|
|
101
|
+
function phpWithoutOpenTag(html: string): string {
|
|
102
|
+
return html.replace(/<span[^>]*><\?<\/span><span[^>]*>php<\/span>(<span[^>]*>) /, '$1');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* Where a suggestion comes from, such as `use Raxos\Database\Orm\Attribute\PrimaryKey`, on one line that gives
|
|
107
|
+
* way from its start, so the end of the name stays readable and a word is never cut in two. The full text is in the tooltip.
|
|
108
|
+
*/
|
|
109
|
+
export function SourceLine({ text, language }: { text: string; language: string }) {
|
|
110
|
+
const { theme, highlight } = useEditorRendering();
|
|
111
|
+
const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
|
|
112
|
+
const highlighted = language === 'php' && text.startsWith('use ');
|
|
113
|
+
const key = `${theme}\0${text}`;
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!highlighted) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (highlight === undefined) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let alive = true;
|
|
123
|
+
highlight(`<?php ${text}`, 'php', theme)
|
|
124
|
+
.then((result) => alive && setHtml({ key, html: phpWithoutOpenTag(result) }))
|
|
125
|
+
.catch(() => undefined);
|
|
126
|
+
return () => {
|
|
127
|
+
alive = false;
|
|
128
|
+
};
|
|
129
|
+
}, [highlighted, text, theme, key, highlight]);
|
|
130
|
+
|
|
131
|
+
const className =
|
|
132
|
+
'block overflow-hidden text-left font-mono text-code text-ellipsis whitespace-nowrap [direction:rtl] [&_code]:font-mono [&_pre]:m-0 [&_pre]:inline [&_pre]:bg-transparent! [&_pre]:p-0 [&_code]:inline [&_.line]:inline';
|
|
133
|
+
return (
|
|
134
|
+
<Tooltip label={text}>
|
|
135
|
+
{highlighted && html?.key === key ? (
|
|
136
|
+
<div className={className}>
|
|
137
|
+
<bdi dangerouslySetInnerHTML={{ __html: html.html }} />
|
|
138
|
+
</div>
|
|
139
|
+
) : (
|
|
140
|
+
<div className={`${className} text-text-muted`}>
|
|
141
|
+
<bdi>{text}</bdi>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</Tooltip>
|
|
145
|
+
);
|
|
146
|
+
}
|