mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
feat(vcs): show branch and incoming/outgoing commits beside repository roots (#2605)
Settings > Version Control > Git > "Show branch and incoming/outgoing commits in the Project view" (off by default) shows each repository root's branch and its outgoing/incoming commit counts beside the root in the Project view. Git is queried only after a change inside .git or when the set of roots changes, one round at a time. Co-authored-by: pdasilem <74730932+pdasilem@users.noreply.github.com>
This commit is contained in:
@@ -43,6 +43,7 @@ impl Default for VcsSettings {
|
||||
pub struct GitSettings {
|
||||
/// Show blame after the current editor line.
|
||||
pub inline_blame: bool,
|
||||
pub show_branch_in_project_view: bool,
|
||||
/// Path to the git executable; empty = auto-detect.
|
||||
pub executable: String,
|
||||
/// Default of Update Project: `merge` | `rebase`.
|
||||
@@ -74,6 +75,7 @@ impl Default for GitSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inline_blame: false,
|
||||
show_branch_in_project_view: false,
|
||||
executable: String::new(),
|
||||
update_method: "merge".into(),
|
||||
auto_update_on_push_rejected: false,
|
||||
|
||||
@@ -4732,6 +4732,7 @@ export type GitOperation =
|
||||
export type GitSettings = {
|
||||
/** Show blame after the current editor line. */
|
||||
inlineBlame?: boolean,
|
||||
showBranchInProjectView?: boolean,
|
||||
/** Path to the git executable; empty = auto-detect. */
|
||||
executable?: string,
|
||||
/** Default of Update Project: `merge` | `rebase`. */
|
||||
|
||||
@@ -66,6 +66,22 @@
|
||||
text-decoration-skip-ink: none;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.suffix {
|
||||
display: inline-flex;
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
margin-left: 6px;
|
||||
overflow: hidden;
|
||||
color: var(--fg-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.suffixSegment {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
}
|
||||
.detail {
|
||||
min-width: 0;
|
||||
margin-left: 4px;
|
||||
|
||||
@@ -3,7 +3,12 @@ import { act, fireEvent, render, screen, waitFor, within } from "@testing-librar
|
||||
import { emitLocalIdeEvent } from "@/platform/ipc";
|
||||
import { installRepeatedDoubleClick } from "@/platform/doubleClick";
|
||||
import { getContext } from "@/platform/context";
|
||||
import { provideEditorService, registerProjectViewRoot, type EditorService } from "@/workbench/contributions";
|
||||
import {
|
||||
provideEditorService,
|
||||
registerProjectViewRoot,
|
||||
setFileDecorations,
|
||||
type EditorService,
|
||||
} from "@/workbench/contributions";
|
||||
import { activate } from "./index";
|
||||
import { setOsClipboardForTests } from "./clipboard";
|
||||
import { MemoryFs, setProjectFsForTests } from "./fs";
|
||||
@@ -736,3 +741,35 @@ it("hides the path beside workspace roots when Show Root Paths is off, keeping i
|
||||
expect(root?.getAttribute("title")).toBe("/a/service");
|
||||
act(() => setViewOption("showRootPaths", true));
|
||||
});
|
||||
|
||||
it("shows a decoration's suffix after the root name, with icons and struck-out segments", async () => {
|
||||
const { row } = setup();
|
||||
await waitFor(() => expect(row("acme")).toBeTruthy());
|
||||
act(() =>
|
||||
setFileDecorations(
|
||||
"test-suffix",
|
||||
new Map([
|
||||
[
|
||||
ROOT,
|
||||
{
|
||||
suffixColor: "var(--accent)",
|
||||
suffix: [
|
||||
{ text: "main" },
|
||||
{ text: "/" },
|
||||
{ text: "0", strike: true, label: "Up to date with origin/main" },
|
||||
{ text: "2", icon: "vcs-outgoing", label: "2 outgoing commits" },
|
||||
],
|
||||
},
|
||||
],
|
||||
]),
|
||||
),
|
||||
);
|
||||
await waitFor(() => expect(row("acme").textContent).toContain("main/02"));
|
||||
const root = row("acme");
|
||||
expect(root.querySelector("s")?.textContent).toBe("0");
|
||||
expect(root.querySelector("s")?.closest("[style]")?.getAttribute("style")).toContain("var(--accent)");
|
||||
expect(within(root).getByLabelText("2 outgoing commits").querySelector("svg")).not.toBeNull();
|
||||
expect(within(root).getByLabelText("Up to date with origin/main").textContent).toBe("0");
|
||||
act(() => setFileDecorations("test-suffix", new Map()));
|
||||
await waitFor(() => expect(row("acme").textContent).not.toContain("main/"));
|
||||
});
|
||||
|
||||
@@ -26,7 +26,12 @@ import { nativeDropPoint } from "@/platform/nativeDrop";
|
||||
import type { RootFolder } from "@/platform/languageSettings";
|
||||
import { useDetectedRoots, useEffectiveRoots } from "@/platform/projectRoots";
|
||||
import { macStroke, strokeOf } from "@/platform/keymap";
|
||||
import { OWNS_FILE_DROP, prepareMenuGroups, useFileDecoration } from "@/workbench/contributions";
|
||||
import {
|
||||
OWNS_FILE_DROP,
|
||||
prepareMenuGroups,
|
||||
useFileDecoration,
|
||||
type DecorationSegment,
|
||||
} from "@/workbench/contributions";
|
||||
import {
|
||||
FILES_DRAG,
|
||||
HighlightedText,
|
||||
@@ -345,6 +350,7 @@ const Row = memo(function Row({
|
||||
indices={matches}
|
||||
tone="accent"
|
||||
/>
|
||||
{deco.suffix && <DecorationSuffix segments={deco.suffix} color={deco.suffixColor} />}
|
||||
{detail && <span className={styles.detail}>{detail}</span>}
|
||||
{deco.badge && <span className={styles.badge}>{deco.badge}</span>}
|
||||
</>
|
||||
@@ -353,6 +359,33 @@ const Row = memo(function Row({
|
||||
);
|
||||
});
|
||||
|
||||
interface DecorationSuffixProps {
|
||||
segments: readonly DecorationSegment[];
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function DecorationSuffix({ segments, color }: DecorationSuffixProps) {
|
||||
return (
|
||||
<span className={styles.suffix} style={color ? { color } : undefined}>
|
||||
{segments.map((s, i) => {
|
||||
const text = s.strike ? <s>{s.text}</s> : s.text;
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={styles.suffixSegment}
|
||||
role={s.label ? "img" : undefined}
|
||||
aria-label={s.label}
|
||||
title={s.label}
|
||||
>
|
||||
{text}
|
||||
{s.icon && <Icon name={s.icon} size={12} />}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Member rows ("Show Members")
|
||||
|
||||
|
||||
@@ -494,6 +494,11 @@ export const VCS_PAGES: PageSpec[] = [
|
||||
component: GitExecutableRow,
|
||||
},
|
||||
{ kind: "toggle", key: "vcs.git.inlineBlame", label: "Show inline blame for the current line" },
|
||||
{
|
||||
kind: "toggle",
|
||||
key: "vcs.git.showBranchInProjectView",
|
||||
label: "Show branch and incoming/outgoing commits in the Project view",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BranchInfo } from "@/bindings";
|
||||
|
||||
const ipc = vi.hoisted(() => ({ branchInfo: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return { ...real, commands: { ...real.commands, vcsBranchInfo: ipc.branchInfo } };
|
||||
});
|
||||
|
||||
import { emitLocalIdeEvent } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { getFileDecoration } from "@/workbench/contributions";
|
||||
import { installRootSync, rootSyncSuffix } from "../rootSync";
|
||||
import { useVcs } from "../store";
|
||||
|
||||
const info = (head: string | null, upstream: string | null, ahead = 0, behind = 0): BranchInfo => ({
|
||||
head,
|
||||
headCommit: "c1",
|
||||
upstream,
|
||||
ahead,
|
||||
behind,
|
||||
operation: null,
|
||||
});
|
||||
|
||||
describe("rootSyncSuffix", () => {
|
||||
it("strikes out zero when the branch matches its upstream", () => {
|
||||
expect(rootSyncSuffix(info("main", "origin/main"))).toEqual([
|
||||
{ text: "main" },
|
||||
{ text: "/" },
|
||||
{ text: "0", strike: true, label: "Up to date with origin/main" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows outgoing then incoming commits with their icons", () => {
|
||||
expect(rootSyncSuffix(info("main", "origin/main", 1, 3))).toEqual([
|
||||
{ text: "main" },
|
||||
{ text: "/" },
|
||||
{ text: "1", icon: "vcs-outgoing", label: "1 outgoing commits" },
|
||||
{ text: "3", icon: "vcs-incoming", label: "3 incoming commits" },
|
||||
]);
|
||||
expect(rootSyncSuffix(info("main", "origin/main", 0, 2))).toEqual([
|
||||
{ text: "main" },
|
||||
{ text: "/" },
|
||||
{ text: "2", icon: "vcs-incoming", label: "2 incoming commits" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows only the branch without an upstream", () => {
|
||||
expect(rootSyncSuffix(info("feature", null))).toEqual([{ text: "feature" }]);
|
||||
});
|
||||
|
||||
it("shows nothing for a detached HEAD", () => {
|
||||
expect(rootSyncSuffix(info(null, null))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("installRootSync", () => {
|
||||
const enable = (on: boolean) =>
|
||||
useProject.setState({ settings: { vcs: { git: { showBranchInProjectView: on } } } } as never);
|
||||
const gitChanged = () => emitLocalIdeEvent({ type: "vcsChanged", root: "/p" });
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 400));
|
||||
let dispose: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
ipc.branchInfo.mockReset().mockImplementation(async (root: string) => ({
|
||||
status: "ok",
|
||||
data: root === "/p" ? info("main", "origin/main", 1, 0) : info("dev", null),
|
||||
}));
|
||||
useVcs.setState({ available: true, roots: ["/p", "/p/lib"] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dispose?.();
|
||||
useProject.setState({ settings: null });
|
||||
});
|
||||
|
||||
it("decorates every repository root when the setting is on", async () => {
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1"));
|
||||
expect(getFileDecoration("/p/lib")?.suffix).toEqual([{ text: "dev" }]);
|
||||
expect(getFileDecoration("/p")?.suffixColor).toBe("var(--accent)");
|
||||
expect(ipc.branchInfo).toHaveBeenCalledWith("/p");
|
||||
expect(ipc.branchInfo).toHaveBeenCalledWith("/p/lib");
|
||||
});
|
||||
|
||||
it("shows nothing and queries nothing while the setting is off", async () => {
|
||||
enable(false);
|
||||
dispose = installRootSync();
|
||||
gitChanged();
|
||||
await settle();
|
||||
expect(ipc.branchInfo).not.toHaveBeenCalled();
|
||||
expect(getFileDecoration("/p")?.suffix).toBeUndefined();
|
||||
});
|
||||
|
||||
it("queries again after a change inside .git, such as a fetch, push or commit", async () => {
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1"));
|
||||
ipc.branchInfo.mockImplementation(async (root: string) => ({
|
||||
status: "ok",
|
||||
data: root === "/p" ? info("main", "origin/main", 1, 4) : info("dev", null),
|
||||
}));
|
||||
gitChanged();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[3]?.text).toBe("4"));
|
||||
});
|
||||
|
||||
it("does not query git when only the status refreshes or worktree files change", async () => {
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledTimes(2));
|
||||
useVcs.setState((s) => ({ version: s.version + 1 }));
|
||||
emitLocalIdeEvent({ type: "fsChanged", created: [], modified: ["/p/a.txt"], removed: [] } as never);
|
||||
await settle();
|
||||
expect(ipc.branchInfo).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("queries the repositories again when the set of roots changes", async () => {
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledTimes(2));
|
||||
useVcs.setState({ roots: ["/p", "/p/lib", "/p/web"] });
|
||||
await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledWith("/p/web"));
|
||||
});
|
||||
|
||||
it("clears the decorations when the setting is switched off", async () => {
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeDefined());
|
||||
enable(false);
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeUndefined());
|
||||
});
|
||||
|
||||
it("skips a repository whose branch cannot be read", async () => {
|
||||
ipc.branchInfo.mockImplementation(async (root: string) =>
|
||||
root === "/p" ? { status: "error", error: "boom" } : { status: "ok", data: info("dev", null) },
|
||||
);
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p/lib")?.suffix).toEqual([{ text: "dev" }]));
|
||||
expect(getFileDecoration("/p")?.suffix).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps a repository's previous suffix when a later read fails", async () => {
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1"));
|
||||
ipc.branchInfo.mockImplementation(async (root: string) =>
|
||||
root === "/p" ? { status: "error", error: "index.lock" } : { status: "ok", data: info("dev", "origin/dev") },
|
||||
);
|
||||
gitChanged();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p/lib")?.suffix?.[2]?.text).toBe("0"));
|
||||
expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1");
|
||||
});
|
||||
|
||||
it("runs one query round at a time and coalesces Git changes made meanwhile", async () => {
|
||||
const pending: Array<() => void> = [];
|
||||
ipc.branchInfo.mockImplementation(
|
||||
(root: string) =>
|
||||
new Promise((resolve) =>
|
||||
pending.push(() => resolve({ status: "ok", data: info(root === "/p" ? "main" : "dev", null) })),
|
||||
),
|
||||
);
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
expect(ipc.branchInfo).toHaveBeenCalledTimes(2);
|
||||
gitChanged();
|
||||
await settle();
|
||||
gitChanged();
|
||||
await settle();
|
||||
expect(ipc.branchInfo).toHaveBeenCalledTimes(2);
|
||||
pending.splice(0).forEach((done) => done());
|
||||
await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledTimes(4));
|
||||
pending.splice(0).forEach((done) => done());
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toEqual([{ text: "main" }]));
|
||||
await settle();
|
||||
expect(ipc.branchInfo).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("does not publish results that arrive after disposal", async () => {
|
||||
let finish = () => {};
|
||||
ipc.branchInfo.mockImplementation(
|
||||
() => new Promise((resolve) => (finish = () => resolve({ status: "ok", data: info("main", null) }))),
|
||||
);
|
||||
useVcs.setState({ roots: ["/p"] });
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
dispose();
|
||||
finish();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(getFileDecoration("/p")?.suffix).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears the decorations when the project has no repositories any more", async () => {
|
||||
enable(true);
|
||||
dispose = installRootSync();
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeDefined());
|
||||
useVcs.setState({ available: false, roots: [] });
|
||||
await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeUndefined());
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@ import { installFileConfirmations } from "./confirmations";
|
||||
import { installIncomingCheck } from "./incoming";
|
||||
import { closeLogTab, installGitLog, selectGitTab, useGitLog } from "./logStore";
|
||||
import { installVcsStore, onConflictsAppeared, onConflictsCleared, useVcs } from "./store";
|
||||
import { installRootSync } from "./rootSync";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
/** The Resolve/Continue/Abort buttons exist in the title bar only while there is something to resolve. */
|
||||
@@ -213,6 +214,7 @@ export function activate(): () => void {
|
||||
useCommit.setState({ signOff: workflowSettings().vcs.git.signOff });
|
||||
const disposers = [
|
||||
installVcsStore(),
|
||||
installRootSync(),
|
||||
installGitLog(),
|
||||
installDiagnosticsTracking(),
|
||||
installPreparedMessage(),
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { BranchInfo } from "@/bindings";
|
||||
import { call, commands, onIdeEvent } from "@/platform/ipc";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { onWorkflowSettingsChange, workflowSettings } from "@/platform/workflowSettings";
|
||||
import { setFileDecorations, type DecorationSegment, type FileDecoration } from "@/workbench/contributions";
|
||||
import { useVcs } from "./store";
|
||||
|
||||
const PROVIDER = "vcs-root-sync";
|
||||
const GIT_CHANGE_DELAY_MS = 250;
|
||||
const SUFFIX_COLOR = "var(--accent)";
|
||||
|
||||
export function rootSyncSuffix(info: BranchInfo): DecorationSegment[] | null {
|
||||
const { head, upstream, ahead, behind } = info;
|
||||
if (!head) return null;
|
||||
if (!upstream) return [{ text: head }];
|
||||
const counts: DecorationSegment[] = [];
|
||||
if (ahead > 0) counts.push({ text: String(ahead), icon: "vcs-outgoing", label: tx`${ahead} outgoing commits` });
|
||||
if (behind > 0) counts.push({ text: String(behind), icon: "vcs-incoming", label: tx`${behind} incoming commits` });
|
||||
if (!counts.length) counts.push({ text: "0", strike: true, label: tx`Up to date with ${upstream}` });
|
||||
return [{ text: head }, { text: "/" }, ...counts];
|
||||
}
|
||||
|
||||
async function branchInfoOf(root: string): Promise<[string, BranchInfo] | null> {
|
||||
try {
|
||||
return [root, await call(commands.vcsBranchInfo(root))];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sameDecorations(a: ReadonlyMap<string, FileDecoration>, b: ReadonlyMap<string, FileDecoration>): boolean {
|
||||
if (a.size !== b.size) return false;
|
||||
for (const [root, d] of a) if (JSON.stringify(d) !== JSON.stringify(b.get(root))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function installRootSync(): () => void {
|
||||
let disposed = false;
|
||||
let running = false;
|
||||
let pending = false;
|
||||
let shown: ReadonlyMap<string, FileDecoration> = new Map();
|
||||
|
||||
const publish = (next: ReadonlyMap<string, FileDecoration>) => {
|
||||
if (disposed || sameDecorations(next, shown)) return;
|
||||
shown = next;
|
||||
setFileDecorations(PROVIDER, new Map(next));
|
||||
};
|
||||
|
||||
const update = async () => {
|
||||
const { available, roots } = useVcs.getState();
|
||||
if (!available || !workflowSettings().vcs.git.showBranchInProjectView) return publish(new Map());
|
||||
const infos = await Promise.all(roots.map(branchInfoOf));
|
||||
const next = new Map<string, FileDecoration>();
|
||||
roots.forEach((root, i) => {
|
||||
const entry = infos[i];
|
||||
const suffix = entry ? rootSyncSuffix(entry[1]) : shown.get(root)?.suffix;
|
||||
if (suffix) next.set(root, { suffix, suffixColor: SUFFIX_COLOR });
|
||||
});
|
||||
publish(next);
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (running) {
|
||||
pending = true;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
void (async () => {
|
||||
try {
|
||||
do {
|
||||
pending = false;
|
||||
await update();
|
||||
} while (pending && !disposed);
|
||||
} finally {
|
||||
running = false;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const scheduleSoon = () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(schedule, GIT_CHANGE_DELAY_MS);
|
||||
};
|
||||
|
||||
const unsubVcs = useVcs.subscribe((s, prev) => {
|
||||
if (s.available !== prev.available || s.roots.join("\n") !== prev.roots.join("\n")) schedule();
|
||||
});
|
||||
const offGit = onIdeEvent("vcsChanged", scheduleSoon);
|
||||
const offSettings = onWorkflowSettingsChange((s) => s.vcs.git.showBranchInProjectView, schedule);
|
||||
schedule();
|
||||
return () => {
|
||||
disposed = true;
|
||||
clearTimeout(timer);
|
||||
unsubVcs();
|
||||
offGit();
|
||||
offSettings();
|
||||
setFileDecorations(PROVIDER, new Map());
|
||||
};
|
||||
}
|
||||
@@ -83,6 +83,7 @@ export const CLIENT_SETTINGS_KEYS = [
|
||||
"run.showOnStart",
|
||||
"vcs.showDirtyDirectories",
|
||||
"vcs.git.inlineBlame",
|
||||
"vcs.git.showBranchInProjectView",
|
||||
"vcs.git.showPushDialog",
|
||||
"vcs.git.pushDialogOnlyProtected",
|
||||
"vcs.git.combineStashAndShelf",
|
||||
|
||||
@@ -983,5 +983,7 @@
|
||||
"Import from Project": "Import from Project",
|
||||
"No new MCP servers in the project's .mcp.json or .vscode/mcp.json.": "No new MCP servers in the project's .mcp.json or .vscode/mcp.json.",
|
||||
"Imported 1 MCP server from the project.": "Imported 1 MCP server from the project.",
|
||||
"Imported {count} MCP servers from the project.": "Imported {count} MCP servers from the project."
|
||||
"Imported {count} MCP servers from the project.": "Imported {count} MCP servers from the project.",
|
||||
"Show branch and incoming/outgoing commits in the Project view": "Show branch and incoming/outgoing commits in the Project view",
|
||||
"Up to date with {upstream}": "Up to date with {upstream}"
|
||||
}
|
||||
|
||||
@@ -7315,5 +7315,7 @@
|
||||
"Import from Project": "从项目导入",
|
||||
"No new MCP servers in the project's .mcp.json or .vscode/mcp.json.": "项目的 .mcp.json 和 .vscode/mcp.json 中没有新的 MCP 服务器。",
|
||||
"Imported 1 MCP server from the project.": "已从项目导入 1 个 MCP 服务器。",
|
||||
"Imported {count} MCP servers from the project.": "已从项目导入 {count} 个 MCP 服务器。"
|
||||
"Imported {count} MCP servers from the project.": "已从项目导入 {count} 个 MCP 服务器。",
|
||||
"Show branch and incoming/outgoing commits in the Project view": "在项目视图中显示分支及传入/传出提交",
|
||||
"Up to date with {upstream}": "已与 {upstream} 同步"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface IssueLinkValid {
|
||||
|
||||
export interface GitSettingsValid {
|
||||
inlineBlame: boolean;
|
||||
showBranchInProjectView: boolean;
|
||||
executable: string;
|
||||
updateMethod: UpdateMethod;
|
||||
autoUpdateOnPushRejected: boolean;
|
||||
@@ -190,6 +191,7 @@ export const DEFAULT_WORKFLOW: WorkflowSettings = {
|
||||
historyLimit: 1000,
|
||||
git: {
|
||||
inlineBlame: false,
|
||||
showBranchInProjectView: false,
|
||||
executable: "",
|
||||
updateMethod: "merge",
|
||||
autoUpdateOnPushRejected: false,
|
||||
@@ -321,6 +323,7 @@ export function validateVcs(raw: unknown): VcsSettingsValid {
|
||||
historyLimit: num(v.historyLimit, d.historyLimit, L.historyLimit, true),
|
||||
git: {
|
||||
inlineBlame: bool(g.inlineBlame, d.git.inlineBlame),
|
||||
showBranchInProjectView: bool(g.showBranchInProjectView, d.git.showBranchInProjectView),
|
||||
executable: text(g.executable, d.git.executable).trim(),
|
||||
updateMethod: oneOf(g.updateMethod, ["merge", "rebase"] as const, d.git.updateMethod),
|
||||
autoUpdateOnPushRejected: bool(g.autoUpdateOnPushRejected, d.git.autoUpdateOnPushRejected),
|
||||
|
||||
@@ -826,9 +826,18 @@ export function showFindResults(source: FindResultsSource) {
|
||||
// tree, editor tabs, Search Everywhere, etc. Providers (VCS status, problems)
|
||||
// push decorations into this store; consumers read with `useFileDecoration`.
|
||||
|
||||
export interface DecorationSegment {
|
||||
text: string;
|
||||
icon?: string;
|
||||
strike?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface FileDecoration {
|
||||
/** CSS colour, normally a token like `var(--vcs-modified)`. */
|
||||
color?: string;
|
||||
suffix?: readonly DecorationSegment[];
|
||||
suffixColor?: string;
|
||||
/** CSS colour tinting the editor tab (a data source's colour on its console). */
|
||||
tint?: string;
|
||||
/** Short badge text, e.g. a problem count. */
|
||||
|
||||
Reference in New Issue
Block a user