Compare commits

...
Author SHA1 Message Date
l0ng-ai 8ec4032cf7 fix(vcs): let Commit and Shelf tree names use the whole row width
File and folder names in the Commit tool window's changes tree (and the Shelf tree, which shares the styles) were capped at 75% of the row, so a long folder name was cut with an ellipsis while a quarter of the row stayed empty, and the gap grew as the window widened. The name now takes the whole row; when it does not fit, the grey detail after it (the path or file count) is truncated first. Stash rows keep their previous shrinking.

Fixes #1367
2026-11-05 08:25:29 +08:00
l0ng-ai a0ac988963 fix(debugger): debug uv / poetry run configurations in the project's environment (#1395)
debugpy ran the project manager (`uv`, `poetry`, …) as a module. Configurations run through uv, poetry, pdm, pipenv, hatch or rye now pass the manager as the debuggee's interpreter, keep the script / -m module / tool as the target, and run the adapter and launcher on a plain interpreter (debugLauncherPython, now preferred by the backend for the adapter command).
2026-11-05 08:18:18 +08:00
4 changed files with 60 additions and 19 deletions

No files matched your search

+5 -4
View File
@@ -589,11 +589,12 @@ impl DebugManager {
if let Some(command) = custom_command(&spec.command) {
spec.command = command;
}
// debugpy: run the adapter with the debuggee's interpreter (venvs carry their own debugpy).
// debugpy: run the adapter with the debuggee's interpreter (venvs carry their own debugpy),
// or with the launcher's when the debuggee runs through a manager (`uv run python`).
if config.adapter == "debugpy" {
let python = match arguments.get("python") {
Some(Value::String(p)) => Some(p.clone()),
Some(Value::Array(a)) => a.first().and_then(Value::as_str).map(str::to_owned),
let python = match (arguments.get("debugLauncherPython"), arguments.get("python")) {
(Some(Value::String(p)), _) | (_, Some(Value::String(p))) => Some(p.clone()),
(_, Some(Value::Array(a))) => a.first().and_then(Value::as_str).map(str::to_owned),
_ => None,
};
if let Some(python) = python {
@@ -79,6 +79,27 @@ describe("run config → launch config", () => {
expect(extra(globalPytest).python).toBe("python3");
});
it("debugpy: uv / poetry run the debuggee in the project's environment", () => {
const debug = (command: string, args: string[]) =>
launchConfigFor(rc({ kind: "python", command, args, debugAdapter: "debugpy" }), "/p")!;
const script = debug("uv", ["run", "python", "main.py", "-v"]);
expect(script.program).toBe("/p/main.py");
expect(script.args).toEqual(["-v"]);
expect(extra(script)).toMatchObject({ python: ["uv", "run", "python"], debugLauncherPython: "python3" });
expect(extra(script).module).toBeUndefined();
const mod = debug("poetry", ["run", "python3", "-m", "app"]);
expect(extra(mod)).toMatchObject({ module: "app", python: ["poetry", "run", "python3"] });
const tool = debug("uv", ["run", "pytest", "-x"]);
expect(extra(tool)).toMatchObject({ module: "pytest", python: ["uv", "run", "python"] });
expect(tool.args).toEqual(["-x"]);
const django = debug("pdm", ["run", "python", "manage.py", "runserver"]);
expect(extra(django)).toMatchObject({ django: true, python: ["pdm", "run", "python"] });
});
it("js-debug: npm scripts run through the package manager, node scripts directly", () => {
const npm = launchConfigFor(
rc({ kind: "npm", command: "pnpm", args: ["run", "dev"], cwd: "web", debugAdapter: "js-debug" }),
+28 -13
View File
@@ -58,15 +58,20 @@ function cargoLaunch(rc: RunConfig, cwd: string, adapter: string): LaunchConfig
};
}
const PYTHON_NAME = /^python[\d.]*(\.exe)?$/;
function pythonFor(rc: RunConfig, cwd: string): string {
const cmd = rc.command;
const name = cmd.split("/").pop() ?? cmd;
if (/^python[\d.]*(\.exe)?$/.test(name)) return resolveCommand(cmd, cwd);
if (PYTHON_NAME.test(name)) return resolveCommand(cmd, cwd);
// A venv tool such as `.venv/bin/pytest`: use the interpreter next to it.
if (cmd.includes("/")) return resolveCommand(cmd.slice(0, cmd.lastIndexOf("/") + 1) + "python", cwd);
return "python3";
}
/** Project managers whose `run` executes in the project's environment (`uv run python …`). */
const PYTHON_RUNNERS = new Set(["uv", "poetry", "pdm", "pipenv", "hatch", "rye"]);
function debugpyLaunch(rc: RunConfig, cwd: string): LaunchConfig {
const out = base(rc, "debugpy", cwd);
const dbg = workflowSettings().debugger;
@@ -78,19 +83,29 @@ function debugpyLaunch(rc: RunConfig, cwd: string): LaunchConfig {
redirectOutput: true,
};
if (dbg.python.gevent) extra.gevent = true;
const name = rc.command.split("/").pop() ?? rc.command;
const isPython = /^python[\d.]*(\.exe)?$/.test(name);
if (!isPython) {
const name = (rc.command.split("/").pop() ?? rc.command).replace(/\.exe$/, "");
let args = rc.args;
let tool: string | null = PYTHON_NAME.test(name) ? null : name;
if (PYTHON_RUNNERS.has(name) && args[0] === "run" && args[1]) {
// The debuggee runs through the manager (`uv run python <debugpy> …`) to get the project's
// environment; the adapter and its launcher need a plain interpreter.
const python = PYTHON_NAME.test(args[1]) ? args[1] : "python";
extra.python = [resolveCommand(rc.command, cwd), "run", python];
extra.debugLauncherPython = "python3";
tool = PYTHON_NAME.test(args[1]) ? null : args[1];
args = args.slice(2);
}
if (tool) {
// A console script (`pytest`, `uvicorn`, …): run it as a module.
extra.module = name.replace(/\.exe$/, "");
out.args = [...rc.args];
} else if (rc.args[0] === "-m" && rc.args[1]) {
extra.module = rc.args[1];
out.args = rc.args.slice(2);
} else if (rc.args[0]) {
out.program = join(cwd, rc.args[0]);
out.args = rc.args.slice(1);
if (rc.args[0].endsWith("manage.py")) extra.django = true;
extra.module = tool;
out.args = [...args];
} else if (args[0] === "-m" && args[1]) {
extra.module = args[1];
out.args = args.slice(2);
} else if (args[0]) {
out.program = join(cwd, args[0]);
out.args = args.slice(1);
if (args[0].endsWith("manage.py")) extra.django = true;
}
out.extraJson = JSON.stringify(extra);
return out;
+6 -2
View File
@@ -62,9 +62,9 @@
.icon {
flex: none;
}
/* The name takes the whole row; when it doesn't fit, the grey detail after it gives way first. */
.label {
flex: 0 0 auto;
max-width: 75%;
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -74,6 +74,7 @@
font-weight: 600;
}
.detail {
flex: 0 10000 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -190,6 +191,9 @@
align-items: center;
gap: 8px;
}
.stashRow > .detail {
flex-shrink: 1;
}
.stashMessage {
flex: 1;
min-width: 0;