fixes
This commit is contained in:
@@ -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<Index> {
|
||||
const byFile = new Map<string, SymbolDef[]>()
|
||||
const nsByFile = new Map<string, string>()
|
||||
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<Index> {
|
||||
// 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<string[]> {
|
||||
export async function lookupSymbol(root: string, name: string): Promise<SymbolLookup> {
|
||||
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<string, RefGroup>()
|
||||
|
||||
@@ -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
|
||||
<div className={'code-edit' + (wrap ? ' wrap' : '') + (metaDown ? ' sym-live' : '')}>
|
||||
{!wrap && (
|
||||
<div className="ce-gutterwrap">
|
||||
<div className="ce-gutter" ref={gutterRef}>
|
||||
{Array.from({ length: count }, (_, i) => <div key={i}>{i + 1}</div>)}
|
||||
</div>
|
||||
<div className="ce-gutter" ref={gutterRef}>{gutter}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="ce-scroll" ref={scrollRef} onScroll={onScroll} onClick={handleTokenClick}>
|
||||
|
||||
@@ -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 <pre>, 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. */
|
||||
|
||||
Reference in New Issue
Block a user