Compare commits

..
Author SHA1 Message Date
arookieofcandClaude Code 6f262e0cc0 chore: fix README formatting (pre-existing oxfmt violation blocking CI)
Co-Authored-By: Claude Code <[email protected]>
2026-11-10 11:04:10 +08:00
arookieofcandClaude Code 4d39216b00 fix(vcs): let Git Log ref labels size to content (#2447)
Remove the unconditional 180px per-label cap and let the refs row use
the full subject cell when space allows, shrinking with ellipsis (full
name kept in the title tooltip) only when space truly runs out.

Co-Authored-By: Claude Code <[email protected]>
2026-11-10 10:47:09 +08:00
16 changed files with 51 additions and 377 deletions

No files matched your search

-2
View File
@@ -76,6 +76,4 @@ the [official plugins](plugins/official). The plugin API is still a preview.
[Apache 2.0](LICENSE). Bundled third-party assets are listed in [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
<sub>Not affiliated with JetBrains. IntelliJ IDEA is a trademark of JetBrains s.r.o.</sub>
-4
View File
@@ -746,10 +746,6 @@ impl DebugManager {
{
spec.env.push(("JAVA_HOME".into(), home));
}
// Delve looks up Go before applying the debuggee's launch environment.
if config.adapter == "delve" {
spec.env.extend(config.env.iter().map(|(key, value)| (key.clone(), value.clone())));
}
if config.adapter == "rdbg" {
rdbg_command_line(&mut spec, &config, &cwd)?;
}
-33
View File
@@ -1040,39 +1040,6 @@ async fn connects_to_running_debug_server_over_tcp() {
assert!(events.lock().iter().any(|e| matches!(e, IdeEvent::Debug { event: DebugEvent::Started { .. }, .. })));
}
#[tokio::test]
async fn delve_adapter_inherits_the_selected_go_environment() {
struct CaptureLauncher {
inner: FakeLauncher,
env: Arc<Mutex<Vec<(String, String)>>>,
}
impl AdapterLauncher for CaptureLauncher {
fn launch(&self, spec: AdapterSpec, reuse: Option<u16>) -> LaunchFuture {
*self.env.lock() = spec.env.clone();
self.inner.launch(spec, reuse)
}
}
let env = Arc::new(Mutex::new(Vec::new()));
let launcher =
CaptureLauncher { inner: FakeLauncher { log: Arc::default(), script: Script::default() }, env: env.clone() };
let manager = DebugManager::with_launcher(Arc::new(LanguageRegistry::from_configs(Vec::new())), Arc::new(launcher));
let (owner, _) = recording_owner("main");
let config = LaunchConfig {
name: "Go".into(),
adapter: "delve".into(),
request: "launch".into(),
program: Some("/project".into()),
args: vec![],
cwd: None,
env: BTreeMap::from([("PATH".into(), "/chosen/go/bin".into()), ("GOROOT".into(), "/chosen/go".into())]),
extra_json: Some(json!({"mode":"debug"}).to_string()),
};
let id = manager.start(config, owner).await.unwrap();
assert!(env.lock().contains(&("PATH".into(), "/chosen/go/bin".into())));
assert!(env.lock().contains(&("GOROOT".into(), "/chosen/go".into())));
manager.stop(id).await.unwrap();
}
/// Go Remote / Python remote: the adapter runs on another machine (`dlv --headless`,
/// `debugpy --listen`); `debugServer: "host:port"` connects there instead of spawning one.
#[tokio::test]
-1
View File
@@ -64,7 +64,6 @@ async fn debug_start_inner(
let settings = settings.clone();
config = host
.blocking(move || {
ide_sdk::go::prepare_debug_compiler(&mut config.env, &config.extra_json)?;
ide_sdk::go::configure_environment(&root, &settings, &mut config.env);
ide_sdk::go::apply_to_debug(
&mut config.env,
+1 -188
View File
@@ -131,117 +131,6 @@ fn delve_build_flags(args: &[String], files: bool) -> Value {
)
}
/// An explicitly selected Go executable must also be the compiler Delve finds on PATH.
pub fn prepare_debug_compiler(
env: &mut std::collections::BTreeMap<String, String>,
extra_json: &Option<String>,
) -> ide_core::IdeResult<()> {
#[cfg(windows)]
for key in ["PATH", "GOROOT"] {
if let Some(previous) = env.keys().find(|name| name.eq_ignore_ascii_case(key) && name.as_str() != key).cloned()
{
let value = env.remove(&previous).unwrap();
env.entry(key.into()).or_insert(value);
}
}
let extra: Value = extra_json.as_deref().and_then(|value| serde_json::from_str(value).ok()).unwrap_or_default();
let Some(executable) = extra["__goExecutable"].as_str().filter(|value| *value != "go" && *value != "go.exe") else {
return Ok(());
};
let path = env.get("PATH").map(std::ffi::OsString::from).unwrap_or_else(crate::exec::search_path);
let executable = ide_core::process::find_executable(executable, &path)
.ok_or_else(|| ide_core::IdeError::unavailable(format!("Go executable not found: {executable}")))?;
if !executable.file_name().is_some_and(|name| {
if cfg!(windows) {
let name = name.to_string_lossy();
name.eq_ignore_ascii_case("go") || name.eq_ignore_ascii_case("go.exe")
} else {
name == "go"
}
}) {
return Err(ide_core::IdeError::invalid("Delve requires a Go SDK executable named go or go.exe"));
}
if !env.contains_key("GOROOT") {
let mut command = std::process::Command::new(&executable);
command.args(["env", "GOROOT"]).envs(env.iter());
let goroot = crate::exec::query_command(command, std::time::Duration::from_secs(10))
.ok_or_else(|| ide_core::IdeError::unavailable("Cannot query the selected Go SDK"))?;
env.insert("GOROOT".into(), goroot);
}
let mut paths = vec![executable.parent().unwrap().to_path_buf()];
paths.extend(std::env::split_paths(&path));
let path = std::env::join_paths(paths).map_err(|error| ide_core::IdeError::invalid(error.to_string()))?;
env.insert("PATH".into(), path.to_string_lossy().into_owned());
Ok(())
}
fn resolve_debug_target(
program: &mut Option<String>,
env: &std::collections::BTreeMap<String, String>,
extra: &mut Value,
flags: &mut Vec<String>,
) -> ide_core::IdeResult<()> {
let Some(targets) = extra.get("__goTargets") else { return Ok(()) };
let targets: Vec<String> =
serde_json::from_value(targets.clone()).map_err(|error| ide_core::IdeError::invalid(error.to_string()))?;
let dir = extra["__goBuildDir"]
.as_str()
.map(std::path::Path::new)
.ok_or_else(|| ide_core::IdeError::invalid("Go debug build directory is missing"))?;
if targets.iter().any(|target| target.ends_with(".go")) {
if !targets.iter().all(|target| target.ends_with(".go")) {
return Err(ide_core::IdeError::invalid("Select Go source files or one package for debugging"));
}
let files: Vec<_> =
targets.iter().map(|target| dunce::canonicalize(dir.join(target))).collect::<Result<_, _>>()?;
let first = files.first().ok_or_else(|| ide_core::IdeError::invalid("Go debug target is missing"))?;
if files.iter().any(|file| file.parent() != first.parent()) {
return Err(ide_core::IdeError::invalid("Go source files must be in the same directory"));
}
*program = Some(first.to_string_lossy().into_owned());
flags.extend(files.iter().skip(1).map(|file| file.to_string_lossy().into_owned()));
extra["__goSourceArgs"] = json!(true);
return Ok(());
}
let path = env.get("PATH").map(std::ffi::OsString::from).unwrap_or_else(crate::exec::search_path);
let go = ide_core::process::find_executable("go", &path)
.ok_or_else(|| ide_core::IdeError::unavailable("Go executable not found"))?;
let mut command = std::process::Command::new(go);
command.args(["list", "-find", "-f", "{{.Dir}}"]).current_dir(dir).envs(env.iter());
let mut args = flags.iter();
while let Some(flag) = args.next() {
let name = flag.split('=').next().unwrap_or(flag);
if ["-tags", "-mod", "-modfile", "-overlay", "-compiler"].contains(&name) {
command.arg(flag);
if name == flag
&& let Some(value) = args.next()
{
command.arg(value);
}
} else if ["-race", "-msan", "-asan"].contains(&name) {
command.arg(flag);
}
}
command.args(&targets);
let output = crate::exec::query_command(command, std::time::Duration::from_secs(10)).ok_or_else(|| {
ide_core::IdeError::invalid(format!(
"Cannot resolve Go debug target: {}. Check its module path and build flags.",
targets.join(" ")
))
})?;
let mut dirs: Vec<_> = output.lines().filter(|line| !line.trim().is_empty()).collect();
dirs.sort();
dirs.dedup();
if dirs.len() != 1 {
return Err(ide_core::IdeError::invalid(format!(
"Go debugging requires one package; this target selects {}. Select a package or test instead.",
dirs.len()
)));
}
*program = Some(dirs[0].to_owned());
Ok(())
}
pub fn apply_to_debug(
env: &mut std::collections::BTreeMap<String, String>,
program: &mut Option<String>,
@@ -281,7 +170,6 @@ pub fn apply_to_debug(
.collect::<Vec<_>>();
merge_flags(&mut args, &flags);
}
resolve_debug_target(program, env, &mut extra, &mut args)?;
if let Some(files) = extra.get("__goFiles").and_then(Value::as_array) {
if extra["mode"] == "test" {
if let Some(file) = files.first().and_then(Value::as_str).map(std::path::Path::new) {
@@ -302,10 +190,7 @@ pub fn apply_to_debug(
}
// Delve appends its -o after array build flags; positional source files
// require the string form so all compiler flags precede the file list.
extra["buildFlags"] = delve_build_flags(
&args,
(extra.get("__goFiles").is_some() && extra["mode"] != "test") || extra["__goSourceArgs"] == true,
);
extra["buildFlags"] = delve_build_flags(&args, extra.get("__goFiles").is_some() && extra["mode"] != "test");
}
if let Some(output) = extra.get("output").and_then(Value::as_str).and_then(|p| std::path::Path::new(p).parent()) {
std::fs::create_dir_all(output)?;
@@ -314,10 +199,6 @@ pub fn apply_to_debug(
map.remove("__goUseTags");
map.remove("__goToolArgs");
map.remove("__goFiles");
map.remove("__goExecutable");
map.remove("__goTargets");
map.remove("__goBuildDir");
map.remove("__goSourceArgs");
}
*extra_json = Some(extra.to_string());
Ok(())
@@ -328,74 +209,6 @@ mod command_tests {
use super::*;
use std::collections::BTreeMap;
#[test]
fn debug_targets_resolve_imports_and_reject_multiple_packages() {
if crate::exec::which("go").is_none() {
return;
}
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
std::fs::write(root.join("go.mod"), "module example.com/debugtargets\ngo 1.22\n").unwrap();
for name in ["a", "b"] {
std::fs::create_dir(root.join(name)).unwrap();
std::fs::write(root.join(name).join("lib.go"), "package p\nfunc Value() {}\n").unwrap();
}
let mut env = BTreeMap::from([("GOWORK".into(), "off".into())]);
let launch = |target: &str| {
Some(json!({"mode":"test", "__goToolArgs":[], "__goTargets":[target], "__goBuildDir":root}).to_string())
};
let mut program = None;
let mut extra = launch("example.com/debugtargets/a");
apply_to_debug(&mut env, &mut program, &mut Vec::new(), &mut extra, &GoBuildTags::default()).unwrap();
assert_eq!(program.as_deref(), root.join("a").to_str());
let value: Value = serde_json::from_str(extra.as_deref().unwrap()).unwrap();
assert!(value.get("__goTargets").is_none());
let error =
apply_to_debug(&mut env, &mut program, &mut Vec::new(), &mut launch("./..."), &GoBuildTags::default())
.unwrap_err();
assert!(error.to_string().contains("requires one package"), "{error}");
std::fs::write(root.join("main.go"), "package main\nfunc main(){helper()}\n").unwrap();
std::fs::write(root.join("helper.go"), "package main\nfunc helper(){}\n").unwrap();
let mut extra = Some(
json!({"mode":"debug","__goBuildDir":root,"__goTargets":["main.go","helper.go"],"__goToolArgs":[]})
.to_string(),
);
apply_to_debug(&mut env, &mut program, &mut Vec::new(), &mut extra, &GoBuildTags::default()).unwrap();
assert_eq!(program.as_deref(), root.join("main.go").to_str());
let value: Value = serde_json::from_str(extra.as_deref().unwrap()).unwrap();
assert_eq!(
value["buildFlags"],
format!("'{}'", root.join("helper.go").to_string_lossy().replace('\\', "\\\\").replace('\'', "\\'"))
);
}
#[cfg(windows)]
#[test]
fn compiler_selection_preserves_case_insensitive_windows_environment_keys() {
let mut env =
BTreeMap::from([("Path".into(), "explicit path".into()), ("GoRoot".into(), "explicit root".into())]);
prepare_debug_compiler(&mut env, &None).unwrap();
assert_eq!(env["PATH"], "explicit path");
assert_eq!(env["GOROOT"], "explicit root");
assert!(!env.contains_key("Path"));
assert!(!env.contains_key("GoRoot"));
}
#[test]
fn the_selected_go_compiler_precedes_the_existing_path() {
let Some(go) = crate::exec::which("go") else {
return;
};
let mut env = BTreeMap::from([("PATH".into(), "/unrelated".into())]);
let extra = Some(json!({"__goExecutable":go}).to_string());
prepare_debug_compiler(&mut env, &extra).unwrap();
assert_eq!(std::env::split_paths(&env["PATH"]).next().as_deref(), go.parent());
assert!(!env["GOROOT"].is_empty());
env.insert("GOROOT".into(), "explicit".into());
prepare_debug_compiler(&mut env, &extra).unwrap();
assert_eq!(env["GOROOT"], "explicit");
}
#[test]
fn detected_go_runs_inherit_tags_only_for_commands_that_accept_build_flags() {
let tags = GoBuildTags { custom: "integration".into(), ..Default::default() };
-11
View File
@@ -5,7 +5,6 @@
*/
import { writeClipboardText } from "@/platform/clipboard";
import { pasteEditorClipboard } from "./cm/clipboard";
import type { ReactNode } from "react";
import { EditorSelection } from "@codemirror/state";
import { create } from "zustand";
import { setContext } from "@/platform/context";
@@ -212,16 +211,6 @@ export function tabIcon(input: EditorInput, kinds: EditorKinds): string {
}
}
/** A kind's trailing marker (status dot …); `null` when it has none. */
export function tabTrailing(input: EditorInput, kinds: EditorKinds): ReactNode {
const kind = kinds.get(input.kind);
try {
return kind?.trailing?.(input) ?? null;
} catch {
return null;
}
}
export class EditorController implements SlotHost {
manager: DocumentManager;
readonly slots = new Map<string, EditorSlot>();
+2 -18
View File
@@ -46,15 +46,7 @@ import {
type TabItem,
type TabsDnd,
} from "@/ui";
import {
editorController,
isTextInput,
tabIcon,
tabTitle,
tabTrailing,
useTabs,
type VirtualInputData,
} from "../controller";
import { editorController, isTextInput, tabIcon, tabTitle, useTabs, type VirtualInputData } from "../controller";
import { useDocMeta } from "../model/document";
import { inputDirty } from "../model/customDocuments";
import type { LayoutNode, SplitEdge, Tab } from "../model/tabs";
@@ -201,8 +193,6 @@ function PaneView({ paneId }: { paneId: string }) {
: names.filter((n) => n === name).length > 1 && t.input.path
? basename(dirname(t.input.path))
: null;
const kindMark = tabTrailing(t.input, kinds);
const pin = t.pinned ? <Icon name="pin" size={11} className={styles.pin} label="Pinned" /> : null;
return {
id: t.id,
label: (
@@ -223,13 +213,7 @@ function PaneView({ paneId }: { paneId: string }) {
color: colors[i] || undefined,
tint: tints[i] || undefined,
tooltip: virtualTooltip(t.input) ?? (showFullPathOnHover ? (t.input.path ?? name) : undefined),
trailing:
kindMark || pin ? (
<>
{kindMark}
{pin}
</>
) : undefined,
trailing: t.pinned ? <Icon name="pin" size={11} className={styles.pin} label="Pinned" /> : undefined,
};
});
}, [
-25
View File
@@ -1,25 +0,0 @@
/** The tab strip's agent status indicator: spinner while the task is busy, dot once done (#2233). */
import { Badge, Spinner, tooltipAttrs } from "@/ui";
import { isBusy, statusLabel } from "./model";
import { useAgents } from "./store";
export function TaskStatusMark({ task }: { task: string }) {
const status = useAgents((s) => s.tasks.get(task)?.status);
if (!status) return null;
const busy = isBusy(status);
const label = statusLabel(status);
return (
<span {...tooltipAttrs(label, undefined, true)} style={{ display: "inline-flex", alignItems: "center" }}>
{busy ? (
<Spinner size={10} variant="spokes" label={label} />
) : (
<Badge
dot
tone={status === "ready" ? "success" : status === "failed" || status === "limited" ? "danger" : "neutral"}
>
{label}
</Badge>
)}
</span>
);
}
-4
View File
@@ -3,7 +3,6 @@
* Agents tool window lists tasks by what they need from the user, a task opens as an
* editor tab (conversation + changed files), and New Task is an editor page.
*/
import { createElement } from "react";
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
import { executeAction } from "@/platform/actions";
@@ -23,7 +22,6 @@ import { HOME_KIND, REGISTRY_PAGE, SESSION_KIND, TASK_DRAG, TOOL_WINDOW } from "
import { sendPaths, sendSelection } from "./references";
import { focusedSession } from "./sessionFocus";
import { connect, disconnect, openHome, useAgents } from "./store";
import { TaskStatusMark } from "./TaskStatusMark";
import { taskActions } from "./taskActions";
import { replyActions } from "./replyActions";
@@ -90,8 +88,6 @@ export function activate(): () => void {
return useAgents.getState().tasks.get(data.task)?.title ?? data.title ?? "Agent Task";
},
icon: () => "toolwindow-agents",
// Busy / done marker in the tab's trailing slot (#2233).
trailing: (input) => createElement(TaskStatusMark, { task: (input.data as { task: string }).task }),
// Tabs of tasks deleted meanwhile are closed when the tasks load (`connect`).
restorable: true,
// Rebuilding a long conversation on every switch back is slow.
+1 -1
View File
@@ -297,7 +297,7 @@ function onEvent(e: AgentEvent) {
case "task": {
const prev = s.tasks.get(e.task.id);
useAgents.setState({ tasks: new Map(s.tasks).set(e.task.id, e.task) });
if (prev && (prev.title !== e.task.title || prev.status !== e.task.status)) notifyTabTitlesChanged();
if (prev && prev.title !== e.task.title) notifyTabTitlesChanged();
if (e.task.status === "archived") setUnread(e.task.id, false);
else if (attentionChange(prev?.status, e.task.status)) markUnread(e.task.id);
notifyChange(prev, e.task);
+3 -53
View File
@@ -253,13 +253,7 @@ describe("run config → launch config", () => {
it("delve: debug mode for run, test mode for test", () => {
const run = launchConfigFor(rc({ kind: "go", command: "go", args: ["run", "."], debugAdapter: "delve" }), "/p")!;
expect(run.program).toBe("/p");
expect(extra(run)).toMatchObject({
mode: "debug",
__goTargets: ["."],
__goExecutable: "go",
__goToolArgs: [],
__goUseTags: true,
});
expect(extra(run)).toEqual({ mode: "debug", __goToolArgs: [], __goUseTags: true });
const sub = launchConfigFor(
rc({ kind: "go", command: "go", args: ["run", "./cmd/api"], debugAdapter: "delve" }),
"/p",
@@ -269,8 +263,8 @@ describe("run config → launch config", () => {
rc({ kind: "go", command: "go", args: ["test", "./..."], debugAdapter: "delve" }),
"/p",
)!;
expect(extra(test)).toMatchObject({ mode: "test", __goTargets: ["./..."], __goToolArgs: [], __goUseTags: true });
expect(test.program).toBe("/p/...");
expect(extra(test)).toEqual({ mode: "test", __goToolArgs: [], __goUseTags: true });
expect(test.program).toBe("/p");
// A gutter test / subtest: the package, and `-run` for the test binary.
const one = launchConfigFor(
rc({
@@ -425,50 +419,6 @@ it("netcoredbg launches the evaluated DLL with resolved profile arguments, env a
expect(launchConfigFor({ ...c, params: {} }, "/repo")).toBeNull();
});
describe("Go debug target resolution inputs", () => {
it("preserves import paths and multiple source files for Go to resolve", () => {
const imported = launchConfigFor(
rc({ kind: "go", command: "/sdk/bin/go", args: ["test", "example.com/app/calc"], debugAdapter: "delve" }),
"/p",
)!;
expect(imported.program).toBe("example.com/app/calc");
expect(extra(imported)).toMatchObject({
__goTargets: ["example.com/app/calc"],
__goExecutable: "/sdk/bin/go",
__goBuildDir: "/p",
});
const files = launchConfigFor(
rc({ kind: "go", command: "go", args: ["run", "main.go", "helper.go", "hello"], debugAdapter: "delve" }),
"/p",
)!;
expect(files.args).toEqual(["hello"]);
expect(extra(files).__goTargets).toEqual(["main.go", "helper.go"]);
const packages = launchConfigFor(
rc({ kind: "go", command: "go", args: ["build", "./a", "./b"], debugAdapter: "delve" }),
"/p",
)!;
expect(extra(packages).__goTargets).toEqual(["./a", "./b"]);
});
it("keeps a typed recursive package target and the module's selected compiler", () => {
const launch = launchConfigFor(
rc({
kind: "go",
command: "./sdk/bin/go",
args: ["test", "./..."],
debugAdapter: "delve",
params: { goKind: "test", scope: "package", target: "./...", module: "backend" },
}),
"/p",
)!;
expect(extra(launch)).toMatchObject({
__goTargets: ["./..."],
__goBuildDir: "/p/backend",
__goExecutable: "/p/backend/sdk/bin/go",
});
});
});
it("raw and typed Go debug configurations inherit custom tags unless explicitly disabled", () => {
for (const typed of [false, true]) {
for (const useTags of [undefined, "true", "false"]) {
+6 -18
View File
@@ -16,7 +16,7 @@ function join(base: string, p: string) {
/** A command path with a directory part resolved against `cwd`; bare names stay for PATH lookup. */
function resolveCommand(cmd: string, cwd: string) {
return /[\\/]/.test(cmd) ? join(cwd, cmd) : cmd;
return cmd.includes("/") ? join(cwd, cmd) : cmd;
}
function splitAtDashDash(args: readonly string[]): [string[], string[]] {
@@ -225,9 +225,6 @@ function delveLaunch(rc: RunConfig, cwd: string, root: string): LaunchConfig {
dlvCwd: module,
...(p.scope === "file" ? { __goFiles: target.split("|").map((f) => join(module, f.trim())) } : {}),
__goToolArgs: toolArgs,
...(p.scope === "file" ? {} : { __goTargets: [target] }),
__goBuildDir: module,
__goExecutable: resolveCommand(rc.command, module),
__goUseTags: p.useTags !== "false",
...(p.outputDir ? { output: join(root, `${p.outputDir}/go-debug`) } : {}),
});
@@ -264,11 +261,9 @@ function delveLaunch(rc: RunConfig, cwd: string, root: string): LaunchConfig {
}
const selection =
test &&
/^-(?:test\.)?(run|skip|bench|benchtime|count|timeout|parallel|cpu|list|fuzz|fuzztime|fuzzminimizetime)(?:=(.*))?$/.exec(
a,
);
if (test && /^-(?:test\.)?(short|failfast|benchmem|fullpath|v)(?:=|$)/.test(a)) {
testFlags.push(a.startsWith("-test.") ? a : a.replace(/^-/, "-test."));
/^-(run|skip|bench|benchtime|count|timeout|parallel|cpu|list|fuzz|fuzztime|fuzzminimizetime)(?:=(.*))?$/.exec(a);
if (test && /^-(short|failfast|benchmem|fullpath|v)(?:=|$)/.test(a)) {
testFlags.push(a.replace(/^-/, "-test."));
} else if (selection) {
const value = selection[2] ?? rest[++i];
if (value !== undefined) testFlags.push(`-test.${selection[1]}`, value);
@@ -276,25 +271,18 @@ function delveLaunch(rc: RunConfig, cwd: string, root: string): LaunchConfig {
if (test && (a === "-json" || a === "-c")) continue;
flags.push(a);
if (!a.includes("=") && !boolFlags.has(a) && rest[i + 1] !== undefined) flags.push(rest[++i]!);
} else if (
sub === "run" &&
targets.length &&
!(a.endsWith(".go") && targets.every((target) => target.endsWith(".go")))
) {
} else if (!test && targets.length) {
programArgs = rest.slice(i);
break;
} else targets.push(a);
}
const target = targets[0] ?? ".";
out.program = !target.startsWith(".") && !isAbsolute(target) && !target.endsWith(".go") ? target : join(cwd, target);
out.program = target.endsWith("/...") ? cwd : join(cwd, target);
out.args = [...testFlags, ...programArgs];
out.extraJson = JSON.stringify({
mode: test ? "test" : "debug",
...(flags.length ? { buildFlags: formatArgs(flags) } : {}),
__goToolArgs: flags,
__goTargets: targets.length ? targets : ["."],
__goBuildDir: cwd,
__goExecutable: resolveCommand(rc.command, cwd),
__goUseTags: rc.params?.useTags !== "false",
});
return out;
+1 -15
View File
@@ -6,7 +6,6 @@ import { ToolWindowHeader, useOwnToolWindowHeader } from "@/workbench/toolwindow
import { usePlatform } from "@/platform/platformInfo";
import { commands, isTauri } from "@/platform/ipc";
import {
Badge,
dropZoneAt,
IconButton,
Input,
@@ -14,7 +13,6 @@ import {
Popup,
registerDropTarget,
showContextMenu,
Spinner,
SplitGroup,
SplitHandle,
SplitPanel,
@@ -358,19 +356,8 @@ function GroupTabs({ group }: { group: string }) {
const focusRaf = useRef(0);
const items: TabItem[] = tabs.map((t) => {
const session = sessions[t.session];
const status = session?.status;
const status = sessions[t.session]?.status;
const dead = status === "exited" || status === "error";
// Running / done marker (#2233): a command at work spins, a finished one
// shows a filled dot, an idle shell at the prompt shows none.
const busy = !!session?.commandRunning || status === "starting";
const mark = busy ? (
<Spinner size={10} variant="spokes" label="Running" />
) : dead ? (
<Badge dot tone={status === "error" ? "danger" : "neutral"}>
{status === "error" ? "Failed" : "Exited"}
</Badge>
) : null;
return {
id: t.id,
label: tabTitle(t, sessions),
@@ -378,7 +365,6 @@ function GroupTabs({ group }: { group: string }) {
italic: dead,
color: dead ? "var(--fg-subtle)" : undefined,
tooltip: tabTooltip(t, sessions),
trailing: mark ?? undefined,
};
});
+5 -2
View File
@@ -310,7 +310,9 @@
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 50%;
/* May use the whole subject cell when space allows (#2447); shrinks with
per-label ellipsis before crowding out the commit message. */
max-width: 100%;
overflow: hidden;
margin-left: auto;
}
@@ -328,7 +330,8 @@
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
max-width: 180px;
/* Sizes to content (#2447): truncates with ellipsis only when .refs runs
out of room; the full name stays available via the title tooltip. */
}
/* Outlined tag glyph in the ref's colour. */
.ref::before {
@@ -0,0 +1,32 @@
import { readFileSync } from "node:fs";
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { RefLabels } from "../GitLog";
const css = readFileSync("src/features/vcs/GitLog.module.css", "utf8");
const LONG_LOCAL = "feature-connection-storm-v2";
const LONG_REMOTE = "gitlab/feature-connection-storm-v2";
const REMOTES = new Set([LONG_REMOTE]);
describe("RefLabels long branch names (#2447)", () => {
it("renders full names with title tooltips in regular mode", () => {
render(<RefLabels refs={[LONG_LOCAL, LONG_REMOTE]} remoteNames={REMOTES} />);
expect(screen.getByTitle(LONG_LOCAL)).toHaveTextContent(LONG_LOCAL);
expect(screen.getByTitle(LONG_REMOTE)).toHaveTextContent(LONG_REMOTE);
});
it("absorbs the remote into the local label in compact mode", () => {
render(<RefLabels refs={[LONG_LOCAL, LONG_REMOTE]} compact remoteNames={REMOTES} />);
expect(screen.getByTitle(`${LONG_LOCAL}, ${LONG_REMOTE}`)).toHaveTextContent(`gitlab & ${LONG_LOCAL}`);
});
it("lets labels size to content: no fixed pixel cap, ellipsis and clipping kept", () => {
const refBlocks = [...css.matchAll(/\.ref\s*\{([^}]*)\}/g)].map((m) => m[1]);
expect(refBlocks.length).toBeGreaterThan(0);
for (const block of refBlocks) expect(block).not.toMatch(/max-width:\s*\d+px/);
expect(refBlocks[0]).toMatch(/text-overflow:\s*ellipsis/);
const refsBlock = css.match(/\.refs\s*\{([^}]*)\}/)?.[1] ?? "";
expect(refsBlock).toMatch(/overflow:\s*hidden/);
});
});
-2
View File
@@ -117,8 +117,6 @@ export interface EditorKindContribution {
component: ComponentType<{ input: EditorInput }>;
title: (input: EditorInput) => string;
icon?: (input: EditorInput) => string;
/** Small marker rendered in the tab's trailing slot (a status dot for live state). */
trailing?: (input: EditorInput) => ReactNode;
/**
* Its tabs are saved with the editor session and reopened with the project (their
* `data` must be plain JSON). The editor closes tabs whose target is gone itself.