([]), { when: ["slow"], slowMs: 10 })).toEqual({
value: [1],
diff --git a/src/features/agents/AgentsView.tsx b/src/features/agents/AgentsView.tsx
index 04e2a5a5..d93d078a 100644
--- a/src/features/agents/AgentsView.tsx
+++ b/src/features/agents/AgentsView.tsx
@@ -58,7 +58,9 @@ export function AgentsView() {
roots={roots}
getKey={nodeKey}
getLabel={(n) => (n.kind === "group" ? n.group.label : n.task.title)}
- getChildren={(n) => (n.kind === "group" ? n.group.tasks.map((task) => ({ kind: "task", task }) as Node) : undefined)}
+ getChildren={(n) =>
+ n.kind === "group" ? n.group.tasks.map((task) => ({ kind: "task", task }) as Node) : undefined
+ }
isExpandable={(n) => n.kind === "group"}
expanded={expanded}
onExpandedChange={(_next, node, open) =>
diff --git a/src/features/agents/Composer.tsx b/src/features/agents/Composer.tsx
index ec6e1452..28775935 100644
--- a/src/features/agents/Composer.tsx
+++ b/src/features/agents/Composer.tsx
@@ -33,9 +33,7 @@ import { isImeKey } from "@/platform/ime";
const MAX_PROMPT_HEIGHT = 280;
const MAX_ITEMS = 12;
-type Item =
- | { kind: "file"; path: string; rel: string }
- | { kind: "command"; command: AgentCommand };
+type Item = { kind: "file"; path: string; rel: string } | { kind: "command"; command: AgentCommand };
export interface ComposerProps {
/** Store key of the draft. */
diff --git a/src/features/agents/HomeEditor.tsx b/src/features/agents/HomeEditor.tsx
index ab65f9ce..91df874d 100644
--- a/src/features/agents/HomeEditor.tsx
+++ b/src/features/agents/HomeEditor.tsx
@@ -134,7 +134,10 @@ export function HomeEditor() {
)}
{(error || (provider && !provider.installed) || (provider && !provider.config.length)) && (
-
+
{error ??
(provider && !provider.installed ? (
txTemplate(`${provider.command} was not found on PATH`)
diff --git a/src/features/agents/SessionEditor.tsx b/src/features/agents/SessionEditor.tsx
index 2ebd0756..d67bc114 100644
--- a/src/features/agents/SessionEditor.tsx
+++ b/src/features/agents/SessionEditor.tsx
@@ -502,7 +502,9 @@ function FindBar({
className={styles.findField}
trailing={
query.trim() ? (
- {ranges.length ? `${index + 1}/${ranges.length}` : tx("0 results")}
+
+ {ranges.length ? `${index + 1}/${ranges.length}` : tx("0 results")}
+
) : null
}
onKeyDown={(e) => {
@@ -516,8 +518,20 @@ function FindBar({
}
}}
/>
- step(-1)} />
- step(1)} />
+ step(-1)}
+ />
+ step(1)}
+ />
);
@@ -567,7 +581,10 @@ function Entry({
{entry.items.map((item, i) => (
-
+
{item.content}
))}
@@ -605,7 +622,8 @@ function Entry({
function UserMessage({ task, entry }: { task: AgentTask; entry: UserEntry }) {
const tx = useTx();
- const fail = (e: unknown) => notifications().notify({ level: "error", title: "Couldn't send the message", body: String(e) });
+ const fail = (e: unknown) =>
+ notifications().notify({ level: "error", title: "Couldn't send the message", body: String(e) });
const archived = task.status === "archived";
return (
@@ -714,7 +732,12 @@ function SubagentCard({ task, entry, onOpen }: { task: AgentTask; entry: Subagen
return (
-
+
{title}
@@ -732,7 +755,12 @@ function SubagentCard({ task, entry, onOpen }: { task: AgentTask; entry: Subagen
{working && entry.cancellable && (
- void cancelTask(task.id, entry.id)} />
+ void cancelTask(task.id, entry.id)}
+ />
)}
{working && entry.activity && (
@@ -776,7 +804,13 @@ function WorkingSubagents({ entries, onOpen }: { entries: AgentEntry[] | null; o
return (
{working.map((e) => (
-
onOpen(e.id)} title={e.activity ?? undefined}>
+ onOpen(e.id)}
+ title={e.activity ?? undefined}
+ >
{subagentTitle(e)}
@@ -801,7 +835,9 @@ function PermissionLine({ task, entry }: { task: AgentTask; entry: Extract => e.type === "permission" && !e.chosen);
+ const entry = entries?.find(
+ (e): e is Extract => e.type === "permission" && !e.chosen,
+ );
if (!entry) return null;
const answer = (option: string | null) => {
setBusy(true);
@@ -846,7 +882,9 @@ function SessionComposer({
if (!canSend(composer) || sending || task.status === "archived") return;
setSending(true);
sendMessage(task.id, now)
- .catch((e: unknown) => notifications().notify({ level: "error", title: "Couldn't send the message", body: String(e) }))
+ .catch((e: unknown) =>
+ notifications().notify({ level: "error", title: "Couldn't send the message", body: String(e) }),
+ )
.finally(() => setSending(false));
};
const setOption = (id: string, value: string) =>
@@ -893,7 +931,8 @@ function SessionComposer({
/** Messages waiting for the turn to end: send now, edit, or drop. */
function Queue({ task }: { task: AgentTask }) {
- const fail = (e: unknown) => notifications().notify({ level: "error", title: "Couldn't update the queue", body: String(e) });
+ const fail = (e: unknown) =>
+ notifications().notify({ level: "error", title: "Couldn't update the queue", body: String(e) });
return (
{task.queue.map((q) => (
@@ -902,11 +941,25 @@ function Queue({ task }: { task: AgentTask }) {
{q.prompt.text || Image }
- void sendQueuedNow(task.id, q.id).catch(fail)}>
+ void sendQueuedNow(task.id, q.id).catch(fail)}
+ >
Send now
- void editQueued(task.id, q.id).catch(fail)} />
- void removeQueued(task.id, q.id).catch(fail)} />
+ void editQueued(task.id, q.id).catch(fail)}
+ />
+ void removeQueued(task.id, q.id).catch(fail)}
+ />
))}
diff --git a/src/features/agents/agents.test.ts b/src/features/agents/agents.test.ts
index 43bbeb12..cf06e212 100644
--- a/src/features/agents/agents.test.ts
+++ b/src/features/agents/agents.test.ts
@@ -124,21 +124,18 @@ describe("agent composer", () => {
const config = [option("fast", ["on"], null), option("mode", ["ask", "code"], "code"), option("model", ["m"], "m")];
expect(mainOptions(config).map((o) => o.id)).toEqual(["model", "mode"]);
expect(nextValue(config[1]!)).toBe("ask");
- expect(["default", "acceptEdits", "plan", "auto", "bypassPermissions", "auto_edit", "read-only", "x"].map(modeIcon)).toEqual([
- "lock",
- "edit",
- "todo",
- "run",
- "unlock",
- "edit",
- "eye",
- "lock",
- ]);
+ expect(
+ ["default", "acceptEdits", "plan", "auto", "bypassPermissions", "auto_edit", "read-only", "x"].map(modeIcon),
+ ).toEqual(["lock", "edit", "todo", "run", "unlock", "edit", "eye", "lock"]);
expect(usageLabel({ used: 24_000, size: 200_000 })).toEqual({
short: "12%",
detail: "24k / 200k tokens",
});
- expect(imageFromDataUrl("data:image/png;base64,AAA", "a.png")).toEqual({ mimeType: "image/png", data: "AAA", name: "a.png" });
+ expect(imageFromDataUrl("data:image/png;base64,AAA", "a.png")).toEqual({
+ mimeType: "image/png",
+ data: "AAA",
+ name: "a.png",
+ });
expect(imageFromDataUrl("data:text/plain;base64,AAA")).toBeNull();
});
});
diff --git a/src/features/agents/composerModel.ts b/src/features/agents/composerModel.ts
index 17468b8d..86d14cc9 100644
--- a/src/features/agents/composerModel.ts
+++ b/src/features/agents/composerModel.ts
@@ -28,7 +28,8 @@ export function completionAt(text: string, caret: number): Completion | null {
if (!word) return null;
const from = caret - word[3]!.length - 1;
// Commands only as the message's first word.
- if (word[2] === "/") return before.slice(0, from).trim() === "" ? { kind: "command", query: word[3]!, from, to: caret } : null;
+ if (word[2] === "/")
+ return before.slice(0, from).trim() === "" ? { kind: "command", query: word[3]!, from, to: caret } : null;
return { kind: "file", query: word[3]!, from, to: caret };
}
@@ -75,7 +76,8 @@ export function mentionOf(rel: string, lines?: { from: number; to: number }): st
export function toPrompt(draft: ComposerDraft): AgentPrompt {
const seen = new Set
();
const files = draft.files.filter((f) => {
- if (seen.has(f.path) || !new RegExp(`@${escapeRe(f.rel)}(?![\\w./-])|@${escapeRe(f.rel)}:`).test(draft.text)) return false;
+ if (seen.has(f.path) || !new RegExp(`@${escapeRe(f.rel)}(?![\\w./-])|@${escapeRe(f.rel)}:`).test(draft.text))
+ return false;
seen.add(f.path);
return true;
});
diff --git a/src/features/agents/conversation.test.ts b/src/features/agents/conversation.test.ts
index 78d1d1e6..36d0c854 100644
--- a/src/features/agents/conversation.test.ts
+++ b/src/features/agents/conversation.test.ts
@@ -21,7 +21,17 @@ describe("pathLink", () => {
expect(pathLink("/abs/x.py#L7", cwd)).toEqual({ path: "/abs/x.py", line: 6 });
});
it("ignores code that isn't a path", () => {
- for (const t of ["foo()", "cargo test", "https://x.dev/a.md", "inventory", "1.5", "v2.0", "a.b()", "failing-tests", "tests/"]) {
+ for (const t of [
+ "foo()",
+ "cargo test",
+ "https://x.dev/a.md",
+ "inventory",
+ "1.5",
+ "v2.0",
+ "a.b()",
+ "failing-tests",
+ "tests/",
+ ]) {
expect(pathLink(t, cwd)).toBeNull();
}
});
diff --git a/src/features/agents/model.ts b/src/features/agents/model.ts
index d9504352..a4d66f0d 100644
--- a/src/features/agents/model.ts
+++ b/src/features/agents/model.ts
@@ -37,7 +37,20 @@ export function subagentStatusLabel(s: AgentSubagentStatus): string {
}
}
-const PROVIDER_ICONS = new Set(["claude", "codex", "gemini", "copilot", "cursor", "opencode", "qwen", "kimi", "goose", "auggie", "kiro", "vibe"]);
+const PROVIDER_ICONS = new Set([
+ "claude",
+ "codex",
+ "gemini",
+ "copilot",
+ "cursor",
+ "opencode",
+ "qwen",
+ "kimi",
+ "goose",
+ "auggie",
+ "kiro",
+ "vibe",
+]);
/** The agent's logo; custom agents get the generic agents icon. */
export const providerIcon = (provider: string) =>
@@ -109,7 +122,10 @@ export function age(ms: number, now = Date.now()): string {
}
/** The status change worth a notification, if any. */
-export function attentionChange(prev: AgentStatus | undefined, next: AgentStatus): "finished" | "failed" | "input" | null {
+export function attentionChange(
+ prev: AgentStatus | undefined,
+ next: AgentStatus,
+): "finished" | "failed" | "input" | null {
if (prev === next || prev === undefined) return null;
if (next === "needsInput") return "input";
if (!isBusy(prev)) return null;
diff --git a/src/features/agents/store.ts b/src/features/agents/store.ts
index a3bc43a3..a5e8431f 100644
--- a/src/features/agents/store.ts
+++ b/src/features/agents/store.ts
@@ -15,7 +15,16 @@ import { call, commands, isTauri } from "@/platform/ipc";
import { editorService, notifications } from "@/workbench/contributions";
import { canSend, emptyDraft, toPrompt, type ComposerDraft, type FileRef } from "./composerModel";
import { withQuote } from "./conversation";
-import { HOME_KEY, HOME_KIND, SESSION_KIND, attentionChange, entriesKey, isBusy, sessionKey, upsertEntry } from "./model";
+import {
+ HOME_KEY,
+ HOME_KIND,
+ SESSION_KIND,
+ attentionChange,
+ entriesKey,
+ isBusy,
+ sessionKey,
+ upsertEntry,
+} from "./model";
/** New Task page settings (its text lives in the `HOME` composer). */
export interface Draft {
@@ -138,7 +147,8 @@ export async function connect(): Promise {
const editors = editorService();
for (const t of editors.openTabs?.() ?? []) {
const task = (t.input.data as { task?: string } | undefined)?.task;
- if (t.input.kind === SESSION_KIND && task && !useAgents.getState().tasks.has(task)) editors.closeInput?.(t.input.key);
+ if (t.input.kind === SESSION_KIND && task && !useAgents.getState().tasks.has(task))
+ editors.closeInput?.(t.input.key);
}
}
@@ -189,7 +199,11 @@ function notifyChange(prev: AgentTask | undefined, task: AgentTask) {
if (!change) return;
if (visible.has(task.id) && document.hasFocus()) return;
const body =
- change === "input" ? "Waiting for your answer" : change === "failed" ? (task.error ?? "The agent stopped") : "Finished";
+ change === "input"
+ ? "Waiting for your answer"
+ : change === "failed"
+ ? (task.error ?? "The agent stopped")
+ : "Finished";
notifications().notify({
level: change === "failed" ? "error" : "info",
title: task.title,
@@ -369,7 +383,8 @@ export function addReferences(refs: Array<{ path: string; rel: string; mention:
const root = task?.isolation === "worktree" ? task.cwd : null;
const files: FileRef[] = refs.map((r) => ({ path: root ? `${root}/${r.rel}` : r.path, rel: r.rel }));
const mentions = refs.map((r) => r.mention).join(" ");
- const text = current.text && !/\s$/.test(current.text) ? `${current.text} ${mentions} ` : `${current.text}${mentions} `;
+ const text =
+ current.text && !/\s$/.test(current.text) ? `${current.text} ${mentions} ` : `${current.text}${mentions} `;
updateComposer(key, { text, files: [...current.files, ...files] });
if (task) openTask(task.id);
else openHome();
diff --git a/src/features/analysis/analysis.test.ts b/src/features/analysis/analysis.test.ts
index 76320027..23b14539 100644
--- a/src/features/analysis/analysis.test.ts
+++ b/src/features/analysis/analysis.test.ts
@@ -71,7 +71,10 @@ describe("Inspect Code grouping", () => {
});
it("groups by inspection by default, worst severity first, merging severities", () => {
- const withWarning = [...files, { path: "/p/d.ts", relPath: "d.ts", diagnostics: [diag(5, "warning", "ts", "2304")] }];
+ const withWarning = [
+ ...files,
+ { path: "/p/d.ts", relPath: "d.ts", diagnostics: [diag(5, "warning", "ts", "2304")] },
+ ];
const nodes = buildInspectionNodes(groupInspection(withWarning), false);
expect(nodes.map((n) => (n.kind === "inspection" ? [n.name, n.severity, n.count] : n.kind))).toEqual([
["ts(2304)", "error", 4],
diff --git a/src/features/analysis/cleanup.ts b/src/features/analysis/cleanup.ts
index 2910023c..f1b971cf 100644
--- a/src/features/analysis/cleanup.ts
+++ b/src/features/analysis/cleanup.ts
@@ -93,7 +93,13 @@ export async function runCodeCleanup(choice: ScopeChoice, options: CleanupOption
if (!isTauri() || running) return;
const files = await cleanupFiles(choice);
if (typeof files === "string") {
- notifications().notify({ group: "analysis", level: "warning", title: "Code Cleanup", body: files, key: "code.cleanup" });
+ notifications().notify({
+ group: "analysis",
+ level: "warning",
+ title: "Code Cleanup",
+ body: files,
+ key: "code.cleanup",
+ });
return;
}
if (
@@ -131,7 +137,9 @@ export async function runCodeCleanup(choice: ScopeChoice, options: CleanupOption
notifications().notify({
group: "analysis",
level: "info",
- title: run.cancelled ? `Code Cleanup cancelled after ${done} of ${files.length} files` : `Code Cleanup processed ${done} files`,
+ title: run.cancelled
+ ? `Code Cleanup cancelled after ${done} of ${files.length} files`
+ : `Code Cleanup processed ${done} files`,
key: "code.cleanup",
});
}
diff --git a/src/features/app/NewProject.tsx b/src/features/app/NewProject.tsx
index 09035805..154905f1 100644
--- a/src/features/app/NewProject.tsx
+++ b/src/features/app/NewProject.tsx
@@ -24,7 +24,8 @@ export function NewProjectOverlay() {
export function projectNameError(name: string): string | null {
const n = name.trim();
if (!n) return "Enter a project name";
- if (/[/\\:*?"<>|]/.test(n) || n === "." || n === "..") return "The name contains characters a folder name cannot have";
+ if (/[/\\:*?"<>|]/.test(n) || n === "." || n === "..")
+ return "The name contains characters a folder name cannot have";
return null;
}
@@ -82,7 +83,12 @@ function NewProjectDialog({ onClose }: { onClose: () => void }) {
const browse = async () => {
if (!isTauri()) return;
const { open } = await import("@tauri-apps/plugin-dialog");
- const picked = await open({ directory: true, multiple: false, title: "Project Location", defaultPath: location || undefined });
+ const picked = await open({
+ directory: true,
+ multiple: false,
+ title: "Project Location",
+ defaultPath: location || undefined,
+ });
const dir = Array.isArray(picked) ? picked[0] : picked;
if (dir) setLocation(dir);
};
@@ -124,7 +130,13 @@ function NewProjectDialog({ onClose }: { onClose: () => void }) {
Name:
- setName(e.target.value)} aria-label="Name" />
+ setName(e.target.value)}
+ aria-label="Name"
+ />
diff --git a/src/features/app/__tests__/invalidateCaches.test.tsx b/src/features/app/__tests__/invalidateCaches.test.tsx
index 0bf2b882..c9b999ce 100644
--- a/src/features/app/__tests__/invalidateCaches.test.tsx
+++ b/src/features/app/__tests__/invalidateCaches.test.tsx
@@ -31,7 +31,9 @@ describe("Invalidate Caches", () => {
render( );
await executeAction("file.invalidateCaches");
const dialog = await screen.findByRole("dialog", { name: "Invalidate Caches" });
- const buttons = within(dialog).getAllByRole("button").map((b) => b.textContent);
+ const buttons = within(dialog)
+ .getAllByRole("button")
+ .map((b) => b.textContent);
expect(buttons).toEqual(expect.arrayContaining(["Invalidate and Restart", "Just Restart", "Cancel"]));
fireEvent.click(within(dialog).getByRole("checkbox", { name: "Clear file system cache and Local History" }));
expect(dialog).toHaveTextContent("Local History of all projects will be deleted.");
diff --git a/src/features/app/__tests__/newProject.test.ts b/src/features/app/__tests__/newProject.test.ts
index abbeb6ad..efedeabc 100644
--- a/src/features/app/__tests__/newProject.test.ts
+++ b/src/features/app/__tests__/newProject.test.ts
@@ -26,7 +26,11 @@ describe("Manage IDE Settings › Export Settings…", () => {
"openidea.runAnything.history.v1:/p": "[]",
};
const keys = Object.keys(data);
- const storage = { length: keys.length, key: (i: number) => keys[i] ?? null, getItem: (k: string) => data[k] ?? null };
+ const storage = {
+ length: keys.length,
+ key: (i: number) => keys[i] ?? null,
+ getItem: (k: string) => data[k] ?? null,
+ };
expect(Object.keys(exportedStorage(storage)).sort()).toEqual([
"openidea.liveTemplates.v1",
"openidea.macros.v1",
diff --git a/src/features/app/__tests__/shell.test.tsx b/src/features/app/__tests__/shell.test.tsx
index 990d2966..f1c21743 100644
--- a/src/features/app/__tests__/shell.test.tsx
+++ b/src/features/app/__tests__/shell.test.tsx
@@ -134,7 +134,11 @@ describe("prompt", () => {
});
const dialog = await screen.findByRole("dialog");
const footer = dialog.querySelector("[data-dialog-footer]") as HTMLElement;
- expect([...footer.querySelectorAll("button")].map((b) => b.textContent)).toEqual(["Cancel", "This Window", "New Window"]);
+ expect([...footer.querySelectorAll("button")].map((b) => b.textContent)).toEqual([
+ "Cancel",
+ "This Window",
+ "New Window",
+ ]);
await userEvent.setup().keyboard("{Enter}");
await expect(result).resolves.toMatchObject({ button: "new" });
usePlatform.setState({ os });
diff --git a/src/features/app/projectActions.ts b/src/features/app/projectActions.ts
index 288faa7b..0ede70e0 100644
--- a/src/features/app/projectActions.ts
+++ b/src/features/app/projectActions.ts
@@ -21,7 +21,9 @@ export const EXPORTED_STORAGE = [
"openidea.toolWindowLayouts.",
];
-export function exportedStorage(storage: Pick = localStorage): Record {
+export function exportedStorage(
+ storage: Pick = localStorage,
+): Record {
const out: Record = {};
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
@@ -48,7 +50,12 @@ async function exportSettings() {
await call(commands.settingsExport(path, exportedStorage()));
notifications().notify({ level: "info", title: "Settings exported", body: path, key: "app.settingsExport" });
} catch (e) {
- notifications().notify({ level: "error", title: "Cannot export settings", body: errorText(e), key: "app.settingsExport" });
+ notifications().notify({
+ level: "error",
+ title: "Cannot export settings",
+ body: errorText(e),
+ key: "app.settingsExport",
+ });
}
}
@@ -80,7 +87,12 @@ async function importSettings() {
actions: [{ label: "Restart", run: () => void executeAction("app.restart") }],
});
} catch (e) {
- notifications().notify({ level: "error", title: "Cannot import settings", body: errorText(e), key: "app.settingsImport" });
+ notifications().notify({
+ level: "error",
+ title: "Cannot import settings",
+ body: errorText(e),
+ key: "app.settingsImport",
+ });
}
}
diff --git a/src/features/app/title.ts b/src/features/app/title.ts
index 28834295..1a178ff9 100644
--- a/src/features/app/title.ts
+++ b/src/features/app/title.ts
@@ -7,7 +7,11 @@ import { t, useI18n } from "@/platform/i18n";
import { isTauri } from "@/platform/ipc";
import { basename, useProject } from "@/platform/project";
-export function windowTitle(project: string | null | undefined, file: string | null | undefined, fullPath = false): string {
+export function windowTitle(
+ project: string | null | undefined,
+ file: string | null | undefined,
+ fullPath = false,
+): string {
const name = file ? (fullPath ? file : basename(file)) : null;
if (!project) return name ?? t("app.title.default");
return name ? `${project} – ${name}` : project;
diff --git a/src/features/build-tools/actions.test.tsx b/src/features/build-tools/actions.test.tsx
index bb8ac601..b6e896db 100644
--- a/src/features/build-tools/actions.test.tsx
+++ b/src/features/build-tools/actions.test.tsx
@@ -69,7 +69,11 @@ describe("build tools feature", () => {
it("offers its actions in Search Everywhere only for Maven projects and toggles run options", async () => {
const visible = () => allActions().filter((a) => !a.hidden && a.id.startsWith("maven."));
- expect(visible().map((a) => a.id).sort()).toEqual([
+ expect(
+ visible()
+ .map((a) => a.id)
+ .sort(),
+ ).toEqual([
"maven.analyzeDependencies",
"maven.executeGoal",
"maven.reload",
@@ -85,7 +89,11 @@ describe("build tools feature", () => {
await executeAction("maven.toggleOffline");
expect(useBuild.getState().settings.maven).toMatchObject({ offline: true, skipTests: true });
expect(JSON.parse(localStorage.getItem("openidea.buildTools:/p")!).maven.skipTests).toBe(true);
- expect(allActions().find((a) => a.id === "maven.toggleOffline")!.toggled?.()).toBe(true);
+ expect(
+ allActions()
+ .find((a) => a.id === "maven.toggleOffline")!
+ .toggled?.(),
+ ).toBe(true);
await executeAction("maven.executeGoal");
expect(useBuild.getState().executeOpen).toBe("maven");
@@ -93,8 +101,19 @@ describe("build tools feature", () => {
it("runs a lifecycle phase through the run feature with the toolbar options", async () => {
const started: unknown[] = [];
- const off = registerAction({ id: "run.runConfig", title: "Run", hidden: true, run: ({ arg }) => void started.push(arg) });
- act(() => useBuild.setState({ root: "/p", model: mavenModel, expanded: { maven: new Set(["mvn:pom.xml", "mvn:pom.xml:lifecycle"]), gradle: new Set() } }));
+ const off = registerAction({
+ id: "run.runConfig",
+ title: "Run",
+ hidden: true,
+ run: ({ arg }) => void started.push(arg),
+ });
+ act(() =>
+ useBuild.setState({
+ root: "/p",
+ model: mavenModel,
+ expanded: { maven: new Set(["mvn:pom.xml", "mvn:pom.xml:lifecycle"]), gradle: new Set() },
+ }),
+ );
await executeAction("maven.toggleSkipTests");
render( );
expect(screen.getByText("Build files changed since the last reload")).toBeTruthy();
@@ -106,6 +125,11 @@ describe("build tools feature", () => {
expect(started).toHaveLength(1);
const { config, temporary } = started[0] as { config: RunConfig; temporary: boolean };
expect(temporary).toBe(true);
- expect(config).toMatchObject({ name: "spring-demo [package]", command: "/p/mvnw", args: ["package", "-DskipTests"], kind: "maven" });
+ expect(config).toMatchObject({
+ name: "spring-demo [package]",
+ command: "/p/mvnw",
+ args: ["package", "-DskipTests"],
+ kind: "maven",
+ });
});
});
diff --git a/src/features/build-tools/index.ts b/src/features/build-tools/index.ts
index f70182fd..cb6ad763 100644
--- a/src/features/build-tools/index.ts
+++ b/src/features/build-tools/index.ts
@@ -43,10 +43,16 @@ function expandAll(tool: BuildTool) {
const { model, configs } = useBuild.getState();
if (tool === "maven" && model?.maven) {
const m = model.maven;
- setExpanded(tool, expandableKeys(mavenRoots(m), (n) => mavenChildren(n, m, configs)));
+ setExpanded(
+ tool,
+ expandableKeys(mavenRoots(m), (n) => mavenChildren(n, m, configs)),
+ );
} else if (tool === "gradle" && model?.gradle) {
const g = model.gradle;
- setExpanded(tool, expandableKeys(gradleRoots(g), (n) => gradleChildren(n, g, configs)));
+ setExpanded(
+ tool,
+ expandableKeys(gradleRoots(g), (n) => gradleChildren(n, g, configs)),
+ );
}
}
@@ -78,8 +84,22 @@ function apply(model: BuildModel | null, registered: Partial {
expect(labels(children(roots[0]!))).toEqual(["dev", "ci"]);
const top = children(roots[1]!);
expect(labels(top)).toEqual(["Lifecycle", "Plugins", "core", "Demo App"]);
- expect(labels(children(top[0]!))).toEqual(["clean", "validate", "compile", "test", "package", "verify", "install", "site", "deploy"]);
+ expect(labels(children(top[0]!))).toEqual([
+ "clean",
+ "validate",
+ "compile",
+ "test",
+ "package",
+ "verify",
+ "install",
+ "site",
+ "deploy",
+ ]);
expect(nodeIcon(top[2]!)).toBe("maven-module");
expect(nodeIcon(roots[1]!)).toBe("maven-project");
});
@@ -168,7 +185,11 @@ describe("Maven runs", () => {
it("passes goals, offline, skip tests and changed profiles", () => {
expect(profilesArg(maven.profiles, {})).toBeNull();
expect(profilesArg(maven.profiles, { ci: true, dev: false })).toBe("!dev,ci");
- const config = mavenGoalConfig(core, ["clean", "test"], maven, { offline: true, skipTests: false, profiles: { ci: true } });
+ const config = mavenGoalConfig(core, ["clean", "test"], maven, {
+ offline: true,
+ skipTests: false,
+ profiles: { ci: true },
+ });
expect(config).toMatchObject({
name: "core [clean,test]",
kind: "maven",
@@ -182,20 +203,24 @@ describe("Maven runs", () => {
const skipped = mavenGoalConfig(core, ["test"], maven, { offline: false, skipTests: true, profiles: {} });
expect(skipped.args).toEqual(["test", "-DskipTests"]);
expect(skipped.isTest).toBe(false);
- const root = mavenGoalConfig(maven.projects[0]!, ["clean"], maven, { offline: false, skipTests: false, profiles: {} });
+ const root = mavenGoalConfig(maven.projects[0]!, ["clean"], maven, {
+ offline: false,
+ skipTests: false,
+ profiles: {},
+ });
expect(root.cwd).toBeNull();
expect(root.isTest).toBe(false);
// Settings › Maven options go before the goals.
const configured = { ...core, options: ["-s", "/m2/settings.xml"] };
- expect(mavenGoalConfig(configured, ["test"], maven, { offline: false, skipTests: true, profiles: {} }).args).toEqual(
- ["-s", "/m2/settings.xml", "test", "-DskipTests"],
- );
+ expect(
+ mavenGoalConfig(configured, ["test"], maven, { offline: false, skipTests: true, profiles: {} }).args,
+ ).toEqual(["-s", "/m2/settings.xml", "test", "-DskipTests"]);
});
it("parses typed command lines", () => {
expect(parseGoalLine("mvn clean install -Dmsg='a b'")).toEqual(["clean", "install", "-Dmsg=a b"]);
expect(parseGoalLine("./mvnw -q verify")).toEqual(["-q", "verify"]);
- expect(parseTaskLine("./gradlew :app:run --args \"x y\"")).toEqual([":app:run", "--args", "x y"]);
+ expect(parseTaskLine('./gradlew :app:run --args "x y"')).toEqual([":app:run", "--args", "x y"]);
expect(splitArgs(`a "" 'b c'`)).toEqual(["a", "", "b c"]);
});
@@ -244,7 +269,8 @@ describe("Gradle tree and runs", () => {
stale: false,
syncError: null,
};
- const kids = (n: BuildNode) => gradleChildren(n, gradle, [runConfig({ id: "g", name: "app [run]", kind: "gradle" })]) ?? [];
+ const kids = (n: BuildNode) =>
+ gradleChildren(n, gradle, [runConfig({ id: "g", name: "app [run]", kind: "gradle" })]) ?? [];
it("nests subprojects and groups tasks, other last", () => {
const roots = gradleRoots(gradle);
@@ -260,9 +286,19 @@ describe("Gradle tree and runs", () => {
it("runs tasks by path from the build directory", () => {
const app = gradle.projects[1]!;
const config = gradleTaskConfig(gradle, app, ["test"], { offline: true });
- expect(config).toMatchObject({ name: "app [test]", kind: "gradle", command: "/p/gradlew", args: [":app:test", "--offline"], cwd: null, isTest: true });
+ expect(config).toMatchObject({
+ name: "app [test]",
+ kind: "gradle",
+ command: "/p/gradlew",
+ args: [":app:test", "--offline"],
+ cwd: null,
+ isTest: true,
+ });
expect(gradleTaskConfig(gradle, gradle.projects[0]!, ["wrapper"], { offline: false }).args).toEqual(["wrapper"]);
- expect(gradleTaskConfig(gradle, app, [":lib:jar", "--info"], { offline: false }).args).toEqual([":lib:jar", "--info"]);
+ expect(gradleTaskConfig(gradle, app, [":lib:jar", "--info"], { offline: false }).args).toEqual([
+ ":lib:jar",
+ "--info",
+ ]);
});
it("completes task names and paths", () => {
diff --git a/src/features/build-tools/model.ts b/src/features/build-tools/model.ts
index 036dd86f..51957d28 100644
--- a/src/features/build-tools/model.ts
+++ b/src/features/build-tools/model.ts
@@ -139,10 +139,17 @@ export function gradleRunConfigs(configs: readonly RunConfig[]): RunConfig[] {
}
function normalizeDir(cwd: string | null | undefined): string {
- return (cwd ?? "").replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/+$/, "");
+ return (cwd ?? "")
+ .replace(/\\/g, "/")
+ .replace(/^\.\/?/, "")
+ .replace(/\/+$/, "");
}
-export function mavenChildren(node: BuildNode, model: MavenModel, configs: readonly RunConfig[]): BuildNode[] | undefined {
+export function mavenChildren(
+ node: BuildNode,
+ model: MavenModel,
+ configs: readonly RunConfig[],
+): BuildNode[] | undefined {
switch (node.kind) {
case "profiles":
return model.profiles.map((profile) => ({ kind: "profile", key: `mvn:profile:${profile.id}`, profile }));
@@ -153,7 +160,8 @@ export function mavenChildren(node: BuildNode, model: MavenModel, configs: reado
{ kind: "lifecycle", key: `${k}:lifecycle`, project: p },
{ kind: "plugins", key: `${k}:plugins`, project: p },
];
- if (p.dependencies.length) out.push({ kind: "dependencies", key: `${k}:deps`, project: p, dependencies: p.dependencies });
+ if (p.dependencies.length)
+ out.push({ kind: "dependencies", key: `${k}:deps`, project: p, dependencies: p.dependencies });
const runs = mavenRunConfigs(p, configs);
if (runs.length) out.push({ kind: "runConfigs", key: `${k}:runs`, configs: runs });
for (const pom of p.modules) {
@@ -196,7 +204,13 @@ export function mavenChildren(node: BuildNode, model: MavenModel, configs: reado
function dependencyNodes(parent: string, deps: readonly MavenDependency[]): BuildNode[] {
return deps.flatMap((dependency, i) =>
dependency.omitted == null
- ? [{ kind: "dependency" as const, key: `${parent}/${dependency.groupId}:${dependency.artifactId}:${i}`, dependency }]
+ ? [
+ {
+ kind: "dependency" as const,
+ key: `${parent}/${dependency.groupId}:${dependency.artifactId}:${i}`,
+ dependency,
+ },
+ ]
: [],
);
}
@@ -232,7 +246,11 @@ export function gradleSubprojects(model: GradleModel, path: string): GradleProje
});
}
-export function gradleChildren(node: BuildNode, model: GradleModel, configs: readonly RunConfig[]): BuildNode[] | undefined {
+export function gradleChildren(
+ node: BuildNode,
+ model: GradleModel,
+ configs: readonly RunConfig[],
+): BuildNode[] | undefined {
switch (node.kind) {
case "gradleProject": {
const p = node.project;
@@ -383,7 +401,11 @@ export function parseTaskLine(line: string): string[] {
return args;
}
-export function mavenArgs(goals: readonly string[], model: Pick, options: MavenRunOptions): string[] {
+export function mavenArgs(
+ goals: readonly string[],
+ model: Pick,
+ options: MavenRunOptions,
+): string[] {
const args = [...goals];
const has = (flag: string) => args.some((a) => a === flag || a.startsWith(`${flag}=`));
if (options.offline && !has("-o") && !has("--offline")) args.push("-o");
@@ -465,7 +487,11 @@ export interface Completion {
}
/** Completes the last word of `line` with phases and `prefix:goal`s of `plugins`. */
-export function goalCompletions(line: string, plugins: readonly MavenPlugin[], history: readonly string[] = []): Completion[] {
+export function goalCompletions(
+ line: string,
+ plugins: readonly MavenPlugin[],
+ history: readonly string[] = [],
+): Completion[] {
const at = /\S*$/.exec(line)!.index;
const head = line.slice(0, at);
const word = line.slice(at).toLowerCase();
@@ -476,7 +502,10 @@ export function goalCompletions(line: string, plugins: readonly MavenPlugin[], h
seen.add(label);
out.push({ line: head + label, label, detail });
};
- if (!head.trim()) for (const h of history) if (h.toLowerCase().startsWith(line.toLowerCase()) && h !== line) out.push({ line: h, label: h, detail: "recent" });
+ if (!head.trim())
+ for (const h of history)
+ if (h.toLowerCase().startsWith(line.toLowerCase()) && h !== line)
+ out.push({ line: h, label: h, detail: "recent" });
if (word.startsWith("-")) return out;
for (const phase of [...LIFECYCLE, ...ALL_PHASES]) push(phase, "phase");
const goals = plugins.flatMap((p) => p.goals.map((g) => ({ label: `${p.prefix}:${g}`, detail: p.artifactId })));
@@ -492,7 +521,10 @@ export function taskCompletions(line: string, model: GradleModel, history: reado
const word = line.slice(at).toLowerCase();
const out: Completion[] = [];
const seen = new Set();
- if (!head.trim()) for (const h of history) if (h.toLowerCase().startsWith(line.toLowerCase()) && h !== line) out.push({ line: h, label: h, detail: "recent" });
+ if (!head.trim())
+ for (const h of history)
+ if (h.toLowerCase().startsWith(line.toLowerCase()) && h !== line)
+ out.push({ line: h, label: h, detail: "recent" });
for (const p of model.projects) {
for (const t of p.tasks) {
const label = p.path === ":" ? t.name : `${p.path}:${t.name}`;
@@ -501,14 +533,23 @@ export function taskCompletions(line: string, model: GradleModel, history: reado
out.push({ line: head + label, label, detail: t.group });
}
}
- return out.sort((a, b) => (a.detail === "recent" ? -1 : b.detail === "recent" ? 1 : a.label.length - b.label.length || a.label.localeCompare(b.label)));
+ return out.sort((a, b) =>
+ a.detail === "recent"
+ ? -1
+ : b.detail === "recent"
+ ? 1
+ : a.label.length - b.label.length || a.label.localeCompare(b.label),
+ );
}
/**
* Keys of every expandable node below `roots` (Expand All). Dependency nodes are
* not opened (transitive trees can be huge); their "Dependencies" folder is.
*/
-export function expandableKeys(roots: readonly BuildNode[], children: (n: BuildNode) => BuildNode[] | undefined): Set {
+export function expandableKeys(
+ roots: readonly BuildNode[],
+ children: (n: BuildNode) => BuildNode[] | undefined,
+): Set {
const out = new Set();
const walk = (nodes: readonly BuildNode[]) => {
for (const n of nodes) {
diff --git a/src/features/build-tools/runAnything.ts b/src/features/build-tools/runAnything.ts
index f1fa275a..146b6c1c 100644
--- a/src/features/build-tools/runAnything.ts
+++ b/src/features/build-tools/runAnything.ts
@@ -79,16 +79,14 @@ export const buildToolsRunAnything: RunAnythingProvider = {
const head = /^\S+\s*/.exec(query)![0];
return completions(tool, query.slice(head.length))
.slice(0, 50)
- .map(
- (c): RunAnythingItem => ({
- key: `${c.detail === "recent" ? "h" : "c"}:${c.line}`,
- label: `${WORD[tool]} ${c.line}`,
- detail: c.detail,
- icon: tool === "maven" ? "file-maven" : "file-gradle",
- completion: `${WORD[tool]} ${c.detail === "recent" ? c.line : `${c.line} `}`,
- run: () => void executeBuildLine(tool, c.line),
- }),
- );
+ .map((c): RunAnythingItem => ({
+ key: `${c.detail === "recent" ? "h" : "c"}:${c.line}`,
+ label: `${WORD[tool]} ${c.line}`,
+ detail: c.detail,
+ icon: tool === "maven" ? "file-maven" : "file-gradle",
+ completion: `${WORD[tool]} ${c.detail === "recent" ? c.line : `${c.line} `}`,
+ run: () => void executeBuildLine(tool, c.line),
+ }));
},
execute(line) {
const tool = lineTool(line);
diff --git a/src/features/build-tools/store.ts b/src/features/build-tools/store.ts
index d26f964b..f5a71cab 100644
--- a/src/features/build-tools/store.ts
+++ b/src/features/build-tools/store.ts
@@ -105,9 +105,12 @@ export function updateSettings(patch: (s: BuildSettings) => BuildSettings) {
if (root) writeSettings(root, settings);
}
-export const toggleMavenOffline = () => updateSettings((s) => ({ ...s, maven: { ...s.maven, offline: !s.maven.offline } }));
-export const toggleSkipTests = () => updateSettings((s) => ({ ...s, maven: { ...s.maven, skipTests: !s.maven.skipTests } }));
-export const toggleGradleOffline = () => updateSettings((s) => ({ ...s, gradle: { ...s.gradle, offline: !s.gradle.offline } }));
+export const toggleMavenOffline = () =>
+ updateSettings((s) => ({ ...s, maven: { ...s.maven, offline: !s.maven.offline } }));
+export const toggleSkipTests = () =>
+ updateSettings((s) => ({ ...s, maven: { ...s.maven, skipTests: !s.maven.skipTests } }));
+export const toggleGradleOffline = () =>
+ updateSettings((s) => ({ ...s, gradle: { ...s.gradle, offline: !s.gradle.offline } }));
export function setProfile(id: string, checked: boolean) {
updateSettings((s) => ({ ...s, maven: { ...s.maven, profiles: { ...s.maven.profiles, [id]: checked } } }));
@@ -169,7 +172,8 @@ export async function loadModel(): Promise {
/** Tree keys expanded on first show: the top-level projects. */
export function initialExpanded(tool: BuildTool, model: BuildModel): Set {
- if (tool === "maven") return new Set(model.maven?.projects.filter((p) => !p.aggregated).map((p) => `mvn:${p.pom}`) ?? []);
+ if (tool === "maven")
+ return new Set(model.maven?.projects.filter((p) => !p.aggregated).map((p) => `mvn:${p.pom}`) ?? []);
return new Set(model.gradle ? ["gradle::"] : []);
}
diff --git a/src/features/build/index.ts b/src/features/build/index.ts
index 32e2993f..7af0d787 100644
--- a/src/features/build/index.ts
+++ b/src/features/build/index.ts
@@ -5,7 +5,12 @@
*/
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
-import { maybeEditorService, registerMenuGroup, registerTitleItem, registerToolWindow } from "@/workbench/contributions";
+import {
+ maybeEditorService,
+ registerMenuGroup,
+ registerTitleItem,
+ registerToolWindow,
+} from "@/workbench/contributions";
import { BuildTitleItem } from "./BuildTitleItem";
import { BuildHeaderActions, BuildView } from "./BuildView";
import { BUILD_RUNNING, resetBuildRun, startBuild, stopBuild, useBuildRun } from "./store";
@@ -19,7 +24,9 @@ function moduleTarget(arg: unknown): string | null {
}
function useErrorBadge(): number | null {
- return useBuildRun((s) => (s.status === "failed" ? s.messages.filter((m) => m.severity === "error").length || null : null));
+ return useBuildRun((s) =>
+ s.status === "failed" ? s.messages.filter((m) => m.severity === "error").length || null : null,
+ );
}
export function activate(): () => void {
@@ -35,7 +42,13 @@ export function activate(): () => void {
headerActions: BuildHeaderActions,
useBadge: useErrorBadge,
}),
- registerTitleItem({ id: "build.project", title: "Build Project", align: "right", priority: -1, component: BuildTitleItem }),
+ registerTitleItem({
+ id: "build.project",
+ title: "Build Project",
+ align: "right",
+ priority: -1,
+ component: BuildTitleItem,
+ }),
registerActions([
{
id: "build.buildProject",
diff --git a/src/features/build/store.ts b/src/features/build/store.ts
index 4b75dca1..ff8f7c3f 100644
--- a/src/features/build/store.ts
+++ b/src/features/build/store.ts
@@ -207,7 +207,10 @@ export async function startBuild(kind: BuildKind, file: string | null = null): P
}
} finally {
if (useBuildRun.getState().request === request) useBuildRun.setState({ request: null });
- setContext(BUILD_LAST_STATUS, result?.success ? "success" : useBuildRun.getState().status === "cancelled" ? "cancelled" : "failed");
+ setContext(
+ BUILD_LAST_STATUS,
+ result?.success ? "success" : useBuildRun.getState().status === "cancelled" ? "cancelled" : "failed",
+ );
setContext(BUILD_RUNNING, false);
upsertTask({ id: BUILD_TASK_ID, title: taskTitle, message: null, fraction: 1, done: true });
}
diff --git a/src/features/code-insight/ServerStatus.tsx b/src/features/code-insight/ServerStatus.tsx
index ab9ea711..bda8b5b0 100644
--- a/src/features/code-insight/ServerStatus.tsx
+++ b/src/features/code-insight/ServerStatus.tsx
@@ -50,7 +50,10 @@ export function ServerStatus() {
{s.name}
diff --git a/src/features/code-insight/hierarchy.test.ts b/src/features/code-insight/hierarchy.test.ts
index 4f15ee91..9321c3b4 100644
--- a/src/features/code-insight/hierarchy.test.ts
+++ b/src/features/code-insight/hierarchy.test.ts
@@ -11,7 +11,13 @@ const range = (l1: number, c1: number, l2: number, c2: number) => ({
start: { line: l1, col: c1 },
end: { line: l2, col: c2 },
});
-const outlineItem = (name: string, kind: SymbolKind, r: ReturnType
, nameAt: [number, number], children: OutlineItem[] = []): OutlineItem => ({
+const outlineItem = (
+ name: string,
+ kind: SymbolKind,
+ r: ReturnType,
+ nameAt: [number, number],
+ children: OutlineItem[] = [],
+): OutlineItem => ({
name,
detail: null,
kind,
@@ -21,10 +27,16 @@ const outlineItem = (name: string, kind: SymbolKind, r: ReturnType
});
// class Orders { void list() { int x = 1; } } — class on lines 0-10, `list` on lines 2-6.
const OUTLINE = [
- outlineItem("Orders", "class", range(0, 0, 10, 1), [0, 6], [
- outlineItem("list", "method", range(2, 2, 6, 3), [2, 7]),
- outlineItem("count", "field", range(8, 2, 8, 12), [8, 6]),
- ]),
+ outlineItem(
+ "Orders",
+ "class",
+ range(0, 0, 10, 1),
+ [0, 6],
+ [
+ outlineItem("list", "method", range(2, 2, 6, 3), [2, 7]),
+ outlineItem("count", "field", range(8, 2, 8, 12), [8, 6]),
+ ],
+ ),
];
const item = (name: string, fromRanges: HierarchyItem["fromRanges"] = []): HierarchyItem => ({
@@ -41,7 +53,7 @@ vi.mock("@/platform/ipc", async (orig) => {
const real = await orig();
return {
...real,
- call: async (p: Promise) => p,
+ call: async (p: Promise) => p,
commands: {
...real.commands,
docOutline: async () => OUTLINE,
@@ -56,8 +68,8 @@ vi.mock("@/platform/ipc", async (orig) => {
vi.mock("./ipc", async (orig) => ({
...(await orig()),
caretContext: () => ({ doc: 1, path: "/p/Orders.java", line: 4, col: 10, word: "x", view: null }),
- withDocument: async (_path: string, fn: (doc: number) => Promise) => fn(1),
- lspRequest: async (run: () => Promise) => run(),
+ withDocument: async (_path: string, fn: (doc: number) => Promise) => fn(1),
+ lspRequest: async (run: () => Promise) => run(),
hint: (...a: unknown[]) => hint(...a),
}));
diff --git a/src/features/code-insight/hierarchy.ts b/src/features/code-insight/hierarchy.ts
index 8ef5ef45..cd5d3377 100644
--- a/src/features/code-insight/hierarchy.ts
+++ b/src/features/code-insight/hierarchy.ts
@@ -196,7 +196,11 @@ export async function showHierarchy(mode: HierarchyMode) {
await loadRoot();
} else {
useHierarchy.setState((st) => ({ failed: new Set(st.failed).add(ROOT_ID) }));
- hint(mode === "call" ? "No method at the caret to show the call hierarchy of" : "No type at the caret to show the hierarchy of");
+ hint(
+ mode === "call"
+ ? "No method at the caret to show the call hierarchy of"
+ : "No type at the caret to show the hierarchy of",
+ );
}
}
@@ -206,7 +210,10 @@ export function hierarchyTarget(
direction: HierarchyDirection,
item: HierarchyItem,
): { path: string; line: number; col: number } {
- const at = mode === "call" && direction === "incoming" && item.fromRanges[0] ? item.fromRanges[0].start : item.selectionRange.start;
+ const at =
+ mode === "call" && direction === "incoming" && item.fromRanges[0]
+ ? item.fromRanges[0].start
+ : item.selectionRange.start;
return { path: item.path, line: at.line, col: at.col };
}
diff --git a/src/features/code-insight/ipc.ts b/src/features/code-insight/ipc.ts
index 9fcdb4af..77ea565c 100644
--- a/src/features/code-insight/ipc.ts
+++ b/src/features/code-insight/ipc.ts
@@ -24,12 +24,21 @@ export const isUnavailable = (e: unknown) => e instanceof IpcError && e.kind ===
/** Small hint at the caret (via editor/lsp), or a notification without an editor. */
export function hint(text: string, tone: "info" | "warning" | "error" = "info") {
if (activeEditor()) void executeAction("editor.showHint", { text, tone });
- else notifications().notify({ group: "lsp", level: tone === "error" ? "error" : "info", title: text, key: "code-insight.hint" });
+ else
+ notifications().notify({
+ group: "lsp",
+ level: tone === "error" ? "error" : "info",
+ title: text,
+ key: "code-insight.hint",
+ });
}
export function reportError(e: unknown) {
if (isCancelled(e)) return;
- hint(isUnavailable(e) ? "No language server available" : e instanceof Error ? e.message : String(e), isUnavailable(e) ? "info" : "error");
+ hint(
+ isUnavailable(e) ? "No language server available" : e instanceof Error ? e.message : String(e),
+ isUnavailable(e) ? "info" : "error",
+ );
}
export interface CaretContext {
@@ -52,7 +61,14 @@ export function caretContext(): CaretContext | null {
if (!ed) return null;
const head = ed.view.state.selection.main.head;
const line = ed.view.state.doc.lineAt(head);
- return { doc: ed.ctx.doc, path: ed.ctx.path, line: line.number - 1, col: head - line.from, word: wordAt(ed.view, head), view: ed.view };
+ return {
+ doc: ed.ctx.doc,
+ path: ed.ctx.path,
+ line: line.number - 1,
+ col: head - line.from,
+ word: wordAt(ed.view, head),
+ view: ed.view,
+ };
}
function wordAt(view: EditorView, offset: number): string | null {
@@ -93,7 +109,10 @@ export async function previewText(path: string): Promise {
return lines.join("\n");
}
-export async function readManyFileLines(paths: readonly string[], concurrency = 4): Promise> {
+export async function readManyFileLines(
+ paths: readonly string[],
+ concurrency = 4,
+): Promise> {
const out = new Map();
const queue = [...new Set(paths)];
const worker = async () => {
diff --git a/src/features/code-insight/problemsStore.test.ts b/src/features/code-insight/problemsStore.test.ts
index 2eaf0660..ce036d99 100644
--- a/src/features/code-insight/problemsStore.test.ts
+++ b/src/features/code-insight/problemsStore.test.ts
@@ -90,7 +90,9 @@ describe("local inspections (issue #42)", () => {
clearProblems();
const off = installProblemsEvents();
try {
- setFileDiagnostics("/p/a.py", "spelling", [{ ...d(0, "info"), message: "Typo: In word 'mesage'", source: "Spelling" }]);
+ setFileDiagnostics("/p/a.py", "spelling", [
+ { ...d(0, "info"), message: "Typo: In word 'mesage'", source: "Spelling" },
+ ]);
const [typo] = fileDiagnostics(useProblems.getState().map, "/p/a.py");
expect(typo).toMatchObject({ message: "Typo: In word 'mesage'", typo: true });
const producer = producerFor(useProblems.getState().map, "/p/a.py", typo!);
diff --git a/src/features/code-insight/problemsStore.ts b/src/features/code-insight/problemsStore.ts
index 87df2653..348a2f0d 100644
--- a/src/features/code-insight/problemsStore.ts
+++ b/src/features/code-insight/problemsStore.ts
@@ -26,12 +26,21 @@ export interface SeverityCounts {
}
/** Replaces `source`'s diagnostics for `path`; returns the same map when nothing changed. */
-export function mergeDiagnostics(map: ProblemsMap, path: string, source: string, items: readonly Diagnostic[]): ProblemsMap {
+export function mergeDiagnostics(
+ map: ProblemsMap,
+ path: string,
+ source: string,
+ items: readonly Diagnostic[],
+): ProblemsMap {
const prev = map.get(path);
if (!items.length && !prev?.has(source)) return map;
const next = new Map(map);
const sources = new Map(prev ?? []);
- if (items.length) sources.set(source, items.map((d) => (d.source ? d : { ...d, source })));
+ if (items.length)
+ sources.set(
+ source,
+ items.map((d) => (d.source ? d : { ...d, source })),
+ );
else sources.delete(source);
if (sources.size) next.set(path, sources);
else next.delete(path);
diff --git a/src/features/code-insight/servers.test.ts b/src/features/code-insight/servers.test.ts
index e23f5c20..b9e85dd1 100644
--- a/src/features/code-insight/servers.test.ts
+++ b/src/features/code-insight/servers.test.ts
@@ -33,7 +33,13 @@ describe("server status", () => {
const info: ServerInfo = { id, name: "jdtls", languages: [], root: "/p", status: "starting", pid: null };
// The first event requests a snapshot; the server reports missing before it answers.
emitLocalIdeEvent({ type: "languageServer", server: id, name: "jdtls", status: "starting", message: null });
- emitLocalIdeEvent({ type: "languageServer", server: id, name: "jdtls", status: "missing", message: "install jdtls" });
+ emitLocalIdeEvent({
+ type: "languageServer",
+ server: id,
+ name: "jdtls",
+ status: "missing",
+ message: "install jdtls",
+ });
pending.splice(0).forEach((resolve) => resolve([info]));
await vi.waitFor(() => expect(useServers.getState().servers.get(id)?.languages).toEqual([]));
await new Promise((r) => setTimeout(r, 0));
@@ -81,7 +87,9 @@ describe("serversSummary", () => {
root: "/p",
});
it("doesn't flag a missing server whose languages another one serves", () => {
- expect(serversSummary([entry(1, "vtsls", "missing"), entry(2, "typescript-language-server", "ready")])?.tone).toBe("ok");
+ expect(serversSummary([entry(1, "vtsls", "missing"), entry(2, "typescript-language-server", "ready")])?.tone).toBe(
+ "ok",
+ );
expect(serversSummary([entry(1, "vtsls", "missing")])?.tone).toBe("error");
});
it("doesn't count stopped servers next to running ones", () => {
diff --git a/src/features/code-insight/servers.ts b/src/features/code-insight/servers.ts
index 1da19ec0..3c8243bb 100644
--- a/src/features/code-insight/servers.ts
+++ b/src/features/code-insight/servers.ts
@@ -150,15 +150,20 @@ export const STATUS_LABEL: Record = {
export function servedByOther(server: ServerEntry, servers: readonly ServerEntry[]): boolean {
return (
server.languages.length > 0 &&
- server.languages.every((l) => servers.some((o) => o !== server && o.status !== "missing" && o.languages.includes(l)))
+ server.languages.every((l) =>
+ servers.some((o) => o !== server && o.status !== "missing" && o.languages.includes(l)),
+ )
);
}
/** One-line summary for the status bar. */
-export function serversSummary(servers: readonly ServerEntry[]): { text: string; tone: "ok" | "busy" | "error" | "idle" } | null {
+export function serversSummary(
+ servers: readonly ServerEntry[],
+): { text: string; tone: "ok" | "busy" | "error" | "idle" } | null {
if (!servers.length) return null;
const bad = servers.filter(
- (s) => s.status === "crashed" || s.status === "misconfigured" || (s.status === "missing" && !servedByOther(s, servers)),
+ (s) =>
+ s.status === "crashed" || s.status === "misconfigured" || (s.status === "missing" && !servedByOther(s, servers)),
);
const busy = servers.filter((s) => s.status === "starting" || s.status === "busy");
const tone = bad.length ? "error" : busy.length ? "busy" : servers.some((s) => s.status === "ready") ? "ok" : "idle";
diff --git a/src/features/code-insight/syntaxUsages.test.ts b/src/features/code-insight/syntaxUsages.test.ts
index d675a015..6e19a386 100644
--- a/src/features/code-insight/syntaxUsages.test.ts
+++ b/src/features/code-insight/syntaxUsages.test.ts
@@ -5,7 +5,8 @@ import type { IdeError, Locals, Range } from "@/bindings";
type R = Promise<{ status: "ok"; data: T } | { status: "error"; error: IdeError }>;
const ok = (data: T) => Promise.resolve({ status: "ok" as const, data });
-const err = (kind: IdeError["kind"]) => Promise.resolve({ status: "error" as const, error: { kind, message: kind } as IdeError });
+const err = (kind: IdeError["kind"]) =>
+ Promise.resolve({ status: "error" as const, error: { kind, message: kind } as IdeError });
const cmd = {
lspReferences: vi.fn((): R => err("unavailable")),
@@ -51,25 +52,39 @@ describe("Find Usages fallback", () => {
expect(cmd.lspReferences).toHaveBeenCalledWith(42, 3, { line: 1, col: 0 }, false, true);
expect(res.groups[0]!.usages.length).toBe(1);
expect(res.note).toBe(incompleteNote("indexing", "x"));
- expect(incompleteNote("tooManyFiles", "x")).toBe("May be incomplete: too many files mention 'x' to search them all");
+ expect(incompleteNote("tooManyFiles", "x")).toBe(
+ "May be incomplete: too many files mention 'x' to search them all",
+ );
});
it("lists a certain local's references when the server is not ready", async () => {
cmd.syntaxLocals.mockReturnValueOnce(
ok({
status: "local",
- symbol: { name: "x", kind: "var", definition: r(0, 4, 5), scope: r(0, 0, 20), references: [r(1, 0, 1)], certain: true, conflicts: [] },
+ symbol: {
+ name: "x",
+ kind: "var",
+ definition: r(0, 4, 5),
+ scope: r(0, 0, 20),
+ references: [r(1, 0, 1)],
+ certain: true,
+ conflicts: [],
+ },
}),
);
const res = await fetchUsages(caret);
expect(res.note).toBe(LOCAL_NOTE);
- expect(res.groups.map((g) => [g.path, g.usages.map((u) => [u.line, u.col, u.text])])).toEqual([["/p/a.ts", [[1, 0, "x + y;"]]]]);
+ expect(res.groups.map((g) => [g.path, g.usages.map((u) => [u.line, u.col, u.text])])).toEqual([
+ ["/p/a.ts", [[1, 0, "x + y;"]]],
+ ]);
expect(filesMentioning).not.toHaveBeenCalled();
});
it("searches the project for identifier occurrences of non-locals", async () => {
cmd.lspReferences.mockReturnValueOnce(err("internal"));
- cmd.syntaxLocals.mockReturnValueOnce(ok({ status: "notLocal", name: "y", range: r(1, 4, 5), reason: "unresolved" }));
+ cmd.syntaxLocals.mockReturnValueOnce(
+ ok({ status: "notLocal", name: "y", range: r(1, 4, 5), reason: "unresolved" }),
+ );
cmd.syntaxIdentifiersInFiles.mockReturnValueOnce(
ok([
{ path: "/p/b.ts", occurrences: [{ range: r(2, 0, 1), kind: "identifier", qualified: false }] },
@@ -85,8 +100,13 @@ describe("Find Usages fallback", () => {
it("keeps the server error when the language has no grammar", async () => {
cmd.syntaxSupport.mockReturnValueOnce(ok({ tree: false, locals: false }));
const other = new EditorView({ state: EditorState.create({ doc: "x" }) });
- _addEditor({ view: other, ctx: { doc: 4, path: "/p/a.txt", language: "plaintext", uiMode: null, readOnly: false } });
- await expect(fetchUsages({ doc: 4, path: "/p/a.txt", line: 0, col: 0, word: "x", view: other })).rejects.toMatchObject({
+ _addEditor({
+ view: other,
+ ctx: { doc: 4, path: "/p/a.txt", language: "plaintext", uiMode: null, readOnly: false },
+ });
+ await expect(
+ fetchUsages({ doc: 4, path: "/p/a.txt", line: 0, col: 0, word: "x", view: other }),
+ ).rejects.toMatchObject({
kind: "unavailable",
});
});
diff --git a/src/features/code-insight/syntaxUsages.ts b/src/features/code-insight/syntaxUsages.ts
index ec8e5b21..161c9394 100644
--- a/src/features/code-insight/syntaxUsages.ts
+++ b/src/features/code-insight/syntaxUsages.ts
@@ -28,7 +28,9 @@ function inside(range: Range, pos: Range["start"]) {
/** Null when the structure service cannot answer for this file (no grammar, no editor). */
export async function syntaxUsages(c: CaretContext): Promise {
- const language = c.view ? editorsFor(c.path).find((e) => e.view === c.view)?.ctx.language : editorsFor(c.path)[0]?.ctx.language;
+ const language = c.view
+ ? editorsFor(c.path).find((e) => e.view === c.view)?.ctx.language
+ : editorsFor(c.path)[0]?.ctx.language;
if (!language) return null;
const support = await structure.support(language);
if (!support.tree) return null;
@@ -45,7 +47,9 @@ export async function syntaxUsages(c: CaretContext): Promise !o.qualified && inside(symbol.scope, o.range.start)).map((o) => ({ path: c.path, range: o.range })),
+ locs: occ
+ .filter((o) => !o.qualified && inside(symbol.scope, o.range.start))
+ .map((o) => ({ path: c.path, range: o.range })),
note: TEXT_NOTE,
};
}
diff --git a/src/features/code-insight/usageResults.test.ts b/src/features/code-insight/usageResults.test.ts
index b7782b12..02907c38 100644
--- a/src/features/code-insight/usageResults.test.ts
+++ b/src/features/code-insight/usageResults.test.ts
@@ -10,7 +10,8 @@ vi.mock("./ipc", async (orig) => ({
readManyFileLines: async () => new Map([["/p/b.rs", ["", " used();"]]]),
}));
-const { activeUsageSearch, openUsageTab, pinUsageTab, resetUsages, showUsageResults, useUsages } = await import("./usages");
+const { activeUsageSearch, openUsageTab, pinUsageTab, resetUsages, showUsageResults, useUsages } =
+ await import("./usages");
describe("showUsageResults", () => {
it("shows given usages in the Find Usages tool window", async () => {
@@ -19,7 +20,9 @@ describe("showUsageResults", () => {
expect(show).toHaveBeenCalledWith("usages");
const search = activeUsageSearch(useUsages.getState())!;
expect(search).toMatchObject({ symbol: "used", loading: false, error: null, rerun: null });
- expect(search.groups).toEqual([{ path: "/p/b.rs", usages: [{ path: "/p/b.rs", line: 1, col: 4, endCol: 8, text: " used();" }] }]);
+ expect(search.groups).toEqual([
+ { path: "/p/b.rs", usages: [{ path: "/p/b.rs", line: 1, col: 4, endCol: 8, text: " used();" }] },
+ ]);
});
it("ignores malformed arguments", async () => {
@@ -30,7 +33,15 @@ describe("showUsageResults", () => {
});
describe("usage tabs", () => {
- const search = (id: number) => ({ id, symbol: `s${id}`, loading: false, groups: [], error: null, note: null, rerun: null });
+ const search = (id: number) => ({
+ id,
+ symbol: `s${id}`,
+ loading: false,
+ groups: [],
+ error: null,
+ note: null,
+ rerun: null,
+ });
it("replace the selected tab unless it is pinned", () => {
resetUsages();
diff --git a/src/features/code-insight/usages.ts b/src/features/code-insight/usages.ts
index 1791bcee..39998570 100644
--- a/src/features/code-insight/usages.ts
+++ b/src/features/code-insight/usages.ts
@@ -31,7 +31,10 @@ export interface UsageGroup {
}
/** Groups reference locations by file (sorted by path, then position), attaching line previews. */
-export function groupUsages(locs: readonly Location[], files: ReadonlyMap): UsageGroup[] {
+export function groupUsages(
+ locs: readonly Location[],
+ files: ReadonlyMap,
+): UsageGroup[] {
const byPath = new Map();
const seen = new Set();
for (const l of locs) {
@@ -170,7 +173,9 @@ export async function fetchUsages(c: CaretContext, request: RequestId = nextRequ
let locs: Location[];
let note: string | null = null;
try {
- const refs = await lspRequest(() => commands.lspReferences(request, c.doc, { line: c.line, col: c.col }, false, true));
+ const refs = await lspRequest(() =>
+ commands.lspReferences(request, c.doc, { line: c.line, col: c.col }, false, true),
+ );
locs = refs.locations;
if (refs.incomplete) note = incompleteNote(refs.incomplete, c.word);
} catch (e) {
diff --git a/src/features/code-insight/views.test.ts b/src/features/code-insight/views.test.ts
index 9a2c9d15..15887ab1 100644
--- a/src/features/code-insight/views.test.ts
+++ b/src/features/code-insight/views.test.ts
@@ -94,7 +94,8 @@ describe("sortProblems", () => {
code: null,
});
const items = [d("warning", 1, "b"), d("error", 5, "c"), d("warning", 0, "z"), d("error", 2, "a")];
- const lines = (order: { bySeverity: boolean; byName: boolean }) => sortProblems(items, order).map((x) => x.range.start.line);
+ const lines = (order: { bySeverity: boolean; byName: boolean }) =>
+ sortProblems(items, order).map((x) => x.range.start.line);
it("orders by severity, then position (IDEA's default)", () => {
expect(lines({ bySeverity: true, byName: false })).toEqual([2, 5, 0, 1]);
diff --git a/src/features/coverage/coverage.test.ts b/src/features/coverage/coverage.test.ts
index 311aacc2..22257cc2 100644
--- a/src/features/coverage/coverage.test.ts
+++ b/src/features/coverage/coverage.test.ts
@@ -3,10 +3,31 @@ import { EditorState } from "@codemirror/state";
import type { CoverageSuite, FileCoverage, RunConfig } from "@/platform/ipc";
import { coverageMarkers } from "./gutter";
import { coverageBadges, coverageSupports, coverageTree, formatPercent, lineState, sortTree } from "./model";
-import { MAX_SUITES, addSuite, closeSuite, reportEntries, resetCoverage, setCoverageVisible, shownFileCoverage, useCoverage } from "./store";
+import {
+ MAX_SUITES,
+ addSuite,
+ closeSuite,
+ reportEntries,
+ resetCoverage,
+ setCoverageVisible,
+ shownFileCoverage,
+ useCoverage,
+} from "./store";
function cfg(command: string, args: string[], extra: Partial = {}): RunConfig {
- return { id: "c", name: "c", kind: "shell", command, args, cwd: null, env: {}, detected: true, isTest: false, debugAdapter: null, ...extra };
+ return {
+ id: "c",
+ name: "c",
+ kind: "shell",
+ command,
+ args,
+ cwd: null,
+ env: {},
+ detected: true,
+ isTest: false,
+ debugAdapter: null,
+ ...extra,
+ };
}
function file(path: string, covered: number, total: number, branches: [number, number] = [0, 0]): FileCoverage {
@@ -98,7 +119,11 @@ describe("gutter and suites", () => {
expect(f.lines.map(lineState)).toEqual(["covered", "partial", "uncovered", "covered"]);
const doc = EditorState.create({ doc: "a\nb\nc\n" }).doc;
const colors: string[] = [];
- coverageMarkers(f, doc).between(0, doc.length, (_from, _to, m) => void colors.push((m as unknown as { color: string }).color));
+ coverageMarkers(f, doc).between(
+ 0,
+ doc.length,
+ (_from, _to, m) => void colors.push((m as unknown as { color: string }).color),
+ );
expect(colors).toEqual(["var(--coverage-covered)", "var(--coverage-partial)", "var(--coverage-uncovered)"]);
});
diff --git a/src/features/coverage/executor.ts b/src/features/coverage/executor.ts
index b007042b..ad3918b4 100644
--- a/src/features/coverage/executor.ts
+++ b/src/features/coverage/executor.ts
@@ -4,7 +4,12 @@
*/
import { executeAction } from "@/platform/actions";
import { call, commands, isTauri, type InstallCommand, type RunConfig } from "@/platform/ipc";
-import { notifications, toolWindowService, type ExecutorLaunch, type RunExecutorContribution } from "@/workbench/contributions";
+import {
+ notifications,
+ toolWindowService,
+ type ExecutorLaunch,
+ type RunExecutorContribution,
+} from "@/workbench/contributions";
import { coverageSupports, formatPercent } from "./model";
import { addSuite } from "./store";
import { tx } from "@/platform/i18n";
@@ -35,7 +40,8 @@ export function notifyMissingTool(
const { tool, install } = missing;
const line = install ? [install.command, ...install.args].join(" ") : null;
const actions = [];
- if (install) actions.push({ label: "Install", run: () => void executeAction("run.runConfig", installConfig(tool, install)) });
+ if (install)
+ actions.push({ label: "Install", run: () => void executeAction("run.runConfig", installConfig(tool, install)) });
if (missing.homepage) {
const url = missing.homepage;
actions.push({
@@ -48,7 +54,11 @@ export function notifyMissingTool(
level: "warning",
key: `${group}-missing-${tool}`,
title: `${tool} is not installed`,
- body: line ? tx`${missing.message} Install it with: ${line}` : missing.homepage ? `${missing.message} ${missing.homepage}` : missing.message,
+ body: line
+ ? tx`${missing.message} Install it with: ${line}`
+ : missing.homepage
+ ? `${missing.message} ${missing.homepage}`
+ : missing.message,
sticky: true,
actions,
});
@@ -61,7 +71,13 @@ async function prepare(config: RunConfig): Promise {
}
const plan = await call(commands.coveragePrepare(config));
if (plan.status === "unsupported") {
- notifications().notify({ group: "run", level: "info", key: "coverage-unsupported", title: `Cannot run '${config.name}' with coverage`, body: plan.reason });
+ notifications().notify({
+ group: "run",
+ level: "info",
+ key: "coverage-unsupported",
+ title: `Cannot run '${config.name}' with coverage`,
+ body: plan.reason,
+ });
return null;
}
if (plan.status === "missingTool") {
@@ -90,7 +106,13 @@ async function prepare(config: RunConfig): Promise {
title: `Coverage of '${suite.name}': ${formatPercent(totals[0]!, totals[1]!)} lines`,
});
} catch (e) {
- notifications().notify({ group: "run", level: "error", key: "coverage-done", title: `No coverage data for '${config.name}'`, body: errorText(e) });
+ notifications().notify({
+ group: "run",
+ level: "error",
+ key: "coverage-done",
+ title: `No coverage data for '${config.name}'`,
+ body: errorText(e),
+ });
}
})();
},
diff --git a/src/features/coverage/index.ts b/src/features/coverage/index.ts
index f1035d60..60acde68 100644
--- a/src/features/coverage/index.ts
+++ b/src/features/coverage/index.ts
@@ -49,12 +49,14 @@ export function activate(): () => void {
icon: "run-coverage",
when: "projectOpen",
keywords: ["coverage"],
- run: ({ arg }) => executeAction("run.withExecutor", { executor: "coverage", context: arg ?? true }).then(() => undefined),
+ run: ({ arg }) =>
+ executeAction("run.withExecutor", { executor: "coverage", context: arg ?? true }).then(() => undefined),
},
{
id: "coverage.importReport",
title: "Import a Coverage Report…",
- description: "Show a report collected elsewhere (CI): lcov, JaCoCo XML or a Go cover profile; argument: the file path",
+ description:
+ "Show a report collected elsewhere (CI): lcov, JaCoCo XML or a Go cover profile; argument: the file path",
category: "Run",
icon: "folder-open",
when: "projectOpen",
diff --git a/src/features/coverage/model.ts b/src/features/coverage/model.ts
index d421b4e1..e36d8cea 100644
--- a/src/features/coverage/model.ts
+++ b/src/features/coverage/model.ts
@@ -85,7 +85,16 @@ const join = (a: string, b: string) => (a ? `${a.replace(/\/$/, "")}/${b}` : b);
* (`src/main/java/com/x`) and every directory sums its files.
*/
export function coverageTree(suite: CoverageSuite, root: string, compactDirs = true): CoverageNode[] {
- const top: CoverageNode = { path: root, name: "", dir: true, children: [], linesTotal: 0, linesCovered: 0, branchesTotal: 0, branchesCovered: 0 };
+ const top: CoverageNode = {
+ path: root,
+ name: "",
+ dir: true,
+ children: [],
+ linesTotal: 0,
+ linesCovered: 0,
+ branchesTotal: 0,
+ branchesCovered: 0,
+ };
const prefix = root.replace(/\/$/, "") + "/";
for (const f of suite.files) {
const inside = f.path.startsWith(prefix);
@@ -97,7 +106,16 @@ export function coverageTree(suite: CoverageSuite, root: string, compactDirs = t
base = inside || base ? join(base, part) : `/${part}`;
let child = node.children.find((c) => c.dir && c.name === part);
if (!child) {
- child = { path: base, name: part, dir: true, children: [], linesTotal: 0, linesCovered: 0, branchesTotal: 0, branchesCovered: 0 };
+ child = {
+ path: base,
+ name: part,
+ dir: true,
+ children: [],
+ linesTotal: 0,
+ linesCovered: 0,
+ branchesTotal: 0,
+ branchesCovered: 0,
+ };
node.children.push(child);
}
node = child;
diff --git a/src/features/coverage/store.ts b/src/features/coverage/store.ts
index d820730f..69f4f9f7 100644
--- a/src/features/coverage/store.ts
+++ b/src/features/coverage/store.ts
@@ -92,7 +92,8 @@ export function syncCoverageViews(): () => void {
const root = useProject.getState().project?.root;
const decorations = new Map();
if (suite && root) {
- for (const [path, badge] of coverageBadges(coverageTree(suite.data, root, false))) decorations.set(path, { badge });
+ for (const [path, badge] of coverageBadges(coverageTree(suite.data, root, false)))
+ decorations.set(path, { badge });
}
setFileDecorations("coverage", decorations);
};
@@ -148,7 +149,11 @@ export function rememberImport(report: ImportedReport) {
export function forgetImport(path: string) {
const root = useProject.getState().project?.root;
- if (root) saveImported(root, importedReports(root).filter((r) => r.path !== path));
+ if (root)
+ saveImported(
+ root,
+ importedReports(root).filter((r) => r.path !== path),
+ );
}
/** A row of "Manage Coverage Reports": a loaded suite, or an imported report not loaded now. */
diff --git a/src/features/database/database.test.ts b/src/features/database/database.test.ts
index a906df93..8310f593 100644
--- a/src/features/database/database.test.ts
+++ b/src/features/database/database.test.ts
@@ -21,12 +21,28 @@ import { VENDORS, sourceUrl, vendorById, vendorOf, vendorPreset } from "./vendor
describe("clipboard formats", () => {
it("copies TSV with NULL as empty and whitespace flattened", () => {
- expect(toTsv(["a", "b"], [["1", null], ["x\ty", "line\nbreak"]])).toBe("a\tb\n1\t\nx y\tline break");
+ expect(
+ toTsv(
+ ["a", "b"],
+ [
+ ["1", null],
+ ["x\ty", "line\nbreak"],
+ ],
+ ),
+ ).toBe("a\tb\n1\t\nx y\tline break");
expect(toTsv(null, [["1"]])).toBe("1");
});
it("copies RFC 4180 CSV", () => {
- expect(toCsv(["id", "note"], [["1", 'say "hi", ok'], ["2", null]])).toBe('id,note\r\n1,"say ""hi"", ok"\r\n2,');
+ expect(
+ toCsv(
+ ["id", "note"],
+ [
+ ["1", 'say "hi", ok'],
+ ["2", null],
+ ],
+ ),
+ ).toBe('id,note\r\n1,"say ""hi"", ok"\r\n2,');
});
});
@@ -87,7 +103,9 @@ describe("export formats", () => {
});
it("writes a Markdown table, numbers right-aligned", () => {
- expect(exportData("markdown", { ...data, rows: [["1", "a|b"]] })).toBe("| id | name |\n| --: | --- |\n| 1 | a\\|b |");
+ expect(exportData("markdown", { ...data, rows: [["1", "a|b"]] })).toBe(
+ "| id | name |\n| --: | --- |\n| 1 | a\\|b |",
+ );
});
});
@@ -95,8 +113,12 @@ describe("connection errors", () => {
const ssh = (auth: "password" | "keyPair" | "openSsh") => ({ ssh: { enabled: true, host: "bastion", auth } });
it("asks for what the failed connection needs", () => {
- expect(connectRemedy("protocol", "Unknown SSH host key for bastion:22 (ssh-ed25519 SHA256:abc)")).toBe("trustHostKey");
- expect(connectRemedy("permissionDenied", "SSH password required for me@bastion:22", ssh("password"))).toBe("sshSecret");
+ expect(connectRemedy("protocol", "Unknown SSH host key for bastion:22 (ssh-ed25519 SHA256:abc)")).toBe(
+ "trustHostKey",
+ );
+ expect(connectRemedy("permissionDenied", "SSH password required for me@bastion:22", ssh("password"))).toBe(
+ "sshSecret",
+ );
expect(connectRemedy("permissionDenied", "SSH key passphrase required for /k", ssh("keyPair"))).toBe("sshSecret");
expect(connectRemedy("permissionDenied", "SSH authentication failed for me@bastion:22", ssh("password"))).toBe(
"sshSecret",
@@ -144,14 +166,22 @@ describe("parseSourceUrl", () => {
});
it("reads SQLite files and rejects other drivers", () => {
- expect(parseSourceUrl("jdbc:sqlite:C:/data/shop.db")?.source).toEqual({ driver: "sqlite", file: "C:/data/shop.db", name: "shop" });
+ expect(parseSourceUrl("jdbc:sqlite:C:/data/shop.db")?.source).toEqual({
+ driver: "sqlite",
+ file: "C:/data/shop.db",
+ name: "shop",
+ });
expect(parseSourceUrl("oracle://x")).toBeNull();
});
});
describe("data source types", () => {
it("presets wire-compatible products on the existing drivers", () => {
- expect(vendorPreset(vendorById("cockroachdb")!)).toEqual({ driver: "postgres", vendor: "cockroachdb", port: 26257 });
+ expect(vendorPreset(vendorById("cockroachdb")!)).toEqual({
+ driver: "postgres",
+ vendor: "cockroachdb",
+ port: 26257,
+ });
expect(vendorPreset(vendorById("postgres")!)).toEqual({ driver: "postgres", vendor: "", port: 5432 });
expect(vendorPreset(vendorById("supabase")!).ssl).toBe("require");
expect(VENDORS.every((v) => ["postgres", "mysql", "sqlite"].includes(v.driver))).toBe(true);
@@ -170,7 +200,9 @@ describe("data source types", () => {
expect(sourceUrl({ driver: "postgres", host: "localhost", port: 5432, database: "postgres" })).toBe(
"jdbc:postgresql://localhost:5432/postgres",
);
- expect(sourceUrl({ driver: "postgres", vendor: "redshift", host: "r", port: null, database: "" })).toBe("jdbc:redshift://r:5439");
+ expect(sourceUrl({ driver: "postgres", vendor: "redshift", host: "r", port: null, database: "" })).toBe(
+ "jdbc:redshift://r:5439",
+ );
expect(sourceUrl({ driver: "mysql", vendor: "mariadb", host: "::1", port: 3307, database: "a b" })).toBe(
"jdbc:mariadb://[::1]:3307/a%20b",
);
@@ -178,7 +210,9 @@ describe("data source types", () => {
});
it("reads URLs of compatible products, SSL settings and driver properties", () => {
- const tidb = parseSourceUrl("jdbc:tidb://root@tidb.local:4000/test?sslmode=verify-full&sslrootcert=/ca.pem&sql_mode=ANSI");
+ const tidb = parseSourceUrl(
+ "jdbc:tidb://root@tidb.local:4000/test?sslmode=verify-full&sslrootcert=/ca.pem&sql_mode=ANSI",
+ );
expect(tidb?.source).toEqual({
driver: "mysql",
vendor: "tidb",
@@ -223,7 +257,8 @@ describe("Options tab", () => {
["a:other", 50_000],
["b", 0],
]);
- const idle = (busy: string[]) => idleTargets(["a", "a:other", "b", "c"], sources, used, (t) => busy.includes(t), 60_000);
+ const idle = (busy: string[]) =>
+ idleTargets(["a", "a:other", "b", "c"], sources, used, (t) => busy.includes(t), 60_000);
expect(idle([])).toEqual(["a"]);
expect(idle(["a"])).toEqual([]);
});
diff --git a/src/features/database/objects.test.ts b/src/features/database/objects.test.ts
index 9549aeb5..1d3265e7 100644
--- a/src/features/database/objects.test.ts
+++ b/src/features/database/objects.test.ts
@@ -22,7 +22,11 @@ const col = (name: string, dataType: string, extra: Partial = {}):
...extra,
});
-const users = [col("id", "INTEGER", { primaryKey: true, nullable: false }), col("email", "TEXT", { nullable: false }), col("name", "TEXT")];
+const users = [
+ col("id", "INTEGER", { primaryKey: true, nullable: false }),
+ col("email", "TEXT", { nullable: false }),
+ col("name", "TEXT"),
+];
describe("database object SQL", () => {
it("quotes only when needed", () => {
@@ -34,12 +38,18 @@ describe("database object SQL", () => {
it("copies references", () => {
expect(reference("sqlite", { kind: "table", schema: "main", name: "users" })).toBe("users");
- expect(reference("postgres", { kind: "column", schema: "public", name: "users", column: "email" })).toBe("public.users.email");
+ expect(reference("postgres", { kind: "column", schema: "public", name: "users", column: "email" })).toBe(
+ "public.users.email",
+ );
});
it("renames per dialect", () => {
- expect(renameSql("sqlite", { kind: "table", schema: "main", name: "users" }, "people")).toBe("alter table users rename to people;");
- expect(renameSql("mysql", { kind: "table", schema: "app", name: "users" }, "people")).toBe("rename table app.users to app.people;");
+ expect(renameSql("sqlite", { kind: "table", schema: "main", name: "users" }, "people")).toBe(
+ "alter table users rename to people;",
+ );
+ expect(renameSql("mysql", { kind: "table", schema: "app", name: "users" }, "people")).toBe(
+ "rename table app.users to app.people;",
+ );
expect(renameSql("postgres", { kind: "column", schema: "public", name: "users", column: "email" }, "mail")).toBe(
"alter table public.users rename column email to mail;",
);
@@ -47,14 +57,24 @@ describe("database object SQL", () => {
it("drops with IF EXISTS / CASCADE", () => {
const t = { kind: "table" as const, schema: "public", name: "users" };
- expect(dropSql("postgres", t, { ifExists: true, cascade: true })).toBe("drop table if exists public.users cascade;");
+ expect(dropSql("postgres", t, { ifExists: true, cascade: true })).toBe(
+ "drop table if exists public.users cascade;",
+ );
expect(dropSql("sqlite", { ...t, schema: "main" }, { ifExists: false, cascade: true })).toBe("drop table users;");
- expect(dropSql("mysql", { kind: "index", schema: "app", name: "ix", table: "users" }, { ifExists: false, cascade: false })).toBe(
- "drop index ix on app.users;",
- );
- expect(dropSql("sqlite", { kind: "column", schema: "main", name: "users", column: "name" }, { ifExists: true, cascade: false })).toBe(
- "alter table users drop column name;",
- );
+ expect(
+ dropSql(
+ "mysql",
+ { kind: "index", schema: "app", name: "ix", table: "users" },
+ { ifExists: false, cascade: false },
+ ),
+ ).toBe("drop index ix on app.users;");
+ expect(
+ dropSql(
+ "sqlite",
+ { kind: "column", schema: "main", name: "users", column: "name" },
+ { ifExists: true, cascade: false },
+ ),
+ ).toBe("alter table users drop column name;");
});
it("truncates (SQLite deletes)", () => {
@@ -66,8 +86,12 @@ describe("database object SQL", () => {
it("writes script templates keyed by the primary key", () => {
expect(scriptSql("sqlite", "selectFirst", "main", "users", users)).toBe("select *\nfrom users\nlimit 10;\n");
- expect(scriptSql("sqlite", "insert", "main", "users", users)).toBe("insert into users (id, email, name)\nvalues (?, ?, ?);\n");
- expect(scriptSql("sqlite", "update", "main", "users", users)).toBe("update users\nset email = ?,\n name = ?\nwhere id = ?;\n");
+ expect(scriptSql("sqlite", "insert", "main", "users", users)).toBe(
+ "insert into users (id, email, name)\nvalues (?, ?, ?);\n",
+ );
+ expect(scriptSql("sqlite", "update", "main", "users", users)).toBe(
+ "update users\nset email = ?,\n name = ?\nwhere id = ?;\n",
+ );
expect(scriptSql("sqlite", "delete", "main", "users", users)).toBe("delete\nfrom users\nwhere id = ?;\n");
});
@@ -82,7 +106,16 @@ describe("database object SQL", () => {
const d = users.map(draftOf);
d[1] = { ...d[1]!, name: "mail" };
d[2] = { ...d[2]!, dropped: true };
- d.push({ key: 9, original: null, name: "age", dataType: "INTEGER", notNull: false, default: "0", primaryKey: false, dropped: false });
+ d.push({
+ key: 9,
+ original: null,
+ name: "age",
+ dataType: "INTEGER",
+ notNull: false,
+ default: "0",
+ primaryKey: false,
+ dropped: false,
+ });
expect(alterTableSql("sqlite", "main", "users", "people", d)).toEqual([
"alter table users rename column email to mail;",
"alter table users drop column name;",
diff --git a/src/features/database/sql/analyze.ts b/src/features/database/sql/analyze.ts
index fd59951f..fb0332e3 100644
--- a/src/features/database/sql/analyze.ts
+++ b/src/features/database/sql/analyze.ts
@@ -536,7 +536,8 @@ export function valuesSlot(text: string, pos: number, driver: DbDriver): ValuesS
continue;
}
if (depth === 0 && isW(t, "VALUES")) afterValues = true;
- else if (depth === 1 && !afterValues && columns && (t.kind === "word" || t.kind === "quoted")) columns.push(identName(t));
+ else if (depth === 1 && !afterValues && columns && (t.kind === "word" || t.kind === "quoted"))
+ columns.push(identName(t));
else if (depth === 1 && inTuple && isP(t, ",")) index++;
}
return inTuple ? { target, columns: columns?.length ? columns : null, index } : null;
diff --git a/src/features/database/sql/format.test.ts b/src/features/database/sql/format.test.ts
index 89a91b75..274a3539 100644
--- a/src/features/database/sql/format.test.ts
+++ b/src/features/database/sql/format.test.ts
@@ -10,7 +10,9 @@ const fmt = (sql: string) => {
describe("formatSql", () => {
it("lays out a query like IDEA", () => {
expect(
- fmt("SELECT u.id,u.name , count( * ) AS n FROM users u LEFT JOIN orders o ON o.user_id=u.id WHERE u.id>1 AND o.total BETWEEN 1 AND 5 OR o.status='x' GROUP BY u.id ORDER BY n DESC LIMIT 10;"),
+ fmt(
+ "SELECT u.id,u.name , count( * ) AS n FROM users u LEFT JOIN orders o ON o.user_id=u.id WHERE u.id>1 AND o.total BETWEEN 1 AND 5 OR o.status='x' GROUP BY u.id ORDER BY n DESC LIMIT 10;",
+ ),
).toBe(
[
"SELECT u.id,",
@@ -29,7 +31,11 @@ describe("formatSql", () => {
});
it("aligns subqueries and CTEs after their parenthesis", () => {
- expect(fmt("with big as (select user_id, total from orders where total > 10) select * from big where exists (select 1 from users u where u.id = big.user_id);")).toBe(
+ expect(
+ fmt(
+ "with big as (select user_id, total from orders where total > 10) select * from big where exists (select 1 from users u where u.id = big.user_id);",
+ ),
+ ).toBe(
[
"with big as (select user_id,",
" total",
diff --git a/src/features/database/sql/format.ts b/src/features/database/sql/format.ts
index e842930a..9d31530c 100644
--- a/src/features/database/sql/format.ts
+++ b/src/features/database/sql/format.ts
@@ -25,11 +25,21 @@ interface Item {
}
/** Edits formatting every statement (or those overlapping `range`). */
-export function formatSql(text: string, driver: DbDriver, range: { from: number; to: number } | null = null): SqlEdit[] {
+export function formatSql(
+ text: string,
+ driver: DbDriver,
+ range: { from: number; to: number } | null = null,
+): SqlEdit[] {
const lexed = lex(text, driver);
const items: Item[] = [
...lexed.tokens,
- ...lexed.comments.map((c) => ({ kind: "comment" as const, text: text.slice(c.from, c.to), upper: "", from: c.from, to: c.to })),
+ ...lexed.comments.map((c) => ({
+ kind: "comment" as const,
+ text: text.slice(c.from, c.to),
+ upper: "",
+ from: c.from,
+ to: c.to,
+ })),
].sort((a, b) => a.from - b.from);
const edits: SqlEdit[] = [];
@@ -60,7 +70,23 @@ export function formatSql(text: string, driver: DbDriver, range: { from: number;
return edits;
}
-const NEWLINE_CLAUSES = new Set(["SELECT", "FROM", "WHERE", "GROUP", "HAVING", "ORDER", "LIMIT", "OFFSET", "WINDOW", "RETURNING", "VALUES", "SET", "UNION", "INTERSECT", "EXCEPT"]);
+const NEWLINE_CLAUSES = new Set([
+ "SELECT",
+ "FROM",
+ "WHERE",
+ "GROUP",
+ "HAVING",
+ "ORDER",
+ "LIMIT",
+ "OFFSET",
+ "WINDOW",
+ "RETURNING",
+ "VALUES",
+ "SET",
+ "UNION",
+ "INTERSECT",
+ "EXCEPT",
+]);
const JOIN_START = new Set(["JOIN", "LEFT", "RIGHT", "FULL", "INNER", "CROSS", "NATURAL", "STRAIGHT_JOIN"]);
const JOIN_MODIFIERS = new Set(["LEFT", "RIGHT", "FULL", "INNER", "CROSS", "NATURAL", "OUTER"]);
/** Where a list item continues: the width of `select `, `from `, `set `, `values `. */
@@ -190,11 +216,17 @@ function spaceBefore(prev: Item | null, it: Item, beforePrev: Item | undefined,
/** `into users (` — a column list, written with a space. */
function isInsertTarget(beforePrev: Item | undefined): boolean {
- return beforePrev?.kind === "word" && (beforePrev.upper === "INTO" || beforePrev.upper === "TABLE" || beforePrev.upper === "EXISTS");
+ return (
+ beforePrev?.kind === "word" &&
+ (beforePrev.upper === "INTO" || beforePrev.upper === "TABLE" || beforePrev.upper === "EXISTS")
+ );
}
function isCreateTable(items: Item[]): boolean {
- const words = items.filter((t) => t.kind === "word").slice(0, 3).map((t) => t.upper);
+ const words = items
+ .filter((t) => t.kind === "word")
+ .slice(0, 3)
+ .map((t) => t.upper);
return words[0] === "CREATE" && words.includes("TABLE");
}
diff --git a/src/features/database/sql/params.ts b/src/features/database/sql/params.ts
index e1dd77d7..c773c8b8 100644
--- a/src/features/database/sql/params.ts
+++ b/src/features/database/sql/params.ts
@@ -38,7 +38,11 @@ export function findParams(text: string, driver: DbDriver, numbered = 0): SqlPar
}
/** `text` with each parameter replaced by its value (empty: `NULL`). */
-export function bindParams(text: string, params: readonly SqlParam[], values: Readonly>): string {
+export function bindParams(
+ text: string,
+ params: readonly SqlParam[],
+ values: Readonly>,
+): string {
let out = "";
let at = 0;
for (const p of params) {
diff --git a/src/features/database/sql/resolve.ts b/src/features/database/sql/resolve.ts
index f6b0afbe..4287f0c2 100644
--- a/src/features/database/sql/resolve.ts
+++ b/src/features/database/sql/resolve.ts
@@ -52,10 +52,16 @@ const NOT_COLUMNS = new Set(
const eq = (a: string | null | undefined, b: string | null | undefined) =>
!!a && !!b && a.toLowerCase() === b.toLowerCase();
-export function findTable(model: DbCompletion | null, schema: string | null, name: string): DbCompletionTable | undefined {
+export function findTable(
+ model: DbCompletion | null,
+ schema: string | null,
+ name: string,
+): DbCompletionTable | undefined {
const tables = model?.tables ?? [];
if (schema) return tables.find((t) => eq(t.schema, schema) && eq(t.name, name));
- return tables.find((t) => eq(t.schema, model?.defaultSchema) && eq(t.name, name)) ?? tables.find((t) => eq(t.name, name));
+ return (
+ tables.find((t) => eq(t.schema, model?.defaultSchema) && eq(t.name, name)) ?? tables.find((t) => eq(t.name, name))
+ );
}
const isName = (t: Token | undefined): t is Token => !!t && (t.kind === "word" || t.kind === "quoted");
@@ -223,7 +229,9 @@ export class SqlResolver {
clause !== "values" &&
clause !== "limit" &&
!this.outputAliases(i).has(name.toLowerCase());
- return judged ? { ...span, symbol: null, problem: `Unable to resolve column '${name}'` } : { ...span, symbol: null };
+ return judged
+ ? { ...span, symbol: null, problem: `Unable to resolve column '${name}'` }
+ : { ...span, symbol: null };
}
/** Every unresolved name of the text. */
@@ -273,7 +281,12 @@ export class SqlResolver {
const t = toks[k]!;
if (!isName(t)) continue;
const prev = toks[k - 1]!;
- if ((prev.kind === "word" && prev.upper === "AS") || isP(prev, ")") || prev.kind === "number" || prev.kind === "string")
+ if (
+ (prev.kind === "word" && prev.upper === "AS") ||
+ isP(prev, ")") ||
+ prev.kind === "number" ||
+ prev.kind === "string"
+ )
out.add(identName(t).toLowerCase());
}
return out;
diff --git a/src/features/database/sql/sql.test.ts b/src/features/database/sql/sql.test.ts
index f893f6aa..907ea5f0 100644
--- a/src/features/database/sql/sql.test.ts
+++ b/src/features/database/sql/sql.test.ts
@@ -245,7 +245,9 @@ describe("completion candidates", () => {
insert: "id, email, name, created_at",
});
const two = complete("select | from users u join orders o on o.user_id = u.id");
- expect(two.find((x) => x.kind === "columns" && x.tail === "(u)")?.insert).toBe("u.id, u.email, u.name, u.created_at");
+ expect(two.find((x) => x.kind === "columns" && x.tail === "(u)")?.insert).toBe(
+ "u.id, u.email, u.name, u.created_at",
+ );
});
it("names unaliased tables by schema in column rows", () => {
@@ -320,7 +322,10 @@ describe("VALUES parameter info", () => {
return valuesSlot(sql.slice(0, pos) + sql.slice(pos + 1), pos, "sqlite");
};
it("finds the column of the value at the caret", () => {
- expect(slot("insert into users (email, name) values ('a', |)")).toMatchObject({ columns: ["email", "name"], index: 1 });
+ expect(slot("insert into users (email, name) values ('a', |)")).toMatchObject({
+ columns: ["email", "name"],
+ index: 1,
+ });
expect(slot("insert into users values (|)")).toMatchObject({ columns: null, index: 0 });
expect(slot("insert into users (email) values ('a'), (|")).toMatchObject({ index: 0 });
expect(slot("insert into users (email|)")).toBeNull();
@@ -359,7 +364,9 @@ describe("resolve", () => {
});
it("leaves names alone when it cannot judge them", () => {
- expect(problems("select foo from unknown_table;")).toEqual(["unknown_table: Unable to resolve table 'unknown_table'"]);
+ expect(problems("select foo from unknown_table;")).toEqual([
+ "unknown_table: Unable to resolve table 'unknown_table'",
+ ]);
expect(problems("select * from (select * from users) x where x.whatever = 1;")).toEqual([]);
expect(problems("select rowid, * from users;")).toEqual([]);
});
@@ -384,7 +391,12 @@ describe("params", () => {
it("finds parameters per dialect and binds values", () => {
const names = (sql: string, driver: "sqlite" | "postgres" | "mysql" = "sqlite", n = 0) =>
findParams(sql, driver, n).map((p) => p.name);
- expect(names("select * from t where a = :id and b = ? and c = ?3 and d = @x and e = '?' -- ?")).toEqual([":id", "?1", "?3", "@x"]);
+ expect(names("select * from t where a = :id and b = ? and c = ?3 and d = @x and e = '?' -- ?")).toEqual([
+ ":id",
+ "?1",
+ "?3",
+ "@x",
+ ]);
expect(names("select * from t where a = ? and b = ?", "sqlite", 2)).toEqual(["?3", "?4"]);
expect(names("select @x := 1, @@version, ? from t", "mysql")).toEqual(["?1"]);
expect(names("select j ? 'k', $1, a::int from t where b = :name", "postgres")).toEqual(["$1", ":name"]);
diff --git a/src/features/database/vendors.ts b/src/features/database/vendors.ts
index 67246378..a1e769d7 100644
--- a/src/features/database/vendors.ts
+++ b/src/features/database/vendors.ts
@@ -19,19 +19,63 @@ export interface DbVendor {
export const VENDORS: readonly DbVendor[] = [
{ id: "aurora-mysql", label: "Amazon Aurora MySQL", driver: "mysql", port: 3306, icon: "db-aurora", scheme: "mysql" },
- { id: "redshift", label: "Amazon Redshift", driver: "postgres", port: 5439, icon: "db-redshift", scheme: "redshift", ssl: "require" },
- { id: "cockroachdb", label: "CockroachDB", driver: "postgres", port: 26257, icon: "db-cockroach", scheme: "postgresql" },
+ {
+ id: "redshift",
+ label: "Amazon Redshift",
+ driver: "postgres",
+ port: 5439,
+ icon: "db-redshift",
+ scheme: "redshift",
+ ssl: "require",
+ },
+ {
+ id: "cockroachdb",
+ label: "CockroachDB",
+ driver: "postgres",
+ port: 26257,
+ icon: "db-cockroach",
+ scheme: "postgresql",
+ },
{ id: "greenplum", label: "Greenplum", driver: "postgres", port: 5432, icon: "db-greenplum", scheme: "postgresql" },
{ id: "mariadb", label: "MariaDB", driver: "mysql", port: 3306, icon: "db-mariadb", scheme: "mariadb" },
{ id: "mysql", label: "MySQL", driver: "mysql", port: 3306, icon: "db-mysql", scheme: "mysql" },
{ id: "neon", label: "Neon", driver: "postgres", port: 5432, icon: "db-neon", scheme: "postgresql", ssl: "require" },
- { id: "oceanbase", label: "OceanBase (MySQL mode)", driver: "mysql", port: 2881, icon: "db-oceanbase", scheme: "oceanbase" },
+ {
+ id: "oceanbase",
+ label: "OceanBase (MySQL mode)",
+ driver: "mysql",
+ port: 2881,
+ icon: "db-oceanbase",
+ scheme: "oceanbase",
+ },
{ id: "postgres", label: "PostgreSQL", driver: "postgres", port: 5432, icon: "db-postgres", scheme: "postgresql" },
- { id: "singlestore", label: "SingleStore", driver: "mysql", port: 3306, icon: "db-singlestore", scheme: "singlestore" },
+ {
+ id: "singlestore",
+ label: "SingleStore",
+ driver: "mysql",
+ port: 3306,
+ icon: "db-singlestore",
+ scheme: "singlestore",
+ },
{ id: "sqlite", label: "SQLite", driver: "sqlite", port: null, icon: "db-sqlite", scheme: "sqlite" },
- { id: "supabase", label: "Supabase", driver: "postgres", port: 5432, icon: "db-supabase", scheme: "postgresql", ssl: "require" },
+ {
+ id: "supabase",
+ label: "Supabase",
+ driver: "postgres",
+ port: 5432,
+ icon: "db-supabase",
+ scheme: "postgresql",
+ ssl: "require",
+ },
{ id: "tidb", label: "TiDB", driver: "mysql", port: 4000, icon: "db-tidb", scheme: "mysql" },
- { id: "timescaledb", label: "TimescaleDB", driver: "postgres", port: 5432, icon: "db-timescale", scheme: "postgresql" },
+ {
+ id: "timescaledb",
+ label: "TimescaleDB",
+ driver: "postgres",
+ port: 5432,
+ icon: "db-timescale",
+ scheme: "postgresql",
+ },
{ id: "yugabytedb", label: "YugabyteDB", driver: "postgres", port: 5433, icon: "db-yugabyte", scheme: "postgresql" },
];
diff --git a/src/features/debugger/__tests__/hotswap.test.ts b/src/features/debugger/__tests__/hotswap.test.ts
index 428af307..a8ffc840 100644
--- a/src/features/debugger/__tests__/hotswap.test.ts
+++ b/src/features/debugger/__tests__/hotswap.test.ts
@@ -5,7 +5,9 @@ const ipc = vi.hoisted(() => {
return {
ok,
javaBuildWorkspace: vi.fn(() => ok("succeed" as string)),
- debugRedefineClasses: vi.fn(() => ok({ changedClasses: ["com.x.A", "com.x.B"], errorMessage: null as string | null })),
+ debugRedefineClasses: vi.fn(() =>
+ ok({ changedClasses: ["com.x.A", "com.x.B"], errorMessage: null as string | null }),
+ ),
};
});
@@ -36,7 +38,16 @@ function session(id: number, hotCodeReplace: boolean): DebugSession {
name: "App",
adapter: hotCodeReplace ? "java-debug" : "debugpy",
parent: null,
- launch: { name: "App", adapter: "java-debug", request: "launch", program: null, args: [], cwd: "/p/app", env: {}, extraJson: null },
+ launch: {
+ name: "App",
+ adapter: "java-debug",
+ request: "launch",
+ program: null,
+ args: [],
+ cwd: "/p/app",
+ env: {},
+ extraJson: null,
+ },
process: null,
status: "running",
stoppedThread: null,
diff --git a/src/features/debugger/__tests__/launch.test.ts b/src/features/debugger/__tests__/launch.test.ts
index c49746c2..f9e098dd 100644
--- a/src/features/debugger/__tests__/launch.test.ts
+++ b/src/features/debugger/__tests__/launch.test.ts
@@ -94,25 +94,44 @@ describe("run config → launch config", () => {
expect(extra(node).runtimeExecutable).toBeUndefined();
// A located Node.js (e.g. 22.6+ for TypeScript) is used instead of PATH's.
- const ts = launchConfigFor(rc({ command: "/opt/node22/bin/node", args: ["a.ts"], debugAdapter: "js-debug" }), "/p")!;
+ const ts = launchConfigFor(
+ rc({ command: "/opt/node22/bin/node", args: ["a.ts"], debugAdapter: "js-debug" }),
+ "/p",
+ )!;
expect(ts.program).toBe("/p/a.ts");
expect(extra(ts).runtimeExecutable).toBe("/opt/node22/bin/node");
});
it("js-debug: TypeScript through a runner or type-stripping flag runs as the runtime", () => {
const flag = launchConfigFor(
- rc({ kind: "node", command: "/bin/node", args: ["--experimental-strip-types", "a.ts"], debugAdapter: "js-debug" }),
+ rc({
+ kind: "node",
+ command: "/bin/node",
+ args: ["--experimental-strip-types", "a.ts"],
+ debugAdapter: "js-debug",
+ }),
"/p",
)!;
expect(flag.program).toBeNull();
- expect(extra(flag)).toMatchObject({ runtimeExecutable: "/bin/node", runtimeArgs: ["--experimental-strip-types", "a.ts"] });
- const tsx = launchConfigFor(rc({ kind: "node", command: "npx", args: ["tsx", "a.ts"], debugAdapter: "js-debug" }), "/p")!;
+ expect(extra(flag)).toMatchObject({
+ runtimeExecutable: "/bin/node",
+ runtimeArgs: ["--experimental-strip-types", "a.ts"],
+ });
+ const tsx = launchConfigFor(
+ rc({ kind: "node", command: "npx", args: ["tsx", "a.ts"], debugAdapter: "js-debug" }),
+ "/p",
+ )!;
expect(extra(tsx)).toMatchObject({ runtimeExecutable: "npx", runtimeArgs: ["tsx", "a.ts"] });
});
it("deno: waits on its inspector at a free port that js-debug attaches to", () => {
const l = launchConfigFor(
- rc({ kind: "deno", command: "/bin/deno", args: ["run", "-A", "--inspect", "main.ts", "x"], debugAdapter: "js-debug" }),
+ rc({
+ kind: "deno",
+ command: "/bin/deno",
+ args: ["run", "-A", "--inspect", "main.ts", "x"],
+ debugAdapter: "js-debug",
+ }),
"/p",
)!;
expect(l.adapter).toBe("js-debug");
@@ -123,7 +142,10 @@ describe("run config → launch config", () => {
runtimeArgs: ["run", "--inspect-wait=127.0.0.1:${freePort}", "-A", "main.ts", "x"],
attachSimplePort: "${freePort}",
});
- const test = launchConfigFor(rc({ kind: "deno", command: "deno", args: ["test"], debugAdapter: "js-debug" }), "/p")!;
+ const test = launchConfigFor(
+ rc({ kind: "deno", command: "deno", args: ["test"], debugAdapter: "js-debug" }),
+ "/p",
+ )!;
expect(extra(test).runtimeArgs).toEqual(["test", "--inspect-wait=127.0.0.1:${freePort}"]);
});
@@ -157,12 +179,20 @@ describe("run config → launch config", () => {
expect(test.program).toBe("/p");
// A gutter test / subtest: the package, and `-run` for the test binary.
const one = launchConfigFor(
- rc({ kind: "go", command: "go", args: ["test", "./calc", "-run", "^TestSub$/^zero_case$"], debugAdapter: "delve" }),
+ rc({
+ kind: "go",
+ command: "go",
+ args: ["test", "./calc", "-run", "^TestSub$/^zero_case$"],
+ debugAdapter: "delve",
+ }),
"/p",
)!;
expect(one.program).toBe("/p/calc");
expect(one.args).toEqual(["-test.run", "^TestSub$/^zero_case$"]);
- const eq = launchConfigFor(rc({ kind: "go", command: "go", args: ["test", "-run=^TestA$", "."], debugAdapter: "delve" }), "/p")!;
+ const eq = launchConfigFor(
+ rc({ kind: "go", command: "go", args: ["test", "-run=^TestA$", "."], debugAdapter: "delve" }),
+ "/p",
+ )!;
expect(eq.program).toBe("/p");
expect(eq.args).toEqual(["-test.run", "^TestA$"]);
});
@@ -199,7 +229,13 @@ describe("run config → launch config", () => {
});
it("Godot: the editor's adapter plays the main scene or the chosen one of the project", () => {
- const project = rc({ kind: "godot", command: "/Apps/Godot", args: ["--path", "."], cwd: "game", debugAdapter: "godot" });
+ const project = rc({
+ kind: "godot",
+ command: "/Apps/Godot",
+ args: ["--path", "."],
+ cwd: "game",
+ debugAdapter: "godot",
+ });
const main = launchConfigFor(project, "/p")!;
expect(main).toMatchObject({ adapter: "godot", request: "launch", cwd: "/p/game", program: null });
expect(extra(main)).toEqual({ project: "/p/game", scene: "main" });
diff --git a/src/features/debugger/__tests__/smartStep.test.ts b/src/features/debugger/__tests__/smartStep.test.ts
index b2ef4408..2cab3f4b 100644
--- a/src/features/debugger/__tests__/smartStep.test.ts
+++ b/src/features/debugger/__tests__/smartStep.test.ts
@@ -19,7 +19,10 @@ vi.mock("@/platform/ipc", async (orig) => {
return {
...real,
call: (p: Promise) => p,
- commands: { ...real.commands, debugStepInTarget: (...a: unknown[]) => (ipc.debugStepInTarget(...a), Promise.resolve(null)) },
+ commands: {
+ ...real.commands,
+ debugStepInTarget: (...a: unknown[]) => (ipc.debugStepInTarget(...a), Promise.resolve(null)),
+ },
};
});
@@ -57,7 +60,12 @@ describe("locating Smart Step Into targets", () => {
const line = " total = add(total, square(i))";
const all = [target(0, "square(i)"), target(1, "add(total, square(i))"), target(2, 'print("total", total)')];
expect(targetsOnLine(line, 11, all).map((t) => t.id)).toEqual([0, 1]);
- expect(targetsOnLine(line, 11, [{ ...target(5, "print()"), line: 11 }, { ...target(6, "add()"), line: 12 }]).map((t) => t.id)).toEqual([5]);
+ expect(
+ targetsOnLine(line, 11, [
+ { ...target(5, "print()"), line: 11 },
+ { ...target(6, "add()"), line: 12 },
+ ]).map((t) => t.id),
+ ).toEqual([5]);
expect(targetsOnLine("pass", 0, all)).toBe(all);
});
});
diff --git a/src/features/debugger/editor/execution.ts b/src/features/debugger/editor/execution.ts
index 64da8058..f466e459 100644
--- a/src/features/debugger/editor/execution.ts
+++ b/src/features/debugger/editor/execution.ts
@@ -3,14 +3,7 @@
* IDEA) and inline values of the frame's locals after line ends.
*/
import { StateEffect, StateField, type EditorState, type Extension } from "@codemirror/state";
-import {
- Decoration,
- EditorView,
- ViewPlugin,
- WidgetType,
- type DecorationSet,
- type ViewUpdate,
-} from "@codemirror/view";
+import { Decoration, EditorView, ViewPlugin, WidgetType, type DecorationSet, type ViewUpdate } from "@codemirror/view";
import type { EditorDocContext } from "@/editor/api";
import { useExecutionPoint } from "../state";
import { onWorkflowSettingsChange, workflowSettings } from "@/platform/workflowSettings";
diff --git a/src/features/debugger/editor/smartStep.ts b/src/features/debugger/editor/smartStep.ts
index bd149f12..ec44dfcb 100644
--- a/src/features/debugger/editor/smartStep.ts
+++ b/src/features/debugger/editor/smartStep.ts
@@ -47,7 +47,10 @@ export function locateStepTargets(lineText: string, line: number, targets: StepI
let to = -1;
if (t.column != null && (t.line == null || t.line === line) && t.column < lineText.length) {
from = t.column;
- to = t.endColumn != null && t.endColumn > from ? Math.min(t.endColumn, lineText.length) : identifierEnd(lineText, from);
+ to =
+ t.endColumn != null && t.endColumn > from
+ ? Math.min(t.endColumn, lineText.length)
+ : identifierEnd(lineText, from);
} else {
const name = calleeName(t.label);
if (!name) return null;
@@ -107,7 +110,9 @@ export function startInlineSmartStep(session: DebugSessionId, thread: number, ta
const ranges = locateStepTargets(doc.line(line + 1).text, line, targets);
if (!ranges || ranges.length < 2) return false;
const first = ranges.findIndex((r) => r.id === targets[0]!.id);
- useOverlays.setState({ smartStep: { session, thread, targets, inline: { path, line, ranges, index: Math.max(0, first) } } });
+ useOverlays.setState({
+ smartStep: { session, thread, targets, inline: { path, line, ranges, index: Math.max(0, first) } },
+ });
editor.view.focus();
return true;
}
diff --git a/src/features/debugger/frames.ts b/src/features/debugger/frames.ts
index c6075ae0..115d602a 100644
--- a/src/features/debugger/frames.ts
+++ b/src/features/debugger/frames.ts
@@ -12,7 +12,8 @@ export function isLibraryFrame(frame: StackFrame, root: string | null): boolean
}
export type FrameItem =
- { kind: "frame"; frame: StackFrame; library: boolean } | { kind: "hidden"; count: number; key: string };
+ | { kind: "frame"; frame: StackFrame; library: boolean }
+ | { kind: "hidden"; count: number; key: string };
/**
* Groups runs of library frames. The selected frame and the first frame are
diff --git a/src/features/debugger/hotswap.ts b/src/features/debugger/hotswap.ts
index baedf0fa..d13abdb0 100644
--- a/src/features/debugger/hotswap.ts
+++ b/src/features/debugger/hotswap.ts
@@ -23,7 +23,10 @@ function plural(n: number, word: string) {
}
/** Message for the reload result (IDEA wording). */
-export function hotSwapMessage(changed: number, errors: string[]): { level: "info" | "error"; title: string; body?: string } {
+export function hotSwapMessage(
+ changed: number,
+ errors: string[],
+): { level: "info" | "error"; title: string; body?: string } {
if (errors.length) return { level: "error", title: "Hot Swap failed", body: errors.join("\n") };
if (changed === 0) return { level: "info", title: "Loaded classes are up to date. Nothing to reload." };
return { level: "info", title: tx`${plural(changed, "class")} reloaded` };
diff --git a/src/features/debugger/ui/DebugToolWindow.module.css b/src/features/debugger/ui/DebugToolWindow.module.css
index 3367f744..e9cf9c70 100644
--- a/src/features/debugger/ui/DebugToolWindow.module.css
+++ b/src/features/debugger/ui/DebugToolWindow.module.css
@@ -237,7 +237,7 @@
white-space: pre-wrap;
word-break: break-word;
-webkit-user-select: text;
-cursor: text;
+ cursor: text;
-webkit-user-select: text;
user-select: text;
outline: none;
diff --git a/src/features/github/PullRequestsView.tsx b/src/features/github/PullRequestsView.tsx
index 766209d4..7469fd55 100644
--- a/src/features/github/PullRequestsView.tsx
+++ b/src/features/github/PullRequestsView.tsx
@@ -136,7 +136,9 @@ function Filters() {
({ value, label: t(`github.prState.${value}`) }))}
- onChange={(state) => setQuery({ state })} aria-label="State" />
+ onChange={(state) => setQuery({ state })}
+ aria-label="State"
+ />
setQuery({ authorMe })} label="Mine" />
({
+const comment = (
+ id: number,
+ path: string,
+ line: number | null,
+ inReplyTo: number | null = null,
+ side = "RIGHT",
+): PrReviewComment => ({
id,
inReplyTo,
path,
@@ -57,7 +63,12 @@ describe("pull request model", () => {
});
it("threads replies under their root, ordered by file and line", () => {
- const threads = reviewThreads([comment(1, "b.ts", 9), comment(2, "a.ts", 5), comment(3, "b.ts", 9, 1), comment(4, "b.ts", 9, 3)]);
+ const threads = reviewThreads([
+ comment(1, "b.ts", 9),
+ comment(2, "a.ts", 5),
+ comment(3, "b.ts", 9, 1),
+ comment(4, "b.ts", 9, 3),
+ ]);
expect(threads.map((t) => [t.root.id, t.replies.map((r) => r.id)])).toEqual([
[2, []],
[1, [3, 4]],
@@ -65,7 +76,12 @@ describe("pull request model", () => {
});
it("turns current comments into diff notes", () => {
- const notes = diffNotes("a.ts", [comment(1, "a.ts", 5), comment(2, "a.ts", null), comment(3, "a.ts", 2, null, "LEFT"), comment(4, "b.ts", 1)]);
+ const notes = diffNotes("a.ts", [
+ comment(1, "a.ts", 5),
+ comment(2, "a.ts", null),
+ comment(3, "a.ts", 2, null, "LEFT"),
+ comment(4, "b.ts", 1),
+ ]);
expect(notes.map((n) => [n.side, n.line])).toEqual([
["right", 4],
["left", 1],
@@ -80,7 +96,12 @@ describe("pull request model", () => {
root: "/repo",
files: [{ path: "src/new.ts", kind: "renamed" as const, oldPath: "src/old.ts" }],
};
- const renamed = fileDiffData(pr, { path: "src/new.ts", additions: 1, deletions: 0, changeType: "RENAMED" }, revs, []);
+ const renamed = fileDiffData(
+ pr,
+ { path: "src/new.ts", additions: 1, deletions: 0, changeType: "RENAMED" },
+ revs,
+ [],
+ );
expect(renamed.path).toBe("/repo/src/new.ts");
expect(renamed.left).toMatchObject({ type: "rev", rev: revs.base, path: "/repo/src/old.ts" });
expect(renamed.right).toMatchObject({ type: "rev", rev: revs.head });
diff --git a/src/features/github/index.ts b/src/features/github/index.ts
index db1caf23..f5ee2feb 100644
--- a/src/features/github/index.ts
+++ b/src/features/github/index.ts
@@ -63,7 +63,14 @@ export function activate(): () => void {
when: WHEN,
run: () => openInBrowser(),
},
- { id: "github.refresh", title: "Refresh Pull Requests", category: "GitHub", icon: "refresh", when: WHEN, run: () => refreshAll() },
+ {
+ id: "github.refresh",
+ title: "Refresh Pull Requests",
+ category: "GitHub",
+ icon: "refresh",
+ when: WHEN,
+ run: () => refreshAll(),
+ },
]),
useProject.subscribe((s, prev) => {
if (s.project?.root !== prev.project?.root) resetGithub();
diff --git a/src/features/github/model.ts b/src/features/github/model.ts
index eeab618f..d5dd4fec 100644
--- a/src/features/github/model.ts
+++ b/src/features/github/model.ts
@@ -1,5 +1,13 @@
/** Pure helpers of the Pull Requests view (labels, check summaries, review threads, diff inputs). */
-import type { ChangedFile, PrCheck, PrDiffRevisions, PrFile, PrReviewComment, PullRequestDetails, PullRequestSummary } from "@/platform/ipc";
+import type {
+ ChangedFile,
+ PrCheck,
+ PrDiffRevisions,
+ PrFile,
+ PrReviewComment,
+ PullRequestDetails,
+ PullRequestSummary,
+} from "@/platform/ipc";
import { t, tx } from "@/platform/i18n";
/** Keyed, not source text: "Open" as a state reads differently from the "Open" action in some locales. */
@@ -107,7 +115,9 @@ export function reviewThreads(comments: readonly PrReviewComment[]): ReviewThrea
}
}
return threads.sort(
- (a, b) => a.root.path.localeCompare(b.root.path) || (a.root.line ?? a.root.originalLine ?? 0) - (b.root.line ?? b.root.originalLine ?? 0),
+ (a, b) =>
+ a.root.path.localeCompare(b.root.path) ||
+ (a.root.line ?? a.root.originalLine ?? 0) - (b.root.line ?? b.root.originalLine ?? 0),
);
}
@@ -124,15 +134,24 @@ export interface DiffNote {
export function diffNotes(path: string, comments: readonly PrReviewComment[]): DiffNote[] {
return comments
.filter((c) => c.path === path && c.line != null)
- .map((c) => ({ side: c.side === "LEFT" ? "left" : "right", line: c.line! - 1, author: c.author, body: c.body, date: c.createdAt }));
+ .map((c) => ({
+ side: c.side === "LEFT" ? "left" : "right",
+ line: c.line! - 1,
+ author: c.author,
+ body: c.body,
+ date: c.createdAt,
+ }));
}
-type Side =
- | { type: "rev"; rev: string; path?: string; label?: string }
- | { type: "text"; text: string; label: string };
+type Side = { type: "rev"; rev: string; path?: string; label?: string } | { type: "text"; text: string; label: string };
/** `DiffData` for one changed file of a pull request (merge base ↔ head), with its review comments. */
-export function fileDiffData(pr: PullRequestDetails, file: PrFile, revs: PrDiffRevisions, comments: readonly PrReviewComment[]) {
+export function fileDiffData(
+ pr: PullRequestDetails,
+ file: PrFile,
+ revs: PrDiffRevisions,
+ comments: readonly PrReviewComment[],
+) {
const root = revs.root;
const changed: ChangedFile | undefined = revs.files.find((f) => f.path === file.path);
const added = file.changeType === "ADDED" || changed?.kind === "added";
@@ -146,7 +165,15 @@ export function fileDiffData(pr: PullRequestDetails, file: PrFile, revs: PrDiffR
path: changed?.oldPath ? `${root}/${changed.oldPath}` : undefined,
label: `${pr.summary.baseRef} (${revs.base.slice(0, 8)})`,
};
- const right: Side = deleted ? { type: "text", text: "", label: "(deleted)" } : { type: "rev", rev: revs.head, label: `#${n} ${pr.summary.headRef}` };
+ const right: Side = deleted
+ ? { type: "text", text: "", label: "(deleted)" }
+ : { type: "rev", rev: revs.head, label: `#${n} ${pr.summary.headRef}` };
const name = file.path.slice(file.path.lastIndexOf("/") + 1);
- return { path: `${root}/${file.path}`, left, right, title: `#${n}: ${name}`, annotations: diffNotes(file.path, comments) };
+ return {
+ path: `${root}/${file.path}`,
+ left,
+ right,
+ title: `#${n}: ${name}`,
+ annotations: diffNotes(file.path, comments),
+ };
}
diff --git a/src/features/github/ops.ts b/src/features/github/ops.ts
index 27805772..0bf22f86 100644
--- a/src/features/github/ops.ts
+++ b/src/features/github/ops.ts
@@ -5,7 +5,16 @@ import { call, commands } from "@/platform/ipc";
import { notifications, toolWindowService } from "@/workbench/contributions";
import { createPullRequestDialog, numberDialog, reviewDialog } from "./dialogs";
import { fileDiffData } from "./model";
-import { diffRevisions, errorMessage, loadDetails, loadList, readyRepo, refreshAll, useGithub, withBusy } from "./store";
+import {
+ diffRevisions,
+ errorMessage,
+ loadDetails,
+ loadList,
+ readyRepo,
+ refreshAll,
+ useGithub,
+ withBusy,
+} from "./store";
export const WINDOW = "pullRequests";
@@ -34,14 +43,20 @@ function notReady(): boolean {
export async function checkoutPullRequest(number: number) {
const done = await withBusy(`Checking out #${number}…`, (r, out) => call(commands.ghPrCheckout(r, number, out)));
if (done !== null)
- notifications().notify({ group: "vcs", level: "info", title: `Checked out pull request #${number}`, key: "github-checkout" });
+ notifications().notify({
+ group: "vcs",
+ level: "info",
+ title: `Checked out pull request #${number}`,
+ key: "github-checkout",
+ });
}
/** Git › GitHub › Checkout Pull Request…: the selected one, else asks for a number. */
export async function checkoutAction(arg: unknown) {
if (useGithub.getState().availability?.kind !== "ready") await refreshAll();
if (notReady()) return;
- const n = typeof arg === "number" ? arg : (useGithub.getState().selected ?? (await numberDialog("Checkout Pull Request")));
+ const n =
+ typeof arg === "number" ? arg : (useGithub.getState().selected ?? (await numberDialog("Checkout Pull Request")));
if (n) await checkoutPullRequest(n);
}
@@ -62,10 +77,21 @@ export async function submitReview(number: number, initial: ReviewEvent) {
if (!r) return;
try {
await call(commands.ghPrReview(number, r.event, r.body));
- notifications().notify({ group: "vcs", level: "info", title: `Review submitted on #${number}`, key: "github-review" });
+ notifications().notify({
+ group: "vcs",
+ level: "info",
+ title: `Review submitted on #${number}`,
+ key: "github-review",
+ });
await loadDetails(number);
} catch (e) {
- notifications().notify({ group: "vcs", level: "error", title: "Could not submit the review", body: errorMessage(e), key: "github-review" });
+ notifications().notify({
+ group: "vcs",
+ level: "error",
+ title: "Could not submit the review",
+ body: errorMessage(e),
+ key: "github-review",
+ });
}
}
@@ -75,7 +101,13 @@ export async function addComment(number: number, body: string): Promise
await loadDetails(number);
return true;
} catch (e) {
- notifications().notify({ group: "vcs", level: "error", title: "Could not add the comment", body: errorMessage(e), key: "github-comment" });
+ notifications().notify({
+ group: "vcs",
+ level: "error",
+ title: "Could not add the comment",
+ body: errorMessage(e),
+ key: "github-comment",
+ });
return false;
}
}
@@ -92,12 +124,21 @@ export async function createPullRequest() {
const info = await call(commands.vcsBranchInfo(null));
head = info.head;
needsPush = !info.upstream || info.ahead > 0;
- if (head) title = head.replace(/^[^/]+\//, "").replace(/[-_]+/g, " ").replace(/^\w/, (c) => c.toUpperCase());
+ if (head)
+ title = head
+ .replace(/^[^/]+\//, "")
+ .replace(/[-_]+/g, " ")
+ .replace(/^\w/, (c) => c.toUpperCase());
} catch {
// Not a git repository: gh will explain.
}
if (!head) {
- notifications().notify({ group: "vcs", level: "warning", title: "Check out a branch to create a pull request from", key: "github-create" });
+ notifications().notify({
+ group: "vcs",
+ level: "warning",
+ title: "Check out a branch to create a pull request from",
+ key: "github-create",
+ });
return;
}
if (head === repo?.defaultBranch) {
diff --git a/src/features/github/store.ts b/src/features/github/store.ts
index 8b25b3de..02da43d3 100644
--- a/src/features/github/store.ts
+++ b/src/features/github/store.ts
@@ -113,7 +113,12 @@ let detailSeq = 0;
export async function loadDetails(number: number): Promise {
const seq = ++detailSeq;
const same = useGithub.getState().details?.summary.number === number;
- useGithub.setState({ selected: number, detailsLoading: true, detailsError: null, ...(same ? {} : { details: null, reviewComments: [] }) });
+ useGithub.setState({
+ selected: number,
+ detailsLoading: true,
+ detailsError: null,
+ ...(same ? {} : { details: null, reviewComments: [] }),
+ });
try {
const [details, comments] = await Promise.all([
call(commands.ghPrView(number)),
@@ -136,9 +141,17 @@ export function select(number: number | null) {
}
/** Runs a cancellable, streaming gh operation shown in the view's banner. */
-export async function withBusy(title: string, run: (request: RequestId, out: Channel) => Promise): Promise {
+export async function withBusy(
+ title: string,
+ run: (request: RequestId, out: Channel) => Promise,
+): Promise {
if (useGithub.getState().busy) {
- notifications().notify({ group: "vcs", level: "info", title: "Another GitHub operation is still running", key: "github-busy" });
+ notifications().notify({
+ group: "vcs",
+ level: "info",
+ title: "Another GitHub operation is still running",
+ key: "github-busy",
+ });
return null;
}
const request = nextRequestId();
@@ -149,7 +162,13 @@ export async function withBusy(title: string, run: (request: RequestId, out:
return await run(request, out);
} catch (e) {
if (!isCancelled(e))
- notifications().notify({ group: "vcs", level: "error", title: `${title.replace(/…$/, "")} failed`, body: errorMessage(e), key: "github-op" });
+ notifications().notify({
+ group: "vcs",
+ level: "error",
+ title: `${title.replace(/…$/, "")} failed`,
+ body: errorMessage(e),
+ key: "github-op",
+ });
return null;
} finally {
useGithub.setState({ busy: null });
@@ -166,7 +185,14 @@ const diffCache = new Map();
/** Another project: start over. */
export function resetGithub() {
diffCache.clear();
- useGithub.setState({ availability: null, list: [], selected: null, details: null, reviewComments: [], query: DEFAULT_QUERY });
+ useGithub.setState({
+ availability: null,
+ list: [],
+ selected: null,
+ details: null,
+ reviewComments: [],
+ query: DEFAULT_QUERY,
+ });
}
/** Fetches what the local repository lacks to diff `pr`, once per head commit. */
@@ -176,7 +202,17 @@ export async function diffRevisions(pr: PullRequestDetails): Promise
- call(commands.ghPrPrepareDiff(r, pr.summary.number, pr.headOid, pr.baseOid, pr.summary.baseRef, repo?.nameWithOwner ?? "", out)),
+ call(
+ commands.ghPrPrepareDiff(
+ r,
+ pr.summary.number,
+ pr.headOid,
+ pr.baseOid,
+ pr.summary.baseRef,
+ repo?.nameWithOwner ?? "",
+ out,
+ ),
+ ),
);
if (revs) diffCache.set(key, revs);
return revs;
diff --git a/src/features/http-client/format.ts b/src/features/http-client/format.ts
index 13a3c43f..2dc94ab7 100644
--- a/src/features/http-client/format.ts
+++ b/src/features/http-client/format.ts
@@ -30,7 +30,20 @@ export function prettyJson(body: string): string {
}
const VOID_HTML = new Set([
- "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr",
+ "area",
+ "base",
+ "br",
+ "col",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr",
]);
/**
diff --git a/src/features/http-client/httpClient.test.ts b/src/features/http-client/httpClient.test.ts
index ee3877cd..5bb4ba7c 100644
--- a/src/features/http-client/httpClient.test.ts
+++ b/src/features/http-client/httpClient.test.ts
@@ -24,12 +24,23 @@ describe("response bodies", () => {
it("re-indents XML, keeping text-only elements on one line", () => {
expect(prettyMarkup('1 x ')).toBe(
- ['', "", " 1 ", " ", " ", " x ", " ", " ", " "].join("\n"),
+ [
+ '',
+ "",
+ " 1 ",
+ " ",
+ " ",
+ " x ",
+ " ",
+ " ",
+ " ",
+ ].join("\n"),
);
});
it("re-indents HTML with void elements and raw script contents", () => {
- const html = "Hi
";
+ const html =
+ "Hi
";
expect(prettyMarkup(html, true)).toBe(
[
"",
@@ -69,14 +80,14 @@ describe("http highlighting", () => {
}
it("tags separators, methods, URLs, headers, variables and comments", () => {
- const t = tokens("### Users\n# @name list\nGET https://{{host}}/users\nAccept: json\n\n{\"a\": 1}\n");
+ const t = tokens('### Users\n# @name list\nGET https://{{host}}/users\nAccept: json\n\n{"a": 1}\n');
const has = (name: string, text: string) => t.some(([n, s]) => n.includes(name) && s === text);
expect(has("heading", "### Users")).toBe(true);
expect(has("keyword", "GET")).toBe(true);
expect(has("link", "https://")).toBe(true);
expect(has("variableName", "{{host}}")).toBe(true);
expect(has("propertyName", "Accept")).toBe(true);
- expect(has("string", ' json')).toBe(true);
+ expect(has("string", " json")).toBe(true);
expect(has("string", '"a"')).toBe(true);
expect(has("number", "1")).toBe(true);
expect(has("meta", "# @name")).toBe(true);
diff --git a/src/features/http-client/store.ts b/src/features/http-client/store.ts
index 16afd399..0ff936b9 100644
--- a/src/features/http-client/store.ts
+++ b/src/features/http-client/store.ts
@@ -108,7 +108,11 @@ export async function selectEntry(id: string) {
const ex = await call(commands.httpHistoryGet(id));
useHttpClient.setState((s) => ({ exchanges: { ...s.exchanges, [id]: ex } }));
} catch (e) {
- notifications().notify({ level: "error", title: "Cannot open the response", body: e instanceof Error ? e.message : String(e) });
+ notifications().notify({
+ level: "error",
+ title: "Cannot open the response",
+ body: e instanceof Error ? e.message : String(e),
+ });
}
}
diff --git a/src/features/libraries/index.ts b/src/features/libraries/index.ts
index 5923f7af..cee6b362 100644
--- a/src/features/libraries/index.ts
+++ b/src/features/libraries/index.ts
@@ -56,7 +56,8 @@ const MANIFESTS = new Set([
export function touchesManifest(paths: readonly string[]): boolean {
return paths.some(
(p) =>
- !/[\\/]node_modules[\\/]/.test(p) && MANIFESTS.has(p.slice(Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")) + 1)),
+ !/[\\/]node_modules[\\/]/.test(p) &&
+ MANIFESTS.has(p.slice(Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")) + 1)),
);
}
diff --git a/src/features/libraries/libraries.test.ts b/src/features/libraries/libraries.test.ts
index d7596c44..7d64dc1b 100644
--- a/src/features/libraries/libraries.test.ts
+++ b/src/features/libraries/libraries.test.ts
@@ -28,11 +28,13 @@ describe("External Libraries", () => {
it("gives each ecosystem's groups their icon", () => {
const root = librariesRoot(
- ["jdk", "nodeCore", "nodeModules", "rust", "cargo", "go", "goModules", "maven", "gradle", "other"].map((kind) => ({
- name: kind,
- kind,
- roots: [{ name: "r", path: `/libs/${kind}` }],
- })),
+ ["jdk", "nodeCore", "nodeModules", "rust", "cargo", "go", "goModules", "maven", "gradle", "other"].map(
+ (kind) => ({
+ name: kind,
+ kind,
+ roots: [{ name: "r", path: `/libs/${kind}` }],
+ }),
+ ),
)!;
expect(root.children?.map((g) => g.icon)).toEqual([
"file-java",
diff --git a/src/features/macros/MacrosDialog.tsx b/src/features/macros/MacrosDialog.tsx
index 19512bc1..17e2f9a4 100644
--- a/src/features/macros/MacrosDialog.tsx
+++ b/src/features/macros/MacrosDialog.tsx
@@ -60,7 +60,14 @@ function EditMacrosDialog({ onClose }: { onClose: () => void }) {
};
const remove = async () => {
if (!macro) return;
- if (await confirm({ title: "Delete Macro", message: tx`Delete the macro '${macro.name}'?`, ok: "Delete", icon: "warning" }))
+ if (
+ await confirm({
+ title: "Delete Macro",
+ message: tx`Delete the macro '${macro.name}'?`,
+ ok: "Delete",
+ icon: "warning",
+ })
+ )
deleteMacro(macro.name);
};
diff --git a/src/features/macros/editorKeys.ts b/src/features/macros/editorKeys.ts
index 7e1fb2f6..3f140fef 100644
--- a/src/features/macros/editorKeys.ts
+++ b/src/features/macros/editorKeys.ts
@@ -81,7 +81,9 @@ export const EDITOR_KEY_COMMANDS: Record = {
};
/** The recorded name of a key event, or null when it is not an editor key a macro records. */
-export function editorKeyName(e: Pick): string | null {
+export function editorKeyName(
+ e: Pick,
+): string | null {
const name = [e.metaKey && "Meta", e.ctrlKey && "Ctrl", e.altKey && "Alt", e.shiftKey && "Shift", e.key]
.filter(Boolean)
.join("+");
diff --git a/src/features/macros/index.ts b/src/features/macros/index.ts
index 3c483796..e1ff97ba 100644
--- a/src/features/macros/index.ts
+++ b/src/features/macros/index.ts
@@ -98,19 +98,17 @@ function registerSavedMacroActions(): () => void {
names = key;
dispose();
dispose = registerActions(
- saved.map(
- (m): Action => ({
- id: playActionId(m.name),
- title: m.name,
- category: "Macros",
- keywords: ["macro", "play"],
- when: "!macroRecording",
- run: () => {
- const macro = useMacros.getState().saved.find((x) => x.name === m.name);
- if (macro) return playMacro(macro.steps, macro.name);
- },
- }),
- ),
+ saved.map((m): Action => ({
+ id: playActionId(m.name),
+ title: m.name,
+ category: "Macros",
+ keywords: ["macro", "play"],
+ when: "!macroRecording",
+ run: () => {
+ const macro = useMacros.getState().saved.find((x) => x.name === m.name);
+ if (macro) return playMacro(macro.steps, macro.name);
+ },
+ })),
);
};
sync();
@@ -179,7 +177,10 @@ export function activate(): () => void {
keywords: ["macro", "replay"],
when: "!macroRecording && macrosSaved",
run: async () => {
- const name = await chooseItem({ title: "Play Saved Macro", items: useMacros.getState().saved.map((m) => m.name) });
+ const name = await chooseItem({
+ title: "Play Saved Macro",
+ items: useMacros.getState().saved.map((m) => m.name),
+ });
const macro = useMacros.getState().saved.find((m) => m.name === name);
if (macro) await playMacro(macro.steps, macro.name);
},
diff --git a/src/features/macros/model.test.ts b/src/features/macros/model.test.ts
index 556e839d..37fdf646 100644
--- a/src/features/macros/model.test.ts
+++ b/src/features/macros/model.test.ts
@@ -56,7 +56,10 @@ describe("macros", () => {
it("drops malformed stored steps", () => {
localStorage.setItem(
STORAGE_KEY,
- JSON.stringify({ last: [{ kind: "text", text: "a" }, { kind: "action" }, { kind: "action", id: "x", arg: {} }], saved: [{ name: "" }] }),
+ JSON.stringify({
+ last: [{ kind: "text", text: "a" }, { kind: "action" }, { kind: "action", id: "x", arg: {} }],
+ saved: [{ name: "" }],
+ }),
);
_reloadMacros();
expect(useMacros.getState().last).toEqual([{ kind: "text", text: "a" }]);
diff --git a/src/features/macros/model.ts b/src/features/macros/model.ts
index 398ffbe8..061ab2dc 100644
--- a/src/features/macros/model.ts
+++ b/src/features/macros/model.ts
@@ -32,7 +32,8 @@ export const STORAGE_KEY = "openidea.macros.v1";
function isStep(v: unknown): v is MacroStep {
if (!v || typeof v !== "object") return false;
const s = v as Record;
- if (s.kind === "action") return typeof s.id === "string" && ["undefined", "string", "number", "boolean"].includes(typeof s.arg);
+ if (s.kind === "action")
+ return typeof s.id === "string" && ["undefined", "string", "number", "boolean"].includes(typeof s.arg);
if (s.kind === "text") return typeof s.text === "string";
return s.kind === "key" && typeof s.key === "string";
}
@@ -84,7 +85,8 @@ export function recordStep(step: MacroStep) {
/** An action's argument when it can be stored (plain values only). */
export function storableArg(arg: unknown): string | number | boolean | undefined | null {
if (arg === undefined) return undefined;
- if (typeof arg === "string" || typeof arg === "boolean" || (typeof arg === "number" && Number.isFinite(arg))) return arg;
+ if (typeof arg === "string" || typeof arg === "boolean" || (typeof arg === "number" && Number.isFinite(arg)))
+ return arg;
return null;
}
@@ -147,7 +149,11 @@ export async function playSteps(
try {
for (const step of macroSteps) {
const ok =
- step.kind === "action" ? await run.action(step.id, step.arg) : step.kind === "text" ? run.text(step.text) : run.key(step.key);
+ step.kind === "action"
+ ? await run.action(step.id, step.arg)
+ : step.kind === "text"
+ ? run.text(step.text)
+ : run.key(step.key);
if (!ok) return false;
// Let the UI settle (popups open, editors mount) before the next step.
await new Promise((r) => setTimeout(r, 0));
diff --git a/src/features/plugins/__tests__/plugins.test.tsx b/src/features/plugins/__tests__/plugins.test.tsx
index c6412dfb..7e637357 100644
--- a/src/features/plugins/__tests__/plugins.test.tsx
+++ b/src/features/plugins/__tests__/plugins.test.tsx
@@ -184,7 +184,10 @@ describe("bridge events", () => {
useThemeOverride.setState({ theme: "dark-hc" });
applyThemeAttributes("dark-hc", root);
expect([root.dataset.theme, root.dataset.contrast]).toEqual(["dark", "high"]);
- applyPluginTheme({ pluginId: "p", theme: { id: "l", name: "L", kind: "light", tokens: { "--accent": "#123456" } } });
+ applyPluginTheme({
+ pluginId: "p",
+ theme: { id: "l", name: "L", kind: "light", tokens: { "--accent": "#123456" } },
+ });
expect([root.dataset.theme, root.dataset.contrast]).toEqual(["light", undefined]);
expect(root.dataset.pluginThemeKind).toBe("light");
// The appearance runtime re-applying the built-in theme keeps the plugin's base.
diff --git a/src/features/profiler/executor.ts b/src/features/profiler/executor.ts
index 6d2e630d..ce5a3a59 100644
--- a/src/features/profiler/executor.ts
+++ b/src/features/profiler/executor.ts
@@ -1,7 +1,12 @@
/** The "Profile" executor: runs a configuration under a sampling profiler, then opens the snapshot. */
import { executeAction } from "@/platform/actions";
import { call, commands, isTauri, type ProfilerInstall, type RunConfig } from "@/platform/ipc";
-import { notifications, toolWindowService, type ExecutorLaunch, type RunExecutorContribution } from "@/workbench/contributions";
+import {
+ notifications,
+ toolWindowService,
+ type ExecutorLaunch,
+ type RunExecutorContribution,
+} from "@/workbench/contributions";
import { profilerSupports } from "./model";
import { addSnapshot } from "./store";
import { tx } from "@/platform/i18n";
@@ -49,7 +54,13 @@ async function prepare(config: RunConfig): Promise {
}
const plan = await call(commands.profilerPrepare(config));
if (plan.status === "unsupported") {
- notifications().notify({ group: "run", level: "info", key: "profiler-unsupported", title: `Cannot profile '${config.name}'`, body: plan.reason });
+ notifications().notify({
+ group: "run",
+ level: "info",
+ key: "profiler-unsupported",
+ title: `Cannot profile '${config.name}'`,
+ body: plan.reason,
+ });
return null;
}
if (plan.status === "missingTool") {
@@ -71,7 +82,13 @@ async function prepare(config: RunConfig): Promise {
/* no workbench (tests) */
}
} catch (e) {
- notifications().notify({ group: "run", level: "error", key: "profiler-done", title: `No profile for '${config.name}'`, body: errorText(e) });
+ notifications().notify({
+ group: "run",
+ level: "error",
+ key: "profiler-done",
+ title: `No profile for '${config.name}'`,
+ body: errorText(e),
+ });
}
})();
},
diff --git a/src/features/profiler/index.ts b/src/features/profiler/index.ts
index 3e8dafdd..5f8f7069 100644
--- a/src/features/profiler/index.ts
+++ b/src/features/profiler/index.ts
@@ -41,7 +41,8 @@ export function activate(): () => void {
icon: "profile",
when: "projectOpen",
keywords: ["profiler", "flame graph"],
- run: ({ arg }) => executeAction("run.withExecutor", { executor: "profiler", context: arg ?? true }).then(() => undefined),
+ run: ({ arg }) =>
+ executeAction("run.withExecutor", { executor: "profiler", context: arg ?? true }).then(() => undefined),
},
]),
useProject.subscribe((s, prev) => {
diff --git a/src/features/profiler/model.ts b/src/features/profiler/model.ts
index f3417dc8..6fe270c9 100644
--- a/src/features/profiler/model.ts
+++ b/src/features/profiler/model.ts
@@ -25,7 +25,11 @@ export function profilerSupports(config: Pick {
diff --git a/src/features/profiler/store.ts b/src/features/profiler/store.ts
index d6a293d6..f7139b2d 100644
--- a/src/features/profiler/store.ts
+++ b/src/features/profiler/store.ts
@@ -28,7 +28,10 @@ let seq = 0;
export function addSnapshot(name: string, profile: Profile): Snapshot {
const snapshot: Snapshot = { id: `profile${++seq}`, name, createdAt: Date.now(), profile };
- useProfiler.setState((s) => ({ snapshots: [snapshot, ...s.snapshots].slice(0, MAX_SNAPSHOTS), activeId: snapshot.id }));
+ useProfiler.setState((s) => ({
+ snapshots: [snapshot, ...s.snapshots].slice(0, MAX_SNAPSHOTS),
+ activeId: snapshot.id,
+ }));
return snapshot;
}
diff --git a/src/features/project-tree/ProjectView.test.tsx b/src/features/project-tree/ProjectView.test.tsx
index 476a83a2..897995bc 100644
--- a/src/features/project-tree/ProjectView.test.tsx
+++ b/src/features/project-tree/ProjectView.test.tsx
@@ -57,7 +57,10 @@ function setup() {
within(tree)
.queryAllByRole("treeitem")
.map((r) => r.textContent);
- const row = (label: string) => within(tree).getAllByRole("treeitem").find((r) => r.textContent?.startsWith(label))!;
+ const row = (label: string) =>
+ within(tree)
+ .getAllByRole("treeitem")
+ .find((r) => r.textContent?.startsWith(label))!;
return { tree, rows, row };
}
@@ -225,7 +228,9 @@ describe("ProjectView", () => {
act(() => useProjectTree.setState({ expanded: new Set([ROOT, "/w/acme/src"]) }));
await waitFor(() => expect(rows()).toContain("App.kt"));
await act(() => fs.createFile("/w/acme/src/New.kt"));
- await waitFor(() => expect(rows()).toEqual(["acme/w/acme", "build", "src", "App.kt", "New.kt", "Util.kt", "README.md"]));
+ await waitFor(() =>
+ expect(rows()).toEqual(["acme/w/acme", "build", "src", "App.kt", "New.kt", "Util.kt", "README.md"]),
+ );
});
it("renders large folders virtualized", async () => {
diff --git a/src/features/project-tree/ProjectView.tsx b/src/features/project-tree/ProjectView.tsx
index 69dd5c01..a0bf123a 100644
--- a/src/features/project-tree/ProjectView.tsx
+++ b/src/features/project-tree/ProjectView.tsx
@@ -96,7 +96,9 @@ export const treeHandle: { current: TreeHandle | null } = { current: null };
export function showNewPopup(): boolean {
const lead = treeHandle.current?.leadKey();
const row = lead ? document.querySelector(`[data-project-key="${CSS.escape(lead)}"]`) : null;
- const rect = (row ?? document.querySelector('[role="tree"][aria-label="Project"]'))?.getBoundingClientRect();
+ const rect = (
+ row ?? document.querySelector('[role="tree"][aria-label="Project"]')
+ )?.getBoundingClientRect();
if (!rect) return false;
openNewPopup({ x: rect.left + 24, y: rect.bottom }, newMenuItems(targetFolder()));
return true;
@@ -261,7 +263,11 @@ const Row = memo(function Row({
onDoubleClick={(e) => onDoubleClick(node, e)}
onPointerDown={(e) => onPointerDown(node, e)}
>
-
+
{isEditing && editing ? (
("[data-project-path]") ?? el.closest("[data-project-path]");
+ const content =
+ (row ?? el).querySelector("[data-project-path]") ?? el.closest("[data-project-path]");
if (!content) return null;
const path = content.dataset.projectPath!;
if (content.dataset.dir) return { key: content.dataset.projectKey!, path };
@@ -410,7 +417,9 @@ export function ProjectView() {
if (target.every((n) => n.group)) return;
// Contributed groups may read the file first (bounded wait).
const at = { x: e.clientX, y: e.clientY };
- void prepareMenuGroups("projectView", menuGroupContext(target)).then(() => showContextMenu(at, contextMenuItems(target)));
+ void prepareMenuGroups("projectView", menuGroupContext(target)).then(() =>
+ showContextMenu(at, contextMenuItems(target)),
+ );
}, []);
const focusTree = useCallback(() => requestAnimationFrame(() => handle.current?.focus()), []);
@@ -630,7 +639,9 @@ export function ProjectView() {
onBlur={() => setContext("projectViewFocus", false)}
>
{!model.roots.length && (
- {txTemplate(`Nothing to show in ${VIEW_MODES.find(([id]) => id === mode)?.[1] ?? "this view"}`)}
+
+ {txTemplate(`Nothing to show in ${VIEW_MODES.find(([id]) => id === mode)?.[1] ?? "this view"}`)}
+
)}
aria-label="Project"
diff --git a/src/features/project-tree/RefactoringPreview.tsx b/src/features/project-tree/RefactoringPreview.tsx
index 65ef5afe..00886ca9 100644
--- a/src/features/project-tree/RefactoringPreview.tsx
+++ b/src/features/project-tree/RefactoringPreview.tsx
@@ -55,7 +55,17 @@ async function readLines(path: string): Promise {
}
}
-function FileGroup({ file, root, included, onToggle }: { file: EditFile; root: string; included: boolean; onToggle: (on: boolean) => void }) {
+function FileGroup({
+ file,
+ root,
+ included,
+ onToggle,
+}: {
+ file: EditFile;
+ root: string;
+ included: boolean;
+ onToggle: (on: boolean) => void;
+}) {
const [lines, setLines] = useState(null);
useEffect(() => {
let live = true;
@@ -64,7 +74,9 @@ function FileGroup({ file, root, included, onToggle }: { file: EditFile; root: s
live = false;
};
}, [file.path]);
- const sorted = [...file.edits].sort((a, b) => a.range.start.line - b.range.start.line || a.range.start.col - b.range.start.col);
+ const sorted = [...file.edits].sort(
+ (a, b) => a.range.start.line - b.range.start.line || a.range.start.col - b.range.start.col,
+ );
return (
diff --git a/src/features/project-tree/actions.ts b/src/features/project-tree/actions.ts
index 621d6805..b3bf0d21 100644
--- a/src/features/project-tree/actions.ts
+++ b/src/features/project-tree/actions.ts
@@ -417,7 +417,11 @@ export function registerProjectActions(): () => void {
when: FOCUS,
run: ({ arg }) => {
const files = nodesFrom(arg).filter((n) => !n.dir);
- if (files.length === 2) delegate("diff.compareFiles", files.map((f) => f.path));
+ if (files.length === 2)
+ delegate(
+ "diff.compareFiles",
+ files.map((f) => f.path),
+ );
else if (files[0]) delegate("diff.compareWith", files[0].path);
},
},
diff --git a/src/features/project-tree/contextMenu.test.ts b/src/features/project-tree/contextMenu.test.ts
index c1613ac0..082af17f 100644
--- a/src/features/project-tree/contextMenu.test.ts
+++ b/src/features/project-tree/contextMenu.test.ts
@@ -52,7 +52,10 @@ let disposers: Array<() => void> = [];
beforeEach(() => {
useProjectTree.setState({ root: ROOT });
- useProject.setState({ project: { name: "p", root: ROOT, isGit: true, settings: {} } as never, settings: {} as never });
+ useProject.setState({
+ project: { name: "p", root: ROOT, isGit: true, settings: {} } as never,
+ settings: {} as never,
+ });
disposers = [
registerActions(OTHER_ACTIONS.map((id) => ({ id, title: id, run: () => undefined }))),
registerMenuGroup({
@@ -121,7 +124,8 @@ describe("project view context menu", () => {
expect(l).not.toContain("Refactor");
expect(l).not.toContain("Mark Directory as");
const resolved = resolveMenuItems(items);
- for (const label of ["Cut", "Copy", "Delete…"]) expect(resolved.find((r) => r.label === label)?.disabled).toBe(true);
+ for (const label of ["Cut", "Copy", "Delete…"])
+ expect(resolved.find((r) => r.label === label)?.disabled).toBe(true);
});
it("drops project-only groups for scratch files", () => {
@@ -141,7 +145,10 @@ describe("project view context menu", () => {
it("never shows an empty submenu", () => {
disposers.forEach((d) => d());
disposers = [];
- useProject.setState({ project: { name: "p", root: ROOT, isGit: true, settings: {} } as never, settings: {} as never });
+ useProject.setState({
+ project: { name: "p", root: ROOT, isGit: true, settings: {} } as never,
+ settings: {} as never,
+ });
const l = labels(contextMenuItems([node(`${ROOT}/a.py`)]));
expect(l).not.toContain("Git");
expect(l).not.toContain("Local History");
diff --git a/src/features/project-tree/contextMenu.ts b/src/features/project-tree/contextMenu.ts
index 80ae286a..1e7df784 100644
--- a/src/features/project-tree/contextMenu.ts
+++ b/src/features/project-tree/contextMenu.ts
@@ -111,7 +111,12 @@ function act(action: string, arg?: unknown, label?: string): MenuItem | null {
}
/** Runs another feature's action directly (the project view decides availability), with its shortcut. */
-function delegated(id: string, arg: unknown, label: string, extra: { icon?: string; shortcut?: string } = {}): MenuItem | null {
+function delegated(
+ id: string,
+ arg: unknown,
+ label: string,
+ extra: { icon?: string; shortcut?: string } = {},
+): MenuItem | null {
if (!getAction(id)) return null;
return { id, label, ...extra, shortcut: extra.shortcut ?? shortcutLabel(id), run: () => void delegate(id, arg) };
}
@@ -328,9 +333,21 @@ export function contextMenuItems(nodes: ProjectNode[]): MenuItem[] {
// IDEA: Compare With… (one file) / Compare Files (two files).
if (files.length === 2 && nodes.length === 2)
- push(delegated("diff.compareFiles", files.map((f) => f.path), "Compare Files", { icon: "diff" }));
+ push(
+ delegated(
+ "diff.compareFiles",
+ files.map((f) => f.path),
+ "Compare Files",
+ { icon: "diff" },
+ ),
+ );
else if (singleFile)
- push(delegated("diff.compareWith", singleFile.path, "Compare With…", { icon: "diff", shortcut: keyLabel("project.compareWith") }));
+ push(
+ delegated("diff.compareWith", singleFile.path, "Compare With…", {
+ icon: "diff",
+ shortcut: keyLabel("project.compareWith"),
+ }),
+ );
if (inProject && single?.dir && !single.root && !single.placeholder) {
const current = directoryRole(single.path);
@@ -375,15 +392,27 @@ function libraryMenuItems(nodes: ProjectNode[]): MenuItem[] {
id: "copyPath",
label: "Copy Path/Reference…",
submenu: [
- { id: "abs", label: "Absolute Path", shortcut: keyLabel("project.copyAbsolutePath"), run: () => void copyPaths("absolute", nodes) },
+ {
+ id: "abs",
+ label: "Absolute Path",
+ shortcut: keyLabel("project.copyAbsolutePath"),
+ run: () => void copyPaths("absolute", nodes),
+ },
{ id: "name", label: "File Name", run: () => void copyPaths("name", nodes) },
],
},
{ kind: "separator" },
!!folder && delegated("find.inPath", { directory: folder }, "Find in Files…", { icon: "search" }),
!!folder && { kind: "separator" },
- submenu("openIn", "Open In", [{ id: "finder", label: revealLabel(), icon: "external-link", run: () => void revealInFinder(nodes) }]),
- single && !single.dir && delegated("diff.compareWith", single.path, "Compare With…", { icon: "diff", shortcut: keyLabel("project.compareWith") }),
+ submenu("openIn", "Open In", [
+ { id: "finder", label: revealLabel(), icon: "external-link", run: () => void revealInFinder(nodes) },
+ ]),
+ single &&
+ !single.dir &&
+ delegated("diff.compareWith", single.path, "Compare With…", {
+ icon: "diff",
+ shortcut: keyLabel("project.compareWith"),
+ }),
];
return items.filter((i): i is MenuItem => !!i);
}
diff --git a/src/features/project-tree/extraRoots.test.ts b/src/features/project-tree/extraRoots.test.ts
index 402461ed..67e3bc6d 100644
--- a/src/features/project-tree/extraRoots.test.ts
+++ b/src/features/project-tree/extraRoots.test.ts
@@ -36,7 +36,12 @@ describe("withExtraRoots", () => {
["/data/scratches", [entry("/data/scratches/scratch.java")]],
]);
const base = buildModel({ path: "/p", name: "proj" }, listings, DEFAULT_TREE_OPTIONS);
- const m = withExtraRoots(base, [{ path: "/data/scratches", label: "Scratches", icon: "scratches" }], listings, DEFAULT_TREE_OPTIONS);
+ const m = withExtraRoots(
+ base,
+ [{ path: "/data/scratches", label: "Scratches", icon: "scratches" }],
+ listings,
+ DEFAULT_TREE_OPTIONS,
+ );
expect(m.roots.map((r) => [r.label, r.root, r.icon])).toEqual([
["proj", true, undefined],
["Scratches", true, "scratches"],
@@ -60,7 +65,12 @@ describe("project view with a scratches root", () => {
const fs = new MemoryFs({ "/p/README.md": "x", "/data/scratches/scratch.py": "x" }, "/p");
setProjectFsForTests(fs, "/p");
initProjectTree("/p", "proj");
- unregister = registerProjectViewRoot({ id: "scratches", label: "Scratches", icon: "scratches", path: "/data/scratches" });
+ unregister = registerProjectViewRoot({
+ id: "scratches",
+ label: "Scratches",
+ icon: "scratches",
+ path: "/data/scratches",
+ });
});
afterEach(() => {
unregister();
diff --git a/src/features/project-tree/fileRefactor.test.tsx b/src/features/project-tree/fileRefactor.test.tsx
index fe8e57f3..34eb34f9 100644
--- a/src/features/project-tree/fileRefactor.test.tsx
+++ b/src/features/project-tree/fileRefactor.test.tsx
@@ -27,7 +27,10 @@ describe("file refactoring edits", () => {
files: [edit("/p/main.ts", [0, 20, 26, "./b/util"]), { kind: "rename", from: "/p/a", to: "/p/b" }],
};
const text: WorkspaceEdit = {
- files: [edit("/p/main.ts", [0, 22, 26, "b/util"], [3, 3, 10, "b/util.ts"]), edit("/p/README.md", [1, 0, 9, "b/util.ts"])],
+ files: [
+ edit("/p/main.ts", [0, 22, 26, "b/util"], [3, 3, 10, "b/util.ts"]),
+ edit("/p/README.md", [1, 0, 9, "b/util.ts"]),
+ ],
};
const merged = mergeEdits(lsp, text);
expect(merged.files.map((f) => f.kind === "edit" && [f.path, f.edits.length])).toEqual([
@@ -54,7 +57,10 @@ describe("runFileRefactoring", () => {
provideNotificationService({ notify });
backend = {
calls: [],
- edits: vi.fn(async () => ({ edit: { files: [edit("/p/main.ts", [0, 20, 26, "./b/util"])] }, errors: ["tsserver: busy"] })),
+ edits: vi.fn(async () => ({
+ edit: { files: [edit("/p/main.ts", [0, 20, 26, "./b/util"])] },
+ errors: ["tsserver: busy"],
+ })),
move: vi.fn(async (moves: FileMove[], e: WorkspaceEdit) => {
backend.calls.push(`move ${moves.length} ${e.files.length}`);
return 7;
@@ -117,8 +123,8 @@ describe("runFileRefactoring", () => {
expect(backend.edits).not.toHaveBeenCalled();
const ctrl = new AbortController();
ctrl.abort();
- expect(await runFileRefactoring(moves, { references: true, textOccurrences: false }, "Move", { signal: ctrl.signal })).toBe(
- "cancelled",
- );
+ expect(
+ await runFileRefactoring(moves, { references: true, textOccurrences: false }, "Move", { signal: ctrl.signal }),
+ ).toBe("cancelled");
});
});
diff --git a/src/features/project-tree/fileRefactor.ts b/src/features/project-tree/fileRefactor.ts
index 79fb5483..63e3a65e 100644
--- a/src/features/project-tree/fileRefactor.ts
+++ b/src/features/project-tree/fileRefactor.ts
@@ -81,7 +81,10 @@ export function memoryRefactorBackend(): FileRefactorBackend {
done.push(m);
}
} catch (e) {
- for (const m of done.reverse()) await fs().rename(m.to, m.from).catch(() => {});
+ for (const m of done.reverse())
+ await fs()
+ .rename(m.to, m.from)
+ .catch(() => {});
throw e;
}
records.set(++next, moves);
@@ -142,7 +145,9 @@ export function mergeEdits(lsp: WorkspaceEdit, text: WorkspaceEdit): WorkspaceEd
/** Whether `edit` changes files other than the moved ones (IDEA previews those refactorings). */
export function touchesOtherFiles(edit: WorkspaceEdit, moves: readonly FileMove[]): boolean {
- return edit.files.some((f) => f.kind === "edit" && f.edits.length && !moves.some((m) => isSameOrInside(f.path, m.from)));
+ return edit.files.some(
+ (f) => f.kind === "edit" && f.edits.length && !moves.some((m) => isSameOrInside(f.path, m.from)),
+ );
}
// ---------------------------------------------------------------------------
@@ -263,7 +268,11 @@ export async function runFileRefactoring(
notifications().notify({
level: "info",
title,
- body: !edited ? undefined : edited === 1 ? tx`Updated references in ${edited} file` : tx`Updated references in ${edited} files`,
+ body: !edited
+ ? undefined
+ : edited === 1
+ ? tx`Updated references in ${edited} file`
+ : tx`Updated references in ${edited} files`,
actions: [{ label: "Undo", run: () => void undoFileRefactoring(id) }],
key: "project.refactor.done",
});
diff --git a/src/features/project-tree/fs.ts b/src/features/project-tree/fs.ts
index 7e9bf13e..6aee22bc 100644
--- a/src/features/project-tree/fs.ts
+++ b/src/features/project-tree/fs.ts
@@ -37,8 +37,7 @@ export const tauriFs: ProjectFs = {
type MemNode = { kind: "file"; size: number } | { kind: "dir" };
-const err = (kind: "notFound" | "alreadyExists" | "invalidInput", message: string) =>
- new IpcError({ kind, message });
+const err = (kind: "notFound" | "alreadyExists" | "invalidInput", message: string) => new IpcError({ kind, message });
/** Minimal in-memory file system with the backend's semantics (no overwrite, folders first). */
export class MemoryFs implements ProjectFs {
@@ -112,7 +111,8 @@ export class MemoryFs implements ProjectFs {
async rename(from: string, to: string) {
if (!this.nodes.has(from)) throw err("notFound", `${from} not found`);
- if (this.nodes.has(to) && from.toLowerCase() !== to.toLowerCase()) throw err("alreadyExists", `${to} already exists`);
+ if (this.nodes.has(to) && from.toLowerCase() !== to.toLowerCase())
+ throw err("alreadyExists", `${to} already exists`);
if (isInside(to, from)) throw err("invalidInput", "cannot move a folder into itself");
for (const p of [...this.nodes.keys()]) {
if (p !== from && !isInside(p, from)) continue;
diff --git a/src/features/project-tree/model.test.ts b/src/features/project-tree/model.test.ts
index 1ae5aa79..e7909b51 100644
--- a/src/features/project-tree/model.test.ts
+++ b/src/features/project-tree/model.test.ts
@@ -13,7 +13,17 @@ import {
const entry = (path: string, kind: "file" | "dir" = "file", extra: Partial
= {}): FileEntry => {
const name = path.slice(path.lastIndexOf("/") + 1);
- return { name, path, kind, size: 0, mtimeMs: 1, ignored: false, excluded: false, hidden: name.startsWith("."), ...extra };
+ return {
+ name,
+ path,
+ kind,
+ size: 0,
+ mtimeMs: 1,
+ ignored: false,
+ excluded: false,
+ hidden: name.startsWith("."),
+ ...extra,
+ };
};
const ROOT = { path: "/r", name: "acme" };
diff --git a/src/features/project-tree/model.ts b/src/features/project-tree/model.ts
index f35d3800..ed2c6373 100644
--- a/src/features/project-tree/model.ts
+++ b/src/features/project-tree/model.ts
@@ -286,13 +286,26 @@ export function withExtraRoots(
if (ns === null && model.roots.some((r) => !r.group && isSameOrInside(path, r.path))) return null;
const sub = buildModel({ path, name: extra.label }, listings, opts, ro ? null : placeholder);
const subRoot = sub.roots[0]!;
- const rootNode: ProjectNode = { ...subRoot, key: rekey(subRoot.key), root: !parentKey, icon: extra.icon, detail: extra.detail };
+ const rootNode: ProjectNode = {
+ ...subRoot,
+ key: rekey(subRoot.key),
+ root: !parentKey,
+ icon: extra.icon,
+ detail: extra.detail,
+ };
if (ro) rootNode.readOnly = true;
const nodes = new Map();
for (const [k, v] of sub.byKey)
- nodes.set(k, k === subRoot.key ? rootNode : ns !== null || ro ? { ...v, key: rekey(k), readOnly: ro || v.readOnly } : v);
+ nodes.set(
+ k,
+ k === subRoot.key ? rootNode : ns !== null || ro ? { ...v, key: rekey(k), readOnly: ro || v.readOnly } : v,
+ );
for (const n of nodes.values()) out.byKey.set(n.key, n);
- for (const [k, v] of sub.children) out.children.set(rekey(k), v.map((n) => nodes.get(n.key) ?? n));
+ for (const [k, v] of sub.children)
+ out.children.set(
+ rekey(k),
+ v.map((n) => nodes.get(n.key) ?? n),
+ );
for (const [p, k] of sub.keyOfPath) if (!out.keyOfPath.has(p)) out.keyOfPath.set(p, rekey(k));
return rootNode;
}
@@ -341,7 +354,11 @@ export function withReadOnlyFolders(model: TreeModel, folders: readonly string[]
}
if (!changed) return model;
const children = new Map();
- for (const [k, kids] of model.children) children.set(k, kids.map((n) => byKey.get(n.key) ?? n));
+ for (const [k, kids] of model.children)
+ children.set(
+ k,
+ kids.map((n) => byKey.get(n.key) ?? n),
+ );
return { roots: model.roots.map((n) => byKey.get(n.key) ?? n), children, byKey, keyOfPath: model.keyOfPath };
}
@@ -387,7 +404,12 @@ export function buildPathsModel(roots: readonly ExtraRoot[], paths: readonly str
const ensureRoot = (r: ExtraRoot & { path: string }, rank: number) => {
const existing = byKey.get(r.path);
if (existing) return existing;
- const n = node(r.path, r.label, true, { root: true, icon: r.icon || undefined, detail: r.detail, readOnly: r.readOnly });
+ const n = node(r.path, r.label, true, {
+ root: true,
+ icon: r.icon || undefined,
+ detail: r.detail,
+ readOnly: r.readOnly,
+ });
rootNodes.push([rank, n]);
return n;
};
@@ -396,7 +418,8 @@ export function buildPathsModel(roots: readonly ExtraRoot[], paths: readonly str
if (byKey.has(path)) continue;
let rank = -1;
for (let i = 0; i < folders.length; i++)
- if (isInside(path, folders[i]!.path) && (rank < 0 || folders[i]!.path.length > folders[rank]!.path.length)) rank = i;
+ if (isInside(path, folders[i]!.path) && (rank < 0 || folders[i]!.path.length > folders[rank]!.path.length))
+ rank = i;
const root =
rank >= 0
? ensureRoot(folders[rank]!, rank)
diff --git a/src/features/project-tree/operations.ts b/src/features/project-tree/operations.ts
index 1c8b5421..73d84180 100644
--- a/src/features/project-tree/operations.ts
+++ b/src/features/project-tree/operations.ts
@@ -358,7 +358,11 @@ export async function safeToDelete(files: string[]): Promise {
result = await fileUsages(files, abort.signal);
} catch {
if (user.choice) return user.choice === "skip";
- notifications().notify({ level: "warning", title: "Usage search did not finish; nothing was deleted", key: "project.safeDelete" });
+ notifications().notify({
+ level: "warning",
+ title: "Usage search did not finish; nothing was deleted",
+ key: "project.safeDelete",
+ });
return false;
} finally {
clearTimeout(timer);
@@ -385,7 +389,12 @@ export async function findFileUsages(path: string) {
return;
}
if (result.status !== "ok")
- notifications().notify({ level: "info", title: `Cannot find usages of '${name}'`, body: result.reason, group: "search" });
+ notifications().notify({
+ level: "info",
+ title: `Cannot find usages of '${name}'`,
+ body: result.reason,
+ group: "search",
+ });
else if (!result.usages.length)
notifications().notify({ level: "info", title: `No usages of '${name}' found`, group: "search" });
else viewUsages(name, result.usages);
@@ -487,7 +496,8 @@ export async function paste(target = targetFolder()) {
// win over ours — unless they are the copy we handed to the OS ourselves.
if (external.length && !sameFiles(external, clipboard?.paths)) return pasteFromOsClipboard(external, target);
if (!clipboard?.paths.length) return;
- const isDir = (p: string) => get().listings.has(p) || currentModel().byKey.get(currentModel().keyOfPath.get(p) ?? p)?.dir === true;
+ const isDir = (p: string) =>
+ get().listings.has(p) || currentModel().byKey.get(currentModel().keyOfPath.get(p) ?? p)?.dir === true;
const single = clipboard.paths.length === 1 && isDir(clipboard.paths[0]!);
if (!clipboard.cut) return openCopyDialogFor(clipboard.paths, target, single);
const paths = clipboard.paths.filter((p) => parentOf(p) !== target);
@@ -506,7 +516,11 @@ async function pasteFromOsClipboard(paths: string[], target: string) {
/** The same files, as the clipboard keeps them (`/`-separated, any order). */
function sameFiles(files: string[], paths: string[] | undefined): boolean {
if (!paths || files.length !== paths.length) return false;
- const key = (list: string[]) => list.map((p) => (caseInsensitive ? p.toLowerCase() : p)).sort().join("\n");
+ const key = (list: string[]) =>
+ list
+ .map((p) => (caseInsensitive ? p.toLowerCase() : p))
+ .sort()
+ .join("\n");
return key(files) === key(paths);
}
@@ -554,7 +568,8 @@ export async function transfer(
errors.push(`${name}: cannot copy a folder into itself`);
continue;
}
- const srcIsDir = get().listings.has(src) || currentModel().byKey.get(currentModel().keyOfPath.get(src) ?? src)?.dir === true;
+ const srcIsDir =
+ get().listings.has(src) || currentModel().byKey.get(currentModel().keyOfPath.get(src) ?? src)?.dir === true;
const finalName = paths.length === 1 && newName ? newName : uniqueName(name, isTaken, srcIsDir);
const to = joinPath(target, finalName);
try {
@@ -613,10 +628,12 @@ export async function dropPaths(paths: string[], target: string, copy: boolean)
if (!movable.length) return;
// IDEA opens the Move refactoring dialog for drops inside the project.
if (!copy) {
- const node = movable.length === 1 ? currentModel().byKey.get(currentModel().keyOfPath.get(movable[0]!) ?? "") : undefined;
+ const node =
+ movable.length === 1 ? currentModel().byKey.get(currentModel().keyOfPath.get(movable[0]!) ?? "") : undefined;
return openMoveDialogFor(movable, target, node?.dir);
}
- const node = movable.length === 1 ? currentModel().byKey.get(currentModel().keyOfPath.get(movable[0]!) ?? "") : undefined;
+ const node =
+ movable.length === 1 ? currentModel().byKey.get(currentModel().keyOfPath.get(movable[0]!) ?? "") : undefined;
openCopyDialogFor(movable, target, node?.dir);
}
diff --git a/src/features/project-tree/packages.ts b/src/features/project-tree/packages.ts
index 5131b274..510b106f 100644
--- a/src/features/project-tree/packages.ts
+++ b/src/features/project-tree/packages.ts
@@ -6,7 +6,8 @@ const JVM_BUILD = /(^|\/)(pom\.xml|build\.gradle(\.kts)?)$/;
const PY_BUILD = /(^|\/)(pyproject\.toml|setup\.py|setup\.cfg|Pipfile|requirements\.txt)$/;
const JVM_DIR = /(^|\/)(java|kotlin|scala|groovy)$/;
-const moduleDir = (buildFile: string) => (buildFile.includes("/") ? buildFile.slice(0, buildFile.lastIndexOf("/")) : "");
+const moduleDir = (buildFile: string) =>
+ buildFile.includes("/") ? buildFile.slice(0, buildFile.lastIndexOf("/")) : "";
const within = (rel: string, dir: string) => !dir || rel === dir || rel.startsWith(`${dir}/`);
/**
diff --git a/src/features/project-tree/roots.test.ts b/src/features/project-tree/roots.test.ts
index e9b37e92..6f16cf41 100644
--- a/src/features/project-tree/roots.test.ts
+++ b/src/features/project-tree/roots.test.ts
@@ -68,7 +68,10 @@ describe("creating packages", () => {
fs = new MemoryFs({ "/p/src/main/java/App.java": "x", "/p/pkg/__init__.py": "" }, "/p");
setProjectFsForTests(fs, "/p");
initProjectTree("/p", "p");
- useProject.setState({ project: { name: "p", root: "/p", isGit: false, settings: {} } as never, settings: {} as never });
+ useProject.setState({
+ project: { name: "p", root: "/p", isGit: false, settings: {} } as never,
+ settings: {} as never,
+ });
useProjectRoots.setState({ root: "/p", detected: MAVEN });
});
afterEach(() => {
diff --git a/src/features/project-tree/selectIn.test.ts b/src/features/project-tree/selectIn.test.ts
index 0c8bcdc5..046b3480 100644
--- a/src/features/project-tree/selectIn.test.ts
+++ b/src/features/project-tree/selectIn.test.ts
@@ -17,7 +17,11 @@ afterEach(() => {
describe("Select In (⌥F1)", () => {
it("lists the project view, contributed targets, Finder and the terminal", () => {
disposers.push(
- registerMenuGroup({ id: "test.selectIn", menus: ["selectIn"], items: () => [{ label: "Structure", run: () => {} }] }),
+ registerMenuGroup({
+ id: "test.selectIn",
+ menus: ["selectIn"],
+ items: () => [{ label: "Structure", run: () => {} }],
+ }),
registerActions([{ id: "terminal.openAt", title: "Open Terminal At", run: () => {} }]),
);
expect(labels(selectInTargets("/p/src/a.ts"))).toEqual([
diff --git a/src/features/project-tree/store.ts b/src/features/project-tree/store.ts
index 95a9dc26..df7d4c97 100644
--- a/src/features/project-tree/store.ts
+++ b/src/features/project-tree/store.ts
@@ -189,16 +189,36 @@ export function selectModel(s: ProjectTreeState): TreeModel {
if (s.root) {
if (s.mode === "projectFiles") model = project();
else if (s.mode === "scratches")
- model = withExtraRoots(EMPTY_MODEL, contributed.filter((r) => r.scope === "scratches"), s.listings, s.options, placeholder);
+ model = withExtraRoots(
+ EMPTY_MODEL,
+ contributed.filter((r) => r.scope === "scratches"),
+ s.listings,
+ s.options,
+ placeholder,
+ );
else if (files)
- model = buildPathsModel([{ path: s.root, label: s.name, icon: "" }, ...extraFolders(contributed)], files, s.options.foldersFirst);
+ model = buildPathsModel(
+ [{ path: s.root, label: s.name, icon: "" }, ...extraFolders(contributed)],
+ files,
+ s.options.foldersFirst,
+ );
else model = withExtraRoots(project(), contributed, s.listings, s.options, placeholder);
const libraries = extraFolders(contributed).flatMap((r) =>
r.readOnly && r.path !== undefined && isSameOrInside(r.path, s.root!) ? [r.path] : [],
);
model = withReadOnlyFolders(model, libraries);
}
- memo = { root: s.root, name: s.name, listings: s.listings, options: s.options, editing: s.editing, mode: s.mode, files, extras, model };
+ memo = {
+ root: s.root,
+ name: s.name,
+ listings: s.listings,
+ options: s.options,
+ editing: s.editing,
+ mode: s.mode,
+ files,
+ extras,
+ model,
+ };
return model;
}
@@ -240,7 +260,12 @@ const autoExpanded = new Set();
/** "Project" restores the persisted expansion; other panes start with their roots open. */
function initialExpanded(root: string | null, mode: ViewMode): Set {
if (!root || isFilteredMode(mode)) return new Set();
- if (mode === "scratches") return new Set(projectViewRoots(useProjectViewRoots.getState().roots).flatMap((r) => (r.scope === "scratches" && r.path ? [r.path] : [])));
+ if (mode === "scratches")
+ return new Set(
+ projectViewRoots(useProjectViewRoots.getState().roots).flatMap((r) =>
+ r.scope === "scratches" && r.path ? [r.path] : [],
+ ),
+ );
const persisted = mode === "project" ? (readJson(expandedKey(root)) ?? []) : [];
const expanded = new Set(persisted.filter((p) => isSameOrInside(p, root)));
expanded.add(root);
@@ -479,7 +504,7 @@ export function isReadOnlyPath(path: string): boolean {
/** Group node keys above the node `key`, outermost first. */
function groupAncestors(model: TreeModel, key: string): string[] {
const out: string[] = [];
- for (let child = key; ; ) {
+ for (let child = key; ;) {
let parent: string | null = null;
for (const [k, kids] of model.children)
if (k.startsWith(GROUP_PREFIX) && kids.some((n) => n.key === child)) {
diff --git a/src/features/project-tree/viewModes.test.ts b/src/features/project-tree/viewModes.test.ts
index a0f040a7..9e0e7ea7 100644
--- a/src/features/project-tree/viewModes.test.ts
+++ b/src/features/project-tree/viewModes.test.ts
@@ -83,7 +83,11 @@ describe("withExtraRoots groups", () => {
["site-packages", undefined, true, false, pyKey("/p/.venv/lib/site-packages"), "/p/.venv/lib/site-packages"],
]);
const os = m.byKey.get(pyKey("/py/lib/python3.12/os.py"))!;
- expect([os.readOnly, os.path, pathOfKey(os.key)]).toEqual([true, "/py/lib/python3.12/os.py", "/py/lib/python3.12/os.py"]);
+ expect([os.readOnly, os.path, pathOfKey(os.key)]).toEqual([
+ true,
+ "/py/lib/python3.12/os.py",
+ "/py/lib/python3.12/os.py",
+ ]);
expect(m.children.get(pyKey("/py/lib/python3.12"))![0]).toBe(os);
expect(m.keyOfPath.get("/py/lib/python3.12/os.py")).toBe(os.key);
});
@@ -117,7 +121,12 @@ describe("buildPathsModel", () => {
describe("localChangedPaths", () => {
it("lists change-list files, all lists or one, optionally without deleted ones", () => {
const lists = [
- { id: "default", name: "Changes", active: true, files: [{ path: "/p/b.ts" }, { path: "/p/gone.ts", deleted: true }] },
+ {
+ id: "default",
+ name: "Changes",
+ active: true,
+ files: [{ path: "/p/b.ts" }, { path: "/p/gone.ts", deleted: true }],
+ },
{ id: "ui", name: "UI", active: false, files: [{ path: "/p/a.ts" }] },
];
expect(localChangedPaths(lists, null, true)).toEqual(["/p/a.ts", "/p/b.ts"]);
@@ -260,7 +269,9 @@ describe("project view modes", () => {
const ids = contextMenuItems([projectFile]).map((i) => ("id" in i ? i.id : i.kind));
expect(ids).not.toContain("delete");
expect(ids).not.toContain("rename");
- expect(contextMenuItems([m.byKey.get("/p/.venv/pyvenv.cfg")!]).map((i) => ("id" in i ? i.id : i.kind))).toContain("delete");
+ expect(contextMenuItems([m.byKey.get("/p/.venv/pyvenv.cfg")!]).map((i) => ("id" in i ? i.id : i.kind))).toContain(
+ "delete",
+ );
});
it("expands library folders in their own namespace", async () => {
diff --git a/src/features/remote-dev/ConnectDialog.tsx b/src/features/remote-dev/ConnectDialog.tsx
index 50a81317..ef8426fd 100644
--- a/src/features/remote-dev/ConnectDialog.tsx
+++ b/src/features/remote-dev/ConnectDialog.tsx
@@ -73,7 +73,12 @@ function ConnectDialog() {
onClose={closeConnectDialog}
width={560}
buttons={[
- { label: connecting ? "Connecting…" : "Open", role: "default", disabled: !ready, onClick: () => void connect() },
+ {
+ label: connecting ? "Connecting…" : "Open",
+ role: "default",
+ disabled: !ready,
+ onClick: () => void connect(),
+ },
{ label: "Cancel", role: "cancel" },
]}
>
@@ -85,9 +90,7 @@ function ConnectDialog() {
}}
>
- {tx(
- "The project runs on the remote host; the IDE backend is installed there on first use.",
- )}
+ {tx("The project runs on the remote host; the IDE backend is installed there on first use.")}
{tx("SSH configuration:")}
diff --git a/src/features/run-anything/model.test.ts b/src/features/run-anything/model.test.ts
index d6ed5ab4..2bafac8e 100644
--- a/src/features/run-anything/model.test.ts
+++ b/src/features/run-anything/model.test.ts
@@ -1,6 +1,15 @@
import { beforeEach, describe, expect, it } from "vitest";
import type { RunAnythingProvider } from "@/workbench/contributions";
-import { buildRows, clearHistory, MAX_HISTORY, matches, pushHistory, readHistory, shellCommand, shellRunConfig } from "./model";
+import {
+ buildRows,
+ clearHistory,
+ MAX_HISTORY,
+ matches,
+ pushHistory,
+ readHistory,
+ shellCommand,
+ shellRunConfig,
+} from "./model";
beforeEach(() => localStorage.clear());
@@ -30,7 +39,8 @@ describe("run anything", () => {
{
id: "npm",
group: "npm scripts",
- items: (q) => [{ key: "dev", label: "npm run dev", icon: "run", run: () => {} }].filter((i) => matches(i.label, q)),
+ items: (q) =>
+ [{ key: "dev", label: "npm run dev", icon: "run", run: () => {} }].filter((i) => matches(i.label, q)),
},
{
id: "broken",
diff --git a/src/features/run-anything/model.ts b/src/features/run-anything/model.ts
index 6a5f5389..b454cfac 100644
--- a/src/features/run-anything/model.ts
+++ b/src/features/run-anything/model.ts
@@ -14,7 +14,9 @@ const GROUP_LIMIT = 8;
export function readHistory(scope: string): string[] {
try {
const v: unknown = JSON.parse(localStorage.getItem(HISTORY_PREFIX + scope) ?? "[]");
- return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string" && !!x.trim()).slice(0, MAX_HISTORY) : [];
+ return Array.isArray(v)
+ ? v.filter((x): x is string => typeof x === "string" && !!x.trim()).slice(0, MAX_HISTORY)
+ : [];
} catch {
return [];
}
@@ -41,7 +43,9 @@ export function clearHistory(scope: string) {
}
}
-export type Row = { kind: "section"; key: string; label: string } | { kind: "item"; key: string; item: RunAnythingItem; group: string };
+export type Row =
+ | { kind: "section"; key: string; label: string }
+ | { kind: "item"; key: string; item: RunAnythingItem; group: string };
/** Case-insensitive: every word of `query` occurs in `text`. */
export function matches(text: string, query: string): boolean {
diff --git a/src/features/run/ConfigChooser.tsx b/src/features/run/ConfigChooser.tsx
index becadcc4..2e9b90bf 100644
--- a/src/features/run/ConfigChooser.tsx
+++ b/src/features/run/ConfigChooser.tsx
@@ -94,7 +94,8 @@ function ConfigChooser() {
const initial = Math.max(
0,
rows.findIndex(
- (r) => (r.type === "config" && r.config.id === selectedId) || (r.type === "current" && selectedId === CURRENT_FILE),
+ (r) =>
+ (r.type === "config" && r.config.id === selectedId) || (r.type === "current" && selectedId === CURRENT_FILE),
),
);
const [active, setActive] = useState(initial);
@@ -138,7 +139,9 @@ function ConfigChooser() {
if (row.type === "current") {
selectConfig(CURRENT_FILE);
if (how === "run" || how === "debug")
- void executeAction(how === "run" ? "run.runContext" : "run.debugContext").then(() => selectConfig(CURRENT_FILE));
+ void executeAction(how === "run" ? "run.runContext" : "run.debugContext").then(() =>
+ selectConfig(CURRENT_FILE),
+ );
return;
}
if (row.type !== "config") return;
@@ -247,7 +250,11 @@ function ConfigChooser() {
- {r.indices ? : {CURRENT_FILE_LABEL} }
+ {r.indices ? (
+
+ ) : (
+ {CURRENT_FILE_LABEL}
+ )}
);
@@ -277,15 +284,21 @@ function ConfigChooser() {
{r.indices ?
: c.name}
{c.target &&
@ {targetName(c, runTargets)} }
- {c.isTest &&
+ {c.isTest && (
+
test
- }
- {!c.detected &&
+
+ )}
+ {!c.detected && (
+
user
- }
- {temporary.includes(c.id) &&
+
+ )}
+ {temporary.includes(c.id) && (
+
temporary
- }
+
+ )}
{isActive && (
({ value: a.id, label: a.label })),
];
if (config.debugAdapter && !ADAPTERS.some((a) => a.id === config.debugAdapter))
- adapterOptions.push({ value: config.debugAdapter, label: txTemplate(`${config.debugAdapter} (no launch template)`) });
+ adapterOptions.push({
+ value: config.debugAdapter,
+ label: txTemplate(`${config.debugAdapter} (no launch template)`),
+ });
const targetOptions: SelectOption[] = [
{ value: "", label: "Local machine" },
diff --git a/src/features/run/ConsoleView.tsx b/src/features/run/ConsoleView.tsx
index 0bac6062..5322b114 100644
--- a/src/features/run/ConsoleView.tsx
+++ b/src/features/run/ConsoleView.tsx
@@ -93,10 +93,14 @@ export type DisplayRow = { kind: "line"; index: number; fold?: Fold } | { kind:
* Folds runs of consecutive output lines (one or more) that contain one of
* `patterns`; runs whose first line id is in `expanded` stay open.
*/
-export function foldRows(lines: readonly ConsoleLine[], patterns: readonly string[], expanded: ReadonlySet): DisplayRow[] {
+export function foldRows(
+ lines: readonly ConsoleLine[],
+ patterns: readonly string[],
+ expanded: ReadonlySet,
+): DisplayRow[] {
const rows: DisplayRow[] = [];
const matches = (l: ConsoleLine) => l.kind !== "system" && patterns.some((p) => l.text.includes(p));
- for (let i = 0; i < lines.length; ) {
+ for (let i = 0; i < lines.length;) {
if (patterns.length && matches(lines[i]!)) {
let j = i + 1;
while (j < lines.length && matches(lines[j]!)) j++;
@@ -139,7 +143,17 @@ export interface ConsoleViewProps {
"aria-label"?: string;
}
-export function ConsoleView({ model, version, softWrap, follow, onFollowChange, cwd, onClear, input, ...aria }: ConsoleViewProps) {
+export function ConsoleView({
+ model,
+ version,
+ softWrap,
+ follow,
+ onFollowChange,
+ cwd,
+ onClear,
+ input,
+ ...aria
+}: ConsoleViewProps) {
const scrollRef = useRef(null);
const lines = model.lines;
const patterns = useWorkflowSettings((s) => s.console.foldPatterns);
@@ -208,7 +222,9 @@ export function ConsoleView({ model, version, softWrap, follow, onFollowChange,
{ label: "Copy All", run: () => void navigator.clipboard?.writeText(model.text()).catch(() => undefined) },
{ kind: "separator" },
{ label: "Clear All", icon: "trash", disabled: !onClear, run: () => onClear?.() },
- ...(input ? [{ kind: "separator" } as const, { label: "Send EOF", shortcut: "⌃D", run: () => input.onEof() }] : []),
+ ...(input
+ ? [{ kind: "separator" } as const, { label: "Send EOF", shortcut: "⌃D", run: () => input.onEof() }]
+ : []),
]);
};
@@ -249,11 +265,22 @@ export function ConsoleView({ model, version, softWrap, follow, onFollowChange,
);
const foldButton = (fold: Fold, attached: boolean) =>
fold.expanded ? (
- toggleFold(fold.id)}>
+ toggleFold(fold.id)}
+ >
) : (
- toggleFold(fold.id)}>
+ toggleFold(fold.id)}
+ >
{`${attached ? " " : ""}${fold.count === 1 ? tx`<${fold.count} internal line>` : tx`<${fold.count} internal lines>`}`}
);
diff --git a/src/features/run/EditConfigsDialog.tsx b/src/features/run/EditConfigsDialog.tsx
index 124d5aef..06018beb 100644
--- a/src/features/run/EditConfigsDialog.tsx
+++ b/src/features/run/EditConfigsDialog.tsx
@@ -40,7 +40,9 @@ export function EditConfigsOverlay() {
}
type Selection =
- { type: "config"; id: string } | { type: "folder"; kind: string; name: string } | { type: "template"; kind: string };
+ | { type: "config"; id: string }
+ | { type: "folder"; kind: string; name: string }
+ | { type: "template"; kind: string };
export type TreeRow =
| { type: "group"; key: string; kind: string; label: string; icon: string }
diff --git a/src/features/run/TestTreeView.tsx b/src/features/run/TestTreeView.tsx
index c1761647..44378442 100644
--- a/src/features/run/TestTreeView.tsx
+++ b/src/features/run/TestTreeView.tsx
@@ -144,15 +144,20 @@ export function TestTreeView({ results, version, cwd, selected, onSelect, runnin
}
if (ids.length === 1 && nodeStatus(node) === "failed" && parseComparison(node.output))
items.push({ label: "Show Diff", icon: "diff", run: () => void showTestDiff(node) });
- items.push({ label: "Jump to Source", icon: "external-link", shortcut: "F4", disabled: !located, run: () => jumpToSource(node) });
+ items.push({
+ label: "Jump to Source",
+ icon: "external-link",
+ shortcut: "F4",
+ disabled: !located,
+ run: () => jumpToSource(node),
+ });
e.preventDefault();
showContextMenu(e, items);
};
const counts = useMemo(() => results.counts(), [results, version]); // eslint-disable-line react-hooks/exhaustive-deps
const roots = useMemo(() => {
- const keep = (n: TestNode) =>
- (showPassed || n.own !== "passed") && (showIgnored || n.own !== "skipped");
+ const keep = (n: TestNode) => (showPassed || n.own !== "passed") && (showIgnored || n.own !== "skipped");
return showPassed && showIgnored ? [...results.roots] : filterTree(results.roots, keep);
}, [results, version, showPassed, showIgnored]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -235,7 +240,11 @@ export function TestTreeView({ results, version, cwd, selected, onSelect, runnin
}}
empty={
- {running ? tx("Waiting for test results…") : results.isEmpty ? tx("No test results") : tx("Passed tests are hidden")}
+ {running
+ ? tx("Waiting for test results…")
+ : results.isEmpty
+ ? tx("No test results")
+ : tx("Passed tests are hidden")}
}
/>
diff --git a/src/features/run/configs.test.ts b/src/features/run/configs.test.ts
index 5f8c1694..6258c3f8 100644
--- a/src/features/run/configs.test.ts
+++ b/src/features/run/configs.test.ts
@@ -64,7 +64,11 @@ describe("config merge / override", () => {
});
it("groups by kind in order", () => {
- const groups = groupConfigs([cfg({ id: "a", kind: "npm" }), cfg({ id: "b", kind: "cargo" }), cfg({ id: "c", kind: "npm" })]);
+ const groups = groupConfigs([
+ cfg({ id: "a", kind: "npm" }),
+ cfg({ id: "b", kind: "cargo" }),
+ cfg({ id: "c", kind: "npm" }),
+ ]);
expect(groups.map((g) => [g.label, g.configs.map((c) => c.id)])).toEqual([
["npm", ["a", "c"]],
["Cargo", ["b"]],
@@ -97,15 +101,32 @@ describe("validateConfig", () => {
});
it("validates env table rows", () => {
- expect(envFromRows([{ key: "A", value: "1" }, { key: "", value: "" }])).toEqual({ env: { A: "1" } });
+ expect(
+ envFromRows([
+ { key: "A", value: "1" },
+ { key: "", value: "" },
+ ]),
+ ).toEqual({ env: { A: "1" } });
expect(envFromRows([{ key: "", value: "x" }]).error).toMatch(/empty/);
- expect(envFromRows([{ key: "A", value: "1" }, { key: "A", value: "2" }]).error).toMatch(/Duplicate/);
+ expect(
+ envFromRows([
+ { key: "A", value: "1" },
+ { key: "A", value: "2" },
+ ]).error,
+ ).toMatch(/Duplicate/);
});
});
describe("argument parsing", () => {
it("splits with quotes and escapes", () => {
- expect(parseArgs(`run --name "hello world" 'it''s' a\\ b ""`)).toEqual(["run", "--name", "hello world", "its", "a b", ""]);
+ expect(parseArgs(`run --name "hello world" 'it''s' a\\ b ""`)).toEqual([
+ "run",
+ "--name",
+ "hello world",
+ "its",
+ "a b",
+ "",
+ ]);
expect(parseArgs(" ")).toEqual([]);
});
@@ -142,7 +163,12 @@ describe("context configuration", () => {
it("runs a JS test file through the closest test script", () => {
const c = contextConfig("web/src/cart.test.ts", configs)!;
- expect(c).toMatchObject({ args: ["run", "test", "--", "src/cart.test.ts"], cwd: "web", isTest: true, name: "cart.test.ts" });
+ expect(c).toMatchObject({
+ args: ["run", "test", "--", "src/cart.test.ts"],
+ cwd: "web",
+ isTest: true,
+ name: "cart.test.ts",
+ });
expect(c.id).toBe("context:npm:web:test:web/src/cart.test.ts");
expect(contextConfig("src/a.spec.js", configs)!.args).toEqual(["run", "test", "--", "src/a.spec.js"]);
});
@@ -160,7 +186,10 @@ describe("context configuration", () => {
it("finds run targets and script invocations", () => {
expect(contextConfig("src/bin/server.rs", configs)!.id).toBe("cargo:run:server");
- expect(contextConfig("tools/gen.py", configs)).toMatchObject({ command: ".venv/bin/python", args: ["tools/gen.py"] });
+ expect(contextConfig("tools/gen.py", configs)).toMatchObject({
+ command: ".venv/bin/python",
+ args: ["tools/gen.py"],
+ });
expect(contextConfig("scripts/build.sh", configs)).toMatchObject({ command: "sh", args: ["scripts/build.sh"] });
expect(contextConfig("docs/readme.md", configs)).toBeNull();
});
@@ -186,7 +215,10 @@ describe("context configuration", () => {
];
const main = "package main\nfunc main() {}";
expect(contextConfig("api/main.go", nested, main)!.id).toBe("go:run:api");
- expect(contextConfig("api/cmd/tool/main.go", nested, main)).toMatchObject({ args: ["run", "./cmd/tool"], cwd: "api" });
+ expect(contextConfig("api/cmd/tool/main.go", nested, main)).toMatchObject({
+ args: ["run", "./cmd/tool"],
+ cwd: "api",
+ });
expect(contextConfig("api/main_test.go", nested)).toMatchObject({ args: ["test", "."], cwd: "api" });
});
diff --git a/src/features/run/configs.ts b/src/features/run/configs.ts
index bc46d4ce..53f2ffa1 100644
--- a/src/features/run/configs.ts
+++ b/src/features/run/configs.ts
@@ -268,7 +268,8 @@ export function parseArgs(input: string): string[] {
const ch = input[i]!;
if (quote) {
if (ch === quote) quote = null;
- else if (ch === "\\" && quote === '"' && i + 1 < input.length && '"\\$`'.includes(input[i + 1]!)) cur += input[++i];
+ else if (ch === "\\" && quote === '"' && i + 1 < input.length && '"\\$`'.includes(input[i + 1]!))
+ cur += input[++i];
else cur += ch;
} else if (ch === "'" || ch === '"') {
quote = ch;
@@ -290,9 +291,7 @@ export function parseArgs(input: string): string[] {
/** Inverse of `parseArgs` (quotes only when needed). */
export function formatArgs(args: readonly string[]): string {
- return args
- .map((a) => (a === "" ? "''" : /[\s'"\\$`]/.test(a) ? `'${a.replace(/'/g, `'\\''`)}'` : a))
- .join(" ");
+ return args.map((a) => (a === "" ? "''" : /[\s'"\\$`]/.test(a) ? `'${a.replace(/'/g, `'\\''`)}'` : a)).join(" ");
}
/** Display command line, e.g. `pnpm run dev`. */
diff --git a/src/features/run/contextRun.test.ts b/src/features/run/contextRun.test.ts
index 9120f414..3d19e40a 100644
--- a/src/features/run/contextRun.test.ts
+++ b/src/features/run/contextRun.test.ts
@@ -84,7 +84,9 @@ describe("run context menu group", () => {
it("shows nothing when no producer matches or for several paths", () => {
expect(contextMenuItems("editor", { paths: [`${ROOT}/README.md`], text: "# hi", line: 0 })).toEqual([]);
- expect(contextMenuItems("projectView", { paths: [`${ROOT}/a.js`, `${ROOT}/b.js`], dirs: [false, false] })).toEqual([]);
+ expect(contextMenuItems("projectView", { paths: [`${ROOT}/a.js`, `${ROOT}/b.js`], dirs: [false, false] })).toEqual(
+ [],
+ );
useProject.setState({ project: null });
expect(contextMenuItems("editor", { paths: [`${SCRATCHES}/hi.ts`], text: "", line: 0 })).toEqual([]);
});
@@ -143,7 +145,9 @@ describe("run gutter popup", () => {
const text = "describe('math', () => {\n it('adds', () => {\n });\n});\n";
const adds: RunTarget = { line: 1, kind: "test", name: "math > adds", path: ["math", "adds"] };
const itemLabels = (items: MenuItem[]): string[] =>
- items.map((i) => (i.kind === "separator" ? "---" : "label" in i ? `${i.label}${"submenu" in i && i.submenu ? " ▸" : ""}` : ""));
+ items.map((i) =>
+ i.kind === "separator" ? "---" : "label" in i ? `${i.label}${"submenu" in i && i.submenu ? " ▸" : ""}` : "",
+ );
beforeEach(() => {
useProject.setState({ project: { name: "app", root: ROOT, isGit: false, settings: {} } as never });
@@ -167,7 +171,15 @@ describe("run gutter popup", () => {
const run = items[0] as Extract;
expect(run.submenu!.map((i) => ("shortcut" in i ? i.shortcut : null))).toEqual(["test", "npm run test:unit"]);
// ⌃⇧R's chooser lists the same, under a header.
- expect(choiceItems(produceContextChoices({ path: `${ROOT}/src/m.test.ts`, root: ROOT, configs: [npmTest, unit], content: text, line: 1 }, OPTIMISTIC), false)[0]).toEqual({
+ expect(
+ choiceItems(
+ produceContextChoices(
+ { path: `${ROOT}/src/m.test.ts`, root: ROOT, configs: [npmTest, unit], content: text, line: 1 },
+ OPTIMISTIC,
+ ),
+ false,
+ )[0],
+ ).toEqual({
kind: "header",
label: "Choose configuration to run",
});
diff --git a/src/features/run/contextRun.ts b/src/features/run/contextRun.ts
index f25b37a5..11b04b3c 100644
--- a/src/features/run/contextRun.ts
+++ b/src/features/run/contextRun.ts
@@ -75,7 +75,13 @@ export async function produceResolved(input: ContextInput, dir = false): Promise
function reportMissing(p: Produced) {
if (!p.missing) return;
- notifications().notify({ group: "run", level: "warning", title: p.missing.title, body: p.missing.body, key: "run-context-missing" });
+ notifications().notify({
+ group: "run",
+ level: "warning",
+ title: p.missing.title,
+ body: p.missing.body,
+ key: "run-context-missing",
+ });
}
/** Builds with `build` in the Run window; resolves whether it succeeded. */
@@ -86,8 +92,8 @@ async function buildFirst(build: RunConfig): Promise {
const check = () => {
const s = useRun.getState().sessions.find((x) => x.id === session.id);
if (!s) return true;
- if (s.status === "failed") return resolve(false), true;
- if (s.status === "finished") return resolve(s.exitCode === 0), true;
+ if (s.status === "failed") return (resolve(false), true);
+ if (s.status === "finished") return (resolve(s.exitCode === 0), true);
return false;
};
if (check()) return;
@@ -111,7 +117,12 @@ export async function launchProduced(p: Produced, debug: boolean): Promise
if (remoteDebugBlocked(config)) return;
if (p.debug) {
if (!getAction("debug.startConfig")) {
- notifications().notify({ group: "run", level: "warning", title: "Debugger is not available", key: "run-no-debugger" });
+ notifications().notify({
+ group: "run",
+ level: "warning",
+ title: "Debugger is not available",
+ key: "run-no-debugger",
+ });
return;
}
if (!(await buildFirst(p.debug.build))) return;
@@ -142,7 +153,9 @@ export async function compiledDebugPlan(config: RunConfig): Promise new RegExp(`\\bfn\\s+${m[2]!.split("::").pop()}\\b|\\bmod\\s+${m[2]}\\b`).test(l));
+ const line = lines.findIndex((l) =>
+ new RegExp(`\\bfn\\s+${m[2]!.split("::").pop()}\\b|\\bmod\\s+${m[2]}\\b`).test(l),
+ );
return line >= 0 ? produceContext({ ...input, line }, env) : null;
}
return produceFile(input, env);
@@ -165,7 +178,9 @@ export async function runContextAction(debug: boolean, arg?: unknown): Promise 1) {
// Several test configurations match equally well: like IDEA, let the user choose.
showContextMenu(chooserPoint(target === null), choiceItems(choices, debug));
@@ -181,15 +196,13 @@ export async function runContextAction(debug: boolean, arg?: unknown): Promise ({
- label: debug ? tx`Debug '${p.name}'` : tx`Run '${p.name}'`,
- verbatim: true,
- icon: debug ? "debug" : "run",
- shortcut: p.detail,
- run: () => void launchProduced(p, debug),
- }),
- ),
+ ...choices.map((p): MenuItem => ({
+ label: debug ? tx`Debug '${p.name}'` : tx`Run '${p.name}'`,
+ verbatim: true,
+ icon: debug ? "debug" : "run",
+ shortcut: p.detail,
+ run: () => void launchProduced(p, debug),
+ })),
];
}
@@ -222,7 +235,13 @@ export function executorItems(config: RunConfig | null | undefined, name: string
if (!config) return [];
return runExecutors()
.filter((e) => e.supports(config))
- .map((e) => ({ kind: "action", action: "run.withExecutor", arg: { executor: e.id, context }, label: e.label(name), icon: e.icon }));
+ .map((e) => ({
+ kind: "action",
+ action: "run.withExecutor",
+ arg: { executor: e.id, context },
+ label: e.label(name),
+ icon: e.icon,
+ }));
}
export interface ContextArg {
@@ -322,7 +341,9 @@ export function contextMenuItems(menu: ContextMenuId, ctx: MenuGroupContext): Me
{ kind: "action", action: "run.runContext", arg, label: tx`Run '${p.name}'`, icon: "run" },
// Like IDEA, Debug only where a debugger can run it.
...(p.debug || p.config?.debugAdapter || p.missing
- ? ([{ kind: "action", action: "run.debugContext", arg, label: tx`Debug '${p.name}'`, icon: "debug" }] as MenuItem[])
+ ? ([
+ { kind: "action", action: "run.debugContext", arg, label: tx`Debug '${p.name}'`, icon: "debug" },
+ ] as MenuItem[])
: []),
{
label: "More Run/Debug",
diff --git a/src/features/run/externalTools.ts b/src/features/run/externalTools.ts
index 9bd99738..0cefe7d1 100644
--- a/src/features/run/externalTools.ts
+++ b/src/features/run/externalTools.ts
@@ -34,7 +34,8 @@ export function macroValue(name: string, ctx: MacroContext): string | undefined
const file = ctx.filePath;
const dir = file ? file.slice(0, Math.max(file.lastIndexOf("/"), 0)) || "/" : "";
const base = file ? file.slice(file.lastIndexOf("/") + 1) : "";
- const rel = (p: string) => (p === ctx.projectRoot ? "" : p.startsWith(ctx.projectRoot + "/") ? p.slice(ctx.projectRoot.length + 1) : p);
+ const rel = (p: string) =>
+ p === ctx.projectRoot ? "" : p.startsWith(ctx.projectRoot + "/") ? p.slice(ctx.projectRoot.length + 1) : p;
switch (name) {
case "FilePath":
return file ?? "";
@@ -87,7 +88,14 @@ function currentContext(): MacroContext | null {
column = sel.head - l.from + 1;
selectedText = state.sliceDoc(sel.from, sel.to);
}
- return { projectRoot: project.root, projectName: project.name, filePath: ed?.ctx.path ?? null, line, column, selectedText };
+ return {
+ projectRoot: project.root,
+ projectName: project.name,
+ filePath: ed?.ctx.path ?? null,
+ line,
+ column,
+ selectedText,
+ };
}
/** The run configuration a tool expands to (no `$Prompt$` handling). */
diff --git a/src/features/run/index.ts b/src/features/run/index.ts
index c57ae1c7..5e6e6336 100644
--- a/src/features/run/index.ts
+++ b/src/features/run/index.ts
@@ -201,7 +201,12 @@ function stopAction() {
}
}
const items = [
- ...running.map((s): MenuItem => ({ label: s.config.name, verbatim: true, icon: "run", run: () => void stopSession(s.id) })),
+ ...running.map((s): MenuItem => ({
+ label: s.config.name,
+ verbatim: true,
+ icon: "run",
+ run: () => void stopSession(s.id),
+ })),
...menuGroupItems("stopProcess", { paths: [] }),
].filter((i): i is MenuItem & { run: () => void } => "run" in i && !!i.run);
if (items.length === 0) return;
@@ -332,7 +337,8 @@ export function activate(): () => void {
{
id: "run.chooseConfig",
title: "Select Run/Debug Configuration…",
- description: "Run a configuration (hold Shift to debug); the title bar widget passes \"select\" to only select one",
+ description:
+ 'Run a configuration (hold Shift to debug); the title bar widget passes "select" to only select one',
category: "Run",
when: "projectOpen",
run: async ({ arg }) => {
@@ -385,7 +391,12 @@ export function activate(): () => void {
(s.activeSession && failedTests(s.activeSession).length ? s.activeSession : null) ??
[...s.sessions].reverse().find((x) => failedTests(x.id).length > 0)?.id;
if (!id || !rerunFailed(id))
- notifications().notify({ group: "run", level: "info", title: "No failed tests to rerun", key: "run-rerun-failed" });
+ notifications().notify({
+ group: "run",
+ level: "info",
+ title: "No failed tests to rerun",
+ key: "run-rerun-failed",
+ });
},
},
{
diff --git a/src/features/run/java.test.ts b/src/features/run/java.test.ts
index abf2308d..974a5bcf 100644
--- a/src/features/run/java.test.ts
+++ b/src/features/run/java.test.ts
@@ -56,7 +56,9 @@ describe("scanJvmTargets (Java)", () => {
});
it("ignores non-main methods named main and Java 25 instance mains count", () => {
- expect(pick("class A {\n int main(String[] a) { return 0; }\n static void main(int x) {}\n}", "A.java")).toEqual([]);
+ expect(pick("class A {\n int main(String[] a) { return 0; }\n static void main(int x) {}\n}", "A.java")).toEqual(
+ [],
+ );
expect(pick("class B {\n void main() {}\n}", "B.java")).toEqual([[1, "main", "B", null, false]]);
});
@@ -140,7 +142,8 @@ describe("application configurations", () => {
it("creates an Application / Spring Boot config named after the class", () => {
const rel = "src/main/java/com/example/demo/SpringDemoApplication.java";
- const src = "package com.example.demo;\n@SpringBootApplication\nclass SpringDemoApplication {\n public static void main(String[] a) {}\n}";
+ const src =
+ "package com.example.demo;\n@SpringBootApplication\nclass SpringDemoApplication {\n public static void main(String[] a) {}\n}";
const main = detectRunTargets(src, rel).find((t) => t.kind === "main")!;
const c = targetConfig(main, rel, maven, src)!;
expect(c).toMatchObject({
@@ -245,7 +248,9 @@ describe("java command lines", () => {
describe("debugging build tool configurations", () => {
it("adds a waiting JDWP agent to spring-boot:run, Surefire and Gradle", () => {
const boot = jdwpLaunch(cfg({ id: "b", kind: "maven", args: ["spring-boot:run"] }))!;
- expect(boot.args[1]).toMatch(/^-Dspring-boot\.run\.jvmArguments=-agentlib:jdwp=.*suspend=y.*address=127\.0\.0\.1:0$/);
+ expect(boot.args[1]).toMatch(
+ /^-Dspring-boot\.run\.jvmArguments=-agentlib:jdwp=.*suspend=y.*address=127\.0\.0\.1:0$/,
+ );
const merged = jdwpLaunch(
cfg({ id: "b", kind: "maven", args: ["spring-boot:run", "-Dspring-boot.run.jvmArguments=-Xmx1g"] }),
)!;
diff --git a/src/features/run/javaDebug.ts b/src/features/run/javaDebug.ts
index 27ccd39a..def29583 100644
--- a/src/features/run/javaDebug.ts
+++ b/src/features/run/javaDebug.ts
@@ -10,7 +10,8 @@ import type { RunConfig } from "@/platform/ipc";
import { JDWP_AGENT, jdwpListeningPort } from "@/platform/javaLaunch";
import { startRun, useRun } from "./store";
-const isMavenTestGoal = (a: string) => /^(test|verify|integration-test|surefire:test|failsafe:integration-test)$/.test(a);
+const isMavenTestGoal = (a: string) =>
+ /^(test|verify|integration-test|surefire:test|failsafe:integration-test)$/.test(a);
const isGradleJvmTask = (a: string) => /(^|:)(test|bootRun|run|integrationTest|check)$/.test(a);
function withProperty(args: readonly string[], key: string, value: string): string[] {
diff --git a/src/features/run/navigate.ts b/src/features/run/navigate.ts
index 77c18078..a81dcfae 100644
--- a/src/features/run/navigate.ts
+++ b/src/features/run/navigate.ts
@@ -17,7 +17,8 @@ async function exists(path: string): Promise