xgen-dex-ide 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/components/ActivityBar.d.ts +21 -0
- package/dist/components/ActivityBar.d.ts.map +1 -0
- package/dist/components/ActivityBar.js +14 -0
- package/dist/components/ActivityBar.js.map +1 -0
- package/dist/components/EditorArea.d.ts +5 -0
- package/dist/components/EditorArea.d.ts.map +1 -0
- package/dist/components/EditorArea.js +468 -0
- package/dist/components/EditorArea.js.map +1 -0
- package/dist/components/Explorer.d.ts +2 -0
- package/dist/components/Explorer.d.ts.map +1 -0
- package/dist/components/Explorer.js +349 -0
- package/dist/components/Explorer.js.map +1 -0
- package/dist/components/IdeView.d.ts +24 -0
- package/dist/components/IdeView.d.ts.map +1 -0
- package/dist/components/IdeView.js +359 -0
- package/dist/components/IdeView.js.map +1 -0
- package/dist/components/QuickOpen.d.ts +2 -0
- package/dist/components/QuickOpen.d.ts.map +1 -0
- package/dist/components/QuickOpen.js +204 -0
- package/dist/components/QuickOpen.js.map +1 -0
- package/dist/components/ScmView.d.ts +6 -0
- package/dist/components/ScmView.d.ts.map +1 -0
- package/dist/components/ScmView.js +283 -0
- package/dist/components/ScmView.js.map +1 -0
- package/dist/components/SearchView.d.ts +2 -0
- package/dist/components/SearchView.d.ts.map +1 -0
- package/dist/components/SearchView.js +97 -0
- package/dist/components/SearchView.js.map +1 -0
- package/dist/components/StatusBar.d.ts +2 -0
- package/dist/components/StatusBar.d.ts.map +1 -0
- package/dist/components/StatusBar.js +71 -0
- package/dist/components/StatusBar.js.map +1 -0
- package/dist/components/TerminalPanel.d.ts +6 -0
- package/dist/components/TerminalPanel.d.ts.map +1 -0
- package/dist/components/TerminalPanel.js +101 -0
- package/dist/components/TerminalPanel.js.map +1 -0
- package/dist/components/hooks.d.ts +25 -0
- package/dist/components/hooks.d.ts.map +1 -0
- package/dist/components/hooks.js +99 -0
- package/dist/components/hooks.js.map +1 -0
- package/dist/components/icons.d.ts +23 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +123 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/primitives.d.ts +57 -0
- package/dist/components/primitives.d.ts.map +1 -0
- package/dist/components/primitives.js +263 -0
- package/dist/components/primitives.js.map +1 -0
- package/dist/fuzzy.d.ts +17 -0
- package/dist/fuzzy.d.ts.map +1 -0
- package/dist/fuzzy.js +62 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/git-model.d.ts +104 -0
- package/dist/git-model.d.ts.map +1 -0
- package/dist/git-model.js +126 -0
- package/dist/git-model.js.map +1 -0
- package/dist/ide.css +2203 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +33 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +98 -0
- package/dist/keys.js.map +1 -0
- package/dist/paths.d.ts +14 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +66 -0
- package/dist/paths.js.map +1 -0
- package/dist/store.d.ts +406 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +1464 -0
- package/dist/store.js.map +1 -0
- package/dist/terminal.d.ts +60 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +293 -0
- package/dist/terminal.js.map +1 -0
- package/dist/text.d.ts +26 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +76 -0
- package/dist/text.js.map +1 -0
- package/dist/tree.d.ts +31 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +105 -0
- package/dist/tree.js.map +1 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# xgen-dex-ide
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npm i xgen-dex-ide monaco-editor
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
> 모노레포(`PlateerLab/xgen-dex-core`) 안에서는 `@dex/ide` 라는 별칭으로 쓴다. 같은 코드이고,
|
|
8
|
+
> 밖으로 나갈 때만 이 이름이다.
|
|
9
|
+
|
|
10
|
+
채팅 안의 IDE 보기. 에이전트가 이미 가진 것(스토리지·샌드박스·git)을 편집기처럼 보여 준다.
|
|
11
|
+
|
|
12
|
+
| 자리 | 무엇 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| 왼쪽 | 활동 막대와 사이드바: 탐색기(스토리지 그대로)·찾기·소스 제어. 보기는 더할 수 있다(`extraViews`) |
|
|
15
|
+
| 가운데 | 편집기 탭(Monaco, 최대 3묶음 나누기)·비교 화면·그림 미리보기, 아래 터미널 |
|
|
16
|
+
| 오른쪽 | 에이전트 채팅(호스트가 넘긴다) |
|
|
17
|
+
|
|
18
|
+
편집기와 같은 쓰임새를 목표로 한다: 미리보기 탭, 끌어서 옮기기, 빠른 열기(`Ctrl+P`), 명령(`Ctrl+Shift+P`),
|
|
19
|
+
저장 충돌 비교, 파일에서 찾기·바꾸기, 스테이지·커밋·푸시·브랜치·스태시, 연결이 끊겨도 이어지는 터미널.
|
|
20
|
+
|
|
21
|
+
## 쓰는 법
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { IdeStore, IdeView, type IdeHost } from '@dex/ide'; // 밖에서는 'xgen-dex-ide'
|
|
25
|
+
import '@dex/ide/ide.css';
|
|
26
|
+
|
|
27
|
+
const store = new IdeStore(host); // 에이전트마다 하나. 채팅 탭을 닫을 때 store.dispose()
|
|
28
|
+
|
|
29
|
+
<IdeView store={store} chat={<Chat />} theme="dark" />;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`IdeStore` 는 화면과 떨어져 산다 — 탭을 바꿔 IDE 가 다시 그려져도 고친 버퍼·되돌리기 기록·터미널
|
|
33
|
+
연결이 남는다. 호스트는 `IdeHost` 를 구현한다:
|
|
34
|
+
|
|
35
|
+
- `listFiles` · `subscribeChanges` — 스토리지 목록과 변경 알림(탐색기)
|
|
36
|
+
- `readFile` · `saveFile(path, bytes, baseSha)` · `stat` · `readRaw` · `fs` — 샌드박스 파일(저장은 연 판의 sha 가 조건, 어긋나면 `IdeError('changed')`)
|
|
37
|
+
- `search` · `replace` · `git` — 샌드박스 안에서 찾기·바꾸기·git
|
|
38
|
+
- `terminals` · `closeTerminal` · `openTerminal(id, size, handlers)` — 터미널 소켓(같은 id 로 다시 붙으면 이어진다)
|
|
39
|
+
- `loadMonaco` — Monaco 와 그 worker 는 호스트의 번들러가 맡는다
|
|
40
|
+
- `storage` · `notify` · `openExternal` · `download` · `copyText` — 선택
|
|
41
|
+
|
|
42
|
+
색은 `.xide-root` 의 `--xide-*` 변수로 바꾼다. xterm 은 처음 터미널을 열 때 불러온다(서버 렌더 안전).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 활동 막대 — 사이드바 보기(탐색기·찾기·소스 제어)를 고른다. 아래쪽은 채팅·터미널.
|
|
3
|
+
*
|
|
4
|
+
* 보기는 등록부(`views`)로 받는다. 지금은 셋이지만 확장(테스트·환경 변수·미리보기 등)이
|
|
5
|
+
* 같은 자리에 들어올 수 있게 IdeView 가 목록을 넘긴다.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactElement } from 'react';
|
|
8
|
+
import { type IconName } from './icons.js';
|
|
9
|
+
import type { IdeState } from '../store.js';
|
|
10
|
+
export interface SideView {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
icon: IconName;
|
|
14
|
+
keybinding?: string;
|
|
15
|
+
badge?: (s: IdeState) => number;
|
|
16
|
+
render: () => ReactElement;
|
|
17
|
+
}
|
|
18
|
+
export declare function ActivityBar({ views }: {
|
|
19
|
+
views: SideView[];
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=ActivityBar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActivityBar.d.ts","sourceRoot":"","sources":["../../src/components/ActivityBar.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,MAAM,CAAC;IAChC,MAAM,EAAE,MAAM,YAAY,CAAC;CAC5B;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,+BAmD3D"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from './icons.js';
|
|
3
|
+
import { useIde, useStore } from './hooks.js';
|
|
4
|
+
import { formatBinding } from '../keys.js';
|
|
5
|
+
export function ActivityBar({ views }) {
|
|
6
|
+
const store = useStore();
|
|
7
|
+
const side = useIde((s) => s.layout.sideView);
|
|
8
|
+
const chatOpen = useIde((s) => s.layout.chatOpen);
|
|
9
|
+
const panelOpen = useIde((s) => s.layout.panelOpen);
|
|
10
|
+
const readonly = useIde((s) => s.readonly);
|
|
11
|
+
const badges = useIde((s) => views.map((v) => (v.badge ? v.badge(s) : 0)));
|
|
12
|
+
return (_jsxs("nav", { className: "xide-activity", "aria-label": "\uBCF4\uAE30", children: [_jsx("div", { className: "xide-activity-top", children: views.map((v, i) => (_jsxs("button", { type: "button", className: `xide-activity-btn${side === v.id ? ' xide--active' : ''}`, title: v.keybinding ? `${v.title} (${formatBinding(v.keybinding)})` : v.title, "aria-label": v.title, "aria-pressed": side === v.id, onClick: () => store.toggleSideView(v.id), children: [_jsx(Icon, { name: v.icon, size: 22 }), badges[i] ? _jsx("span", { className: "xide-activity-badge", children: badges[i] > 999 ? '999+' : badges[i] }) : null] }, v.id))) }), _jsxs("div", { className: "xide-activity-bottom", children: [!readonly ? (_jsx("button", { type: "button", className: `xide-activity-btn${panelOpen ? ' xide--active' : ''}`, title: `터미널 (${formatBinding('Mod+`')})`, "aria-label": "\uD130\uBBF8\uB110", "aria-pressed": panelOpen, onClick: () => (panelOpen ? store.togglePanel() : store.showTerminal()), children: _jsx(Icon, { name: "terminal", size: 22 }) })) : null, _jsx("button", { type: "button", className: `xide-activity-btn${chatOpen ? ' xide--active' : ''}`, title: "\uC5D0\uC774\uC804\uD2B8 \uCC44\uD305", "aria-label": "\uC5D0\uC774\uC804\uD2B8 \uCC44\uD305", "aria-pressed": chatOpen, onClick: () => store.setLayout({ chatOpen: !chatOpen }), children: _jsx(Icon, { name: "chat", size: 22 }) })] })] }));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=ActivityBar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActivityBar.js","sourceRoot":"","sources":["../../src/components/ActivityBar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorArea.d.ts","sourceRoot":"","sources":["../../src/components/EditorArea.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,+BA8BzD"}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* 편집기 영역 — 편집기 묶음(최대 3개, 좌우로 나눔)마다 탭 줄과 편집기 하나.
|
|
4
|
+
*
|
|
5
|
+
* Monaco 편집기는 묶음마다 **하나**를 두고 탭을 바꿀 때 모델만 갈아 끼운다(편집기와 같은
|
|
6
|
+
* 방식). 탭마다 스크롤·커서를 기억했다가 돌아오면 되살린다. 문서(모델)는 저장소가 들고
|
|
7
|
+
* 있어 화면이 다시 그려져도 고친 내용과 되돌리기 기록이 남는다.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import { FileIcon, Icon } from './icons.js';
|
|
11
|
+
import { useIde, useStore } from './hooks.js';
|
|
12
|
+
import { Empty, IconButton, SplitHandle, showMenu } from './primitives.js';
|
|
13
|
+
import { diskUri, modelUri, originalUri, stagedUri } from '../store.js';
|
|
14
|
+
import { basename, dirname } from '../paths.js';
|
|
15
|
+
import { decodeText, formatBytes, imageMime } from '../text.js';
|
|
16
|
+
import { formatBinding } from '../keys.js';
|
|
17
|
+
import { STATUS_LABEL } from '../git-model.js';
|
|
18
|
+
export function EditorArea({ theme }) {
|
|
19
|
+
const groups = useIde((s) => s.groups);
|
|
20
|
+
const [weights, setWeights] = useState({});
|
|
21
|
+
const ref = useRef(null);
|
|
22
|
+
const total = groups.reduce((n, g) => n + (weights[g.id] ?? 1), 0);
|
|
23
|
+
return (_jsx("div", { className: "xide-editor-area", ref: ref, children: groups.map((g, i) => (_jsxs("div", { className: "xide-editor-group-wrap", style: { flex: `${weights[g.id] ?? 1} 1 0` }, children: [i > 0 ? (_jsx(SplitHandle, { axis: "x", label: "\uD3B8\uC9D1\uAE30 \uB108\uBE44", value: Math.round(((weights[groups[i - 1].id] ?? 1) / total) * 1000), min: 150, max: 850, onChange: (v) => {
|
|
24
|
+
const a = groups[i - 1].id;
|
|
25
|
+
const b = g.id;
|
|
26
|
+
const pair = (weights[a] ?? 1) + (weights[b] ?? 1);
|
|
27
|
+
const left = Math.max(0.15, Math.min(pair - 0.15, (v / 1000) * total));
|
|
28
|
+
setWeights((w) => ({ ...w, [a]: left, [b]: pair - left }));
|
|
29
|
+
} })) : null, _jsx(EditorGroupView, { group: g, index: i, theme: theme })] }, g.id))) }));
|
|
30
|
+
}
|
|
31
|
+
function EditorGroupView({ group, index, theme }) {
|
|
32
|
+
const store = useStore();
|
|
33
|
+
const activeGroup = useIde((s) => s.activeGroup);
|
|
34
|
+
const tab = group.tabs.find((t) => t.id === group.activeId) ?? null;
|
|
35
|
+
return (_jsxs("section", { className: `xide-editor-group${activeGroup === group.id ? ' xide--focused' : ''}`, "aria-label": `편집기 ${index + 1}`, onMouseDownCapture: () => store.setActiveGroup(group.id), onFocusCapture: () => store.setActiveGroup(group.id), children: [_jsx(TabBar, { group: group }), tab ? _jsx(Breadcrumbs, { tab: tab }) : null, _jsx("div", { className: "xide-editor-body", children: tab ? _jsx(TabContent, { group: group, tab: tab, theme: theme }, tab.kind === 'diff' ? 'diff' : tab.kind === 'image' ? `img:${tab.path}` : 'code') : _jsx(Welcome, {}) })] }));
|
|
36
|
+
}
|
|
37
|
+
// ── 탭 줄 ─────────────────────────────────────────────────────────────
|
|
38
|
+
function TabBar({ group }) {
|
|
39
|
+
const store = useStore();
|
|
40
|
+
const docs = useIde((s) => s.docs);
|
|
41
|
+
const groupCount = useIde((s) => s.groups.length);
|
|
42
|
+
const layout = useIde((s) => s.layout);
|
|
43
|
+
const [dropAt, setDropAt] = useState(null);
|
|
44
|
+
const scroller = useRef(null);
|
|
45
|
+
const names = useMemo(() => {
|
|
46
|
+
const count = new Map();
|
|
47
|
+
for (const t of group.tabs)
|
|
48
|
+
count.set(basename(t.path), (count.get(basename(t.path)) ?? 0) + 1);
|
|
49
|
+
return count;
|
|
50
|
+
}, [group.tabs]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const el = scroller.current?.querySelector('.xide-tab.xide--active');
|
|
53
|
+
el?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
54
|
+
}, [group.activeId]);
|
|
55
|
+
const tabMenu = (t, e) => showMenu(e, [
|
|
56
|
+
{ id: 'close', label: '닫기', keybinding: 'Mod+W', run: () => void store.closeTab(group.id, t.id) },
|
|
57
|
+
{ id: 'others', label: '다른 탭 닫기', run: () => void store.closeTabs(group.id, 'others', t.id) },
|
|
58
|
+
{ id: 'right', label: '오른쪽 탭 닫기', run: () => void store.closeTabs(group.id, 'right', t.id) },
|
|
59
|
+
{ id: 'saved', label: '저장된 탭 닫기', run: () => void store.closeTabs(group.id, 'saved') },
|
|
60
|
+
{ id: 'all', label: '모두 닫기', run: () => void store.closeTabs(group.id, 'all') },
|
|
61
|
+
'separator',
|
|
62
|
+
{ id: 'copy', label: '경로 복사', run: () => void copyPath(store, t.path) },
|
|
63
|
+
{
|
|
64
|
+
id: 'reveal',
|
|
65
|
+
label: '탐색기에서 보기',
|
|
66
|
+
run: () => {
|
|
67
|
+
store.reveal(t.path);
|
|
68
|
+
store.showSideView('explorer');
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
'separator',
|
|
72
|
+
{ id: 'pin', label: '고정', disabled: !t.preview, run: () => store.pinTab(group.id, t.id) },
|
|
73
|
+
{ id: 'split', label: '오른쪽으로 나누기', keybinding: 'Mod+\\', run: () => store.splitRight({ tabId: t.id }) },
|
|
74
|
+
]);
|
|
75
|
+
const onDragStart = (t, e) => {
|
|
76
|
+
e.dataTransfer.setData('application/x-xide-tab', JSON.stringify({ group: group.id, tab: t.id }));
|
|
77
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
78
|
+
};
|
|
79
|
+
const onDragOver = (i, e) => {
|
|
80
|
+
if (!Array.from(e.dataTransfer.types).includes('application/x-xide-tab'))
|
|
81
|
+
return;
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
84
|
+
setDropAt(e.clientX < r.left + r.width / 2 ? i : i + 1);
|
|
85
|
+
};
|
|
86
|
+
const onDrop = (e) => {
|
|
87
|
+
const raw = e.dataTransfer.getData('application/x-xide-tab');
|
|
88
|
+
const at = dropAt ?? group.tabs.length;
|
|
89
|
+
setDropAt(null);
|
|
90
|
+
if (!raw)
|
|
91
|
+
return;
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
const { group: from, tab } = JSON.parse(raw);
|
|
94
|
+
store.moveTab(from, tab, group.id, at);
|
|
95
|
+
};
|
|
96
|
+
const active = group.tabs.find((t) => t.id === group.activeId);
|
|
97
|
+
const activeDoc = active ? docs[active.path] : undefined;
|
|
98
|
+
return (_jsxs("div", { className: "xide-tabbar", onDragLeave: () => setDropAt(null), onDrop: onDrop, onDragOver: (e) => onDragOver(group.tabs.length - 1, e), children: [_jsx("div", { className: "xide-tabs", ref: scroller, role: "tablist", onWheel: (e) => {
|
|
99
|
+
if (scroller.current && Math.abs(e.deltaY) > Math.abs(e.deltaX))
|
|
100
|
+
scroller.current.scrollLeft += e.deltaY;
|
|
101
|
+
}, onDoubleClick: (e) => {
|
|
102
|
+
if (e.target === e.currentTarget && !store.getState().readonly)
|
|
103
|
+
store.startCreate('', 'file');
|
|
104
|
+
}, children: group.tabs.map((t, i) => {
|
|
105
|
+
const doc = docs[t.path];
|
|
106
|
+
const name = basename(t.path);
|
|
107
|
+
const dup = (names.get(name) ?? 0) > 1;
|
|
108
|
+
const dirty = t.kind === 'file' && !!doc?.dirty;
|
|
109
|
+
const label = t.kind === 'diff'
|
|
110
|
+
? t.diff?.disk
|
|
111
|
+
? `${name} (디스크 ↔ 편집 중)`
|
|
112
|
+
: `${name} (${t.diff?.staged ? '스테이지' : '작업 트리'})`
|
|
113
|
+
: name;
|
|
114
|
+
return (_jsxs("div", { role: "tab", "aria-selected": t.id === group.activeId, className: [
|
|
115
|
+
'xide-tab',
|
|
116
|
+
t.id === group.activeId ? 'xide--active' : '',
|
|
117
|
+
t.preview ? 'xide--preview' : '',
|
|
118
|
+
dirty ? 'xide--dirty' : '',
|
|
119
|
+
dropAt === i ? 'xide--drop-before' : '',
|
|
120
|
+
dropAt === i + 1 && i === group.tabs.length - 1 ? 'xide--drop-after' : '',
|
|
121
|
+
]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join(' '), title: `${t.path}${t.kind === 'diff' && t.diff?.status ? ` · ${STATUS_LABEL[t.diff.status]}` : ''}`, draggable: true, onDragStart: (e) => onDragStart(t, e), onDragOver: (e) => {
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
onDragOver(i, e);
|
|
126
|
+
}, onClick: () => store.activateTab(group.id, t.id), onDoubleClick: () => store.pinTab(group.id, t.id), onAuxClick: (e) => {
|
|
127
|
+
if (e.button === 1) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
void store.closeTab(group.id, t.id);
|
|
130
|
+
}
|
|
131
|
+
}, onContextMenu: (e) => tabMenu(t, e), children: [t.kind === 'diff' ? _jsx(Icon, { name: "split", className: "xide-icon xide-tab-diff" }) : _jsx(FileIcon, { name: name }), _jsx("span", { className: "xide-tab-label", children: label }), dup ? _jsx("span", { className: "xide-tab-dir", children: dirname(t.path) || '/' }) : null, _jsxs("button", { type: "button", className: "xide-tab-close", "aria-label": dirty ? '저장하지 않은 변경이 있습니다. 닫기' : '닫기', title: `닫기 (${formatBinding('Mod+W')})`, onClick: (e) => {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
void store.closeTab(group.id, t.id);
|
|
134
|
+
}, children: [_jsx(Icon, { name: dirty ? 'dot' : 'close', className: "xide-icon xide-tab-close-icon" }), dirty ? _jsx(Icon, { name: "close", className: "xide-icon xide-tab-close-hover" }) : null] })] }, t.id));
|
|
135
|
+
}) }), _jsxs("div", { className: "xide-tabbar-actions", children: [active?.kind === 'diff' && active.diff && !active.diff.disk ? (_jsx(IconButton, { icon: "go-to-file", label: "\uD30C\uC77C \uC5F4\uAE30", onClick: () => void store.openFile(active.path, { preview: false, groupId: group.id }) })) : null, activeDoc?.dirty ? (_jsx(IconButton, { icon: "check", label: "\uC800\uC7A5", keybinding: "Mod+S", onClick: () => void store.save(active.path) })) : null, _jsx(IconButton, { icon: "split", label: "\uC624\uB978\uCABD\uC73C\uB85C \uB098\uB204\uAE30", keybinding: "Mod+\\\\", disabled: !active || groupCount >= 3, onClick: () => store.splitRight() }), _jsx(IconButton, { icon: "more", label: "\uB354 \uBCF4\uAE30", onClick: (e) => showMenu(e, [
|
|
136
|
+
{ id: 'close-all', label: '모두 닫기', run: () => void store.closeTabs(group.id, 'all') },
|
|
137
|
+
{ id: 'close-saved', label: '저장된 탭 닫기', run: () => void store.closeTabs(group.id, 'saved') },
|
|
138
|
+
'separator',
|
|
139
|
+
{ id: 'wrap', label: layout.wordWrap ? '줄 바꿈 끄기' : '줄 바꿈 켜기', keybinding: 'Alt+Z', run: () => store.setLayout({ wordWrap: !layout.wordWrap }) },
|
|
140
|
+
{ id: 'minimap', label: layout.minimap ? '미니맵 끄기' : '미니맵 켜기', run: () => store.setLayout({ minimap: !layout.minimap }) },
|
|
141
|
+
{ id: 'autosave', label: layout.autoSave ? '자동 저장 끄기' : '자동 저장 켜기', run: () => store.setLayout({ autoSave: !layout.autoSave }) },
|
|
142
|
+
]) })] })] }));
|
|
143
|
+
}
|
|
144
|
+
async function copyPath(store, path) {
|
|
145
|
+
const ok = (await store.host.copyText?.(path)) ?? (await navigator.clipboard?.writeText(path).then(() => true, () => false));
|
|
146
|
+
store.notify(ok ? 'success' : 'error', ok ? '경로를 복사했습니다' : '복사하지 못했습니다');
|
|
147
|
+
}
|
|
148
|
+
function Breadcrumbs({ tab }) {
|
|
149
|
+
const store = useStore();
|
|
150
|
+
const parts = tab.path.split('/');
|
|
151
|
+
return (_jsxs("div", { className: "xide-breadcrumbs", "aria-label": "\uACBD\uB85C", children: [parts.map((p, i) => {
|
|
152
|
+
const path = parts.slice(0, i + 1).join('/');
|
|
153
|
+
const last = i === parts.length - 1;
|
|
154
|
+
return (_jsxs("span", { className: "xide-crumb", children: [_jsxs("button", { type: "button", onClick: () => {
|
|
155
|
+
store.reveal(path);
|
|
156
|
+
store.showSideView('explorer');
|
|
157
|
+
}, children: [last ? _jsx(FileIcon, { name: p }) : null, p] }), !last ? _jsx(Icon, { name: "chevron-right" }) : null] }, path));
|
|
158
|
+
}), tab.kind === 'diff' && tab.diff ? (_jsx("span", { className: "xide-crumb-note", children: tab.diff.disk ? '왼쪽: 디스크의 지금 판 · 오른쪽: 편집 중인 판' : tab.diff.staged ? '왼쪽: 마지막 커밋 · 오른쪽: 스테이지' : '왼쪽: 스테이지 · 오른쪽: 작업 트리' })) : null] }));
|
|
159
|
+
}
|
|
160
|
+
function Welcome() {
|
|
161
|
+
const store = useStore();
|
|
162
|
+
const readonly = useIde((s) => s.readonly);
|
|
163
|
+
const rows = [
|
|
164
|
+
['파일 빨리 열기', 'Mod+P', () => store.openQuickOpen('files')],
|
|
165
|
+
['명령 보기', 'Mod+Shift+P', () => store.openQuickOpen('commands')],
|
|
166
|
+
['찾기', 'Mod+Shift+F', () => store.showSideView('search')],
|
|
167
|
+
['소스 제어', 'Mod+Shift+G', () => store.showSideView('scm')],
|
|
168
|
+
];
|
|
169
|
+
if (!readonly)
|
|
170
|
+
rows.push(['터미널', 'Mod+`', () => store.showTerminal()]);
|
|
171
|
+
return (_jsxs("div", { className: "xide-welcome", children: [_jsx("div", { className: "xide-welcome-title", children: "\uC120\uD0DD\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" }), _jsx("div", { className: "xide-welcome-text", children: "\uD0D0\uC0C9\uAE30\uC5D0\uC11C \uD30C\uC77C\uC744 \uACE0\uB974\uAC70\uB098 \uC18C\uC2A4 \uC81C\uC5B4\uC5D0\uC11C \uBC14\uB010 \uD30C\uC77C\uC744 \uB204\uB974\uC138\uC694." }), _jsx("div", { className: "xide-welcome-keys", children: rows.map(([label, key, run]) => (_jsxs("button", { type: "button", onClick: run, children: [_jsx("span", { children: label }), _jsx("kbd", { children: formatBinding(key) })] }, label))) })] }));
|
|
172
|
+
}
|
|
173
|
+
// ── 탭 내용 ───────────────────────────────────────────────────────────
|
|
174
|
+
function TabContent({ group, tab, theme }) {
|
|
175
|
+
if (tab.kind === 'image')
|
|
176
|
+
return _jsx(ImagePane, { path: tab.path });
|
|
177
|
+
if (tab.kind === 'diff' && tab.diff)
|
|
178
|
+
return _jsx(DiffPane, { group: group, tab: tab, diff: tab.diff, theme: theme });
|
|
179
|
+
return _jsx(CodePane, { group: group, tab: tab, theme: theme });
|
|
180
|
+
}
|
|
181
|
+
function useMonaco() {
|
|
182
|
+
const store = useStore();
|
|
183
|
+
const ready = useIde((s) => s.monacoReady);
|
|
184
|
+
const error = useIde((s) => s.monacoError);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!ready)
|
|
187
|
+
void store.loadMonaco().catch(() => undefined);
|
|
188
|
+
}, [ready, store]);
|
|
189
|
+
return { monaco: ready ? store.getMonaco() : null, error };
|
|
190
|
+
}
|
|
191
|
+
function editorOptions(layout, readOnly) {
|
|
192
|
+
return {
|
|
193
|
+
automaticLayout: true,
|
|
194
|
+
fontSize: layout.fontSize,
|
|
195
|
+
fontFamily: "'JetBrains Mono', 'D2Coding', 'Fira Code', Menlo, Consolas, 'Liberation Mono', monospace",
|
|
196
|
+
minimap: { enabled: layout.minimap },
|
|
197
|
+
wordWrap: layout.wordWrap ? 'on' : 'off',
|
|
198
|
+
readOnly,
|
|
199
|
+
scrollBeyondLastLine: true,
|
|
200
|
+
renderWhitespace: 'selection',
|
|
201
|
+
smoothScrolling: true,
|
|
202
|
+
cursorBlinking: 'smooth',
|
|
203
|
+
bracketPairColorization: { enabled: true },
|
|
204
|
+
guides: { bracketPairs: 'active', indentation: true },
|
|
205
|
+
stickyScroll: { enabled: true },
|
|
206
|
+
fixedOverflowWidgets: true,
|
|
207
|
+
tabSize: 2,
|
|
208
|
+
detectIndentation: true,
|
|
209
|
+
renderLineHighlight: 'all',
|
|
210
|
+
unicodeHighlight: { ambiguousCharacters: false },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function monacoTheme(theme) {
|
|
214
|
+
return theme === 'light' ? 'vs' : 'vs-dark';
|
|
215
|
+
}
|
|
216
|
+
function CodePane({ group, tab, theme }) {
|
|
217
|
+
const store = useStore();
|
|
218
|
+
const { monaco, error } = useMonaco();
|
|
219
|
+
const doc = useIde((s) => s.docs[tab.path]);
|
|
220
|
+
const layout = useIde((s) => s.layout);
|
|
221
|
+
const readonly = useIde((s) => s.readonly);
|
|
222
|
+
const host = useRef(null);
|
|
223
|
+
const editorRef = useRef(null);
|
|
224
|
+
const shownTab = useRef(null);
|
|
225
|
+
// 편집기를 만든다(묶음마다 하나).
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!monaco || !host.current)
|
|
228
|
+
return;
|
|
229
|
+
const editor = monaco.editor.create(host.current, { ...editorOptions(layout, readonly), model: null, theme: monacoTheme(theme) });
|
|
230
|
+
editorRef.current = editor;
|
|
231
|
+
store.editors.set(group.id, editor);
|
|
232
|
+
const subs = [
|
|
233
|
+
editor.onDidChangeCursorSelection(() => {
|
|
234
|
+
const pos = editor.getPosition();
|
|
235
|
+
const model = editor.getModel();
|
|
236
|
+
if (!pos || !model)
|
|
237
|
+
return;
|
|
238
|
+
const sel = editor.getSelections() ?? [];
|
|
239
|
+
const selected = sel.reduce((n, r) => n + (model.getValueInRange(r).length || 0), 0);
|
|
240
|
+
store.setCursor({ line: pos.lineNumber, col: pos.column, selected });
|
|
241
|
+
}),
|
|
242
|
+
editor.onDidFocusEditorWidget(() => store.setActiveGroup(group.id)),
|
|
243
|
+
];
|
|
244
|
+
return () => {
|
|
245
|
+
if (shownTab.current)
|
|
246
|
+
store.viewStates.set(`${group.id}:${shownTab.current}`, editor.saveViewState());
|
|
247
|
+
subs.forEach((d) => d.dispose());
|
|
248
|
+
if (store.editors.get(group.id) === editor)
|
|
249
|
+
store.editors.delete(group.id);
|
|
250
|
+
editor.setModel(null);
|
|
251
|
+
editor.dispose();
|
|
252
|
+
editorRef.current = null;
|
|
253
|
+
shownTab.current = null;
|
|
254
|
+
};
|
|
255
|
+
// 편집기는 한 번만 만든다 — 옵션은 아래 효과가 바꾼다.
|
|
256
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
257
|
+
}, [monaco, group.id]);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
editorRef.current?.updateOptions(editorOptions(layout, readonly));
|
|
260
|
+
}, [layout.fontSize, layout.minimap, layout.wordWrap, readonly]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
monaco?.editor.setTheme(monacoTheme(theme));
|
|
263
|
+
}, [monaco, theme]);
|
|
264
|
+
// 탭이 바뀌거나 문서가 준비되면 모델을 갈아 끼운다.
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const editor = editorRef.current;
|
|
267
|
+
if (!monaco || !editor)
|
|
268
|
+
return;
|
|
269
|
+
const model = doc?.status === 'ready' ? monaco.editor.getModel(modelUri(monaco, store.host.workflowId, tab.path)) : null;
|
|
270
|
+
if (shownTab.current && shownTab.current !== tab.id) {
|
|
271
|
+
store.viewStates.set(`${group.id}:${shownTab.current}`, editor.saveViewState());
|
|
272
|
+
}
|
|
273
|
+
if (editor.getModel() !== model) {
|
|
274
|
+
editor.setModel(model);
|
|
275
|
+
if (model) {
|
|
276
|
+
const vs = store.viewStates.get(`${group.id}:${tab.id}`);
|
|
277
|
+
if (vs)
|
|
278
|
+
editor.restoreViewState(vs);
|
|
279
|
+
if (store.getState().activeGroup === group.id)
|
|
280
|
+
editor.focus();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
shownTab.current = model ? tab.id : null;
|
|
284
|
+
const want = store.pendingReveal;
|
|
285
|
+
if (model && want && want.path === tab.path) {
|
|
286
|
+
store.pendingReveal = null;
|
|
287
|
+
const range = new monaco.Range(want.line, want.col, want.line, want.col + want.length);
|
|
288
|
+
editor.setSelection(range);
|
|
289
|
+
editor.revealRangeInCenterIfOutsideViewport(range);
|
|
290
|
+
editor.focus();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
return (_jsxs("div", { className: "xide-code-pane", children: [doc?.diskChanged && doc.status === 'ready' ? _jsx(DiskChangedBar, { path: tab.path, deleted: !!doc.message }) : null, _jsx("div", { className: "xide-monaco", ref: host, style: { display: doc?.status === 'ready' ? undefined : 'none' } }), error ? _jsx(Empty, { icon: "error", children: error }) : null, !error && (!doc || doc.status === 'loading' || !monaco) ? _jsx("div", { className: "xide-pane-note", children: "\uC5EC\uB294 \uC911" }) : null, doc?.status === 'binary' ? (_jsxs(Empty, { icon: "file", children: [_jsx("p", { children: "\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC544\uB2C8\uAC70\uB098 UTF-8 \uC774 \uC544\uB2C8\uB77C \uD3B8\uC9D1\uAE30\uB85C \uC5F4\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4." }), _jsxs("div", { className: "xide-empty-actions", children: [_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.openFile(tab.path, { forceText: true, preview: false }), children: "\uADF8\uB798\uB3C4 \uC5F4\uAE30" }), store.host.download ? (_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.host.download?.(tab.path), children: "\uB0B4\uB824\uBC1B\uAE30" })) : null] })] })) : null, doc?.status === 'too_large' ? (_jsxs(Empty, { icon: "warning", children: [_jsxs("p", { children: ["\uD30C\uC77C\uC774 \uB108\uBB34 \uCEE4\uC11C(", formatBytes(doc.size), ") \uD3B8\uC9D1\uAE30\uB85C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."] }), store.host.download ? (_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.host.download?.(tab.path), children: "\uB0B4\uB824\uBC1B\uAE30" })) : null] })) : null, doc?.status === 'missing' || doc?.status === 'error' ? (_jsxs(Empty, { icon: "warning", children: [_jsx("p", { children: doc.message ?? '파일을 열지 못했습니다' }), _jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.ensureDoc(tab.path), children: "\uB2E4\uC2DC \uC2DC\uB3C4" })] })) : null] }));
|
|
294
|
+
}
|
|
295
|
+
function DiskChangedBar({ path, deleted }) {
|
|
296
|
+
const store = useStore();
|
|
297
|
+
return (_jsxs("div", { className: "xide-banner warning", role: "alert", children: [_jsx(Icon, { name: "warning" }), _jsx("span", { children: deleted
|
|
298
|
+
? '이 파일이 디스크에서 지워졌습니다. 저장하면 다시 만들어집니다.'
|
|
299
|
+
: '편집하는 사이 에이전트나 터미널이 이 파일을 바꿨습니다.' }), !deleted ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.openDiskCompare(path), children: "\uBE44\uAD50" }), _jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.reloadFromDisk(path, { discard: true }), children: "\uBC14\uB010 \uD310 \uBD88\uB7EC\uC624\uAE30" })] })) : null, _jsx("button", { type: "button", className: "xide-btn xide--danger", onClick: () => void store.save(path, { force: true }), children: deleted ? '다시 저장' : '내 판으로 덮어쓰기' })] }));
|
|
300
|
+
}
|
|
301
|
+
/** git 의 어떤 판(HEAD·스테이지)의 내용 — 없으면 빈 문자열(새 파일). */
|
|
302
|
+
async function gitText(store, diff, ref) {
|
|
303
|
+
const out = await store.host.git({
|
|
304
|
+
op: 'show',
|
|
305
|
+
repo: diff.repo,
|
|
306
|
+
path: diff.path,
|
|
307
|
+
ref,
|
|
308
|
+
});
|
|
309
|
+
if (!out.exists || !out.content_b64)
|
|
310
|
+
return '';
|
|
311
|
+
if (out.too_large)
|
|
312
|
+
return '(파일이 너무 커서 비교하지 않습니다)';
|
|
313
|
+
const bin = atob(out.content_b64);
|
|
314
|
+
const bytes = new Uint8Array(bin.length);
|
|
315
|
+
for (let i = 0; i < bin.length; i += 1)
|
|
316
|
+
bytes[i] = bin.charCodeAt(i);
|
|
317
|
+
return decodeText(bytes).text;
|
|
318
|
+
}
|
|
319
|
+
function setModelText(monaco, uri, text, languageFrom) {
|
|
320
|
+
const existing = monaco.editor.getModel(uri);
|
|
321
|
+
if (existing) {
|
|
322
|
+
if (existing.getValue() !== text)
|
|
323
|
+
existing.setValue(text);
|
|
324
|
+
return existing;
|
|
325
|
+
}
|
|
326
|
+
const lang = monaco.editor.getModel(languageFrom)?.getLanguageId();
|
|
327
|
+
return monaco.editor.createModel(text, lang, uri);
|
|
328
|
+
}
|
|
329
|
+
function DiffPane({ group, tab, diff, theme }) {
|
|
330
|
+
const store = useStore();
|
|
331
|
+
const { monaco, error } = useMonaco();
|
|
332
|
+
const doc = useIde((s) => s.docs[tab.path]);
|
|
333
|
+
const statuses = useIde((s) => s.git.statuses);
|
|
334
|
+
const layout = useIde((s) => s.layout);
|
|
335
|
+
const readonly = useIde((s) => s.readonly);
|
|
336
|
+
const host = useRef(null);
|
|
337
|
+
const editorRef = useRef(null);
|
|
338
|
+
const [inline, setInline] = useState(false);
|
|
339
|
+
const [loadError, setLoadError] = useState(null);
|
|
340
|
+
const [ready, setReady] = useState(false);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (!monaco || !host.current)
|
|
343
|
+
return;
|
|
344
|
+
const editor = monaco.editor.createDiffEditor(host.current, {
|
|
345
|
+
...editorOptions(layout, readonly),
|
|
346
|
+
renderSideBySide: !inline,
|
|
347
|
+
originalEditable: false,
|
|
348
|
+
ignoreTrimWhitespace: false,
|
|
349
|
+
enableSplitViewResizing: true,
|
|
350
|
+
renderOverviewRuler: true,
|
|
351
|
+
});
|
|
352
|
+
editorRef.current = editor;
|
|
353
|
+
return () => {
|
|
354
|
+
editor.setModel(null);
|
|
355
|
+
editor.dispose();
|
|
356
|
+
editorRef.current = null;
|
|
357
|
+
};
|
|
358
|
+
}, [monaco]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
editorRef.current?.updateOptions({ ...editorOptions(layout, readonly), renderSideBySide: !inline });
|
|
361
|
+
}, [inline, layout.fontSize, layout.minimap, layout.wordWrap, readonly]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
monaco?.editor.setTheme(monacoTheme(theme));
|
|
364
|
+
}, [monaco, theme]);
|
|
365
|
+
// 양쪽 모델을 준비한다. git 상태가 바뀌면(스테이지·커밋) 원래 쪽을 다시 읽는다.
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (!monaco || !editorRef.current)
|
|
368
|
+
return;
|
|
369
|
+
let alive = true;
|
|
370
|
+
const wf = store.host.workflowId;
|
|
371
|
+
const workUri = modelUri(monaco, wf, tab.path);
|
|
372
|
+
(async () => {
|
|
373
|
+
try {
|
|
374
|
+
let original;
|
|
375
|
+
let modified;
|
|
376
|
+
if (diff.disk) {
|
|
377
|
+
original = monaco.editor.getModel(diskUri(monaco, wf, tab.path));
|
|
378
|
+
if (!doc || doc.status !== 'ready')
|
|
379
|
+
return;
|
|
380
|
+
modified = monaco.editor.getModel(workUri);
|
|
381
|
+
}
|
|
382
|
+
else if (diff.staged) {
|
|
383
|
+
const [head, index] = await Promise.all([gitText(store, diff, 'HEAD'), gitText(store, diff, 'index')]);
|
|
384
|
+
if (!alive)
|
|
385
|
+
return;
|
|
386
|
+
original = setModelText(monaco, originalUri(monaco, wf, diff), head, workUri);
|
|
387
|
+
modified = setModelText(monaco, stagedUri(monaco, wf, diff), diff.status === 'deleted' ? '' : index, workUri);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
const base = diff.status === 'untracked' ? '' : await gitText(store, diff, 'index');
|
|
391
|
+
if (!alive)
|
|
392
|
+
return;
|
|
393
|
+
original = setModelText(monaco, originalUri(monaco, wf, diff), base, workUri);
|
|
394
|
+
if (diff.status === 'deleted') {
|
|
395
|
+
modified = setModelText(monaco, monaco.Uri.from({ scheme: 'xide-empty', path: `/xgen/${wf}/${tab.path}` }), '', workUri);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
if (!doc || doc.status !== 'ready')
|
|
399
|
+
return;
|
|
400
|
+
modified = monaco.editor.getModel(workUri);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!alive || !editorRef.current || !original || !modified)
|
|
404
|
+
return;
|
|
405
|
+
const cur = editorRef.current.getModel();
|
|
406
|
+
if (cur?.original !== original || cur?.modified !== modified) {
|
|
407
|
+
editorRef.current.setModel({ original, modified });
|
|
408
|
+
}
|
|
409
|
+
editorRef.current.getModifiedEditor().updateOptions({ readOnly: readonly || diff.staged || diff.status === 'deleted' });
|
|
410
|
+
setReady(true);
|
|
411
|
+
setLoadError(null);
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
if (alive)
|
|
415
|
+
setLoadError(err instanceof Error ? err.message : '비교할 내용을 읽지 못했습니다');
|
|
416
|
+
}
|
|
417
|
+
})();
|
|
418
|
+
return () => {
|
|
419
|
+
alive = false;
|
|
420
|
+
};
|
|
421
|
+
}, [monaco, diff, doc?.status, statuses, tab.path, store, readonly]);
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
const ed = editorRef.current?.getModifiedEditor();
|
|
424
|
+
if (!ed)
|
|
425
|
+
return;
|
|
426
|
+
const sub = ed.onDidChangeCursorPosition((e) => store.setCursor({ line: e.position.lineNumber, col: e.position.column, selected: 0 }));
|
|
427
|
+
return () => sub.dispose();
|
|
428
|
+
}, [ready, store]);
|
|
429
|
+
const status = diff.repo != null ? statuses[diff.repo] : undefined;
|
|
430
|
+
const stillChanged = !diff.disk &&
|
|
431
|
+
!!status &&
|
|
432
|
+
(diff.staged ? status.staged : [...status.changes, ...status.untracked]).some((c) => c.path === diff.path);
|
|
433
|
+
return (_jsxs("div", { className: "xide-code-pane", children: [_jsxs("div", { className: "xide-diff-toolbar", children: [_jsx("span", { className: "xide-dim", children: diff.disk ? '디스크 판과 편집 중인 판' : `${STATUS_LABEL[diff.status ?? 'modified']} · ${diff.staged ? '스테이지된 변경' : '작업 트리 변경'}` }), _jsx("span", { className: "xide-spacer" }), !readonly && !diff.disk && stillChanged ? (diff.staged ? (_jsxs("button", { type: "button", className: "xide-btn", onClick: () => void store.gitRun({ op: 'unstage', repo: diff.repo, paths: [diff.path] }, '스테이지 취소'), children: [_jsx(Icon, { name: "minus" }), " \uC2A4\uD14C\uC774\uC9C0 \uCDE8\uC18C"] })) : (_jsxs("button", { type: "button", className: "xide-btn", onClick: () => void store.gitRun({ op: 'stage', repo: diff.repo, paths: [diff.path] }, '스테이지'), children: [_jsx(Icon, { name: "plus" }), " \uC2A4\uD14C\uC774\uC9C0"] }))) : null, diff.disk ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.reloadFromDisk(tab.path, { discard: true }).then(() => store.closeTab(group.id, tab.id, { force: true })), children: "\uB514\uC2A4\uD06C \uD310 \uC4F0\uAE30" }), _jsx("button", { type: "button", className: "xide-btn xide--primary", onClick: () => void store.save(tab.path, { force: true }).then((ok) => ok && store.closeTab(group.id, tab.id, { force: true })), children: "\uD3B8\uC9D1 \uC911\uC778 \uD310\uC73C\uB85C \uC800\uC7A5" })] })) : null, _jsx(IconButton, { icon: "layout-sidebar", label: inline ? '나란히 보기' : '한 줄로 보기', active: inline, onClick: () => setInline((v) => !v) }), _jsx(IconButton, { icon: "arrow-up", label: "\uC774\uC804 \uBCC0\uACBD", onClick: () => editorRef.current?.goToDiff?.('previous') }), _jsx(IconButton, { icon: "arrow-down", label: "\uB2E4\uC74C \uBCC0\uACBD", onClick: () => editorRef.current?.goToDiff?.('next') })] }), _jsx("div", { className: "xide-monaco", ref: host }), error || loadError ? _jsx(Empty, { icon: "error", children: error ?? loadError }) : null, !ready && !error && !loadError ? _jsx("div", { className: "xide-pane-note", children: "\uBE44\uAD50\uD560 \uB0B4\uC6A9\uC744 \uC77D\uB294 \uC911" }) : null] }));
|
|
434
|
+
}
|
|
435
|
+
function ImagePane({ path }) {
|
|
436
|
+
const store = useStore();
|
|
437
|
+
const [url, setUrl] = useState(null);
|
|
438
|
+
const [size, setSize] = useState(null);
|
|
439
|
+
const [error, setError] = useState(null);
|
|
440
|
+
const [zoom, setZoom] = useState('fit');
|
|
441
|
+
const version = useIde((s) => s.files.find((f) => f.path === path)?.modifiedAt ?? '');
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
let alive = true;
|
|
444
|
+
let objectUrl = null;
|
|
445
|
+
store.host.readRaw(path).then((bytes) => {
|
|
446
|
+
if (!alive)
|
|
447
|
+
return;
|
|
448
|
+
objectUrl = URL.createObjectURL(new Blob([bytes], { type: imageMime(path) }));
|
|
449
|
+
setUrl(objectUrl);
|
|
450
|
+
setSize({ bytes: bytes.length });
|
|
451
|
+
setError(null);
|
|
452
|
+
}, (err) => alive && setError(err instanceof Error ? err.message : '그림을 읽지 못했습니다'));
|
|
453
|
+
return () => {
|
|
454
|
+
alive = false;
|
|
455
|
+
if (objectUrl)
|
|
456
|
+
URL.revokeObjectURL(objectUrl);
|
|
457
|
+
};
|
|
458
|
+
}, [path, version, store]);
|
|
459
|
+
if (error)
|
|
460
|
+
return _jsx(Empty, { icon: "error", children: error });
|
|
461
|
+
if (!url)
|
|
462
|
+
return _jsx("div", { className: "xide-pane-note", children: "\uC5EC\uB294 \uC911" });
|
|
463
|
+
return (_jsxs("div", { className: `xide-image-pane${zoom === 'fit' ? ' xide--fit' : ''}`, children: [_jsx("div", { className: "xide-image-scroll", onClick: () => setZoom((z) => (z === 'fit' ? 1 : 'fit')), children: _jsx("img", { src: url, alt: basename(path), style: zoom === 'fit' ? undefined : { width: size?.w ? size.w * zoom : undefined }, onLoad: (e) => {
|
|
464
|
+
const img = e.currentTarget;
|
|
465
|
+
setSize((s) => ({ bytes: s?.bytes ?? 0, w: img.naturalWidth, h: img.naturalHeight }));
|
|
466
|
+
} }) }), _jsxs("div", { className: "xide-image-info", children: [size?.w ? `${size.w} × ${size.h}` : '', " ", size ? `· ${formatBytes(size.bytes)}` : '', " \u00B7 ", zoom === 'fit' ? '창에 맞춤' : '원래 크기'] })] }));
|
|
467
|
+
}
|
|
468
|
+
//# sourceMappingURL=EditorArea.js.map
|