bitpull 0.1.24 → 0.1.25
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/bitpull.cjs +241 -76
- package/package.json +1 -1
- package/ui/ide.css +261 -87
- package/ui/ide.html +106 -62
- package/ui/ide.js +589 -163
- package/ui/terminal.js +2 -1
package/ui/ide.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
// BitPull projects: an editor and an AI agent on a folder of this computer.
|
|
1
|
+
// BitPull projects: an editor and an AI agent on a folder of this computer, laid out like a modern code editor.
|
|
2
2
|
// Rule of this file: nothing from a file, a model or the network is ever turned into HTML. Text goes in
|
|
3
|
-
// with textContent or into the editor's models;
|
|
3
|
+
// with textContent or into the editor's models; icons are built element by element (a test checks it).
|
|
4
4
|
"use strict";
|
|
5
5
|
(function () {
|
|
6
6
|
const BP = window.__BITPULL__ || {};
|
|
7
7
|
const token = BP.token || "";
|
|
8
8
|
const $ = (id) => document.getElementById(id);
|
|
9
|
+
// Where the page's files are: /ui/~<build>/, which the webview keeps without asking again.
|
|
10
|
+
const BASE = ((document.currentScript && document.currentScript.src) || "/ui/ide.js").replace(/ide\.js(\?.*)?$/, "");
|
|
9
11
|
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
12
|
+
const MOD = isMac ? "⌘" : "Ctrl+";
|
|
10
13
|
const store = {
|
|
11
14
|
get(k, d) { try { const v = localStorage.getItem(`bitpull.ide.${k}`); return v === null ? d : JSON.parse(v); } catch { return d; } },
|
|
12
15
|
set(k, v) { try { localStorage.setItem(`bitpull.ide.${k}`, JSON.stringify(v)); } catch { /* private mode */ } },
|
|
@@ -27,6 +30,66 @@
|
|
|
27
30
|
}
|
|
28
31
|
const clear = (n) => { while (n.firstChild) n.removeChild(n.firstChild); return n; };
|
|
29
32
|
|
|
33
|
+
// ---- icons: BitPull's own, drawn as lines ------------------------------------------------------------
|
|
34
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
35
|
+
const ICONS = {
|
|
36
|
+
back: ["M10 4 6 8l4 4"], folder: ["M2 4.5h4.5l1.5 1.5h6v7H2z"], "chevron-down": ["M4 6l4 4 4-4"], "chevron-right": ["M6 4l4 4-4 4"],
|
|
37
|
+
search: ["M7 3a4 4 0 1 1 0 8a4 4 0 1 1 0-8z", "M10 10l3.5 3.5"], "layout-left": ["M2 3h12v10H2z", "M6 3v10", "M2.5 3.5h3v9h-3z"],
|
|
38
|
+
"layout-bottom": ["M2 3h12v10H2z", "M2 9.5h12", "M2.5 10h11v2.5h-11z"], "layout-right": ["M2 3h12v10H2z", "M10 3v10", "M10.5 3.5h3v9h-3z"],
|
|
39
|
+
gear: ["M8 5.5a2.5 2.5 0 1 1 0 5a2.5 2.5 0 1 1 0-5z", "M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M3.4 12.6l1.4-1.4M11.2 4.8l1.4-1.4"],
|
|
40
|
+
files: ["M4 2h5l3 3v9H4z", "M9 2v3h3", "M2 4v11h8"], git: ["M5 3a1.5 1.5 0 1 1 0 3a1.5 1.5 0 1 1 0-3z", "M5 10a1.5 1.5 0 1 1 0 3a1.5 1.5 0 1 1 0-3z", "M11 5a1.5 1.5 0 1 1 0 3a1.5 1.5 0 1 1 0-3z", "M5 6v4M11 8c0 2-6 1-6 2"],
|
|
41
|
+
"new-file": ["M4 2h5l3 3v9H4z", "M9 2v3h3", "M8 8v4M6 10h4"], "new-folder": ["M2 4.5h4.5l1.5 1.5h6v7H2z", "M8 8v3.5M6.25 9.75h3.5"],
|
|
42
|
+
refresh: ["M13 8a5 5 0 1 1-1.5-3.5", "M13 3v2.5h-2.5"], collapse: ["M3 3h10v10H3z", "M5.5 8h5"], split: ["M2 3h12v10H2z", "M8 3v10"],
|
|
43
|
+
more: ["M3.5 8h.01M8 8h.01M12.5 8h.01"], problems: ["M8 2a6 6 0 1 1 0 12a6 6 0 1 1 0-12z", "M8 5v3.5M8 11h.01"], output: ["M3 4h10M3 7h10M3 10h7M3 13h5"],
|
|
44
|
+
terminal: ["M2 3h12v10H2z", "M4.5 6.5 6.5 8l-2 1.5M8 10h3"], ports: ["M8 2a6 6 0 1 1 0 12a6 6 0 1 1 0-12z", "M2 8h12M8 2c2 2 2 10 0 12M8 2c-2 2-2 10 0 12"],
|
|
45
|
+
play: ["M5 3.5v9l7-4.5z"], maximize: ["M3 10l5-5 5 5"], restore: ["M3 6l5 5 5-5"], close: ["M4 4l8 8M12 4l-8 8"], plus: ["M8 3v10M3 8h10"],
|
|
46
|
+
undo: ["M5 4 2.5 6.5 5 9", "M3 6.5h6.5a3.5 3.5 0 0 1 0 7H6"], stop: ["M4 4h8v8H4z"], send: ["M8 13V3M4 7l4-4 4 4"],
|
|
47
|
+
error: ["M8 2a6 6 0 1 1 0 12a6 6 0 1 1 0-12z", "M6 6l4 4M10 6l-4 4"], warning: ["M8 2.5 14 13H2z", "M8 6.5v3M8 11.3h.01"],
|
|
48
|
+
rename: ["M3 13l1-3 7-7 2 2-7 7z"], trash: ["M3 4.5h10M6 4.5V3h4v1.5M4.5 4.5l.7 9h5.6l.7-9"], check: ["M3.5 8.5l3 3 6-7"],
|
|
49
|
+
logo: ["M8 1.5 14 5v6l-6 3.5L2 11V5z", "M8 8v6.5M8 8l6-3M8 8 2 5"],
|
|
50
|
+
};
|
|
51
|
+
function icon(name) {
|
|
52
|
+
const svg = document.createElementNS(NS, "svg");
|
|
53
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
54
|
+
svg.setAttribute("class", "i");
|
|
55
|
+
svg.setAttribute("aria-hidden", "true");
|
|
56
|
+
for (const d of ICONS[name] || []) { const p = document.createElementNS(NS, "path"); p.setAttribute("d", d); svg.appendChild(p); }
|
|
57
|
+
return svg;
|
|
58
|
+
}
|
|
59
|
+
const drawIcons = (root) => { for (const n of root.querySelectorAll("[data-icon]")) if (!n.querySelector("svg")) n.prepend(icon(n.dataset.icon)); };
|
|
60
|
+
|
|
61
|
+
/** File icons: a coloured mark per kind of file, BitPull's own. */
|
|
62
|
+
const FILE_KINDS = [
|
|
63
|
+
[/\.(ts|tsx|mts|cts)$/i, "#3178c6", "TS"], [/\.(js|mjs|cjs|jsx)$/i, "#d8bc2c", "JS"], [/\.json$/i, "#cbcb41", "{}"], [/\.(md|markdown)$/i, "#519aba", "M"],
|
|
64
|
+
[/\.(html?|vue|svelte)$/i, "#e44d26", "<>"], [/\.(css|scss|sass|less)$/i, "#42a5f5", "#"], [/\.py$/i, "#3572a5", "Py"], [/\.go$/i, "#00add8", "Go"],
|
|
65
|
+
[/\.rs$/i, "#dea584", "Rs"], [/\.(java|kt|kts)$/i, "#b07219", "J"], [/\.(rb)$/i, "#cc342d", "Rb"], [/\.(php)$/i, "#777bb4", "P"], [/\.(c|h|cpp|hpp|cc)$/i, "#659ad2", "C"],
|
|
66
|
+
[/\.(cs)$/i, "#68217a", "C#"], [/\.(sh|bash|zsh|ps1|bat|cmd)$/i, "#89e051", "$"], [/\.(ya?ml|toml|ini|conf|cfg)$/i, "#cb171e", "≡"], [/\.(sql|db|sqlite)$/i, "#e38c00", "Db"],
|
|
67
|
+
[/\.(png|jpe?g|gif|webp|svg|ico|bmp)$/i, "#a074c4", "▣"], [/\.(lock)$/i, "#8c8c8c", "🔒"], [/\.(txt|log)$/i, "#8c8c8c", "≡"], [/\.(xml|plist)$/i, "#e37933", "<>"],
|
|
68
|
+
];
|
|
69
|
+
const SPECIAL = { dockerfile: ["#2496ed", "D"], "docker-compose.yml": ["#2496ed", "D"], "compose.yaml": ["#2496ed", "D"], ".gitignore": ["#f14e32", "G"], ".gitattributes": ["#f14e32", "G"], ".env": ["#e5c07b", "$"], ".env.example": ["#e5c07b", "$"], license: ["#d0bf41", "©"], makefile: ["#6d8086", "M"], "package.json": ["#cb3837", "N"], "package-lock.json": ["#cb3837", "N"] };
|
|
70
|
+
function fileIcon(name, dir, open) {
|
|
71
|
+
const svg = document.createElementNS(NS, "svg");
|
|
72
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
73
|
+
svg.setAttribute("class", "f");
|
|
74
|
+
svg.setAttribute("aria-hidden", "true");
|
|
75
|
+
const add = (tag, attrs) => { const n = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v); svg.appendChild(n); return n; };
|
|
76
|
+
if (dir) {
|
|
77
|
+
const muted = /^(node_modules|\.git|dist|build|\.next|\.cache|coverage|target|out)$/.test(name);
|
|
78
|
+
const color = muted ? "#7a7a7a" : name === "src" ? "#42a5f5" : /^(test|tests|__tests__|spec)$/.test(name) ? "#4caf50" : /^\./.test(name) ? "#8a8a8a" : "#dcb67a";
|
|
79
|
+
add("path", { d: open ? "M1.5 4h4l1.3 1.3H14v1.2H4.2L2.5 12.5h-1z M4.2 6.5H15l-2 6H2.5z" : "M1.5 3.5h4.3l1.4 1.4h7.3v8.6h-13z", fill: color });
|
|
80
|
+
return svg;
|
|
81
|
+
}
|
|
82
|
+
const low = name.toLowerCase();
|
|
83
|
+
const k = SPECIAL[low] || (low.startsWith(".env") ? SPECIAL[".env"] : null) || (FILE_KINDS.find(([re]) => re.test(name)) || []).slice(1);
|
|
84
|
+
const [color, mark] = k && k.length ? k : ["#8c8c8c", "≡"];
|
|
85
|
+
if (mark.length <= 2 && /[A-Za-z#${<©▣≡]/.test(mark)) {
|
|
86
|
+
add("rect", { x: "1.5", y: "2.5", width: "13", height: "11", rx: "2", fill: color, opacity: ".18" });
|
|
87
|
+
const tx = add("text", { x: "8", y: "11.2", "text-anchor": "middle", "font-size": mark.length > 1 ? "6.6" : "8.5", "font-weight": "700", "font-family": "system-ui, sans-serif", fill: color });
|
|
88
|
+
tx.textContent = mark;
|
|
89
|
+
} else add("path", { d: "M4 1.5h5l3 3v10H4z", fill: color, opacity: ".7" });
|
|
90
|
+
return svg;
|
|
91
|
+
}
|
|
92
|
+
|
|
30
93
|
// ---- words ------------------------------------------------------------------------------------
|
|
31
94
|
let dict = {}, fallback = {};
|
|
32
95
|
const t = (key, vars) => String(dict[key] ?? fallback[key] ?? key).replace(/\{(\w+)\}/g, (_, k) => (vars && vars[k] !== undefined ? vars[k] : `{${k}}`));
|
|
@@ -76,6 +139,30 @@
|
|
|
76
139
|
document.addEventListener("mouseout", () => { clearTimeout(tipTimer); $("tip").hidden = true; });
|
|
77
140
|
document.addEventListener("mousedown", () => { clearTimeout(tipTimer); $("tip").hidden = true; });
|
|
78
141
|
|
|
142
|
+
// ---- menus -----------------------------------------------------------------------------------------
|
|
143
|
+
/** items: { label, keys, check, run, sep, head, sub } */
|
|
144
|
+
function menu(anchor, items, at) {
|
|
145
|
+
const box = clear($("menu"));
|
|
146
|
+
const ul = el("ul");
|
|
147
|
+
for (const it of items) {
|
|
148
|
+
if (it.sep) { ul.append(el("li", { class: "sep" })); continue; }
|
|
149
|
+
if (it.head) { ul.append(el("li", { class: "head", text: it.head })); continue; }
|
|
150
|
+
ul.append(el("li", { role: "menuitem", onclick: (e) => { e.stopPropagation(); if (it.sub) return menu(e.currentTarget, it.sub, "side"); closeMenu(); it.run && it.run(); } },
|
|
151
|
+
el("span", { class: "check" }, it.check ? icon("check") : null), el("span", { text: it.label }), it.keys ? el("span", { class: "keys", text: it.keys }) : it.sub ? el("span", { class: "keys" }, icon("chevron-right")) : null));
|
|
152
|
+
}
|
|
153
|
+
box.append(ul);
|
|
154
|
+
box.hidden = false;
|
|
155
|
+
const r = anchor.getBoundingClientRect();
|
|
156
|
+
const w = box.offsetWidth, h = box.offsetHeight;
|
|
157
|
+
let x = at === "side" ? r.right + 2 : r.left, y = at === "side" ? r.top : r.bottom + 2;
|
|
158
|
+
if (document.documentElement.dir === "rtl" && at !== "side") x = r.right - w;
|
|
159
|
+
box.style.left = `${Math.max(4, Math.min(x, innerWidth - w - 4))}px`;
|
|
160
|
+
box.style.top = `${Math.max(4, y + h > innerHeight ? r.top - h - 2 : y)}px`;
|
|
161
|
+
}
|
|
162
|
+
const closeMenu = () => { $("menu").hidden = true; };
|
|
163
|
+
document.addEventListener("click", (e) => { if (!e.target.closest || !e.target.closest("#menu")) closeMenu(); }, true);
|
|
164
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeMenu(); });
|
|
165
|
+
|
|
79
166
|
// ---- a modal of our own (the app's window has no native dialogs everywhere) -------------------------
|
|
80
167
|
function modal(title, body, buttons) {
|
|
81
168
|
return new Promise((resolve) => {
|
|
@@ -85,7 +172,7 @@
|
|
|
85
172
|
const close = (v) => { $("modal").hidden = true; document.removeEventListener("keydown", onKey, true); resolve(v); };
|
|
86
173
|
const onKey = (e) => { if (e.key === "Escape") { e.preventDefault(); close(null); } };
|
|
87
174
|
document.addEventListener("keydown", onKey, true);
|
|
88
|
-
for (const b of buttons) bar.append(el("button", { class: b.ghost ? "ghost" : b.danger ? "danger" : "", text: b.label, onclick: () => close(b.value === undefined ? true : typeof b.value === "function" ? b.value() : b.value) }));
|
|
175
|
+
for (const b of buttons) bar.append(el("button", { class: b.ghost ? "ghost" : b.danger ? "danger" : "primary", text: b.label, onclick: () => close(b.value === undefined ? true : typeof b.value === "function" ? b.value() : b.value) }));
|
|
89
176
|
$("modal").hidden = false;
|
|
90
177
|
const first = $("modal-body").querySelector("input,textarea");
|
|
91
178
|
(first || bar.lastChild).focus();
|
|
@@ -99,24 +186,27 @@
|
|
|
99
186
|
const confirmBox = (title, text, yes, danger) => modal(title, el("p", { text }), [{ label: t("pj.no"), ghost: true, value: false }, { label: yes, danger, value: true }]);
|
|
100
187
|
|
|
101
188
|
// ---- state ------------------------------------------------------------------------------------
|
|
102
|
-
let
|
|
189
|
+
let editorLoaded = () => {};
|
|
190
|
+
const editorReady = new Promise((r) => { editorLoaded = r; });
|
|
191
|
+
let monaco = null, editor = null, editor2 = null, diffEditor = null, focused = null;
|
|
103
192
|
let folder = "";
|
|
104
|
-
const tabs = []; // { path, model, mtime, saved, view,
|
|
193
|
+
const tabs = []; // { path, model, mtime, saved, view, secret }
|
|
105
194
|
let active = null;
|
|
106
195
|
let models = [];
|
|
107
196
|
let session = null; // the agent shown on the right
|
|
108
197
|
let cursor = 0;
|
|
109
|
-
let review = null;
|
|
198
|
+
let review = null;
|
|
110
199
|
let terminal = null;
|
|
200
|
+
let gitInfo = { branch: "", status: new Map(), ignored: [] };
|
|
201
|
+
let problemCounts = { errors: 0, warnings: 0 };
|
|
111
202
|
|
|
112
203
|
// ---- the terminal is another part of the app, which plugs in here -----------------------------
|
|
113
204
|
/**
|
|
114
205
|
* Terminal hook. The terminal registers itself with BitPullIDE.terminal.register(impl), where impl has:
|
|
115
|
-
* mount(container, { token, folder, lang, t }) draws itself into the
|
|
206
|
+
* mount(container, { token, folder, lang, t }) draws itself into the panel, once;
|
|
116
207
|
* open() shows it and focuses it; close() hides it; newTab() opens another shell in the folder;
|
|
117
|
-
* runVisible(command)
|
|
118
|
-
*
|
|
119
|
-
* folderChanged(folder) optional: the project's folder changed.
|
|
208
|
+
* runVisible(command) shows a command the agent runs, read-only (it runs only on this computer, through
|
|
209
|
+
* the Projects path with its approvals; models never reach it); folderChanged(folder) optional.
|
|
120
210
|
*/
|
|
121
211
|
window.BitPullIDE = {
|
|
122
212
|
terminal: {
|
|
@@ -124,8 +214,20 @@
|
|
|
124
214
|
terminal = impl;
|
|
125
215
|
clear($("terminal-host"));
|
|
126
216
|
impl.mount($("terminal-host"), { token, folder, lang, t });
|
|
127
|
-
//
|
|
128
|
-
|
|
217
|
+
// Its buttons become icons, like the rest of the panel; the words stay as their name.
|
|
218
|
+
const TERM_ICONS = { "term.new": "plus", "term.split": "split", "term.close": "trash" };
|
|
219
|
+
const iconize = () => {
|
|
220
|
+
for (const b of $("terminal-host").querySelectorAll(".bp-term-bar button[data-action]")) {
|
|
221
|
+
const name = TERM_ICONS[b.dataset.action];
|
|
222
|
+
if (!name || b.classList.contains("ib")) continue;
|
|
223
|
+
b.setAttribute("aria-label", b.textContent);
|
|
224
|
+
b.textContent = "";
|
|
225
|
+
b.classList.add("ib");
|
|
226
|
+
b.append(icon(name));
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
new MutationObserver(iconize).observe($("terminal-host"), { childList: true, subtree: true });
|
|
230
|
+
if (layoutState.panelOpen && document.querySelector('[data-bottom="terminal"]').classList.contains("on")) impl.open();
|
|
129
231
|
},
|
|
130
232
|
},
|
|
131
233
|
get folder() { return folder; },
|
|
@@ -133,72 +235,195 @@
|
|
|
133
235
|
function showTerminal() {
|
|
134
236
|
showBottom("terminal");
|
|
135
237
|
if (terminal) terminal.open();
|
|
136
|
-
else clear($("terminal-host")).append(el("p", { class: "muted", text: t("ide.terminal.missing") }));
|
|
238
|
+
else clear($("terminal-host")).append(el("p", { class: "muted pad", text: t("ide.terminal.missing") }));
|
|
137
239
|
}
|
|
138
240
|
|
|
139
|
-
// ---- layout:
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
$("
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
241
|
+
// ---- layout: a grid whose parts can be shown, hidden, moved and resized, and are remembered --------------
|
|
242
|
+
const layoutState = Object.assign(
|
|
243
|
+
{ side: 260, agent: 380, panel: 240, panelWide: 420, sideOpen: true, agentOpen: true, panelOpen: true, panelPos: "bottom", panelAlign: "center", panelMax: false, icons: false, sideView: "files", panes: { tree: true, outline: false, history: false } },
|
|
244
|
+
store.get("layout", {}),
|
|
245
|
+
);
|
|
246
|
+
const narrow = () => innerWidth < 900;
|
|
247
|
+
function layout() {
|
|
248
|
+
const L = layoutState;
|
|
249
|
+
const ws = $("workbench");
|
|
250
|
+
$("ide").classList.toggle("narrow", narrow());
|
|
251
|
+
$("side").hidden = !L.sideOpen;
|
|
252
|
+
$("agent").hidden = !L.agentOpen;
|
|
253
|
+
$("panel").hidden = !L.panelOpen;
|
|
254
|
+
for (const [id, on] of [["toggle-side", L.sideOpen], ["toggle-panel", L.panelOpen], ["toggle-agent", L.agentOpen]]) $(id).classList.toggle("on", on);
|
|
255
|
+
$("panel").className = `at-${L.panelPos}${L.icons ? " icons" : ""}`;
|
|
256
|
+
const sashes = { side: ws.querySelector('[data-split="side"]'), panel: ws.querySelector('[data-split="panel"]'), agent: ws.querySelector('[data-split="agent"]') };
|
|
257
|
+
sashes.side.hidden = !L.sideOpen;
|
|
258
|
+
sashes.agent.hidden = !L.agentOpen;
|
|
259
|
+
sashes.panel.hidden = !L.panelOpen || L.panelMax;
|
|
260
|
+
sashes.side.className = "sash v";
|
|
261
|
+
sashes.agent.className = "sash v";
|
|
262
|
+
sashes.panel.className = `sash ${L.panelPos === "left" || L.panelPos === "right" ? "v" : "h"}`;
|
|
263
|
+
$("editor-area").hidden = L.panelOpen && L.panelMax;
|
|
264
|
+
$("panel-max").replaceChildren(icon(L.panelMax ? "restore" : "maximize"));
|
|
265
|
+
if (narrow()) {
|
|
266
|
+
ws.style.gridTemplateColumns = "minmax(0, 1fr)";
|
|
267
|
+
const rows = ["act"];
|
|
268
|
+
const heights = ["auto"];
|
|
269
|
+
if (L.sideOpen) { rows.push("side"); heights.push("34vh"); }
|
|
270
|
+
if (!(L.panelOpen && L.panelMax)) { rows.push("edit"); heights.push("62vh"); }
|
|
271
|
+
if (L.panelOpen) { rows.push("panel"); heights.push(L.panelMax ? "70vh" : "34vh"); }
|
|
272
|
+
if (L.agentOpen) { rows.push("agent"); heights.push("80vh"); }
|
|
273
|
+
ws.style.gridTemplateAreas = rows.map((r) => `"${r}"`).join(" ");
|
|
274
|
+
ws.style.gridTemplateRows = heights.join(" ");
|
|
275
|
+
} else {
|
|
276
|
+
const side = L.sideOpen ? `${L.side}px` : "0px";
|
|
277
|
+
const agent = L.agentOpen ? `${L.agent}px` : "0px";
|
|
278
|
+
const s1 = L.sideOpen ? "4px" : "0px", s3 = L.agentOpen ? "4px" : "0px";
|
|
279
|
+
const panelOn = L.panelOpen;
|
|
280
|
+
const max = panelOn && L.panelMax;
|
|
281
|
+
if (!panelOn || L.panelPos === "bottom" || L.panelPos === "top") {
|
|
282
|
+
const ph = !panelOn ? "0px" : max ? "minmax(0, 1fr)" : `${L.panel}px`;
|
|
283
|
+
const eh = max ? "0px" : "minmax(0, 1fr)";
|
|
284
|
+
const s2 = panelOn && !max ? "4px" : "0px";
|
|
285
|
+
const justify = L.panelAlign === "justify";
|
|
286
|
+
const top = L.panelPos === "top";
|
|
287
|
+
const editRow = `act side s1 edit s3 agent`;
|
|
288
|
+
const panelRow = justify ? `act panel panel panel panel panel` : `act side s1 panel s3 agent`;
|
|
289
|
+
const sashRow = justify ? `act s2 s2 s2 s2 s2` : `act side s1 s2 s3 agent`;
|
|
290
|
+
const areas = top ? [panelRow, sashRow, editRow] : [editRow, sashRow, panelRow];
|
|
291
|
+
ws.style.gridTemplateAreas = areas.map((r) => `"${r}"`).join(" ");
|
|
292
|
+
ws.style.gridTemplateColumns = `48px ${side} ${s1} minmax(0, 1fr) ${s3} ${agent}`;
|
|
293
|
+
ws.style.gridTemplateRows = top ? `${ph} ${s2} ${eh}` : `${eh} ${s2} ${ph}`;
|
|
294
|
+
} else {
|
|
295
|
+
const pw = max ? "minmax(0, 1fr)" : `${L.panelWide}px`;
|
|
296
|
+
const ew = max ? "0px" : "minmax(0, 1fr)";
|
|
297
|
+
const s2 = max ? "0px" : "4px";
|
|
298
|
+
const left = L.panelPos === "left";
|
|
299
|
+
ws.style.gridTemplateAreas = left ? `"act side s1 panel s2 edit s3 agent"` : `"act side s1 edit s2 panel s3 agent"`;
|
|
300
|
+
ws.style.gridTemplateColumns = left ? `48px ${side} ${s1} ${pw} ${s2} ${ew} ${s3} ${agent}` : `48px ${side} ${s1} ${ew} ${s2} ${pw} ${s3} ${agent}`;
|
|
301
|
+
ws.style.gridTemplateRows = "minmax(0, 1fr)";
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const p of document.querySelectorAll(".pane")) p.classList.toggle("open", !!L.panes[p.dataset.pane]);
|
|
305
|
+
store.set("layout", L);
|
|
306
|
+
relayout();
|
|
307
|
+
}
|
|
308
|
+
/** Monaco and the terminal measure themselves after the grid has settled. */
|
|
309
|
+
function relayout() {
|
|
310
|
+
requestAnimationFrame(() => {
|
|
311
|
+
for (const ed of [editor, editor2, diffEditor]) if (ed) ed.layout();
|
|
312
|
+
if (terminal && terminal.fit) terminal.fit();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
addEventListener("resize", () => layout());
|
|
316
|
+
for (const s of document.querySelectorAll(".sash")) {
|
|
153
317
|
s.addEventListener("mousedown", (e) => {
|
|
154
318
|
e.preventDefault();
|
|
155
319
|
const which = s.dataset.split;
|
|
156
|
-
const
|
|
320
|
+
const L = layoutState;
|
|
157
321
|
const rtl = document.documentElement.dir === "rtl";
|
|
322
|
+
const vertical = s.classList.contains("v");
|
|
323
|
+
const start = { x: e.clientX, y: e.clientY, side: L.side, agent: L.agent, panel: L.panel, panelWide: L.panelWide };
|
|
324
|
+
s.classList.add("drag");
|
|
158
325
|
const move = (m) => {
|
|
159
|
-
const dx = (m.clientX - start.x) * (rtl ? -1 : 1);
|
|
160
|
-
if (which === "side")
|
|
161
|
-
if (which === "agent")
|
|
162
|
-
if (which === "
|
|
163
|
-
|
|
326
|
+
const dx = (m.clientX - start.x) * (rtl ? -1 : 1), dy = m.clientY - start.y;
|
|
327
|
+
if (which === "side") L.side = Math.min(700, Math.max(170, start.side + dx));
|
|
328
|
+
if (which === "agent") L.agent = Math.min(900, Math.max(280, start.agent - dx));
|
|
329
|
+
if (which === "panel" && !vertical) L.panel = Math.min(innerHeight - 140, Math.max(90, start.panel + (L.panelPos === "top" ? dy : -dy)));
|
|
330
|
+
if (which === "panel" && vertical) L.panelWide = Math.min(innerWidth - 400, Math.max(220, start.panelWide + (L.panelPos === "left" ? dx : -dx)));
|
|
331
|
+
layout();
|
|
164
332
|
};
|
|
165
|
-
const up = () => { removeEventListener("mousemove", move); removeEventListener("mouseup", up); };
|
|
333
|
+
const up = () => { s.classList.remove("drag"); removeEventListener("mousemove", move); removeEventListener("mouseup", up); };
|
|
166
334
|
addEventListener("mousemove", move);
|
|
167
335
|
addEventListener("mouseup", up);
|
|
168
336
|
});
|
|
337
|
+
s.addEventListener("dblclick", () => {
|
|
338
|
+
const which = s.dataset.split;
|
|
339
|
+
Object.assign(layoutState, which === "side" ? { side: 260 } : which === "agent" ? { agent: 380 } : { panel: 240, panelWide: 420 });
|
|
340
|
+
layout();
|
|
341
|
+
});
|
|
169
342
|
}
|
|
343
|
+
const toggle = (k) => { layoutState[k] = !layoutState[k]; if (k === "panelOpen" && !layoutState.panelOpen) layoutState.panelMax = false; layout(); };
|
|
344
|
+
$("toggle-side").addEventListener("click", () => toggle("sideOpen"));
|
|
345
|
+
$("toggle-panel").addEventListener("click", () => { if (!layoutState.panelOpen) showBottom(currentBottom()); else toggle("panelOpen"); });
|
|
346
|
+
$("toggle-agent").addEventListener("click", () => toggle("agentOpen"));
|
|
347
|
+
$("panel-max").addEventListener("click", () => toggle("panelMax"));
|
|
348
|
+
|
|
170
349
|
function showSide(name) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const
|
|
174
|
-
|
|
350
|
+
layoutState.sideOpen = true;
|
|
351
|
+
layoutState.sideView = name;
|
|
352
|
+
for (const b of document.querySelectorAll("#activity button[data-side]")) b.classList.toggle("on", b.dataset.side === name);
|
|
353
|
+
for (const p of document.querySelectorAll("#side > section")) p.hidden = p.dataset.panel !== name;
|
|
354
|
+
layout();
|
|
175
355
|
if (name === "git") gitRefresh();
|
|
176
|
-
if (name === "outline") renderOutline();
|
|
177
|
-
if (name === "history") renderHistory();
|
|
356
|
+
if (name === "outline" || name === "files") renderOutline();
|
|
357
|
+
if (name === "history" || name === "files") renderHistory();
|
|
178
358
|
if (name === "search") $("search-q").focus();
|
|
179
359
|
}
|
|
180
|
-
for (const b of document.querySelectorAll("#activity button")) b.addEventListener("click", () => {
|
|
181
|
-
if (b.classList.contains("on") &&
|
|
360
|
+
for (const b of document.querySelectorAll("#activity button[data-side]")) b.addEventListener("click", () => {
|
|
361
|
+
if (b.classList.contains("on") && layoutState.sideOpen) { layoutState.sideOpen = false; layout(); } else showSide(b.dataset.side);
|
|
182
362
|
});
|
|
363
|
+
$("views-more").addEventListener("click", (e) => menu(e.currentTarget, [
|
|
364
|
+
{ label: t("ide.explorer"), keys: `${MOD}⇧E`, run: () => showSide("files") },
|
|
365
|
+
{ label: t("ide.search"), keys: `${MOD}⇧F`, run: () => showSide("search") },
|
|
366
|
+
{ label: t("ide.git"), keys: isMac ? "⌃⇧G" : "Ctrl+Shift+G", run: () => showSide("git") },
|
|
367
|
+
{ label: t("ide.outline"), run: () => showSide("outline") },
|
|
368
|
+
{ label: t("ide.history"), run: () => showSide("history") },
|
|
369
|
+
], "side"));
|
|
370
|
+
for (const p of document.querySelectorAll(".pane")) p.querySelector(".pane-head").addEventListener("click", (e) => {
|
|
371
|
+
if (e.target.closest(".pane-acts")) return;
|
|
372
|
+
layoutState.panes[p.dataset.pane] = !layoutState.panes[p.dataset.pane];
|
|
373
|
+
layout();
|
|
374
|
+
if (p.dataset.pane === "outline") renderOutline();
|
|
375
|
+
if (p.dataset.pane === "history") renderHistory();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const currentBottom = () => (document.querySelector("[data-bottom].on") || {}).dataset?.bottom || "problems";
|
|
183
379
|
function showBottom(name) {
|
|
184
|
-
|
|
380
|
+
layoutState.panelOpen = true;
|
|
185
381
|
for (const b of document.querySelectorAll("[data-bottom]")) b.classList.toggle("on", b.dataset.bottom === name);
|
|
186
382
|
for (const p of document.querySelectorAll("[data-bpanel]")) p.hidden = p.dataset.bpanel !== name;
|
|
187
|
-
|
|
383
|
+
if (name === "ports") { document.querySelector('[data-bottom="ports"]').hidden = false; refreshPorts(); }
|
|
384
|
+
$("problems-run").hidden = name !== "problems";
|
|
385
|
+
layout();
|
|
188
386
|
}
|
|
189
387
|
for (const b of document.querySelectorAll("[data-bottom]")) b.addEventListener("click", () => (b.dataset.bottom === "terminal" ? showTerminal() : showBottom(b.dataset.bottom)));
|
|
190
|
-
$("bottom-close").addEventListener("click", () => {
|
|
388
|
+
$("bottom-close").addEventListener("click", () => { layoutState.panelOpen = false; layoutState.panelMax = false; layout(); if (terminal) terminal.close(); });
|
|
389
|
+
$("panel-more").addEventListener("click", (e) => {
|
|
390
|
+
const L = layoutState;
|
|
391
|
+
const pos = (p) => ({ label: t(`ide.panel.pos.${p}`), check: L.panelPos === p, run: () => { L.panelPos = p; layout(); } });
|
|
392
|
+
menu(e.currentTarget, [
|
|
393
|
+
{ label: t("ide.ports"), run: () => showBottom("ports") },
|
|
394
|
+
{ sep: true },
|
|
395
|
+
{ label: t("ide.panel.position"), sub: ["bottom", "top", "left", "right"].map(pos) },
|
|
396
|
+
{ label: t("ide.panel.align"), sub: ["center", "justify"].map((a) => ({ label: t(`ide.panel.align.${a}`), check: L.panelAlign === a, run: () => { L.panelAlign = a; layout(); } })) },
|
|
397
|
+
{ label: t("ide.panel.icons"), check: L.icons, run: () => { L.icons = !L.icons; layout(); } },
|
|
398
|
+
{ label: t("ide.panel.hide"), keys: `${MOD}J`, run: () => $("bottom-close").click() },
|
|
399
|
+
]);
|
|
400
|
+
});
|
|
191
401
|
const output = (line) => { const o = $("output"); o.textContent += `${line}\n`; if (o.textContent.length > 400_000) o.textContent = o.textContent.slice(-300_000); o.scrollTop = o.scrollHeight; };
|
|
192
402
|
|
|
403
|
+
// ---- ports: servers started from the app --------------------------------------------------------
|
|
404
|
+
async function refreshPorts() {
|
|
405
|
+
const body = $("ports").tBodies[0];
|
|
406
|
+
try {
|
|
407
|
+
const r = await api("/api/ws/ports");
|
|
408
|
+
clear(body);
|
|
409
|
+
for (const p of r.ports) body.append(el("tr", {}, el("td", { text: String(p.port) }), el("td", { text: p.address }), el("td", { text: `${p.command} (${p.pid})` }),
|
|
410
|
+
el("td", {}, el("button", { class: "ghost", text: t("ide.ports.open"), "data-tip": "tip.ide.ports.open", onclick: () => api("/api/ws/preview", { url: p.url }).catch((err) => output(explain(err))) }))));
|
|
411
|
+
$("ports-empty").hidden = r.ports.length > 0;
|
|
412
|
+
$("ports-count").hidden = !r.ports.length;
|
|
413
|
+
$("ports-count").textContent = String(r.ports.length);
|
|
414
|
+
if (r.ports.length) document.querySelector('[data-bottom="ports"]').hidden = false;
|
|
415
|
+
} catch (err) { $("ports-empty").hidden = false; $("ports-empty").textContent = explain(err); }
|
|
416
|
+
}
|
|
417
|
+
|
|
193
418
|
// ---- Monaco, served by the app itself -------------------------------------------------------------
|
|
194
419
|
const MONACO_LANG = { de: "de", es: "es", fr: "fr", it: "it", ru: "ru", zh: "zh-cn", pt: "pt-br" };
|
|
195
420
|
function loadMonaco() {
|
|
196
421
|
return new Promise((resolve, reject) => {
|
|
197
422
|
const s = document.createElement("script");
|
|
198
|
-
s.src =
|
|
423
|
+
s.src = `${BASE}monaco/vs/loader.js`;
|
|
199
424
|
s.onerror = () => reject(new Error("editor_missing"));
|
|
200
425
|
s.onload = () => {
|
|
201
|
-
window.require.config({ paths: { vs:
|
|
426
|
+
window.require.config({ paths: { vs: `${BASE}monaco/vs` }, "vs/nls": { availableLanguages: { "*": MONACO_LANG[lang] || "en" } } });
|
|
202
427
|
// This version of the loader does not always call back. The global appears early, as soon as the
|
|
203
428
|
// core is loaded; the editor is complete (stylesheet, languages, all its services) only when the
|
|
204
429
|
// main module has run, which registers the JSON language: creating an editor before fails.
|
|
@@ -211,54 +436,81 @@
|
|
|
211
436
|
const m = window.monaco;
|
|
212
437
|
const langs = m && m.editor && m.languages && m.languages.getLanguages ? m.languages.getLanguages().length : 0;
|
|
213
438
|
if (langs > 50 && !langsSince) langsSince = Date.now();
|
|
214
|
-
if (m && m.editor && m.languages && (m.languages.json || (langsSince && Date.now() - langsSince >
|
|
439
|
+
if (m && m.editor && m.languages && (m.languages.json || (langsSince && Date.now() - langsSince > 400))) { clearInterval(wait); resolve(m); }
|
|
215
440
|
else if (Date.now() - started > 30000) { clearInterval(wait); reject(new Error("editor_missing")); }
|
|
216
|
-
},
|
|
441
|
+
}, 25);
|
|
217
442
|
};
|
|
218
443
|
document.body.appendChild(s);
|
|
219
444
|
});
|
|
220
445
|
}
|
|
221
446
|
const dark = () => matchMedia("(prefers-color-scheme: dark)").matches;
|
|
447
|
+
function defineThemes() {
|
|
448
|
+
monaco.editor.defineTheme("bitpull-dark", { base: "vs-dark", inherit: true, rules: [], colors: { "editor.background": "#1f1f1f", "editorGutter.background": "#1f1f1f", "minimap.background": "#1f1f1f", "editorLineNumber.foreground": "#6e7681", "editorLineNumber.activeForeground": "#cccccc", "editor.lineHighlightBorder": "#282828", "editorIndentGuide.background1": "#404040", "editorWidget.background": "#202020" } });
|
|
449
|
+
monaco.editor.defineTheme("bitpull-light", { base: "vs", inherit: true, rules: [], colors: { "editor.background": "#ffffff", "editorLineNumber.foreground": "#6e7681" } });
|
|
450
|
+
}
|
|
451
|
+
const theme = () => (dark() ? "bitpull-dark" : "bitpull-light");
|
|
452
|
+
const EDITOR_OPTS = () => ({ automaticLayout: false, theme: theme(), minimap: { enabled: true, renderCharacters: false, scale: 1 }, fontSize: 13, lineHeight: 20, fontFamily: 'Menlo, Monaco, "Cascadia Code", Consolas, "Courier New", monospace', tabSize: 2, renderWhitespace: "selection", smoothScrolling: true, scrollBeyondLastLine: false, fixedOverflowWidgets: true, padding: { top: 4 }, stickyScroll: { enabled: true }, bracketPairColorization: { enabled: true }, guides: { bracketPairs: "active", indentation: true } });
|
|
453
|
+
function wireEditor(ed) {
|
|
454
|
+
ed.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => saveActive());
|
|
455
|
+
ed.addAction({ id: "bitpull.inline", label: t("ide.inline"), keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK], contextMenuGroupId: "1_modification", run: () => inlineEdit() });
|
|
456
|
+
ed.onDidFocusEditorText(() => { focused = ed; const m = ed.getModel(); const tab = m && tabs.find((x) => x.model === m); if (tab && tab !== active) { active = tab; renderTabs(); renderCrumbs(); } });
|
|
457
|
+
ed.onDidChangeModelContent(() => { renderTabs(); clearTimeout(outlineTimer); outlineTimer = setTimeout(renderOutline, 600); });
|
|
458
|
+
ed.onDidChangeCursorPosition(() => { markOutline(); renderPos(); });
|
|
459
|
+
}
|
|
222
460
|
function setupEditor() {
|
|
223
|
-
|
|
224
|
-
editor = monaco.editor.create($("editor"), { ...
|
|
225
|
-
diffEditor = monaco.editor.createDiffEditor($("diff"), { ...
|
|
226
|
-
|
|
227
|
-
editor
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
461
|
+
defineThemes();
|
|
462
|
+
editor = monaco.editor.create($("editor"), { ...EDITOR_OPTS(), model: null });
|
|
463
|
+
diffEditor = monaco.editor.createDiffEditor($("diff"), { ...EDITOR_OPTS(), renderSideBySide: true, originalEditable: false });
|
|
464
|
+
focused = editor;
|
|
465
|
+
wireEditor(editor);
|
|
466
|
+
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => monaco.editor.setTheme(theme()));
|
|
467
|
+
// The editors follow their boxes: one observer, not a timer per editor.
|
|
468
|
+
new ResizeObserver(() => relayout()).observe($("editor-wrap"));
|
|
231
469
|
}
|
|
232
470
|
let outlineTimer = null;
|
|
233
471
|
|
|
234
472
|
// ---- files --------------------------------------------------------------------------------------
|
|
235
473
|
const opened = new Set(["."]);
|
|
236
|
-
|
|
237
|
-
|
|
474
|
+
const treeCache = new Map(); // path → entries, so that reopening a folder draws at once
|
|
475
|
+
const gitLetter = (path) => gitInfo.status.get(path) || null;
|
|
476
|
+
const isIgnored = (path, dir) => gitInfo.ignored.some((p) => (p.endsWith("/") ? `${path}/`.startsWith(p) || path === p.slice(0, -1) : path === p));
|
|
477
|
+
const dirChanged = (path) => { for (const [p, l] of gitInfo.status) if (p.startsWith(`${path}/`) && l !== "D") return l === "U" || l === "A" ? "U" : "M"; return null; };
|
|
478
|
+
async function loadTree(path, into, depth) {
|
|
479
|
+
let entries = treeCache.get(path);
|
|
480
|
+
const fresh = api("/api/ws/tree", { folder, path }).then((r) => { treeCache.set(path, r.entries); return r.entries; });
|
|
481
|
+
if (!entries) entries = await fresh; else fresh.then((e) => { if (JSON.stringify(e) !== JSON.stringify(entries)) drawTree(path, into, depth, e); }).catch(() => {});
|
|
482
|
+
drawTree(path, into, depth, entries);
|
|
483
|
+
}
|
|
484
|
+
function drawTree(path, into, depth, entries) {
|
|
238
485
|
clear(into);
|
|
239
|
-
for (const e of
|
|
486
|
+
for (const e of entries) {
|
|
240
487
|
const li = el("li", { role: "treeitem" });
|
|
241
|
-
const
|
|
242
|
-
|
|
488
|
+
const open = e.dir && opened.has(e.path);
|
|
489
|
+
const letter = e.dir ? dirChanged(e.path) : gitLetter(e.path);
|
|
490
|
+
const cls = ["item", open ? "open" : "", e.name.startsWith(".") ? "dot" : "", isIgnored(e.path, e.dir) ? "ignored" : "", letter ? `git-${letter}` : "", active && active.path === e.path ? "on" : ""].filter(Boolean).join(" ");
|
|
491
|
+
const item = el("div", { class: cls, title: e.path, tabindex: "-1", style: `padding-inline-start:${8 + depth * 8}px` },
|
|
492
|
+
el("span", { class: "twist" }, e.dir ? icon("chevron-right") : null),
|
|
493
|
+
e.secret ? icon("warning") : fileIcon(e.name, e.dir, open),
|
|
243
494
|
el("span", { class: "name", text: e.name }),
|
|
495
|
+
letter && !e.dir ? el("span", { class: "git", text: letter }) : letter ? el("span", { class: "git", text: "•" }) : null,
|
|
244
496
|
el("span", { class: "acts" },
|
|
245
|
-
el("button", { class: "
|
|
246
|
-
el("button", { class: "
|
|
247
|
-
|
|
497
|
+
el("button", { class: "ib", "data-tip": "tip.ide.rename", onclick: (ev) => { ev.stopPropagation(); renamePath(e.path); } }, icon("rename")),
|
|
498
|
+
el("button", { class: "ib", "data-tip": "tip.ide.delete", onclick: (ev) => { ev.stopPropagation(); deletePath(e.path, e.dir); } }, icon("trash"))));
|
|
499
|
+
for (let d = 0; d < depth; d++) item.append(el("span", { class: "guide", style: `inset-inline-start:${16 + d * 8}px` }));
|
|
248
500
|
li.append(item);
|
|
249
501
|
if (e.dir) {
|
|
250
502
|
const sub = el("ul", { role: "group" });
|
|
251
503
|
li.append(sub);
|
|
252
|
-
if (
|
|
504
|
+
if (open) loadTree(e.path, sub, depth + 1).catch(() => {});
|
|
253
505
|
item.addEventListener("click", () => {
|
|
254
|
-
if (opened.has(e.path)) { opened.delete(e.path); clear(sub);
|
|
255
|
-
|
|
506
|
+
if (opened.has(e.path)) { opened.delete(e.path); clear(sub); item.classList.remove("open"); item.replaceChild(fileIcon(e.name, true, false), item.children[1]); }
|
|
507
|
+
else { opened.add(e.path); item.classList.add("open"); item.replaceChild(fileIcon(e.name, true, true), item.children[1]); loadTree(e.path, sub, depth + 1).catch((err) => output(explain(err))); }
|
|
256
508
|
});
|
|
257
509
|
} else item.addEventListener("click", () => openFile(e.path));
|
|
258
510
|
into.append(li);
|
|
259
511
|
}
|
|
260
512
|
}
|
|
261
|
-
const refreshTree = () => folder
|
|
513
|
+
const refreshTree = () => { if (!folder) return; treeCache.clear(); loadTree(".", $("tree"), 0).catch((err) => output(explain(err))); };
|
|
262
514
|
async function newPath(dir) {
|
|
263
515
|
const name = await askText(t(dir ? "ide.new_dir" : "ide.new_file"), t("ide.new.hint"), active && !dir ? active.path.replace(/[^/]*$/, "") : "");
|
|
264
516
|
if (!name) return;
|
|
@@ -280,27 +532,56 @@
|
|
|
280
532
|
}
|
|
281
533
|
$("new-file").addEventListener("click", () => newPath(false));
|
|
282
534
|
$("new-dir").addEventListener("click", () => newPath(true));
|
|
283
|
-
$("refresh").addEventListener("click", refreshTree);
|
|
535
|
+
$("refresh").addEventListener("click", () => { refreshTree(); refreshGit(); });
|
|
536
|
+
$("collapse-all").addEventListener("click", () => { opened.clear(); opened.add("."); refreshTree(); });
|
|
284
537
|
|
|
285
|
-
// ---- tabs
|
|
538
|
+
// ---- tabs, breadcrumbs, the editor ------------------------------------------------------------------
|
|
286
539
|
const isDirty = (tab) => tab.model && tab.model.getAlternativeVersionId() !== tab.saved;
|
|
287
540
|
function renderTabs() {
|
|
288
541
|
const bar = clear($("tabs"));
|
|
289
542
|
for (const tab of tabs) {
|
|
290
543
|
const name = tab.path.split("/").pop();
|
|
291
|
-
bar.append(el("div", { class: `tab${tab === active ? " on" : ""}`, role: "tab", title: tab.path, onclick: () => activate(tab) },
|
|
292
|
-
|
|
293
|
-
el("button", { "data-tip": "tip.ide.close",
|
|
544
|
+
bar.append(el("div", { class: `tab${tab === active ? " on" : ""}${isDirty(tab) ? " dirty" : ""}`, role: "tab", title: tab.path, onclick: () => activate(tab), onauxclick: (e) => { if (e.button === 1) closeTab(tab); } },
|
|
545
|
+
fileIcon(name, false), el("span", { text: name }),
|
|
546
|
+
el("button", { class: "x", "data-tip": "tip.ide.close", onclick: (e) => { e.stopPropagation(); closeTab(tab); } }, icon("close"))));
|
|
294
547
|
}
|
|
548
|
+
const on = bar.querySelector(".tab.on");
|
|
549
|
+
if (on) on.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
295
550
|
$("welcome").hidden = !!active || !!review;
|
|
296
551
|
$("editor").hidden = !active || !!review;
|
|
552
|
+
$("editor2").hidden = !splitOn || !active || !!review;
|
|
553
|
+
$("crumbs").hidden = !active || !!review;
|
|
554
|
+
}
|
|
555
|
+
function renderCrumbs() {
|
|
556
|
+
const c = clear($("crumbs"));
|
|
557
|
+
if (!active) return;
|
|
558
|
+
const parts = active.path.split("/");
|
|
559
|
+
parts.forEach((p, i) => {
|
|
560
|
+
if (i) c.append(el("span", { class: "sep" }, icon("chevron-right")));
|
|
561
|
+
c.append(el("span", { class: "c" }, i === parts.length - 1 ? fileIcon(p, false) : null, el("span", { text: p })));
|
|
562
|
+
});
|
|
563
|
+
renderPos();
|
|
564
|
+
}
|
|
565
|
+
function renderPos() {
|
|
566
|
+
const ed = focused || editor;
|
|
567
|
+
const pos = ed && ed.getPosition();
|
|
568
|
+
$("sb-pos").hidden = !active || !pos;
|
|
569
|
+
if (pos) $("sb-pos").textContent = t("ide.status.pos", { line: pos.lineNumber, col: pos.column });
|
|
570
|
+
const m = active && active.model;
|
|
571
|
+
$("sb-lang").hidden = !m;
|
|
572
|
+
$("sb-enc").hidden = !m;
|
|
573
|
+
if (m) { const id = m.getLanguageId(); const l = monaco.languages.getLanguages().find((x) => x.id === id); $("sb-lang").textContent = (l && l.aliases && l.aliases[0]) || id; }
|
|
297
574
|
}
|
|
298
575
|
async function openFile(path, line) {
|
|
299
|
-
if (!monaco) return;
|
|
300
576
|
let tab = tabs.find((x) => x.path === path);
|
|
577
|
+
let r;
|
|
578
|
+
if (!tab) {
|
|
579
|
+
// The file is read while the editor may still be loading: a click right after opening the page counts.
|
|
580
|
+
try { [r] = await Promise.all([api("/api/ws/read", { folder, path }), editorReady]); } catch (err) { output(explain(err)); return; }
|
|
581
|
+
if (!monaco) return;
|
|
582
|
+
tab = tabs.find((x) => x.path === path);
|
|
583
|
+
}
|
|
301
584
|
if (!tab) {
|
|
302
|
-
let r;
|
|
303
|
-
try { r = await api("/api/ws/read", { folder, path }); } catch (err) { output(explain(err)); return; }
|
|
304
585
|
if (r.binary) { output(t("ide.binary", { path })); return; }
|
|
305
586
|
const uri = monaco.Uri.from({ scheme: "file", path: `/${path}` });
|
|
306
587
|
const model = monaco.editor.getModel(uri) || monaco.editor.createModel(r.text, undefined, uri);
|
|
@@ -309,26 +590,32 @@
|
|
|
309
590
|
if (r.secret) output(t("ide.secret_open", { path }));
|
|
310
591
|
}
|
|
311
592
|
activate(tab);
|
|
312
|
-
|
|
593
|
+
const ed = focused || editor;
|
|
594
|
+
if (line) { ed.revealLineInCenter(line); ed.setPosition({ lineNumber: line, column: 1 }); ed.focus(); }
|
|
313
595
|
}
|
|
314
596
|
function activate(tab) {
|
|
315
|
-
|
|
597
|
+
const ed = focused || editor;
|
|
598
|
+
if (active && active.model && ed.getModel() === active.model) active.view = ed.saveViewState();
|
|
316
599
|
active = tab;
|
|
317
|
-
closeReview();
|
|
318
|
-
|
|
319
|
-
if (tab.view)
|
|
600
|
+
if (review) closeReview();
|
|
601
|
+
if (ed.getModel() !== tab.model) ed.setModel(tab.model);
|
|
602
|
+
if (tab.view) ed.restoreViewState(tab.view);
|
|
320
603
|
renderTabs();
|
|
321
|
-
|
|
322
|
-
|
|
604
|
+
renderCrumbs();
|
|
605
|
+
relayout();
|
|
606
|
+
clearTimeout(outlineTimer);
|
|
607
|
+
outlineTimer = setTimeout(() => { renderOutline(); if (layoutState.panes.history || layoutState.sideView === "history") renderHistory(); }, 120);
|
|
323
608
|
for (const n of document.querySelectorAll("#tree .item")) n.classList.toggle("on", n.title === tab.path);
|
|
324
|
-
|
|
609
|
+
ed.focus();
|
|
325
610
|
}
|
|
326
611
|
async function closeTab(tab, force) {
|
|
327
612
|
if (!force && isDirty(tab) && !(await confirmBox(t("ide.unsaved"), t("ide.unsaved.text", { path: tab.path }), t("ide.discard"), true))) return;
|
|
328
613
|
tabs.splice(tabs.indexOf(tab), 1);
|
|
329
|
-
|
|
614
|
+
for (const ed of [editor, editor2]) if (ed && ed.getModel() === tab.model) ed.setModel(null);
|
|
615
|
+
if (active === tab) { active = null; if (tabs.length) activate(tabs[tabs.length - 1]); }
|
|
330
616
|
tab.model.dispose();
|
|
331
617
|
renderTabs();
|
|
618
|
+
renderCrumbs();
|
|
332
619
|
}
|
|
333
620
|
async function saveActive(force) {
|
|
334
621
|
const tab = active;
|
|
@@ -338,6 +625,7 @@
|
|
|
338
625
|
tab.mtime = r.mtime;
|
|
339
626
|
tab.saved = tab.model.getAlternativeVersionId();
|
|
340
627
|
renderTabs();
|
|
628
|
+
refreshGit();
|
|
341
629
|
} catch (err) {
|
|
342
630
|
if (String(err.message) !== "file_changed_on_disk") return output(explain(err));
|
|
343
631
|
const choice = await modal(t("ide.conflict"), el("p", { text: t("ide.conflict.text", { path: tab.path }) }), [{ label: t("pj.no"), ghost: true, value: null }, { label: t("ide.conflict.reload"), ghost: true, value: "reload" }, { label: t("ide.conflict.overwrite"), danger: true, value: "overwrite" }]);
|
|
@@ -353,6 +641,22 @@
|
|
|
353
641
|
tab.saved = tab.model.getAlternativeVersionId();
|
|
354
642
|
renderTabs();
|
|
355
643
|
}
|
|
644
|
+
// Split editor: a second editor beside the first, on the same file; each keeps its own position.
|
|
645
|
+
let splitOn = false;
|
|
646
|
+
function toggleSplit() {
|
|
647
|
+
splitOn = !splitOn;
|
|
648
|
+
if (splitOn && !editor2) { editor2 = monaco.editor.create($("editor2"), { ...EDITOR_OPTS(), model: null }); wireEditor(editor2); }
|
|
649
|
+
if (editor2) editor2.setModel(splitOn && active ? active.model : null);
|
|
650
|
+
if (!splitOn) focused = editor;
|
|
651
|
+
renderTabs();
|
|
652
|
+
relayout();
|
|
653
|
+
}
|
|
654
|
+
$("split-editor").addEventListener("click", () => { if (active) toggleSplit(); });
|
|
655
|
+
$("editor-more").addEventListener("click", (e) => menu(e.currentTarget, [
|
|
656
|
+
{ label: t("ide.save"), keys: `${MOD}S`, run: () => saveActive() },
|
|
657
|
+
{ label: t("ide.split"), check: splitOn, run: () => { if (active) toggleSplit(); } },
|
|
658
|
+
{ label: t("ide.close.all"), run: async () => { for (const tab of [...tabs]) await closeTab(tab); } },
|
|
659
|
+
]));
|
|
356
660
|
|
|
357
661
|
// ---- reviewing a change in the editor, piece by piece ---------------------------------------------
|
|
358
662
|
function closeReview() {
|
|
@@ -369,10 +673,10 @@
|
|
|
369
673
|
* one by one (it is changed in the right-hand side, which the person then keeps or saves).
|
|
370
674
|
*/
|
|
371
675
|
function showReview({ title, before, after, path, pieces, accept, reject, acceptLabel, rejectLabel }) {
|
|
372
|
-
const
|
|
676
|
+
const langUri = monaco.Uri.from({ scheme: "file", path: `/${path || "x.txt"}` });
|
|
373
677
|
const original = monaco.editor.createModel(before ?? "", undefined, monaco.Uri.from({ scheme: "bp-before", path: `/${Date.now()}/${path || "x"}` }));
|
|
374
678
|
const modified = monaco.editor.createModel(after ?? "", undefined, monaco.Uri.from({ scheme: "bp-after", path: `/${Date.now()}/${path || "x"}` }));
|
|
375
|
-
const language = monaco.editor.getModel(
|
|
679
|
+
const language = monaco.editor.getModel(langUri) ? monaco.editor.getModel(langUri).getLanguageId() : null;
|
|
376
680
|
if (language) { monaco.editor.setModelLanguage(original, language); monaco.editor.setModelLanguage(modified, language); }
|
|
377
681
|
if (review) { diffEditor.setModel(null); review.original.dispose(); review.modified.dispose(); }
|
|
378
682
|
diffEditor.setModel({ original, modified });
|
|
@@ -380,7 +684,9 @@
|
|
|
380
684
|
$("review").hidden = false;
|
|
381
685
|
$("diff").hidden = false;
|
|
382
686
|
$("editor").hidden = true;
|
|
687
|
+
$("editor2").hidden = true;
|
|
383
688
|
$("welcome").hidden = true;
|
|
689
|
+
$("crumbs").hidden = true;
|
|
384
690
|
$("review-title").textContent = title;
|
|
385
691
|
$("review-accept").textContent = acceptLabel || t("ide.review.keep");
|
|
386
692
|
$("review-reject").textContent = rejectLabel || t("ide.review.revert_all");
|
|
@@ -391,10 +697,11 @@
|
|
|
391
697
|
const draw = () => {
|
|
392
698
|
clear(hunks);
|
|
393
699
|
const changes = diffEditor.getLineChanges() || [];
|
|
394
|
-
changes.forEach((c, i) => hunks.append(el("button", { class: "ghost
|
|
700
|
+
changes.forEach((c, i) => hunks.append(el("button", { class: "ghost", "data-tip": "tip.ide.review.piece", text: t("ide.review.piece", { n: i + 1 }), onclick: () => revertPiece(c, original, modified) })));
|
|
395
701
|
};
|
|
396
702
|
diffEditor.onDidUpdateDiff(draw);
|
|
397
703
|
}
|
|
704
|
+
relayout();
|
|
398
705
|
}
|
|
399
706
|
/** Puts one changed piece back as it was. */
|
|
400
707
|
function revertPiece(c, original, modified) {
|
|
@@ -435,24 +742,56 @@
|
|
|
435
742
|
} catch (err) { output(explain(err)); }
|
|
436
743
|
}
|
|
437
744
|
|
|
438
|
-
// ---- quick open, search
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const input = el("input", { type: "text", placeholder: t("ide.quick.placeholder") });
|
|
745
|
+
// ---- quick open, the command palette, search -------------------------------------------------------
|
|
746
|
+
function picker(title, placeholder, source, onPick) {
|
|
747
|
+
const input = el("input", { type: "text", placeholder });
|
|
442
748
|
const list = el("ul", { class: "list" });
|
|
443
749
|
let results = [], sel = 0;
|
|
444
|
-
const draw = () => { clear(list); results.forEach((r, i) => list.append(el("li", { class: i === sel ? "on" : "",
|
|
445
|
-
const pick = (r) => { $("modal").hidden = true;
|
|
750
|
+
const draw = () => { clear(list); results.forEach((r, i) => list.append(el("li", { class: i === sel ? "on" : "", onclick: () => pick(r) }, r.icon || null, el("span", { class: "name", text: r.label }), r.keys ? el("span", { class: "keys", text: r.keys }) : null))); const on = list.querySelector("li.on"); if (on) on.scrollIntoView({ block: "nearest" }); };
|
|
751
|
+
const pick = (r) => { $("modal").hidden = true; onPick(r); };
|
|
446
752
|
let timer = null;
|
|
447
|
-
|
|
753
|
+
const update = () => { clearTimeout(timer); timer = setTimeout(async () => { try { results = await source(input.value.trim()); sel = 0; draw(); } catch { results = []; draw(); } }, 80); };
|
|
754
|
+
input.addEventListener("input", update);
|
|
448
755
|
input.addEventListener("keydown", (e) => {
|
|
449
756
|
if (e.key === "ArrowDown") { sel = Math.min(results.length - 1, sel + 1); draw(); e.preventDefault(); }
|
|
450
757
|
if (e.key === "ArrowUp") { sel = Math.max(0, sel - 1); draw(); e.preventDefault(); }
|
|
451
758
|
if (e.key === "Enter" && results[sel]) pick(results[sel]);
|
|
452
759
|
});
|
|
453
|
-
|
|
760
|
+
update();
|
|
761
|
+
modal(title, [input, list], [{ label: t("ide.close"), ghost: true, value: null }]);
|
|
762
|
+
}
|
|
763
|
+
function quickOpen() {
|
|
764
|
+
if (!folder) return;
|
|
765
|
+
picker(t("ide.quick"), t("ide.quick.placeholder"), async (q) => {
|
|
766
|
+
if (!q) return tabs.map((x) => ({ label: x.path, path: x.path, icon: fileIcon(x.path.split("/").pop(), false) }));
|
|
767
|
+
return (await api("/api/ws/search", { folder, query: q, names: true })).results.slice(0, 60).map((r) => ({ label: r.path, path: r.path, icon: fileIcon(r.path.split("/").pop(), false) }));
|
|
768
|
+
}, (r) => openFile(r.path));
|
|
454
769
|
}
|
|
455
|
-
|
|
770
|
+
const COMMANDS = () => [
|
|
771
|
+
{ label: t("ide.quick"), keys: `${MOD}P`, run: quickOpen },
|
|
772
|
+
{ label: t("ide.save"), keys: `${MOD}S`, run: () => saveActive() },
|
|
773
|
+
{ label: t("ide.agent.new"), keys: `${MOD}L`, run: () => newAgent() },
|
|
774
|
+
{ label: t("ide.inline"), keys: `${MOD}K`, run: () => inlineEdit() },
|
|
775
|
+
{ label: t("ide.cmd.mode"), keys: "⇧Tab", run: () => cycleMode() },
|
|
776
|
+
{ label: t("ide.cmd.toggle.side"), keys: `${MOD}B`, run: () => toggle("sideOpen") },
|
|
777
|
+
{ label: t("ide.cmd.toggle.panel"), keys: `${MOD}J`, run: () => $("toggle-panel").click() },
|
|
778
|
+
{ label: t("ide.cmd.toggle.agent"), keys: `${MOD}⌥B`, run: () => toggle("agentOpen") },
|
|
779
|
+
{ label: t("ide.cmd.terminal"), keys: isMac ? "⌃`" : "Ctrl+`", run: showTerminal },
|
|
780
|
+
{ label: t("ide.explorer"), keys: `${MOD}⇧E`, run: () => showSide("files") },
|
|
781
|
+
{ label: t("ide.search"), keys: `${MOD}⇧F`, run: () => showSide("search") },
|
|
782
|
+
{ label: t("ide.git"), run: () => showSide("git") },
|
|
783
|
+
{ label: t("ide.problems.run"), run: () => $("problems-run").click() },
|
|
784
|
+
{ label: t("ide.ports"), run: () => showBottom("ports") },
|
|
785
|
+
{ label: t("ide.split"), run: () => { if (active) toggleSplit(); } },
|
|
786
|
+
{ label: t("ide.preview"), run: () => openPreview() },
|
|
787
|
+
{ label: t("pj.limits"), run: () => editLimits() },
|
|
788
|
+
{ label: t("pj.folder.add"), run: () => addFolder() },
|
|
789
|
+
...["bottom", "top", "left", "right"].map((p) => ({ label: `${t("ide.panel.position")}: ${t(`ide.panel.pos.${p}`)}`, run: () => { layoutState.panelPos = p; layout(); } })),
|
|
790
|
+
];
|
|
791
|
+
function palette() {
|
|
792
|
+
picker(t("ide.palette"), t("ide.palette.placeholder"), async (q) => COMMANDS().filter((c) => !q || c.label.toLowerCase().includes(q.toLowerCase())), (c) => c.run());
|
|
793
|
+
}
|
|
794
|
+
$("palette-btn").addEventListener("click", palette);
|
|
456
795
|
let searchTimer = null;
|
|
457
796
|
async function runSearch() {
|
|
458
797
|
const q = $("search-q").value.trim();
|
|
@@ -462,12 +801,25 @@
|
|
|
462
801
|
try {
|
|
463
802
|
const r = await api("/api/ws/search", { folder, query: q, caseSensitive: $("search-case").checked, regex: $("search-regex").checked });
|
|
464
803
|
$("search-info").textContent = t(r.truncated ? "ide.search.many" : "ide.search.count", { n: r.results.length });
|
|
465
|
-
for (const hit of r.results) out.append(el("li", { title: `${hit.path}:${hit.line}`, onclick: () => openFile(hit.path, hit.line) }, el("span", { class: "
|
|
804
|
+
for (const hit of r.results) out.append(el("li", { title: `${hit.path}:${hit.line}`, onclick: () => openFile(hit.path, hit.line) }, fileIcon(hit.path.split("/").pop(), false), el("span", { class: "name", text: hit.text.trim() }), el("span", { class: "muted small", text: `${hit.path.split("/").pop()}:${hit.line}` })));
|
|
466
805
|
} catch (err) { $("search-info").textContent = explain(err); }
|
|
467
806
|
}
|
|
468
807
|
for (const id of ["search-q", "search-case", "search-regex"]) $(id).addEventListener("input", () => { clearTimeout(searchTimer); searchTimer = setTimeout(runSearch, 250); });
|
|
469
808
|
|
|
470
809
|
// ---- git: the person's own actions -------------------------------------------------------------------
|
|
810
|
+
/** Branch and colours in the tree, quietly: a folder without git simply shows none. */
|
|
811
|
+
async function refreshGit() {
|
|
812
|
+
if (!folder) return;
|
|
813
|
+
try {
|
|
814
|
+
const r = await api("/api/ws/git/status", { folder });
|
|
815
|
+
const status = new Map();
|
|
816
|
+
for (const f of r.files) status.set(f.path.replace(/\/$/, ""), f.unstaged === "?" ? "U" : f.staged === "A" ? "A" : f.unstaged === "D" || f.staged === "D" ? "D" : "M");
|
|
817
|
+
gitInfo = { branch: (r.branch || "").split("...")[0], status, ignored: r.ignored || [] };
|
|
818
|
+
} catch { gitInfo = { branch: "", status: new Map(), ignored: [] }; }
|
|
819
|
+
$("sb-branch").hidden = !gitInfo.branch;
|
|
820
|
+
$("sb-branch").lastChild.textContent = gitInfo.branch;
|
|
821
|
+
if (treeCache.size) loadTree(".", $("tree"), 0).catch(() => {});
|
|
822
|
+
}
|
|
471
823
|
async function gitRefresh() {
|
|
472
824
|
if (!folder) return;
|
|
473
825
|
const staged = clear($("git-staged")), changes = clear($("git-changes"));
|
|
@@ -476,14 +828,16 @@
|
|
|
476
828
|
const r = await api("/api/ws/git/status", { folder });
|
|
477
829
|
$("git-branch").textContent = r.branch ? t("ide.git.branch", { branch: r.branch }) : "";
|
|
478
830
|
for (const f of r.files) {
|
|
479
|
-
const row = (
|
|
480
|
-
el("span", { class: "
|
|
481
|
-
el("button", { class: "
|
|
482
|
-
|
|
483
|
-
if (f.
|
|
831
|
+
const row = (letter, stage) => el("li", { title: f.path, onclick: () => gitDiff(f.path, stage === false) },
|
|
832
|
+
fileIcon(f.path.split("/").pop(), false), el("span", { class: "name", text: f.path.split("/").pop() }), el("span", { class: "muted small name", text: f.path.includes("/") ? f.path.replace(/\/[^/]*$/, "") : "" }), el("span", { class: "grow" }),
|
|
833
|
+
el("button", { class: "ib", "data-tip": stage ? "tip.ide.git.stage" : "tip.ide.git.unstage", onclick: async (e) => { e.stopPropagation(); try { await api(stage ? "/api/ws/git/stage" : "/api/ws/git/unstage", { folder, paths: [f.path] }); gitRefresh(); } catch (err) { $("git-msg").textContent = explain(err); } } }, icon(stage ? "plus" : "undo")),
|
|
834
|
+
el("span", { class: `badge ${letter === "U" || letter === "A" ? "good" : letter === "D" ? "bad" : "warn"}`, text: letter }));
|
|
835
|
+
if (f.staged) staged.append(row(f.staged, false));
|
|
836
|
+
if (f.unstaged) changes.append(row(f.unstaged === "?" ? "U" : f.unstaged, true));
|
|
484
837
|
}
|
|
485
838
|
if (!r.files.length) $("git-msg").textContent = t("ide.git.clean");
|
|
486
839
|
} catch (err) { $("git-branch").textContent = ""; $("git-msg").textContent = explain(err); }
|
|
840
|
+
refreshGit();
|
|
487
841
|
}
|
|
488
842
|
async function gitDiff(path, staged) {
|
|
489
843
|
try {
|
|
@@ -493,6 +847,7 @@
|
|
|
493
847
|
} catch (err) { $("git-msg").textContent = explain(err); }
|
|
494
848
|
}
|
|
495
849
|
$("git-refresh").addEventListener("click", gitRefresh);
|
|
850
|
+
$("sb-branch").addEventListener("click", () => showSide("git"));
|
|
496
851
|
$("git-commit").addEventListener("click", async () => {
|
|
497
852
|
try { const r = await api("/api/ws/git/commit", { folder, message: $("git-message").value }); $("git-message").value = ""; output(r.output); gitRefresh(); } catch (err) { $("git-msg").textContent = explain(err); }
|
|
498
853
|
});
|
|
@@ -502,6 +857,14 @@
|
|
|
502
857
|
});
|
|
503
858
|
|
|
504
859
|
// ---- problems, outline, history, preview --------------------------------------------------------------
|
|
860
|
+
function renderProblemCounts() {
|
|
861
|
+
$("sb-errors").textContent = String(problemCounts.errors);
|
|
862
|
+
$("sb-warnings").textContent = String(problemCounts.warnings);
|
|
863
|
+
const n = problemCounts.errors + problemCounts.warnings;
|
|
864
|
+
$("problems-count").hidden = !n;
|
|
865
|
+
$("problems-count").textContent = String(n);
|
|
866
|
+
}
|
|
867
|
+
$("sb-problems").addEventListener("click", () => showBottom("problems"));
|
|
505
868
|
$("problems-run").addEventListener("click", async () => {
|
|
506
869
|
if (!folder) return;
|
|
507
870
|
const list = clear($("problems"));
|
|
@@ -513,10 +876,13 @@
|
|
|
513
876
|
if (!r.ran.length) list.append(el("li", { class: "muted", text: t("ide.problems.none_setup") }));
|
|
514
877
|
else if (!r.problems.length) list.append(el("li", { class: "muted", text: t("ide.problems.clean", { tools: r.ran.join(", ") }) }));
|
|
515
878
|
const byFile = {};
|
|
879
|
+
problemCounts = { errors: 0, warnings: 0 };
|
|
516
880
|
for (const p of r.problems) {
|
|
517
881
|
(byFile[p.file] = byFile[p.file] || []).push(p);
|
|
518
|
-
|
|
882
|
+
problemCounts[p.severity === "error" ? "errors" : "warnings"]++;
|
|
883
|
+
list.append(el("li", { title: p.message, onclick: () => openFile(p.file, p.line) }, icon(p.severity === "error" ? "error" : "warning"), el("span", { class: "name", text: p.message }), el("span", { class: "muted small", text: `${p.file}:${p.line} · ${p.source}` })));
|
|
519
884
|
}
|
|
885
|
+
renderProblemCounts();
|
|
520
886
|
for (const m of monaco.editor.getModels()) {
|
|
521
887
|
if (m.uri.scheme !== "file") continue;
|
|
522
888
|
const ps = byFile[m.uri.path.slice(1)] || [];
|
|
@@ -534,37 +900,48 @@
|
|
|
534
900
|
/^\s*(?:public|private|protected|static|\s)+[\w<>[\],\s]+\s+(\w+)\s*\([^;]*$/,
|
|
535
901
|
/^(#{1,6})\s+(.+)$/,
|
|
536
902
|
];
|
|
903
|
+
const outlineLists = () => [$("outline"), $("outline-view")];
|
|
537
904
|
function renderOutline() {
|
|
538
|
-
const
|
|
539
|
-
|
|
905
|
+
const want = layoutState.panes.outline || layoutState.sideView === "outline";
|
|
906
|
+
for (const list of outlineLists()) clear(list);
|
|
907
|
+
if (!want || !active || !active.model) return;
|
|
540
908
|
const lines = active.model.getValue().split("\n");
|
|
541
909
|
if (lines.length > 20000) return;
|
|
910
|
+
const items = [];
|
|
542
911
|
lines.forEach((text, i) => {
|
|
543
912
|
for (const re of OUTLINE) {
|
|
544
913
|
const m = re.exec(text);
|
|
545
914
|
if (!m) continue;
|
|
546
915
|
const name = re === OUTLINE[5] ? `${" ".repeat(m[1].length - 1)}${m[2]}` : m.slice(1).find(Boolean);
|
|
547
|
-
if (name)
|
|
916
|
+
if (name) items.push([i + 1, name]);
|
|
548
917
|
break;
|
|
549
918
|
}
|
|
550
919
|
});
|
|
920
|
+
for (const list of outlineLists()) for (const [line, name] of items) list.append(el("li", { "data-line": line, onclick: () => openFile(active.path, line) }, el("span", { class: "name", text: name }), el("span", { class: "muted small", text: String(line) })));
|
|
551
921
|
markOutline();
|
|
552
922
|
}
|
|
553
923
|
function markOutline() {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
for (const
|
|
558
|
-
|
|
924
|
+
const ed = focused || editor;
|
|
925
|
+
if (!ed || !ed.getPosition()) return;
|
|
926
|
+
const line = ed.getPosition().lineNumber;
|
|
927
|
+
for (const list of outlineLists()) {
|
|
928
|
+
let best = null;
|
|
929
|
+
for (const li of list.children) if (Number(li.dataset.line) <= line) best = li;
|
|
930
|
+
for (const li of list.children) li.classList.toggle("on", li === best);
|
|
931
|
+
}
|
|
559
932
|
}
|
|
560
933
|
async function renderHistory() {
|
|
561
|
-
const
|
|
562
|
-
|
|
934
|
+
const lists = [$("history"), $("history-view")];
|
|
935
|
+
for (const l of lists) clear(l);
|
|
936
|
+
if (!(layoutState.panes.history || layoutState.sideView === "history")) return;
|
|
937
|
+
if (!active) { for (const l of lists) l.append(el("li", { class: "muted", text: t("ide.history.none") })); return; }
|
|
563
938
|
try {
|
|
564
939
|
const r = await api("/api/ws/history", { folder, path: active.path });
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
940
|
+
for (const l of lists) {
|
|
941
|
+
if (!r.versions.length) l.append(el("li", { class: "muted", text: t("ide.history.none") }));
|
|
942
|
+
for (const v of r.versions) l.append(el("li", { onclick: () => showVersion(active.path, v.id) }, el("span", { class: "badge", text: t(`ide.history.${v.why}`) }), el("span", { class: "name", text: new Date(v.at).toLocaleString(lang) })));
|
|
943
|
+
}
|
|
944
|
+
} catch (err) { for (const l of lists) l.append(el("li", { class: "muted", text: explain(err) })); }
|
|
568
945
|
}
|
|
569
946
|
async function showVersion(path, id) {
|
|
570
947
|
try {
|
|
@@ -575,12 +952,42 @@
|
|
|
575
952
|
reject: async () => { if (tab) { tab.model.setValue(v.text); activate(tab); } else await writeFrom(path, v.text); } });
|
|
576
953
|
} catch (err) { output(explain(err)); }
|
|
577
954
|
}
|
|
578
|
-
|
|
955
|
+
async function openPreview() {
|
|
579
956
|
const url = await askText(t("ide.preview"), t("ide.preview.hint"), store.get("preview", "http://localhost:3000"));
|
|
580
957
|
if (!url) return;
|
|
581
958
|
store.set("preview", url);
|
|
582
959
|
try { await api("/api/ws/preview", { url }); } catch (err) { output(explain(err)); }
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// ---- settings, folder ---------------------------------------------------------------------------------
|
|
963
|
+
$("settings-btn").addEventListener("click", (e) => menu(e.currentTarget, [
|
|
964
|
+
{ label: t("ide.palette"), keys: `${MOD}⇧P`, run: palette },
|
|
965
|
+
{ label: t("pj.limits"), run: () => editLimits() },
|
|
966
|
+
{ label: t("ide.preview"), run: openPreview },
|
|
967
|
+
{ label: t("ide.terminal.settings"), run: terminalSettings },
|
|
968
|
+
{ sep: true },
|
|
969
|
+
{ label: t("ide.panel.position"), sub: ["bottom", "top", "left", "right"].map((p) => ({ label: t(`ide.panel.pos.${p}`), check: layoutState.panelPos === p, run: () => { layoutState.panelPos = p; layout(); } })) },
|
|
970
|
+
{ label: t("ide.layout.reset"), run: () => { Object.assign(layoutState, { side: 260, agent: 380, panel: 240, panelWide: 420, sideOpen: true, agentOpen: true, panelOpen: true, panelPos: "bottom", panelAlign: "center", panelMax: false }); layout(); } },
|
|
971
|
+
]));
|
|
972
|
+
async function terminalSettings() {
|
|
973
|
+
const box = el("div");
|
|
974
|
+
try { const mod = await import(`${BASE}terminal.js`); mod.mountTerminalSetting(box, { token, t }); } catch { box.append(el("p", { class: "muted", text: t("ide.terminal.missing") })); }
|
|
975
|
+
await modal(t("ide.terminal.settings"), box, [{ label: t("ide.ok"), value: null }]);
|
|
976
|
+
}
|
|
977
|
+
const folderLabel = (f) => f.split(/[\\/]/).filter(Boolean).pop() || f;
|
|
978
|
+
$("folder-btn").addEventListener("click", (e) => {
|
|
979
|
+
const sel = $("folder");
|
|
980
|
+
menu(e.currentTarget, [
|
|
981
|
+
...[...sel.options].filter((o) => o.value).map((o) => ({ label: folderLabel(o.value), keys: o.value.length > 40 ? `…${o.value.slice(-38)}` : o.value, check: o.value === folder, run: () => { sel.value = o.value; setFolder(o.value); } })),
|
|
982
|
+
{ sep: true },
|
|
983
|
+
{ label: t("pj.folder.add"), run: addFolder },
|
|
984
|
+
]);
|
|
583
985
|
});
|
|
986
|
+
async function addFolder() {
|
|
987
|
+
const path = await askText(t("pj.folder.add"), t("pj.folder.confirm"), "");
|
|
988
|
+
if (!path) return;
|
|
989
|
+
try { await api("/api/folders", { path }); await loadFolders(); const last = (await api("/api/projects")).folders.slice(-1)[0]; $("folder").value = last; setFolder(last); } catch (err) { output(explain(err)); }
|
|
990
|
+
}
|
|
584
991
|
|
|
585
992
|
// ---- models, folder, mode, limits ---------------------------------------------------------------------
|
|
586
993
|
async function loadModels() {
|
|
@@ -595,16 +1002,19 @@
|
|
|
595
1002
|
for (const [label, list] of groups) {
|
|
596
1003
|
if (!list.length) continue;
|
|
597
1004
|
const g = el("optgroup", { label: t(label) });
|
|
598
|
-
for (const m of list) g.append(el("option", { value: m.key, disabled:
|
|
1005
|
+
for (const m of list) g.append(el("option", { value: m.key, disabled: m.available === false, text: m.available === false ? `${m.label} · ${t("ide.model.unavailable")}` : m.label }));
|
|
599
1006
|
sel.append(g);
|
|
600
1007
|
}
|
|
601
1008
|
if (!models.length) sel.append(el("option", { value: "", text: t("ide.model.none") }));
|
|
602
1009
|
const keep = store.get("model", "");
|
|
603
|
-
const first = models.find((m) => m.available && m.key === keep) || models.find((m) => m.available && m.own) || models.find((m) => m.available);
|
|
1010
|
+
const first = models.find((m) => m.available !== false && m.key === keep) || models.find((m) => m.available !== false && m.own) || models.find((m) => m.available !== false);
|
|
604
1011
|
if (first) sel.value = first.key;
|
|
1012
|
+
renderStatusModel();
|
|
605
1013
|
}
|
|
606
|
-
const chosenModel = () => models.find((m) => m.key === $("model").value && m.available) || null;
|
|
607
|
-
$("model").
|
|
1014
|
+
const chosenModel = () => models.find((m) => m.key === $("model").value && m.available !== false) || null;
|
|
1015
|
+
function renderStatusModel() { const m = chosenModel(); $("sb-model").textContent = m ? m.label : t("ide.model.none"); }
|
|
1016
|
+
$("model").addEventListener("change", () => { store.set("model", $("model").value); renderStatusModel(); });
|
|
1017
|
+
$("sb-model").addEventListener("click", () => { layoutState.agentOpen = true; layout(); $("model").focus(); });
|
|
608
1018
|
async function loadFolders() {
|
|
609
1019
|
const r = await api("/api/projects");
|
|
610
1020
|
const sel = clear($("folder"));
|
|
@@ -619,23 +1029,29 @@
|
|
|
619
1029
|
for (const tab of [...tabs]) closeTab(tab, true);
|
|
620
1030
|
folder = f;
|
|
621
1031
|
store.set("folder", f);
|
|
1032
|
+
$("folder-name").textContent = f ? folderLabel(f) : t("ide.folder.none");
|
|
1033
|
+
$("tree-title").textContent = f ? folderLabel(f) : "";
|
|
1034
|
+
$("palette-label").textContent = f ? `${folderLabel(f)} — ${t("ide.palette.hint", { keys: `${MOD}⇧P` })}` : t("ide.palette");
|
|
622
1035
|
session = null;
|
|
623
1036
|
cursor = 0;
|
|
624
1037
|
clear($("agent-log"));
|
|
625
1038
|
opened.clear();
|
|
626
1039
|
opened.add(".");
|
|
1040
|
+
treeCache.clear();
|
|
1041
|
+
await refreshGit();
|
|
627
1042
|
refreshTree();
|
|
628
1043
|
if (terminal && terminal.folderChanged) terminal.folderChanged(f);
|
|
629
1044
|
await refreshAgents();
|
|
630
1045
|
}
|
|
631
1046
|
$("folder").addEventListener("change", () => setFolder($("folder").value));
|
|
632
|
-
$("folder-add").addEventListener("click", async () => {
|
|
633
|
-
const path = await askText(t("pj.folder.add"), t("pj.folder.confirm"), "");
|
|
634
|
-
if (!path) return;
|
|
635
|
-
try { await api("/api/folders", { path }); await loadFolders(); $("folder").value = (await api("/api/projects")).folders.slice(-1)[0]; setFolder($("folder").value); } catch (err) { output(explain(err)); }
|
|
636
|
-
});
|
|
637
1047
|
const MODES = ["ask", "edits", "auto"];
|
|
638
|
-
function renderModeHint() {
|
|
1048
|
+
function renderModeHint() {
|
|
1049
|
+
const mode = $("mode").value;
|
|
1050
|
+
$("mode-hint").textContent = mode === "auto" ? t("ide.mode.auto.on", { keys: "⇧Tab" }) : t(`pj.mode.${mode}.hint`);
|
|
1051
|
+
$("mode-hint").classList.toggle("auto", mode === "auto");
|
|
1052
|
+
$("sb-mode").textContent = t(`pj.mode.${mode}`);
|
|
1053
|
+
$("sb-mode").classList.toggle("auto", mode === "auto");
|
|
1054
|
+
}
|
|
639
1055
|
async function setMode(mode) {
|
|
640
1056
|
const prev = $("mode").value;
|
|
641
1057
|
$("mode").value = mode;
|
|
@@ -644,7 +1060,9 @@
|
|
|
644
1060
|
try { await api("/api/projects/mode", { id: session.id, mode, remoteConfirmed: mode === "auto" && session.leaves ? await remoteOk(true) : undefined }); }
|
|
645
1061
|
catch (err) { $("mode").value = prev; renderModeHint(); output(explain(err)); }
|
|
646
1062
|
}
|
|
1063
|
+
const cycleMode = () => setMode(MODES[(MODES.indexOf($("mode").value) + 1) % MODES.length]);
|
|
647
1064
|
$("mode").addEventListener("change", () => setMode($("mode").value));
|
|
1065
|
+
$("sb-mode").addEventListener("click", cycleMode);
|
|
648
1066
|
/** Content that leaves this computer: the person says yes once, knowing it. */
|
|
649
1067
|
async function remoteOk(auto) {
|
|
650
1068
|
return !!(await confirmBox(t("ide.remote.title"), t(auto ? "pj.auto.confirm" : "pj.remote"), t("pj.yes")));
|
|
@@ -661,10 +1079,7 @@
|
|
|
661
1079
|
return { k, input, block: el("div", {}, el("label", { class: "row" }, el("strong", { text: t(`pj.limit.${k}`) }), input, el("span", { class: "muted small", text: t("ide.limits.auto_value", { n: session.autoLimits[k] }) })), el("p", { class: "muted small", text: t(`ide.limits.${k}.what`) })) };
|
|
662
1080
|
};
|
|
663
1081
|
const fields = ["steps", "minutes", "credits"].map(field);
|
|
664
|
-
const body = [
|
|
665
|
-
el("p", { text: t("ide.limits.intro") }), ...fields.map((f) => f.block),
|
|
666
|
-
limitsExplained(),
|
|
667
|
-
];
|
|
1082
|
+
const body = [el("p", { text: t("ide.limits.intro") }), ...fields.map((f) => f.block), limitsExplained()];
|
|
668
1083
|
const choice = await modal(t("pj.limits"), body, [{ label: t("pj.no"), ghost: true, value: null }, { label: t("ide.limits.reset"), ghost: true, value: "auto" }, { label: t("pj.limits.save"), value: "save" }]);
|
|
669
1084
|
try {
|
|
670
1085
|
if (choice === "auto") session = await api("/api/projects/limits", { id: session.id, auto: true });
|
|
@@ -672,7 +1087,6 @@
|
|
|
672
1087
|
renderAgent(session);
|
|
673
1088
|
} catch (err) { output(explain(err)); }
|
|
674
1089
|
}
|
|
675
|
-
$("limits-open").addEventListener("click", editLimits);
|
|
676
1090
|
$("limit-edit").addEventListener("click", editLimits);
|
|
677
1091
|
|
|
678
1092
|
// ---- agents ---------------------------------------------------------------------------------------
|
|
@@ -682,18 +1096,16 @@
|
|
|
682
1096
|
agentList = (r.sessions || []).filter((s) => s.folder === folder).sort((x, y) => y.updated - x.updated);
|
|
683
1097
|
drawAgents();
|
|
684
1098
|
if (!session && agentList.length) selectAgent(agentList[0].id);
|
|
685
|
-
$("agents-toggle").textContent = session ? session.title || t("ide.agent.untitled") :
|
|
1099
|
+
$("agents-toggle").textContent = session ? session.title || t("ide.agent.untitled") : "";
|
|
686
1100
|
}
|
|
687
1101
|
function drawAgents() {
|
|
688
1102
|
const q = $("agents-q").value.trim().toLowerCase();
|
|
689
1103
|
const list = clear($("agents-list"));
|
|
690
1104
|
for (const a of agentList.filter((x) => !q || (x.title || "").toLowerCase().includes(q))) {
|
|
691
|
-
list.append(el("li", { class: session && a.id === session.id ? "on" : "", onclick: () =>
|
|
692
|
-
el("span", { class: "
|
|
1105
|
+
list.append(el("li", { class: session && a.id === session.id ? "on" : "", title: a.title || "", onclick: () => selectAgent(a.id) },
|
|
1106
|
+
el("span", { class: "name grow", text: a.title || t("ide.agent.untitled") }), el("span", { class: "muted small", text: t(`pj.status.${a.status}`).replace(/[.…]$/, "") })));
|
|
693
1107
|
}
|
|
694
|
-
if (!agentList.length) list.append(el("li", { class: "muted", text: t("ide.agent.none") }));
|
|
695
1108
|
}
|
|
696
|
-
$("agents-toggle").addEventListener("click", () => { $("agents").hidden = !$("agents").hidden; drawAgents(); });
|
|
697
1109
|
$("agents-q").addEventListener("input", drawAgents);
|
|
698
1110
|
async function selectAgent(id) {
|
|
699
1111
|
session = await api("/api/projects/state", { id, after: 0 });
|
|
@@ -702,6 +1114,7 @@
|
|
|
702
1114
|
$("mode").value = session.mode;
|
|
703
1115
|
renderModeHint();
|
|
704
1116
|
renderAgent(session);
|
|
1117
|
+
drawAgents();
|
|
705
1118
|
}
|
|
706
1119
|
async function newAgent() {
|
|
707
1120
|
const m = chosenModel();
|
|
@@ -716,6 +1129,8 @@
|
|
|
716
1129
|
clear($("agent-log"));
|
|
717
1130
|
renderAgent(session);
|
|
718
1131
|
await refreshAgents();
|
|
1132
|
+
layoutState.agentOpen = true;
|
|
1133
|
+
layout();
|
|
719
1134
|
$("prompt").focus();
|
|
720
1135
|
return session;
|
|
721
1136
|
} catch (err) { output(explain(err)); return null; }
|
|
@@ -732,7 +1147,7 @@
|
|
|
732
1147
|
cursor = e.n + 1;
|
|
733
1148
|
log.append(eventNode(e));
|
|
734
1149
|
if (e.kind === "output") { output(e.text); if (terminal && terminal.agentOutput) terminal.agentOutput(e.text); }
|
|
735
|
-
if (e.kind === "action" && e.action === "run") { output(`$ ${e.text}`); if (terminal && terminal.runVisible) terminal.runVisible(e.text); }
|
|
1150
|
+
if (e.kind === "action" && e.action === "run") { output(`$ ${e.text}`); if (terminal && terminal.runVisible) terminal.runVisible(e.text); setTimeout(refreshPorts, 2500); }
|
|
736
1151
|
if (e.kind === "notice" && (e.code === "command_ended" || e.code === "command_timeout") && terminal && terminal.agentDone) terminal.agentDone(e.text);
|
|
737
1152
|
}
|
|
738
1153
|
log.scrollTop = log.scrollHeight;
|
|
@@ -745,8 +1160,8 @@
|
|
|
745
1160
|
renderPending(v.pending);
|
|
746
1161
|
const ch = clear($("agent-changes"));
|
|
747
1162
|
if (v.changes && v.changes.length) {
|
|
748
|
-
ch.append(el("div", { class: "
|
|
749
|
-
for (const c of v.changes) ch.append(el("div", {}, el("a", { href: "#", text: c.path, onclick: (e) => { e.preventDefault(); reviewAgentChange(c.path); } })));
|
|
1163
|
+
ch.append(el("div", { class: "sub-head", text: t("pj.tab.changes") }));
|
|
1164
|
+
for (const c of v.changes) ch.append(el("div", { class: "row" }, fileIcon(c.path.split("/").pop(), false), el("a", { href: "#", text: c.path, onclick: (e) => { e.preventDefault(); reviewAgentChange(c.path); } })));
|
|
750
1165
|
}
|
|
751
1166
|
$("agents-toggle").textContent = v.title || t("ide.agent.untitled");
|
|
752
1167
|
for (const tab of tabs) if (!isDirty(tab) && v.changes && v.changes.some((c) => c.path === tab.path)) reloadTab(tab).catch(() => {});
|
|
@@ -755,7 +1170,7 @@
|
|
|
755
1170
|
if (e.kind === "user") return el("div", { class: "ev user", text: e.text });
|
|
756
1171
|
if (e.kind === "assistant") return el("div", { class: "ev", text: e.text });
|
|
757
1172
|
if (e.kind === "output") return el("div", { class: "ev output", text: e.text });
|
|
758
|
-
if (e.kind === "action") return el("div", { class: "ev action", text: `${t(`pj.act.${e.action}`)} ${e.path || e.text || ""}` });
|
|
1173
|
+
if (e.kind === "action") return el("div", { class: "ev action" }, icon(e.action === "run" ? "terminal" : e.action === "search" ? "search" : "files"), el("span", { text: `${t(`pj.act.${e.action}`)} ${e.path || e.text || ""}` }));
|
|
759
1174
|
if (e.kind === "error") return el("div", { class: "ev error", text: `${explain(e.code)}${e.text ? `\n${e.text}` : ""}` });
|
|
760
1175
|
const key = `pj.note.${e.code}`;
|
|
761
1176
|
return el("div", { class: "ev notice", text: t(key, { code: e.text }) });
|
|
@@ -777,7 +1192,7 @@
|
|
|
777
1192
|
box.append(el("strong", { text: t("pj.ask.command") }), el("code", { text: p.command }), el("p", { class: "muted small", text: `${t("pj.ask.cwd", { cwd: p.cwd })} · ${t("pj.where.computer")}` }));
|
|
778
1193
|
if (p.risk) box.append(el("p", { class: "small", text: t(`pj.risk.${p.risk}`) }));
|
|
779
1194
|
}
|
|
780
|
-
box.append(el("div", { class: "row" }, el("button", { "data-tip": "tip.pj.approve", text: t("pj.approve"), onclick: () => answer(true) }), el("button", { class: "ghost", "data-tip": "tip.pj.refuse", text: t("pj.refuse"), onclick: () => answer(false) })));
|
|
1195
|
+
box.append(el("div", { class: "row" }, el("button", { class: "primary", "data-tip": "tip.pj.approve", text: t("pj.approve"), onclick: () => answer(true) }), el("button", { class: "ghost", "data-tip": "tip.pj.refuse", text: t("pj.refuse"), onclick: () => answer(false) })));
|
|
781
1196
|
}
|
|
782
1197
|
|
|
783
1198
|
/** @-mentions: typing @ suggests files of the folder. */
|
|
@@ -791,7 +1206,7 @@
|
|
|
791
1206
|
function drawMentions() {
|
|
792
1207
|
const box = clear($("mention"));
|
|
793
1208
|
box.hidden = !mentionItems.length;
|
|
794
|
-
mentionItems.forEach((p, i) => box.append(el("li", { class: i === mentionSel ? "on" : "",
|
|
1209
|
+
mentionItems.forEach((p, i) => box.append(el("li", { class: i === mentionSel ? "on" : "", onmousedown: (e) => { e.preventDefault(); insertMention(p); } }, fileIcon(p.split("/").pop(), false), el("span", { class: "name", text: p }))));
|
|
795
1210
|
}
|
|
796
1211
|
function insertMention(path) {
|
|
797
1212
|
const ta = $("prompt");
|
|
@@ -843,11 +1258,13 @@
|
|
|
843
1258
|
if (!session || document.hidden) return;
|
|
844
1259
|
try { renderAgent(await api("/api/projects/state", { id: session.id, after: cursor })); } catch (err) { if (/project_not_found/.test(err.message)) { session = null; refreshAgents(); } }
|
|
845
1260
|
}, 800);
|
|
1261
|
+
setInterval(() => { if (!document.hidden && folder) refreshGit(); }, 15000);
|
|
846
1262
|
|
|
847
1263
|
/** Rewrites the selected text with the agent's model, shown as a change to accept. */
|
|
848
1264
|
async function inlineEdit() {
|
|
849
1265
|
if (!active) return;
|
|
850
|
-
const
|
|
1266
|
+
const ed = focused || editor;
|
|
1267
|
+
const sel = ed.getSelection();
|
|
851
1268
|
const model = active.model;
|
|
852
1269
|
const range = sel && !sel.isEmpty() ? sel : new monaco.Range(sel.startLineNumber, 1, sel.startLineNumber, model.getLineMaxColumn(sel.startLineNumber));
|
|
853
1270
|
const instruction = await askText(t("ide.inline"), t("ide.inline.hint"), "");
|
|
@@ -868,43 +1285,52 @@
|
|
|
868
1285
|
}
|
|
869
1286
|
|
|
870
1287
|
// ---- keys ------------------------------------------------------------------------------------------
|
|
871
|
-
const KEYS = [["P", "ide.key.quick"], ["⇧F", "ide.key.search"], ["B", "ide.key.side"], ["J", "ide.key.terminal"], ["L", "ide.key.agent"], ["K", "ide.key.inline"], ["S", "ide.key.save"]];
|
|
1288
|
+
const KEYS = [["⇧P", "ide.palette"], ["P", "ide.key.quick"], ["⇧F", "ide.key.search"], ["B", "ide.key.side"], ["J", "ide.key.terminal"], ["L", "ide.key.agent"], ["K", "ide.key.inline"], ["S", "ide.key.save"]];
|
|
872
1289
|
function drawKeys() {
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
for (const [k, label] of KEYS)
|
|
876
|
-
|
|
1290
|
+
const table = clear($("keys"));
|
|
1291
|
+
const keys = (s) => el("td", {}, ...s.split(/(?=⇧)|(?<=⇧)/).map((k) => el("kbd", { text: k })));
|
|
1292
|
+
for (const [k, label] of KEYS) table.append(el("tr", {}, el("td", { text: t(label) }), keys(`${MOD}${k}`)));
|
|
1293
|
+
table.append(el("tr", {}, el("td", { text: t("ide.key.mode") }), keys("⇧Tab")));
|
|
877
1294
|
}
|
|
878
1295
|
document.addEventListener("keydown", (e) => {
|
|
879
1296
|
const mod = isMac ? e.metaKey : e.ctrlKey;
|
|
880
1297
|
const inEditor = e.target && e.target.closest && e.target.closest(".monaco-editor");
|
|
881
|
-
if (e.key === "Tab" && e.shiftKey && !inEditor && !mentionItems.length) {
|
|
882
|
-
|
|
883
|
-
setMode(MODES[(MODES.indexOf($("mode").value) + 1) % MODES.length]);
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
1298
|
+
if (e.key === "Tab" && e.shiftKey && !inEditor && !mentionItems.length) { e.preventDefault(); cycleMode(); return; }
|
|
1299
|
+
if (e.ctrlKey && e.key === "`") { e.preventDefault(); showTerminal(); return; }
|
|
886
1300
|
if (!mod) return;
|
|
887
1301
|
const k = e.key.toLowerCase();
|
|
888
|
-
if (k === "p" &&
|
|
1302
|
+
if (k === "p" && e.shiftKey) { e.preventDefault(); palette(); }
|
|
1303
|
+
else if (k === "p") { e.preventDefault(); quickOpen(); }
|
|
889
1304
|
else if (k === "f" && e.shiftKey) { e.preventDefault(); showSide("search"); }
|
|
890
|
-
else if (k === "
|
|
891
|
-
else if (k === "
|
|
1305
|
+
else if (k === "e" && e.shiftKey) { e.preventDefault(); showSide("files"); }
|
|
1306
|
+
else if (k === "b" && e.altKey) { e.preventDefault(); toggle("agentOpen"); }
|
|
1307
|
+
else if (k === "b" && !e.shiftKey) { e.preventDefault(); toggle("sideOpen"); }
|
|
1308
|
+
else if (k === "j") { e.preventDefault(); if (layoutState.panelOpen && currentBottom() === "terminal") { layoutState.panelOpen = false; layout(); if (terminal) terminal.close(); } else showTerminal(); }
|
|
892
1309
|
else if (k === "l") { e.preventDefault(); newAgent(); }
|
|
893
1310
|
else if (k === "s" && !inEditor) { e.preventDefault(); saveActive(); }
|
|
1311
|
+
else if (k === "\\") { e.preventDefault(); if (active) toggleSplit(); }
|
|
894
1312
|
}, true);
|
|
895
1313
|
addEventListener("beforeunload", (e) => { if (tabs.some(isDirty)) { e.preventDefault(); e.returnValue = ""; } });
|
|
896
1314
|
|
|
897
1315
|
// ---- start ------------------------------------------------------------------------------------------
|
|
898
1316
|
(async () => {
|
|
1317
|
+
drawIcons(document);
|
|
899
1318
|
await loadWords();
|
|
900
1319
|
$("ide").hidden = false;
|
|
901
|
-
$("back").href = `/#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}`;
|
|
902
|
-
|
|
1320
|
+
$("back").addEventListener("click", () => { location.href = `/#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}`; });
|
|
1321
|
+
for (const b of document.querySelectorAll("#activity button[data-side]")) b.classList.toggle("on", b.dataset.side === layoutState.sideView && layoutState.sideOpen);
|
|
1322
|
+
for (const p of document.querySelectorAll("#side > section")) p.hidden = p.dataset.panel !== layoutState.sideView;
|
|
1323
|
+
layout();
|
|
903
1324
|
drawKeys();
|
|
904
1325
|
renderModeHint();
|
|
1326
|
+
renderProblemCounts();
|
|
1327
|
+
showBottom(store.get("bottom", "problems") === "terminal" ? "problems" : store.get("bottom", "problems"));
|
|
1328
|
+
// Folders and models while the editor loads: the tree shows before Monaco is ready.
|
|
1329
|
+
const lists = Promise.all([loadModels().catch((err) => output(explain(err))), loadFolders().catch((err) => output(explain(err)))]).then(() => ($("folder").value ? setFolder($("folder").value) : null));
|
|
905
1330
|
try { monaco = await loadMonaco(); setupEditor(); } catch (err) { output(t("ide.err.editor_missing")); }
|
|
1331
|
+
editorLoaded();
|
|
906
1332
|
renderTabs();
|
|
907
|
-
await
|
|
908
|
-
|
|
1333
|
+
await lists;
|
|
1334
|
+
for (const b of document.querySelectorAll("[data-bottom]")) b.addEventListener("click", () => store.set("bottom", b.dataset.bottom));
|
|
909
1335
|
})();
|
|
910
1336
|
})();
|