From df949413937149f09d494f065839d488f92f2a48 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Tue, 11 Aug 2026 09:45:33 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=AC=EB=B3=BC=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EB=95=8C=20=EC=A0=95=ED=99=95=20=EC=9D=BC?= =?UTF-8?q?=EC=B9=98=EA=B0=80=20=EB=B0=80=EB=A0=A4=EB=82=98=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flattenNavTree 의 정렬은 한 파일 안에서만 돈다. 파일 가로질러 모은 다음 상한에서 자르는데, 앞쪽 파일이 부분 일치를 잔뜩 내면 정확히 그 이름인 정의가 잘려나간다. 그러면 find_references 가 "정의를 못 찾았습니다" 라고 답한다. 있는데도. 자르기 전에 전체를 다시 세운다. orderSymbols 로 빼서 테스트. find_symbol 이 잘랐다는 말도 안 하고 있었다. 목록이 전부인 줄 알기 딱 좋다. --- ide/src/App.tsx | 4 ++- ide/src/editor/symbolIndex.ts | 18 +++++++++---- ide/src/engine/navTree.test.ts | 46 +++++++++++++++++++++++++++++++++- ide/src/engine/navTree.ts | 18 +++++++++++++ 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/ide/src/App.tsx b/ide/src/App.tsx index 1050c5d..ba88aed 100644 --- a/ide/src/App.tsx +++ b/ide/src/App.tsx @@ -3494,7 +3494,9 @@ export class App extends React.Component<{ playOpening?: boolean }, S> { ? `"${query}" 는 훑은 범위 안에 없었습니다. 파일이 많아 색인을 다 훑지 못했으니(${symbolIndex.TS_MODEL_CAP}개까지) 없다고 단정하지 말고 search_files 로 확인하세요.` : `"${query}" 로 찾은 심볼 없음. 색인은 있습니다(${r.sources.join(", ")}) — 이름이 다르거나 색인이 없는 언어의 파일일 수 있으니 search_files 도 해 보세요.`; } - return r.hits.map(h => `${h.rel}:${h.line}:${h.column} ${h.container ? h.container + "." : ""}${h.name}`).join(String.fromCharCode(10)); + // 잘랐으면 잘랐다고 말한다 — 안 그러면 이 목록이 전부인 줄 안다. + return r.hits.map(h => `${h.rel}:${h.line}:${h.column} ${h.container ? h.container + "." : ""}${h.name}`).join(String.fromCharCode(10)) + + (r.sliced ? String.fromCharCode(10) + String.fromCharCode(10) + "… 상한에서 잘렸습니다. 이름을 더 정확히 주면 좁혀집니다." : ""); } if (call.name === "find_references") { const name = String(call.input?.name ?? "").trim(); diff --git a/ide/src/editor/symbolIndex.ts b/ide/src/editor/symbolIndex.ts index 48af1f6..23864dc 100644 --- a/ide/src/editor/symbolIndex.ts +++ b/ide/src/editor/symbolIndex.ts @@ -14,7 +14,7 @@ import monaco from "./monacoSetup"; import * as lspClient from "./lspClient"; import * as projectModels from "./projectModels"; -import { flattenNavTree, isTestPath } from "../engine/navTree"; +import { flattenNavTree, orderSymbols } from "../engine/navTree"; export interface SymbolHit { name: string; @@ -37,6 +37,8 @@ export interface SymbolAnswer { capped: boolean; /** 파일이 너무 많아 색인을 아예 만들지 않았다. "이 언어는 지원 안 함" 과 다르다. */ tooBig: boolean; + /** 상한에서 잘랐다 — 보여준 것이 전부가 아니다. */ + sliced: boolean; } /** 워커가 주는 파일 이름(모델 uri) → 워크스페이스 상대 경로 */ @@ -122,7 +124,7 @@ export function isTooBig(): boolean { return projectModels.isPreloadSkipped(); } /** 이름으로 심볼을 찾는다. 두 통로에 모두 물어보고 합친다. */ export async function findSymbols(query: string, max = 100): Promise { const q = String(query ?? "").trim(); - if (!q) return { hits: [], sources: [], capped: false, tooBig: false }; + if (!q) return { hits: [], sources: [], capped: false, tooBig: false, sliced: false }; const [ts, lsp] = await Promise.all([fromTypescript(q, max), fromLsp(q)]); const sources: ("ts" | "lsp")[] = []; if (ts.available) sources.push("ts"); @@ -137,9 +139,15 @@ export async function findSymbols(query: string, max = 100): Promise !isTestPath(h.rel)), ...hits.filter(h => isTestPath(h.rel))]; - return { hits: ordered.slice(0, max), sources, capped: ts.capped, tooBig: projectModels.isPreloadSkipped() }; + // 자르기 전에 전체를 다시 세운다 — 정확 일치가 뒤쪽 파일에 있어도 살아남게. + const ordered = orderSymbols(hits, q); + return { + hits: ordered.slice(0, max), + sources, + capped: ts.capped, + tooBig: projectModels.isPreloadSkipped(), + sliced: ordered.length > max, + }; } // ── 참조 찾기 ─────────────────────────────────────────────────────────────── diff --git a/ide/src/engine/navTree.test.ts b/ide/src/engine/navTree.test.ts index 8bc3d39..c660605 100644 --- a/ide/src/engine/navTree.test.ts +++ b/ide/src/engine/navTree.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { flattenNavTree, isTestPath, TS_KIND } from "./navTree"; +import { flattenNavTree, isTestPath, orderSymbols, TS_KIND } from "./navTree"; /** 실제 워커가 주는 모양 그대로 — 맨 위는 파일 자신이고 이름에 따옴표가 붙는다. */ const TREE = { @@ -132,3 +132,47 @@ describe("isTestPath", () => { } }); }); + +describe("orderSymbols", () => { + const s = (name: string, rel: string) => ({ name, rel }); + + // 이 케이스 때문에 있다. 앞쪽 파일이 부분 일치를 잔뜩 내면 정확 일치가 잘려 + // 나가고, 참조 찾기가 "정의를 못 찾았습니다" 라고 답한다 — 있는데도. + it("정확히 같은 이름을 파일을 가로질러 맨 앞으로", () => { + const hits = [ + s("fooBarBaz", "a.ts"), s("fooBarQux", "b.ts"), s("foo", "z.ts"), + ]; + expect(orderSymbols(hits, "foo").map(h => h.rel)).toEqual(["z.ts", "a.ts", "b.ts"]); + }); + + it("앞에서 시작하는 것이 그다음", () => { + const hits = [s("xxfoo", "a.ts"), s("foobar", "b.ts")]; + expect(orderSymbols(hits, "foo").map(h => h.rel)).toEqual(["b.ts", "a.ts"]); + }); + + it("같은 등급이면 테스트 파일이 뒤", () => { + const hits = [s("foo", "a.test.ts"), s("foo", "a.ts")]; + expect(orderSymbols(hits, "foo").map(h => h.rel)).toEqual(["a.ts", "a.test.ts"]); + }); + + it("정확 일치는 테스트 파일이어도 부분 일치보다 앞", () => { + const hits = [s("fooBar", "a.ts"), s("foo", "a.test.ts")]; + expect(orderSymbols(hits, "foo").map(h => h.rel)).toEqual(["a.test.ts", "a.ts"]); + }); + + it("등급이 같으면 들어온 순서를 지킨다", () => { + const hits = [s("foo", "b.ts"), s("foo", "a.ts")]; + expect(orderSymbols(hits, "foo").map(h => h.rel)).toEqual(["b.ts", "a.ts"]); + }); + + it("원본을 건드리지 않는다", () => { + const hits = [s("zz", "a.ts"), s("foo", "b.ts")]; + const before = hits.map(h => h.rel); + orderSymbols(hits, "foo"); + expect(hits.map(h => h.rel)).toEqual(before); + }); + + it("빈 목록", () => { + expect(orderSymbols([], "foo")).toEqual([]); + }); +}); diff --git a/ide/src/engine/navTree.ts b/ide/src/engine/navTree.ts index 1f3a309..6476cf3 100644 --- a/ide/src/engine/navTree.ts +++ b/ide/src/engine/navTree.ts @@ -83,3 +83,21 @@ function firstOffset(node: any): number { const n = Number(node?.spans?.[0]?.start); return Number.isFinite(n) && n >= 0 ? n : 0; } + +/** 여러 파일에서 모은 심볼을 한 줄로 세운다. + * + * flattenNavTree 의 정렬은 **한 파일 안에서만** 돈다. 파일을 가로질러 모은 뒤 + * 자르면, 앞쪽 파일이 부분 일치를 잔뜩 내는 바람에 **정확히 그 이름인 정의가 + * 잘려 나간다.** 그러면 참조 찾기가 "정의를 못 찾았습니다" 라고 답한다 — + * 있는데도. 그래서 자르기 전에 전체를 다시 세운다. + * + * 순서: 정확히 같은 이름 → 앞에서 시작 → 나머지. 테스트 파일은 각 묶음 안에서 뒤로. */ +export function orderSymbols(hits: readonly T[], query: string): T[] { + const q = String(query ?? "").toLowerCase(); + const rank = (h: T): number => { + const n = h.name.toLowerCase(); + const base = n === q ? 0 : n.startsWith(q) ? 2 : 4; + return base + (isTestPath(h.rel) ? 1 : 0); + }; + return hits.map((h, i) => ({ h, i })).sort((a, b) => rank(a.h) - rank(b.h) || a.i - b.i).map(x => x.h); +}