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

+24 -3
Переглянути файл
@@ -2,8 +2,9 @@
* The base extension set of every text editor, with compartments for the
* parts that change at runtime (language, settings, contributions).
*/
import { Compartment, EditorState, Prec, type Extension } from "@codemirror/state";
import { Compartment, EditorState, Prec, type Extension, type Range } from "@codemirror/state";
import {
Decoration,
EditorView,
dropCursor,
highlightActiveLineGutter,
@@ -11,7 +12,14 @@ import {
keymap,
lineNumbers,
} from "@codemirror/view";
import { bracketMatching, indentOnInput, indentUnit, syntaxHighlighting, type LanguageSupport } from "@codemirror/language";
import {
bracketMatching,
indentOnInput,
indentUnit,
syntaxHighlighting,
type LanguageSupport,
type MatchResult,
} from "@codemirror/language";
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
import { highlightSelectionMatches } from "@codemirror/search";
@@ -172,11 +180,24 @@ export function stickyExtension(settings: EditorSettings | undefined): Extension
return settings?.stickyScroll === false || isDistractionFree() ? [] : [stickyScroll, stickyMaxLines.of(prefsOf(settings).codeEditing.stickyLinesMax)];
}
const matchingBracket = Decoration.mark({ class: "cm-matchingBracket" });
const matchingTag = Decoration.mark({ class: "cm-matchingBracket cm-matchingTag" });
const nonmatchingBracket = Decoration.mark({ class: "cm-nonmatchingBracket" });
/** Like the default, but matched tag names (HTML / XML / JSX) get their own mark, as IDEA's MATCHED_TAG_NAME. */
function renderMatch(match: MatchResult, state: EditorState): readonly Range<Decoration>[] {
const mark = (from: number, to: number) =>
!match.matched ? nonmatchingBracket : /^[()[\]{}]$/.test(state.sliceDoc(from, to)) ? matchingBracket : matchingTag;
const out = [mark(match.start.from, match.start.to).range(match.start.from, match.start.to)];
if (match.end) out.push(mark(match.end.from, match.end.to).range(match.end.from, match.end.to));
return out;
}
export function heavyExtension(docLength: number, settings?: EditorSettings): Extension {
if (docLength > HEAVY_LIMIT) return [];
const prefs = prefsOf(settings);
return [
prefs.codeEditing.highlightBraces ? bracketMatching() : [],
prefs.codeEditing.highlightBraces ? bracketMatching({ renderMatch }) : [],
prefs.appearance.indentGuides ? indentGuides : [],
prefs.codeEditing.highlightSelection ? highlightSelectionMatches({ minSelectionLength: 2, maxMatches: 500 }) : [],
];
+6 -1
Переглянути файл
@@ -67,12 +67,17 @@ export const openideaTheme = EditorView.theme({
},
".cm-activeLine": { backgroundColor: "var(--editor-caret-row)" },
".cm-selectionMatch": { backgroundColor: "var(--bg-identifier-under-caret, color-mix(in srgb, var(--fg) 9%, transparent))" },
// IDEA's MATCHED_BRACE_ATTRIBUTES (bold in dark schemes only) and MATCHED_TAG_NAME (never bold).
".cm-matchingBracket, &.cm-focused .cm-matchingBracket": {
backgroundColor: "var(--editor-brace)",
color: "inherit",
fontWeight: "700",
fontWeight: "var(--matched-brace-weight, 700)",
borderRadius: "1px",
},
".cm-matchingTag, &.cm-focused .cm-matchingTag": {
backgroundColor: "var(--bg-matched-tag, var(--editor-brace))",
fontWeight: "inherit",
},
".cm-nonmatchingBracket, &.cm-focused .cm-nonmatchingBracket": {
color: "var(--danger)",
backgroundColor: "color-mix(in srgb, var(--danger) 14%, transparent)",
+1 -1
Переглянути файл
@@ -17,7 +17,7 @@ describe("navigation bar popup model", () => {
});
it("lists folder children without IDE-hidden names, and symbol children", () => {
const e = (name: string, kind: FileEntry["kind"]) => ({ name, path: `/p/${name}`, kind, size: 0, mtimeMs: 0, ignored: false, hidden: false }) as FileEntry;
const e = (name: string, kind: FileEntry["kind"]) => ({ name, path: `/p/${name}`, kind, size: 0, mtimeMs: 0, ignored: false, excluded: false, hidden: false }) as FileEntry;
expect(dirChildren([e(".git", "dir"), e("src", "dir"), e("a.ts", "file")]).map((c) => `${c.kind}:${c.label}`)).toEqual(["dir:src", "file:a.ts"]);
expect(symbolChildren("/p/a.ts", [sym("A", [sym("b")])]).map((c) => c.label)).toEqual(["A"]);
});