From 79c7a45807898419a89286452930206c9a0bcb07 Mon Sep 17 00:00:00 2001 From: Jonathan van Rij Date: Thu, 3 Sep 2026 09:21:43 +0200 Subject: [PATCH] fixes --- src/main/symbols-service.ts | 24 ++++++++++++++++++------ src/renderer/src/editor.tsx | 15 +++++++++++---- src/renderer/src/styles.css | 7 +++++-- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/main/symbols-service.ts b/src/main/symbols-service.ts index ba3e8a7..aa46cd2 100644 --- a/src/main/symbols-service.ts +++ b/src/main/symbols-service.ts @@ -1,7 +1,8 @@ /* PHP symbol index + reference lookup. * * Two jobs, both on ripgrep: - * - the index: every class/interface/trait/enum DECLARED in the project, as + * - the index: every class/interface/trait/enum DECLARED in the project AND in + * vendor (so a framework class is clickable too), as * name → {path, line}. The renderer needs the bare name list to decide which * tokens it may underline, and that decision is per visible token per frame, * so it cannot be a search. One rg pass answers it for every token at once. @@ -49,6 +50,11 @@ const BASE_IGNORE = [ 'node_modules', '.git', 'out', 'dist', 'build', '.cache', 'vendor', 'coverage', '.helder', '.next', '.nuxt', '.turbo', '.idea', '.vscode', ] +/** `vendor/` is out of the reference search — nobody wants to read how Symfony + * uses Throwable — but it IS indexed for declarations, so a framework class is + * still a name we can underline and open. Measured on a Laravel app: 48 project + * classes in 30 ms, 7k more with vendor in 170 ms. */ +const DECL_IGNORE = BASE_IGNORE.filter((d) => d !== 'vendor') /** Declarations only. `readonly`/`final`/`abstract` may precede the keyword, and * an enum may carry a backing type. Anchored at the line start (with optional @@ -63,13 +69,14 @@ const NS_RE = /^[ \t]*namespace[ \t]+([A-Za-z_\\][\w\\]*)[ \t]*;/ const TRAIT_RE = /^[ \t]+use[ \t]+([A-Za-z_\\][\w\\ \t,]*?)[ \t]*[;{]/ const TRAIT_RG = '^\\s+use\\s+[A-Za-z_\\\\][\\w\\\\ \t,]*[;{]' +const MAX_DEFS = 25 const MAX_REF_FILES = 300 const MAX_LINE = 1000 -function ignoreArgs(): string[] { +function ignoreArgs(dirs: string[] = BASE_IGNORE): string[] { const cfg = getConfig() const args: string[] = [] - for (const d of BASE_IGNORE) args.push('--glob', `!${d}`) + for (const d of dirs) args.push('--glob', `!${d}`) for (const g of cfg.files.exclude) if (g) args.push('--glob', `!${g}`) if (!cfg.files.followGitignore) args.push('--no-ignore') return args @@ -165,7 +172,7 @@ async function build(root: string): Promise { const byFile = new Map() const nsByFile = new Map() await rgJson( - ['--json', '--hidden', '--glob', '*.php', ...ignoreArgs(), '-e', DECL_RG, '--', root], + ['--json', '--hidden', '--glob', '*.php', ...ignoreArgs(DECL_IGNORE), '-e', DECL_RG, '--', root], (abs, line, text) => { const rel = toRel(root, abs) const ns = parseNamespace(text) @@ -188,7 +195,7 @@ async function build(root: string): Promise { // pass. A `use` belongs to the last declaration above it in the same file — // which is also why the declaration pass has to run first. await rgJson( - ['--json', '--hidden', '--glob', '*.php', ...ignoreArgs(), '-e', TRAIT_RG, '--', root], + ['--json', '--hidden', '--glob', '*.php', ...ignoreArgs(DECL_IGNORE), '-e', TRAIT_RG, '--', root], (abs, line, text) => { const names = parseTraitUse(text) if (!names.length) return @@ -229,7 +236,12 @@ export async function symbolNames(root: string): Promise { export async function lookupSymbol(root: string, name: string): Promise { if (!/^[A-Za-z_]\w*$/.test(name)) return { name, defs: [], refs: [], refCount: 0 } const ix = await getIndex(root) - const defs = ix.byName.get(name) ?? [] + // The project's own declaration comes first: a common name like `Handler` is + // declared a dozen times inside vendor, and none of those is what was clicked. + const defs = (ix.byName.get(name) ?? []) + .slice() + .sort((a, b) => Number(a.path.startsWith('vendor/')) - Number(b.path.startsWith('vendor/')) || a.path.localeCompare(b.path)) + .slice(0, MAX_DEFS) const declared = new Set(defs.map((d) => d.path + ':' + d.line)) const order: string[] = [] const groups = new Map() diff --git a/src/renderer/src/editor.tsx b/src/renderer/src/editor.tsx index 01af19d..ca55ed6 100644 --- a/src/renderer/src/editor.tsx +++ b/src/renderer/src/editor.tsx @@ -94,7 +94,16 @@ function CodeEditor({ path, text, lang, wrap, flash, onChange, onContext, onSymb // eslint-disable-next-line react-hooks/exhaustive-deps [text, lang, wrap, symVer], ) - const count = useMemo(() => text.split('\n').length, [text]) + /* One text block, not one element per line: the font is monospace and every + * row is exactly 20px, so the numbers land on the same grid a list of divs + * gave — for one node instead of thousands on a long file. Nothing styles a + * single number today; the day something must, this goes back to spans. */ + const gutter = useMemo(() => { + const n = text.split('\n').length + let out = '1' + for (let i = 2; i <= n; i++) out += '\n' + i + return out + }, [text]) // The band sits behind the text, so it needs the geometry of the line. Unwrapped // that is arithmetic; wrapped, a line can be several rows tall, so measure it. const [flashBox, setFlashBox] = useState<{ top: number; height: number } | null>(null) @@ -200,9 +209,7 @@ function CodeEditor({ path, text, lang, wrap, flash, onChange, onContext, onSymb
{!wrap && (
-
- {Array.from({ length: count }, (_, i) =>
{i + 1}
)} -
+
{gutter}
)}
diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index 9e51f9f..234f9dc 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -582,8 +582,11 @@ kbd { flex:0 0 auto; font-family:var(--mono); font-size:11.5px; color:var(--fg-3 /* editable buffer — transparent textarea over a highlighted
, synced gutter */
 .code-edit { flex:1; min-height:0; display:flex; overflow:hidden; }
 .ce-gutterwrap { flex:0 0 54px; overflow:hidden; position:relative; }
-.ce-gutter { padding-top:6px; will-change:transform; }
-.ce-gutter div { height:20px; line-height:20px; text-align:right; padding-right:14px; color:var(--fg-3); font-family:var(--code-font); font-size:12px; user-select:none; }
+.ce-gutter {
+  padding:6px 14px 0 0; will-change:transform;
+  white-space:pre; text-align:right; line-height:20px;
+  color:var(--fg-3); font-family:var(--code-font); font-size:12px; user-select:none;
+}
 .ce-scroll { flex:1; min-width:0; overflow:auto; position:relative; }
 /* min-height keeps the inset:0 textarea filling the pane on short/empty files,
    so a click anywhere in the blank area below the last line still lands. */