diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d562a5ea6..ac2dbb3ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- Vue templates are indexed as uses: `:prop="x"`, `@event="save"`, `v-if` / `v-for` / `v-model` values and `{{ mustache }}` expressions now link to the script state, functions and imports they bind, so "where is `myLinkBrandingLocked` used" answers from the graph and `codegraph_explore` shows the template lines that bind a symbol you named. + +- `codegraph_explore` no longer spends its budget on files that match only a plain word in the query when the query also names a symbol exactly: after the named symbol's file, files wired to that symbol come before an unrelated file that shares a word like "section". + - The Claude Code prompt hook no longer runs on the task-notification messages Claude Code injects when a background agent finishes, removing a multi-second stall on every such turn. (#1832) - Rust calls on `self` now stay with the enclosing type instead of linking to an unrelated type’s same-named method. Thanks @L4XB. (#1861) diff --git a/__tests__/explore-nl-stopword-collision.test.ts b/__tests__/explore-nl-stopword-collision.test.ts index df67b016f2..579eb49f44 100644 --- a/__tests__/explore-nl-stopword-collision.test.ts +++ b/__tests__/explore-nl-stopword-collision.test.ts @@ -174,3 +174,52 @@ describe('codegraph_explore — interface members never corroborate a bare word' expect(files[0]).toBe('src/extensionHostProcess.ts'); }); }); + +describe('codegraph_explore — precise-before-plain file tier', () => { + let testDir: string; + let cg: CodeGraph; + + afterEach(() => { + cg?.destroy(); + if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true }); + }); + + it('a file tied to the exactly-named symbol outranks files that match only a plain query word', async () => { + // `WorkspaceMyLinkView appearance section myLinkBrandingLocked` rendered the + // view first and then filled the budget with a PHP layout class that matched + // only "section", ahead of everything wired to `myLinkBrandingLocked`. + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-precise-first-')); + const profile = path.join(testDir, 'src', 'profile'); + const layout = path.join(testDir, 'src', 'layout'); + fs.mkdirSync(profile, { recursive: true }); + fs.mkdirSync(layout, { recursive: true }); + fs.writeFileSync(path.join(profile, 'branding.ts'), + 'export function myLinkBrandingLocked(plan: string): boolean {\n' + + " return plan !== 'pro';\n" + + '}\n'); + fs.writeFileSync(path.join(profile, 'panel.ts'), + "import { myLinkBrandingLocked } from './branding';\n" + + 'export function drawOwnDomainPanel(plan: string): string {\n' + + " return myLinkBrandingLocked(plan) ? 'upgrade' : 'edit';\n" + + '}\n'); + // A dense mesh of `section` symbols: FTS roots for the plain word "section". + fs.writeFileSync(path.join(layout, 'sections.ts'), + 'export function section(name: string): string {\n return renderSection(name);\n}\n' + + 'export function renderSection(name: string): string {\n return sectionHeader(name) + sectionBody(name);\n}\n' + + 'export function sectionHeader(name: string): string {\n return `

${name}

`;\n}\n' + + 'export function sectionBody(name: string): string {\n return section.name + name;\n}\n' + + 'export function sectionList(names: string[]): string {\n return names.map(section).join("");\n}\n'); + cg = CodeGraph.initSync(testDir); + await cg.indexAll(); + + const res = await new ToolHandler(cg).execute('codegraph_explore', { + query: 'myLinkBrandingLocked appearance section', + }); + const files = sourcedFiles(res.content[0].text as string); + const panel = files.indexOf('src/profile/panel.ts'); + const sections = files.indexOf('src/layout/sections.ts'); + expect(files[0]).toBe('src/profile/branding.ts'); + expect(panel).toBeGreaterThan(0); + if (sections !== -1) expect(panel).toBeLessThan(sections); + }); +}); diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 46a3cc3d54..1c95a90937 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -9219,6 +9219,50 @@ import MyButton from './MyButton.vue'; expect(refs).not.toContain('Span'); }); + it('should extract identifiers the Vue template binds, skipping locals, keys and globals', () => { + const code = ` + + +`; + const result = extractFromSource('WorkspaceMyLinkView.vue', code); + const templateRefs = result.unresolvedReferences.filter((r) => r.line <= 11); + const named = (kind: string) => templateRefs.filter((r) => r.referenceKind === kind).map((r) => r.referenceName); + + // Bound script state is a reference, on the template line that binds it. + const locked = templateRefs.filter((r) => r.referenceName === 'myLinkBrandingLocked'); + expect(locked.map((r) => r.line).sort()).toEqual([2, 4]); + expect(named('references')).toContain('isActive'); + expect(named('references')).toContain('domains'); + // A bare event handler and a template call are calls. + expect(named('calls')).toContain('saveDomain'); + expect(named('calls')).toContain('formatDomain'); + // Locals, object keys, member tails, globals, $-helpers and string contents bind nothing. + const all = templateRefs.map((r) => r.referenceName); + for (const absent of ['item', 'index', 'row', 'active', 'id', 'label', 'Math', 'max', '$t']) { + expect(all).not.toContain(absent); + } + expect(templateRefs.filter((r) => r.referenceName === 'isActive')).toHaveLength(1); + }); + it('should extract from both