@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,20 @@
|
|
|
1
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
|
+
const NONE = { error: 0, warning: 0, info: 0 };
|
|
3
|
+
function countsStore(language) {
|
|
4
|
+
let current = language?.diagnostics.counts() ?? NONE;
|
|
5
|
+
return {
|
|
6
|
+
get: () => {
|
|
7
|
+
const next = language?.diagnostics.counts() ?? NONE;
|
|
8
|
+
if (next.error !== current.error || next.warning !== current.warning || next.info !== current.info) {
|
|
9
|
+
current = next;
|
|
10
|
+
}
|
|
11
|
+
return current;
|
|
12
|
+
},
|
|
13
|
+
subscribe: (listener) => language?.diagnostics.onChange(listener) ?? (() => undefined)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/* How many errors, warnings and infos the file has, said again when the servers report. */
|
|
17
|
+
export function useProblemCounts(language) {
|
|
18
|
+
const store = useMemo(() => countsStore(language), [language]);
|
|
19
|
+
return useSyncExternalStore(store.subscribe, store.get);
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EditorFolds, FoldRole } from '@adecore/editor';
|
|
2
|
+
export interface RevealLineRequest {
|
|
3
|
+
readonly line: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ViewState {
|
|
6
|
+
readonly scrollTop: number;
|
|
7
|
+
readonly line: number;
|
|
8
|
+
readonly column: number;
|
|
9
|
+
readonly folds: EditorFolds;
|
|
10
|
+
}
|
|
11
|
+
export declare const viewStates: Map<string, ViewState>;
|
|
12
|
+
export declare function openingPlace(reveal: RevealLineRequest | null, last: ViewState | undefined, placeholderScroll: number, foldDefaults: readonly FoldRole[]): {
|
|
13
|
+
line?: number;
|
|
14
|
+
column?: number;
|
|
15
|
+
scrollTop?: number;
|
|
16
|
+
folds?: EditorFolds;
|
|
17
|
+
foldDefaults?: readonly FoldRole[];
|
|
18
|
+
};
|
|
19
|
+
export declare function followViewStates(endpointId: string, from: string, to: string): void;
|
|
20
|
+
export declare function viewStateKey(key: string): string;
|
|
21
|
+
export declare function forgetMovesFrom(key: string): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function endpointKey(namespace, path) {
|
|
2
|
+
return `${namespace}:${path}`;
|
|
3
|
+
}
|
|
4
|
+
/* Where each file's editor was when it went, so switching tabs or a node scrolling back into view opens it there again. Per window, never stored. */
|
|
5
|
+
export const viewStates = new Map();
|
|
6
|
+
/*
|
|
7
|
+
* A line asked for wins; otherwise where this file's editor last was, and for a file opened the first time, where the
|
|
8
|
+
* viewer before it was scrolled and with the kinds of fold the settings choose folded. What was folded comes back
|
|
9
|
+
* whichever way it opens.
|
|
10
|
+
*/
|
|
11
|
+
export function openingPlace(reveal, last, placeholderScroll, foldDefaults) {
|
|
12
|
+
const folds = last === undefined ? { foldDefaults } : { folds: last.folds };
|
|
13
|
+
if (reveal !== null) {
|
|
14
|
+
return { line: reveal.line, ...folds };
|
|
15
|
+
}
|
|
16
|
+
if (last !== undefined) {
|
|
17
|
+
return { line: last.line, column: last.column, scrollTop: last.scrollTop, ...folds };
|
|
18
|
+
}
|
|
19
|
+
return { scrollTop: placeholderScroll, ...folds };
|
|
20
|
+
}
|
|
21
|
+
/* Where a place the editors know by the key it had goes now: a file, or the files of a folder that moved. */
|
|
22
|
+
const moves = [];
|
|
23
|
+
function movedKey(key, from, to) {
|
|
24
|
+
if (key === from) {
|
|
25
|
+
return to;
|
|
26
|
+
}
|
|
27
|
+
return key.startsWith(`${from}/`) ? `${to}${key.slice(from.length)}` : null;
|
|
28
|
+
}
|
|
29
|
+
/*
|
|
30
|
+
* A file or folder moved, so where its editors were follows it. An editor that is still open writes its place once
|
|
31
|
+
* more under the key it mounted with as it closes, which `viewStateKey` sends to the new one; a new editor on the
|
|
32
|
+
* old path forgets the move.
|
|
33
|
+
*/
|
|
34
|
+
export function followViewStates(endpointId, from, to) {
|
|
35
|
+
const oldKey = endpointKey(endpointId, from);
|
|
36
|
+
const newKey = endpointKey(endpointId, to);
|
|
37
|
+
for (const [key, state] of [...viewStates]) {
|
|
38
|
+
const target = movedKey(key, oldKey, newKey);
|
|
39
|
+
if (target !== null) {
|
|
40
|
+
viewStates.delete(key);
|
|
41
|
+
viewStates.set(target, state);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
moves.push({ from: oldKey, to: newKey });
|
|
45
|
+
}
|
|
46
|
+
/* The key an editor that mounted under `key` leaves its place under. */
|
|
47
|
+
export function viewStateKey(key) {
|
|
48
|
+
for (const { from, to } of moves) {
|
|
49
|
+
const target = movedKey(key, from, to);
|
|
50
|
+
if (target !== null) {
|
|
51
|
+
return target;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return key;
|
|
55
|
+
}
|
|
56
|
+
/* An editor mounts under `key`, which is a file of its own again whatever moved away from there. */
|
|
57
|
+
export function forgetMovesFrom(key) {
|
|
58
|
+
for (let i = moves.length - 1; i >= 0; i--) {
|
|
59
|
+
if (movedKey(key, moves[i].from, moves[i].to) !== null) {
|
|
60
|
+
moves.splice(i, 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function replacedWords(removed: readonly string[], added: readonly string[]): Array<Array<[number, number]>>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { diffLines } from '@adecore/merge';
|
|
2
|
+
/* A word, a run of spaces or one other character, so a change inside a line is found by word and not by letter. */
|
|
3
|
+
const TOKEN = /\w+|\s+|[^\w\s]/g;
|
|
4
|
+
/*
|
|
5
|
+
* The character ranges of each removed line that the added lines changed, `[start, end)` per line, for
|
|
6
|
+
* the stronger tint on the words a change replaced. Lines are only compared when as many were added as
|
|
7
|
+
* removed, since then each removed line has the line it became; any other change has no such pairing
|
|
8
|
+
* and draws no words.
|
|
9
|
+
*/
|
|
10
|
+
export function replacedWords(removed, added) {
|
|
11
|
+
return removed.map((line, index) => (removed.length === added.length ? replacedRanges(line, added[index]) : []));
|
|
12
|
+
}
|
|
13
|
+
function replacedRanges(removed, added) {
|
|
14
|
+
const before = removed.match(TOKEN) ?? [];
|
|
15
|
+
const after = added.match(TOKEN) ?? [];
|
|
16
|
+
const starts = [];
|
|
17
|
+
let offset = 0;
|
|
18
|
+
for (const token of before) {
|
|
19
|
+
starts.push(offset);
|
|
20
|
+
offset += token.length;
|
|
21
|
+
}
|
|
22
|
+
starts.push(offset);
|
|
23
|
+
const ranges = [];
|
|
24
|
+
for (const change of diffLines(before, after)) {
|
|
25
|
+
const text = removed.slice(starts[change.baseStart], starts[change.baseEnd]);
|
|
26
|
+
if (text.trim() !== '') {
|
|
27
|
+
const start = starts[change.baseStart] + (text.length - text.trimStart().length);
|
|
28
|
+
ranges.push([start, start + text.trim().length]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return ranges;
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Editor } from '@adecore/editor';
|
|
2
|
+
import { type ApplyWorkspaceEditResult, type RenameFile, type TextEdit, type WorkspaceEdit } from '@adecore/lsp';
|
|
3
|
+
import type { DiskText } from './host-types.ts';
|
|
4
|
+
export interface StagedFile {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly disk: DiskText;
|
|
7
|
+
readonly text: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectFiles {
|
|
10
|
+
read(path: string): Promise<DiskText | null>;
|
|
11
|
+
stage(files: readonly StagedFile[]): void;
|
|
12
|
+
save(files: readonly StagedFile[]): Promise<string | null>;
|
|
13
|
+
rename(from: string, to: string): Promise<string | null>;
|
|
14
|
+
}
|
|
15
|
+
export interface WorkspaceEditHost {
|
|
16
|
+
editorOf(uri: string): Editor | undefined;
|
|
17
|
+
readonly files: ProjectFiles | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function entriesOf(edit: WorkspaceEdit): Map<string, TextEdit[][]> | null;
|
|
20
|
+
export declare function renamesOf(edit: WorkspaceEdit): RenameFile[];
|
|
21
|
+
export declare function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEditHost): Promise<ApplyWorkspaceEditResult>;
|
|
@@ -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.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.17.0-beta.2",
|
|
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.2",
|
|
61
|
+
"@adecore/lsp": "0.17.0-beta.2",
|
|
62
|
+
"@adecore/merge": "0.17.0-beta.2",
|
|
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.2",
|
|
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
|
+
}
|