fix(ui): Match PyCharm project view, matched tags and text weight

- Project widget initials follow IDEA's AvatarUtils.initials ("idea" → "I").
- "Show directories with changed descendants" is off by default, as in IDEA.
- FileEntry gains `excluded`: only excluded folders get the excluded icon and
  dimming; git-ignored ones keep the plain icon and the ignored colour.
- `.github` folders get IDEA's GitHub folder icon.
- Matched tag names are never bold and use their own background
  (MATCHED_TAG_NAME); matched braces are bold in dark schemes only.
- Revert Greyscale antialiasing to `antialiased` for UI and editor; `auto`
  rendered text heavier than PyCharm.

Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
Цей коміт міститься в:
l0ng-ai committed 2026-10-24 22:56:48 +08:00
1 джерело cacddee19d
коміт 046fd37064
26 files changed
+143 -36

No files matched your search

+3 -1
Переглянути файл
@@ -234,6 +234,8 @@ const Row = memo(function Row({
const isEditing =
!!editing && (node.placeholder ? editing.kind === "new" : editing.kind === "rename" && editing.key === node.key);
const faded = node.ignored || deco.faded;
// IDEA dims the icon of excluded folders only; git-ignored ones just get the ignored colour.
const dimIcon = node.excluded;
return (
<span
className={clsx(styles.content, cut && styles.cut, dropTarget && styles.dropTarget)}
@@ -244,7 +246,7 @@ const Row = memo(function Row({
onClick={(e) => onClick(node, e)}
onPointerDown={(e) => onPointerDown(node, e)}
>
<Icon name={icon} size={16} className={clsx(styles.icon, node.dir && styles.folderIcon, faded && styles.faded)} />
<Icon name={icon} size={16} className={clsx(styles.icon, node.dir && styles.folderIcon, dimIcon && styles.faded)} />
{isEditing && editing ? (
<InlineEditor
editing={editing}
+1
Переглянути файл
@@ -50,6 +50,7 @@ function nodesFrom(arg: unknown): ProjectNode[] {
root: p === useProjectTree.getState().root,
entry: null,
ignored: false,
excluded: false,
symlink: false,
};
});
+1
Переглянути файл
@@ -19,6 +19,7 @@ const node = (path: string, dir = false, root = false): ProjectNode => ({
root,
entry: null,
ignored: false,
excluded: false,
symlink: false,
});
+1
Переглянути файл
@@ -21,6 +21,7 @@ const entry = (path: string, kind: "file" | "dir" = "file"): FileEntry => ({
size: 0,
mtimeMs: 1,
ignored: false,
excluded: false,
hidden: false,
});
+9 -5
Переглянути файл
@@ -1,6 +1,6 @@
/** Folder icon kind in the project view, from the effective roots. */
import { rootRole, type FolderRole, type RootFolder } from "@/platform/languageSettings";
import { relativePath } from "@/platform/project";
import { basename, relativePath } from "@/platform/project";
import type { FolderKind } from "@/ui";
import type { ProjectNode } from "./model";
@@ -12,13 +12,17 @@ const ROLE_KIND: Record<FolderRole, FolderKind> = {
excluded: "excluded",
};
/** Project Structure marks and roots detected from build files; ignored entries look excluded. */
/**
* Project Structure marks and roots detected from build files. Like IDEA, only
* excluded folders get the excluded icon; git-ignored ones keep the plain one.
*/
export function folderKind(
node: Pick<ProjectNode, "path" | "ignored">,
node: Pick<ProjectNode, "path" | "excluded">,
roots: readonly RootFolder[],
root: string | null,
): FolderKind {
if (node.ignored) return "excluded";
if (node.excluded) return "excluded";
const role = root ? rootRole(roots, relativePath(root, node.path)) : null;
return role ? ROLE_KIND[role] : "plain";
if (role) return ROLE_KIND[role];
return basename(node.path) === ".github" ? "github" : "plain";
}
+3 -1
Переглянути файл
@@ -78,13 +78,15 @@ export class MemoryFs implements ProjectFs {
.map((p) => {
const c = this.nodes.get(p)!;
const name = nameOf(p);
const excluded = [...this.ignored].some((i) => p === i || isInside(p, i));
return {
name,
path: p,
kind: c.kind,
size: c.kind === "file" ? c.size : 0,
mtimeMs: 0,
ignored: [...this.ignored].some((i) => p === i || isInside(p, i)),
ignored: excluded,
excluded,
hidden: name.startsWith("."),
} satisfies FileEntry;
})
+1 -1
Переглянути файл
@@ -13,7 +13,7 @@ import {
const entry = (path: string, kind: "file" | "dir" = "file", extra: Partial<FileEntry> = {}): FileEntry => {
const name = path.slice(path.lastIndexOf("/") + 1);
return { name, path, kind, size: 0, mtimeMs: 1, ignored: 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" };
+8 -1
Переглянути файл
@@ -36,6 +36,8 @@ export interface ProjectNode {
root: boolean;
entry: FileEntry | null;
ignored: boolean;
/** Excluded by the exclude settings / Project Structure (not just git-ignored). */
excluded: boolean;
symlink: boolean;
/** Placeholder row for the inline "new file / folder" editor. */
placeholder?: "file" | "dir";
@@ -120,6 +122,7 @@ export function buildModel(
root: true,
entry: null,
ignored: false,
excluded: false,
symlink: false,
};
byKey.set(rootNode.key, rootNode);
@@ -137,6 +140,7 @@ export function buildModel(
root: false,
entry,
ignored: entry.ignored,
excluded: entry.excluded,
symlink: entry.kind === "symlink",
};
}
@@ -151,7 +155,7 @@ export function buildModel(
const visible = visibleEntries(listing, opts);
if (visible.length !== 1) break;
const only = visible[0]!;
if (!isDirEntry(only) || only.ignored !== entry.ignored) break;
if (!isDirEntry(only) || only.ignored !== entry.ignored || only.excluded !== entry.excluded) break;
if (placeholder && placeholder.parent === only.path) break;
end = only;
chain.push(only.path);
@@ -168,6 +172,7 @@ export function buildModel(
root: false,
entry: end,
ignored: entry.ignored,
excluded: entry.excluded,
symlink: entry.kind === "symlink",
};
};
@@ -186,6 +191,7 @@ export function buildModel(
root: false,
entry: null,
ignored: false,
excluded: false,
symlink: false,
placeholder: placeholder.kind,
};
@@ -312,6 +318,7 @@ const sameEntry = (a: FileEntry, b: FileEntry) =>
a.size === b.size &&
a.mtimeMs === b.mtimeMs &&
a.ignored === b.ignored &&
a.excluded === b.excluded &&
a.hidden === b.hidden;
/**
+1
Переглянути файл
@@ -11,6 +11,7 @@ const node = (path: string, dir = false): ProjectNode => ({
root: path === "/p",
entry: null,
ignored: false,
excluded: false,
symlink: false,
});
+2 -1
Переглянути файл
@@ -26,13 +26,14 @@ const MAVEN: RootFolder[] = [
describe("folder icons", () => {
it("follow detected and marked roots", () => {
const kind = (rel: string, ignored = false) => folderKind({ path: `/p/${rel}`, ignored }, MAVEN, "/p");
const kind = (rel: string, excluded = false) => folderKind({ path: `/p/${rel}`, excluded }, MAVEN, "/p");
expect(kind("src/main/java")).toBe("source");
expect(kind("src/test/java")).toBe("test");
expect(kind("src/test/resources")).toBe("testResources");
expect(kind("target")).toBe("excluded");
expect(kind("src")).toBe("plain");
expect(kind("docs", true)).toBe("excluded");
expect(kind(".github")).toBe("github");
});
});