fixes
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
/* PHP symbol index + reference lookup.
|
/* PHP symbol index + reference lookup.
|
||||||
*
|
*
|
||||||
* Two jobs, both on ripgrep:
|
* 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
|
* 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,
|
* 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.
|
* 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',
|
'node_modules', '.git', 'out', 'dist', 'build', '.cache',
|
||||||
'vendor', 'coverage', '.helder', '.next', '.nuxt', '.turbo', '.idea', '.vscode',
|
'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
|
/** Declarations only. `readonly`/`final`/`abstract` may precede the keyword, and
|
||||||
* an enum may carry a backing type. Anchored at the line start (with optional
|
* 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_RE = /^[ \t]+use[ \t]+([A-Za-z_\\][\w\\ \t,]*?)[ \t]*[;{]/
|
||||||
const TRAIT_RG = '^\\s+use\\s+[A-Za-z_\\\\][\\w\\\\ \t,]*[;{]'
|
const TRAIT_RG = '^\\s+use\\s+[A-Za-z_\\\\][\\w\\\\ \t,]*[;{]'
|
||||||
|
|
||||||
|
const MAX_DEFS = 25
|
||||||
const MAX_REF_FILES = 300
|
const MAX_REF_FILES = 300
|
||||||
const MAX_LINE = 1000
|
const MAX_LINE = 1000
|
||||||
|
|
||||||
function ignoreArgs(): string[] {
|
function ignoreArgs(dirs: string[] = BASE_IGNORE): string[] {
|
||||||
const cfg = getConfig()
|
const cfg = getConfig()
|
||||||
const args: string[] = []
|
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}`)
|
for (const g of cfg.files.exclude) if (g) args.push('--glob', `!${g}`)
|
||||||
if (!cfg.files.followGitignore) args.push('--no-ignore')
|
if (!cfg.files.followGitignore) args.push('--no-ignore')
|
||||||
return args
|
return args
|
||||||
@@ -165,7 +172,7 @@ async function build(root: string): Promise<Index> {
|
|||||||
const byFile = new Map<string, SymbolDef[]>()
|
const byFile = new Map<string, SymbolDef[]>()
|
||||||
const nsByFile = new Map<string, string>()
|
const nsByFile = new Map<string, string>()
|
||||||
await rgJson(
|
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) => {
|
(abs, line, text) => {
|
||||||
const rel = toRel(root, abs)
|
const rel = toRel(root, abs)
|
||||||
const ns = parseNamespace(text)
|
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 —
|
// 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.
|
// which is also why the declaration pass has to run first.
|
||||||
await rgJson(
|
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) => {
|
(abs, line, text) => {
|
||||||
const names = parseTraitUse(text)
|
const names = parseTraitUse(text)
|
||||||
if (!names.length) return
|
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> {
|
export async function lookupSymbol(root: string, name: string): Promise<SymbolLookup> {
|
||||||
if (!/^[A-Za-z_]\w*$/.test(name)) return { name, defs: [], refs: [], refCount: 0 }
|
if (!/^[A-Za-z_]\w*$/.test(name)) return { name, defs: [], refs: [], refCount: 0 }
|
||||||
const ix = await getIndex(root)
|
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 declared = new Set(defs.map((d) => d.path + ':' + d.line))
|
||||||
const order: string[] = []
|
const order: string[] = []
|
||||||
const groups = new Map<string, RefGroup>()
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[text, lang, wrap, symVer],
|
[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
|
// 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.
|
// that is arithmetic; wrapped, a line can be several rows tall, so measure it.
|
||||||
const [flashBox, setFlashBox] = useState<{ top: number; height: number } | null>(null)
|
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' : '')}>
|
<div className={'code-edit' + (wrap ? ' wrap' : '') + (metaDown ? ' sym-live' : '')}>
|
||||||
{!wrap && (
|
{!wrap && (
|
||||||
<div className="ce-gutterwrap">
|
<div className="ce-gutterwrap">
|
||||||
<div className="ce-gutter" ref={gutterRef}>
|
<div className="ce-gutter" ref={gutterRef}>{gutter}</div>
|
||||||
{Array.from({ length: count }, (_, i) => <div key={i}>{i + 1}</div>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="ce-scroll" ref={scrollRef} onScroll={onScroll} onClick={handleTokenClick}>
|
<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 */
|
/* editable buffer — transparent textarea over a highlighted <pre>, synced gutter */
|
||||||
.code-edit { flex:1; min-height:0; display:flex; overflow:hidden; }
|
.code-edit { flex:1; min-height:0; display:flex; overflow:hidden; }
|
||||||
.ce-gutterwrap { flex:0 0 54px; overflow:hidden; position:relative; }
|
.ce-gutterwrap { flex:0 0 54px; overflow:hidden; position:relative; }
|
||||||
.ce-gutter { padding-top:6px; will-change:transform; }
|
.ce-gutter {
|
||||||
.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; }
|
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; }
|
.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,
|
/* 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. */
|
so a click anywhere in the blank area below the last line still lands. */
|
||||||
|
|||||||
Reference in New Issue
Block a user