@adecore/editor-react 0.0.1 → 0.17.0-beta.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/LICENSE +110 -0
- package/README.md +55 -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 +15 -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 +4 -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 +29 -0
- package/dist/code-actions.js +329 -0
- package/dist/code-authors.d.ts +17 -0
- package/dist/code-authors.js +80 -0
- package/dist/code-vision-model.d.ts +20 -0
- package/dist/code-vision-model.js +184 -0
- package/dist/code-vision.d.ts +48 -0
- package/dist/code-vision.js +234 -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 +464 -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 +64 -0
- package/dist/editor-language.js +148 -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 +97 -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 +47 -0
- package/dist/holder.d.ts +5 -0
- package/dist/holder.js +19 -0
- package/dist/host-types.d.ts +43 -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 +261 -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 +73 -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 +22 -0
- package/dist/navigation.js +156 -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 +29 -0
- package/dist/peek.js +216 -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 +34 -0
- package/dist/project-language.js +187 -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 +20 -0
- package/dist/rename.js +258 -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 +40 -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 +77 -0
- package/dist/symbols.d.ts +11 -0
- package/dist/symbols.js +97 -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 +45 -0
- package/src/FindReplace.tsx +89 -0
- package/src/HoverCard.tsx +121 -0
- package/src/HoverSections.tsx +146 -0
- package/src/LanguagePopups.tsx +61 -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 +387 -0
- package/src/code-authors.ts +104 -0
- package/src/code-vision-model.ts +230 -0
- package/src/code-vision.ts +267 -0
- package/src/completion-call.ts +139 -0
- package/src/completion-model.ts +440 -0
- package/src/completion.ts +501 -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 +163 -0
- package/src/editor-react.css +43 -0
- package/src/find-query.ts +74 -0
- package/src/folding.ts +112 -0
- package/src/highlight-layers.ts +37 -0
- package/src/highlights.ts +123 -0
- package/src/history.ts +57 -0
- package/src/holder.ts +19 -0
- package/src/host-types.ts +49 -0
- package/src/hover-content.ts +264 -0
- package/src/hover.ts +281 -0
- package/src/index.ts +63 -0
- package/src/inlay-hints.ts +87 -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 +169 -0
- package/src/paths.ts +3 -0
- package/src/peek-model.ts +147 -0
- package/src/peek.ts +248 -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 +220 -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 +291 -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 +48 -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 +86 -0
- package/src/symbols.ts +112 -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,147 @@
|
|
|
1
|
+
import { applyTextEdits, fileUriToPath } from '@adecore/lsp';
|
|
2
|
+
function refused(failureReason) {
|
|
3
|
+
return { applied: false, failureReason };
|
|
4
|
+
}
|
|
5
|
+
/* The steps of an edit; null when it creates or deletes a file, which an edit is not applied with. */
|
|
6
|
+
function stepsOf(edit) {
|
|
7
|
+
const steps = [];
|
|
8
|
+
for (const change of edit.documentChanges ?? []) {
|
|
9
|
+
if ('textDocument' in change) {
|
|
10
|
+
steps.push({ kind: 'edits', uri: change.textDocument.uri, edits: change.edits });
|
|
11
|
+
}
|
|
12
|
+
else if (change.kind === 'rename') {
|
|
13
|
+
steps.push({ kind: 'rename', oldUri: change.oldUri, newUri: change.newUri });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (edit.documentChanges === undefined) {
|
|
20
|
+
for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
|
|
21
|
+
steps.push({ kind: 'edits', uri, edits });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return steps;
|
|
25
|
+
}
|
|
26
|
+
/* The text edits of an edit per document, as the entries a document has in it; null when it creates or deletes a file. */
|
|
27
|
+
export function entriesOf(edit) {
|
|
28
|
+
const steps = stepsOf(edit);
|
|
29
|
+
if (steps === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const entries = new Map();
|
|
33
|
+
for (const step of steps) {
|
|
34
|
+
if (step.kind === 'edits') {
|
|
35
|
+
entries.set(step.uri, [...(entries.get(step.uri) ?? []), step.edits]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return entries;
|
|
39
|
+
}
|
|
40
|
+
/* The files an edit moves, which a preview of lines cannot show. */
|
|
41
|
+
export function renamesOf(edit) {
|
|
42
|
+
return (edit.documentChanges ?? []).filter((change) => 'kind' in change && change.kind === 'rename');
|
|
43
|
+
}
|
|
44
|
+
function contentChangeOf(edit) {
|
|
45
|
+
return { range: edit.range, text: edit.newText };
|
|
46
|
+
}
|
|
47
|
+
/*
|
|
48
|
+
* Applies an LSP workspace edit on the client, step by step in the order it says. A file an editor holds takes
|
|
49
|
+
* its edits as one undo step. Any other file gets its new text as an unsaved draft that a person saves, and
|
|
50
|
+
* nothing is written to disk on a server's word alone, unless the edit also moves a file: the edits and the
|
|
51
|
+
* move are then one change, so every file is saved through the machine and the move is made there. The
|
|
52
|
+
* edit is checked against every text first, so one that does not fit changes nothing.
|
|
53
|
+
*/
|
|
54
|
+
export async function applyWorkspaceEdit(edit, host) {
|
|
55
|
+
const steps = stepsOf(edit);
|
|
56
|
+
if (steps === null) {
|
|
57
|
+
return refused('Creating and deleting files is not supported yet');
|
|
58
|
+
}
|
|
59
|
+
const planned = await plan(steps, host);
|
|
60
|
+
if (typeof planned === 'string') {
|
|
61
|
+
return refused(planned);
|
|
62
|
+
}
|
|
63
|
+
const moving = planned.some((item) => item.kind === 'rename');
|
|
64
|
+
const staged = [];
|
|
65
|
+
for (const item of planned) {
|
|
66
|
+
if (item.kind === 'rename') {
|
|
67
|
+
const reason = await host.files.rename(item.oldPath, item.newPath);
|
|
68
|
+
if (reason !== null) {
|
|
69
|
+
return refused(reason);
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (item.after === item.before) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const editor = host.editorOf(item.uri);
|
|
77
|
+
if (editor) {
|
|
78
|
+
// The edits stay apart, so the caret and what is drawn over the text stay where they were between them.
|
|
79
|
+
if (!editor.applyEdits(item.edits.map(contentChangeOf))) {
|
|
80
|
+
return refused(`${item.uri} is read only`);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const path = fileUriToPath(item.uri);
|
|
85
|
+
if (path === null || item.disk === null) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const file = { path, disk: item.disk, text: item.after };
|
|
89
|
+
if (!moving) {
|
|
90
|
+
staged.push(file);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const reason = await host.files.save([file]);
|
|
94
|
+
if (reason !== null) {
|
|
95
|
+
return refused(reason);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (staged.length > 0) {
|
|
99
|
+
host.files?.stage(staged);
|
|
100
|
+
}
|
|
101
|
+
return { applied: true };
|
|
102
|
+
}
|
|
103
|
+
/* Reads every text the edit needs and works out what each step leaves, or says why it does not fit. */
|
|
104
|
+
async function plan(steps, host) {
|
|
105
|
+
const texts = new Map();
|
|
106
|
+
// A file that has moved is read where it still stands: nothing moves until the plan is made.
|
|
107
|
+
const origins = new Map();
|
|
108
|
+
const planned = [];
|
|
109
|
+
for (const step of steps) {
|
|
110
|
+
if (step.kind === 'rename') {
|
|
111
|
+
const oldPath = fileUriToPath(step.oldUri);
|
|
112
|
+
const newPath = fileUriToPath(step.newUri);
|
|
113
|
+
if (oldPath === null || newPath === null || host.files === null) {
|
|
114
|
+
return `${step.oldUri} cannot be moved from here`;
|
|
115
|
+
}
|
|
116
|
+
origins.set(step.newUri, origins.get(step.oldUri) ?? step.oldUri);
|
|
117
|
+
planned.push({ kind: 'rename', oldPath, newPath });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const source = origins.get(step.uri) ?? step.uri;
|
|
121
|
+
if (!texts.has(source)) {
|
|
122
|
+
const editor = host.editorOf(source);
|
|
123
|
+
if (editor) {
|
|
124
|
+
texts.set(source, { text: editor.getText(), disk: null });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const path = fileUriToPath(source);
|
|
128
|
+
const disk = path === null || host.files === null ? null : await host.files.read(path);
|
|
129
|
+
if (disk === null) {
|
|
130
|
+
return `${source} cannot be read as text`;
|
|
131
|
+
}
|
|
132
|
+
texts.set(source, { text: disk.text, disk });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const held = texts.get(source);
|
|
136
|
+
let after;
|
|
137
|
+
try {
|
|
138
|
+
after = applyTextEdits(held.text, step.edits);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return error instanceof Error ? error.message : String(error);
|
|
142
|
+
}
|
|
143
|
+
planned.push({ kind: 'edits', uri: step.uri, edits: step.edits, before: held.text, after, disk: held.disk });
|
|
144
|
+
texts.set(source, { text: after, disk: held.disk });
|
|
145
|
+
}
|
|
146
|
+
return planned;
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor-react",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"
|
|
3
|
+
"version": "0.17.0-beta.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Language-enabled editors, feature coordination and React popups over injected language services.",
|
|
5
6
|
"license": "FSL-1.1-MIT",
|
|
7
|
+
"author": "Bas Milius",
|
|
6
8
|
"repository": {
|
|
7
9
|
"type": "git",
|
|
8
10
|
"url": "git+https://github.com/basmilius/adecore.git",
|
|
9
11
|
"directory": "packages/editor-react"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/basmilius/adecore/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://adecore.dev/editor-react/",
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"**/*.css"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"!src/**/*.test.ts",
|
|
24
|
+
"!src/**/*.test.tsx",
|
|
25
|
+
"!src/test-support.ts",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"!src/test-setup.ts"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"source": "./src/index.ts",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./models": {
|
|
36
|
+
"source": "./src/models.ts",
|
|
37
|
+
"types": "./dist/models.d.ts",
|
|
38
|
+
"default": "./dist/models.js"
|
|
39
|
+
},
|
|
40
|
+
"./testing": {
|
|
41
|
+
"source": "./src/testing.ts",
|
|
42
|
+
"types": "./dist/testing.d.ts",
|
|
43
|
+
"default": "./dist/testing.js"
|
|
44
|
+
},
|
|
45
|
+
"./editor-react.css": {
|
|
46
|
+
"source": "./src/editor-react.css",
|
|
47
|
+
"default": "./dist/editor-react.css"
|
|
48
|
+
},
|
|
49
|
+
"./locales/*.json": {
|
|
50
|
+
"source": "./src/locales/*.json",
|
|
51
|
+
"default": "./dist/locales/*.json"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "bun scripts/build.ts",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "bun test --conditions=source"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@adecore/editor": "0.17.0-beta.1",
|
|
61
|
+
"@adecore/lsp": "0.17.0-beta.1",
|
|
62
|
+
"@adecore/merge": "0.17.0-beta.1",
|
|
63
|
+
"zustand": "^5.0.15",
|
|
64
|
+
"lucide-react": "^1.47.0",
|
|
65
|
+
"react-markdown": "^10.1.0",
|
|
66
|
+
"remark-gfm": "^4.0.1",
|
|
67
|
+
"remark-breaks": "^4.0.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@adecore/ui": "^0.17.0-beta.1",
|
|
71
|
+
"react": "^19.3.0",
|
|
72
|
+
"react-dom": "^19.3.0",
|
|
73
|
+
"i18next": "^26.4.2",
|
|
74
|
+
"react-i18next": "^17.0.15"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@adecore/ui": "workspace:*",
|
|
78
|
+
"@types/bun": "^1.4.2",
|
|
79
|
+
"@types/react": "^19.3.0",
|
|
80
|
+
"@types/react-dom": "^19.3.0",
|
|
81
|
+
"typescript": "^7.0.2"
|
|
10
82
|
}
|
|
11
83
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, type ReactNode } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import type { EditorRect } from '@adecore/editor';
|
|
4
|
+
import { placePopup, type PlaceOptions } from './popup-placement.ts';
|
|
5
|
+
|
|
6
|
+
export interface AnchoredPopupProps {
|
|
7
|
+
/* The character the popup belongs to, in the page's pixels. */
|
|
8
|
+
rect: EditorRect;
|
|
9
|
+
className?: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
placement?: PlaceOptions;
|
|
12
|
+
onPointerEnter?(): void;
|
|
13
|
+
onPointerLeave?(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* A card next to a character of the editor, in a layer of the page itself: a canvas puts a scale on its
|
|
18
|
+
* nodes, and a card inside one would be drawn at that scale and cut off by the node's edge. It is
|
|
19
|
+
* placed from the character's screen rectangle, so it is right at any zoom, and it never leaves the window.
|
|
20
|
+
*/
|
|
21
|
+
export function AnchoredPopup({ rect, className, children, placement, onPointerEnter, onPointerLeave }: AnchoredPopupProps) {
|
|
22
|
+
const element = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
// Every render, since the character moves when the editor scrolls and the card's own size follows its content.
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
const popup = element.current;
|
|
27
|
+
if (popup === null) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
popup.style.maxHeight = '';
|
|
31
|
+
const size = { width: popup.offsetWidth, height: popup.offsetHeight };
|
|
32
|
+
const placed = placePopup(rect, size, { width: window.innerWidth, height: window.innerHeight }, placement);
|
|
33
|
+
popup.style.left = `${Math.round(placed.left)}px`;
|
|
34
|
+
popup.style.top = `${Math.round(placed.top)}px`;
|
|
35
|
+
popup.style.maxHeight = `${Math.floor(placed.maxHeight)}px`;
|
|
36
|
+
popup.style.visibility = 'visible';
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return createPortal(
|
|
40
|
+
<div
|
|
41
|
+
ref={element}
|
|
42
|
+
className={`fixed top-0 left-0 z-(--z-popup) overflow-y-auto rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow) ${className ?? ''}`}
|
|
43
|
+
style={{ visibility: 'hidden' }}
|
|
44
|
+
onPointerEnter={onPointerEnter}
|
|
45
|
+
onPointerLeave={onPointerLeave}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</div>,
|
|
49
|
+
document.body
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { EditorMarkColor, EditorRect } from '@adecore/editor';
|
|
3
|
+
import { AnchoredPopup } from './AnchoredPopup.tsx';
|
|
4
|
+
|
|
5
|
+
export interface AttributionCardProps {
|
|
6
|
+
rect: EditorRect;
|
|
7
|
+
title: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
metadata?: ReactNode;
|
|
10
|
+
prompt?: string;
|
|
11
|
+
color?: EditorMarkColor;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
onHold?(inside: boolean): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AttributionCard({ rect, title, subtitle, metadata, prompt, color, icon, actions, onHold }: AttributionCardProps) {
|
|
18
|
+
return (
|
|
19
|
+
<AnchoredPopup rect={rect} className="flex w-105 flex-col text-xs" onPointerEnter={() => onHold?.(true)} onPointerLeave={() => onHold?.(false)}>
|
|
20
|
+
<div className="flex flex-col gap-2 px-3 pt-3 pb-2.5">
|
|
21
|
+
<div className="flex items-center gap-2">
|
|
22
|
+
<span className="flex shrink-0 items-center" style={{ color: color?.startsWith('--') ? `var(${color})` : color }}>
|
|
23
|
+
{icon ?? <span className="size-2 rounded-full bg-current" />}
|
|
24
|
+
</span>
|
|
25
|
+
<span className="shrink-0 text-sm font-semibold text-text">{title}</span>
|
|
26
|
+
{subtitle !== undefined && <span className="min-w-0 truncate text-sm text-text-muted">{subtitle}</span>}
|
|
27
|
+
</div>
|
|
28
|
+
{metadata !== undefined && <div className="text-text-muted">{metadata}</div>}
|
|
29
|
+
{prompt !== undefined && prompt !== '' && (
|
|
30
|
+
<q className="line-clamp-4 rounded-md bg-surface-hover px-2.5 py-2 text-sm leading-4.5 text-text select-text">{prompt}</q>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
{actions !== undefined && <div className="flex gap-1 border-t border-border px-1.5 py-1.25">{actions}</div>}
|
|
34
|
+
</AnchoredPopup>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CircleCheck, Sparkles, TriangleAlert } from 'lucide-react';
|
|
2
|
+
import { Icon } from '@adecore/ui';
|
|
3
|
+
import type { Baseline, BaselineLevel } from './hover-content.ts';
|
|
4
|
+
|
|
5
|
+
const BASELINE_MARKS: Record<BaselineLevel, { icon: typeof CircleCheck; color: string }> = {
|
|
6
|
+
widely: { icon: CircleCheck, color: 'text-status-idle' },
|
|
7
|
+
newly: { icon: Sparkles, color: 'text-status-running' },
|
|
8
|
+
limited: { icon: TriangleAlert, color: 'text-status-needs-you' }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/* Where a web feature stands across browsers, as one line under a CSS property. */
|
|
12
|
+
export function BaselineStatus({ baseline }: { baseline: Baseline }) {
|
|
13
|
+
const { icon, color } = BASELINE_MARKS[baseline.level];
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-start gap-1.5 text-xs/[18px] text-text-muted select-text">
|
|
16
|
+
<Icon icon={icon} size={14} className={`mt-px shrink-0 ${color}`} />
|
|
17
|
+
<span className="min-w-0">{baseline.text}</span>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, type ReactNode } from 'react';
|
|
2
|
+
import type { Editor } from '@adecore/editor';
|
|
3
|
+
import { diffSegments, emphasisOf } from './proposal-model.ts';
|
|
4
|
+
|
|
5
|
+
export interface ChangeReviewProps {
|
|
6
|
+
editor: Editor;
|
|
7
|
+
selected: string;
|
|
8
|
+
proposal: string;
|
|
9
|
+
startLine?: number;
|
|
10
|
+
actions?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ChangeReview({ editor, selected, proposal, startLine = 1, actions, className, label }: ChangeReviewProps) {
|
|
16
|
+
const code = useRef<HTMLDivElement>(null);
|
|
17
|
+
useLayoutEffect(() => {
|
|
18
|
+
const element = code.current;
|
|
19
|
+
if (element === null) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
element.replaceChildren();
|
|
23
|
+
for (const segment of diffSegments(selected, proposal, startLine)) {
|
|
24
|
+
const row = element.ownerDocument.createElement('div');
|
|
25
|
+
element.append(row);
|
|
26
|
+
editor.renderCode(row, segment.text, {
|
|
27
|
+
firstLine: segment.firstLine,
|
|
28
|
+
...(segment.kind === 'same'
|
|
29
|
+
? {}
|
|
30
|
+
: { sign: segment.kind === 'removed' ? '-' : '+', color: segment.kind === 'removed' ? '--editor-deleted' : '--editor-added' }),
|
|
31
|
+
faded: segment.kind === 'removed',
|
|
32
|
+
emphasis: emphasisOf(segment)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return () => element.replaceChildren();
|
|
36
|
+
}, [editor, selected, proposal, startLine]);
|
|
37
|
+
return (
|
|
38
|
+
<section className={className} aria-label={label}>
|
|
39
|
+
<div ref={code} />
|
|
40
|
+
{actions}
|
|
41
|
+
</section>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { GitCommitHorizontal, User, Users } from 'lucide-react';
|
|
4
|
+
import { Button, Icon, Popover, Separator, Tooltip, useNow } from '@adecore/ui';
|
|
5
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
6
|
+
import { relativeTime } from './relative-time.ts';
|
|
7
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
8
|
+
import type { AuthorsView } from './popups.ts';
|
|
9
|
+
|
|
10
|
+
const MINUTE_MS = 60_000;
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Who wrote a declaration, by lines, and the newest commit that touched it. It opens beside the entry that
|
|
14
|
+
* was pressed and closes on Escape or a press anywhere else; Escape hands the keyboard back to the editor.
|
|
15
|
+
*/
|
|
16
|
+
export function CodeAuthorsCard({ language, view }: { language: EditorLanguage; view: AuthorsView }) {
|
|
17
|
+
const { t } = useTranslation('editor');
|
|
18
|
+
const now = Math.floor(useNow(MINUTE_MS) / 1000);
|
|
19
|
+
const { authors, uncommittedLines, latest } = view.authorship;
|
|
20
|
+
const anchor = useMemo(() => {
|
|
21
|
+
const { left, top, right, bottom } = view.anchor;
|
|
22
|
+
return { getBoundingClientRect: () => new DOMRect(left, top, right - left, bottom - top) };
|
|
23
|
+
}, [view.anchor]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Popover.Root open onOpenChange={(open, details) => !open && language.codeVision.closeAuthors(details.reason === 'escape-key')}>
|
|
27
|
+
<Popover.Popup anchor={anchor} side="bottom" align="start" sideOffset={4} className="flex w-72 flex-col gap-2 p-3 text-xs">
|
|
28
|
+
<Popover.Title className="flex items-center gap-1.5 font-medium text-text">
|
|
29
|
+
<Icon icon={authors.length > 1 ? Users : User} size={14} className="shrink-0 text-text-muted" />
|
|
30
|
+
<span className="min-w-0 truncate">{view.name}</span>
|
|
31
|
+
</Popover.Title>
|
|
32
|
+
<ul className="flex flex-col gap-1">
|
|
33
|
+
{authors.map((author) => (
|
|
34
|
+
<li key={author.name} className="flex items-baseline gap-2">
|
|
35
|
+
<Tooltip label={author.email}>
|
|
36
|
+
<span className="min-w-0 grow truncate text-text">{author.name}</span>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
<span className="shrink-0 text-text-muted">
|
|
39
|
+
{t('language.codeVision.lines', { count: author.lines, formatted: formatNumber(author.lines) })}
|
|
40
|
+
</span>
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
43
|
+
{uncommittedLines > 0 && (
|
|
44
|
+
<li className="flex items-baseline gap-2 text-text-muted">
|
|
45
|
+
<span className="min-w-0 grow truncate">{t('language.codeVision.uncommitted')}</span>
|
|
46
|
+
<span className="shrink-0">
|
|
47
|
+
{t('language.codeVision.lines', { count: uncommittedLines, formatted: formatNumber(uncommittedLines) })}
|
|
48
|
+
</span>
|
|
49
|
+
</li>
|
|
50
|
+
)}
|
|
51
|
+
</ul>
|
|
52
|
+
{latest !== null && (
|
|
53
|
+
<>
|
|
54
|
+
<Separator />
|
|
55
|
+
<div className="flex flex-col gap-1">
|
|
56
|
+
<span className="text-text-muted">{t('language.codeVision.latest')}</span>
|
|
57
|
+
<span className="text-text select-text">{latest.summary}</span>
|
|
58
|
+
<span className="flex items-center gap-1.5 text-text-muted">
|
|
59
|
+
<span className="font-mono select-text">{latest.shortHash}</span>
|
|
60
|
+
<span className="min-w-0 truncate">{latest.author}</span>
|
|
61
|
+
<span className="shrink-0">{relativeTime(latest.at, now)}</span>
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
{view.openCommit !== null && (
|
|
65
|
+
<div className="flex justify-end">
|
|
66
|
+
<Popover.Close
|
|
67
|
+
render={
|
|
68
|
+
<Button size="xs" variant="secondary" onClick={view.openCommit}>
|
|
69
|
+
<Icon icon={GitCommitHorizontal} size={14} />
|
|
70
|
+
{t('language.codeVision.showCommit')}
|
|
71
|
+
</Button>
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
</Popover.Popup>
|
|
79
|
+
</Popover.Root>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -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
|
+
}
|