Compare commits

...
Author SHA1 Message Date
l0ng-ai c5bccf020e fix(vcs): don't commit a file that was added to the index and then deleted
A file added with git add and then deleted from disk (AD) is no change
against HEAD, so the Commit view lists it nowhere. The commit plan skipped
it, but it stayed in the index copy the commit is made from, so committing
other files also committed the deleted file. Such staged entries are now
excluded like unchecked staged files.
2026-11-12 19:15:10 +08:00
l0ng-ai 55fcbf281a fix(editor): keep hover documentation inside its popup near the window edge (#2946)
Near the window edge CodeMirror caps the hover tooltip's height, but the documentation section kept its own 380px maximum and ran past the popup's border. The tooltip is now a flex column whose sections shrink and scroll within it.

Fixes #2910
2026-11-12 19:13:31 +08:00
4 changed files with 59 additions and 2 deletions

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
import { expect, test } from "@playwright/test";
/** Near the window edge CodeMirror caps a hover tooltip's height (#2910). */
test("hover sections scroll inside a height-capped tooltip", async ({ page }) => {
await page.goto("/");
const r = await page.evaluate(async () => {
const viewMod = await import("/node_modules/.vite/deps/@codemirror_view.js" as string);
const stateMod = await import("/node_modules/.vite/deps/@codemirror_state.js" as string);
const { lspTheme } = await import("/src/editor/lsp/theme.ts" as string);
const host = document.createElement("div");
host.style.cssText = "position:fixed;inset:0;z-index:99999;background:white";
document.body.append(host);
const view = new viewMod.EditorView({
state: stateMod.EditorState.create({ doc: "x", extensions: lspTheme }),
parent: host,
});
const tip = document.createElement("div");
tip.className = "cm-tooltip cm-tooltip-hover";
tip.style.cssText = "position:absolute;top:10px;left:10px;height:200px";
const sec = document.createElement("div");
sec.className = "cm-tooltip-section cm-lsp-hover";
sec.innerHTML = Array.from({ length: 40 }, (_, i) => `<p>line ${i}</p>`).join("");
tip.append(sec);
view.dom.append(tip);
const t = tip.getBoundingClientRect(),
s = sec.getBoundingClientRect();
return {
tip: Math.round(t.height),
secBottom: Math.round(s.bottom),
tipBottom: Math.round(t.bottom),
scrolls: sec.scrollHeight > sec.clientHeight,
};
});
expect(r.secBottom).toBeLessThanOrEqual(r.tipBottom);
expect(r.scrolls).toBe(true);
});
+4 -1
View File
@@ -192,7 +192,10 @@ export const lspTheme = EditorView.theme({
".cm-lsp-doc-signature": { marginTop: "0", color: "var(--fg-emphasis)" },
// --- hover / quick documentation / signature / hints ---------------------
".cm-tooltip.cm-tooltip-hover": { ...tooltipSurface },
// Near the window edge CodeMirror caps the tooltip's height to the space left: its sections then
// shrink and scroll inside the popup instead of running past its border.
".cm-tooltip.cm-tooltip-hover": { ...tooltipSurface, display: "flex", flexDirection: "column" },
".cm-tooltip-hover > .cm-tooltip-section": { minHeight: 0, flexShrink: 1 },
".cm-lsp-hover": { maxWidth: "580px", maxHeight: "380px", overflow: "auto", padding: "10px 14px" },
".cm-lsp-hover-diagnostics + .cm-lsp-markdown": {
borderTop: "1px solid var(--border-popup)",
@@ -283,6 +283,19 @@ describe("commit inclusion", () => {
]);
});
it("leaves out a file added to the index and then deleted, which no group lists", () => {
const files = [fs("src/A.java", null, "modified"), fs("src/Gone.java", "added", "deleted")];
expect(
buildChangeTree(files, "/r", false)
.flatMap((g) => g.children ?? [])
.map((n) => n.label),
).toEqual(["A.java"]);
const plan = planCommit(files, new Map());
expect(plan.stage).toEqual(["/r/src/A.java"]);
expect(plan.exclude).toEqual(["/r/src/Gone.java"]);
expect(plan.included.map((f) => f.relPath)).toEqual(["src/A.java"]);
});
it("aggregates checkbox state for groups", () => {
const checks = new Map<string, CheckState>();
expect(aggregateCheck([status[0]!, status[3]!], checks)).toMatchObject({ checked: true, indeterminate: false });
+6 -1
View File
@@ -390,7 +390,12 @@ export function renamedFromPath(fs: FileStatus): string | null {
export function planCommit(status: readonly FileStatus[], checks: ReadonlyMap<string, CheckState>): CommitPlan {
const plan: CommitPlan = { stage: [], exclude: [], included: [], partial: [] };
for (const fs of status) {
if (!groupOf(fs)) continue;
if (!groupOf(fs)) {
// Listed nowhere, yet staged: added to the index, then deleted from disk. It is no
// change against HEAD, so the commit must not pick up the staged addition.
if (fs.staged && fs.staged !== "conflicted" && fs.unstaged !== "ignored") plan.exclude.push(stripSlash(fs.path));
continue;
}
const c = checkOf(fs, checks);
const path = stripSlash(fs.path);
if (c === "none") {