From ac7f92ce6b20f50710924c2789fb3a685f1c75a4 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Mon, 10 Aug 2026 11:00:22 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A4=91=EB=8B=A8=EC=A0=90=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EA=B0=90=EC=A7=80=EC=99=80=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=A4=ED=8C=A8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 방금 만든 코드에서 둘 나옴. setDebugState 가 중단점 목록을 길이로 비교했다. 하나 끄고 하나 켜면 길이가 같아서 사건이 안 나가고, 확장은 옛 목록 그대로 믿는다. 자리로 지문 만들어 비교하게 함. bpKey 로 빼서 테스트. find_references 가 워커 조회에 실패해도 "참조 없음(정의만 있음)" 이라고 답했다. 모델이 그걸 안 쓰는 코드로 읽고 지운다. asked 로 구분해서 못 물어본 경우엔 단정하지 말라고 답한다. --- ide/src/App.tsx | 7 ++++++- ide/src/editor/symbolIndex.ts | 19 +++++++++++------- ide/src/engine/bpKey.test.ts | 36 +++++++++++++++++++++++++++++++++++ ide/src/engine/bpKey.ts | 15 +++++++++++++++ ide/src/ext/vscodeShim.ts | 5 ++++- 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 ide/src/engine/bpKey.test.ts create mode 100644 ide/src/engine/bpKey.ts diff --git a/ide/src/App.tsx b/ide/src/App.tsx index 7177973..43a5669 100644 --- a/ide/src/App.tsx +++ b/ide/src/App.tsx @@ -3504,7 +3504,12 @@ export class App extends React.Component<{ playOpening?: boolean }, S> { + r.ambiguous.map(a2 => ` ${a2.rel}:${a2.line}`).join(NL); } if (!r.at) return `"${name}" 의 정의를 못 찾았습니다. 이름이 정확한지 보고, 아니면 search_files 를 쓰세요.`; - if (!r.hits.length) return `${r.at.rel}:${r.at.line} 의 ${r.at.name} — 참조 없음(정의만 있음).`; + if (!r.hits.length) { + // 못 물어본 것을 "없다" 로 말하면 모델이 안 쓰는 코드로 단정하고 지운다. + return r.asked + ? `${r.at.rel}:${r.at.line} 의 ${r.at.name} — 참조 없음(정의만 있음).` + : `${r.at.rel}:${r.at.line} 의 ${r.at.name} — 참조를 조회하지 못했습니다(색인이 아직 준비되지 않음). 없다고 단정하지 말고 search_files 로 확인하세요.`; + } return `${r.at.rel}:${r.at.line} 의 ${r.at.name} 을(를) 쓰는 곳 ${r.hits.length}개:` + NL + r.hits.map(h => ` ${h.rel}:${h.line}:${h.column}`).join(NL); } diff --git a/ide/src/editor/symbolIndex.ts b/ide/src/editor/symbolIndex.ts index 46bd761..ce2ba18 100644 --- a/ide/src/editor/symbolIndex.ts +++ b/ide/src/editor/symbolIndex.ts @@ -152,37 +152,42 @@ export interface RefAnswer { ambiguous: { rel: string; line: number }[]; /** 심볼 색인 자체가 없다. */ noIndex: boolean; + /** 참조를 실제로 물어봤는가. false 면 "참조 없음" 이라고 말하면 안 된다 — + * 안 물어본 것과 물어봤는데 없는 것은 다르다. */ + asked: boolean; } /** 이름으로 정의를 찾고, 그 자리에서 참조를 묻는다. */ export async function findReferences(name: string, inFile?: string): Promise { const q = String(name ?? "").trim(); - if (!q) return { at: null, hits: [], ambiguous: [], noIndex: false }; + if (!q) return { at: null, hits: [], ambiguous: [], noIndex: false, asked: false }; const found = await findSymbols(q, 50); - if (!found.sources.length) return { at: null, hits: [], ambiguous: [], noIndex: true }; + if (!found.sources.length) return { at: null, hits: [], ambiguous: [], noIndex: true, asked: false }; // 이름이 정확히 같은 것만 남긴다. 부분 일치까지 세면 엉뚱한 것을 기준으로 잡는다. let exact = found.hits.filter(h => h.name === q); if (inFile) exact = exact.filter(h => h.rel === inFile || h.rel.endsWith("/" + inFile)); - if (!exact.length) return { at: null, hits: [], ambiguous: [], noIndex: false }; + if (!exact.length) return { at: null, hits: [], ambiguous: [], noIndex: false, asked: false }; if (exact.length > 1) { - return { at: null, hits: [], ambiguous: exact.map(h => ({ rel: h.rel, line: h.line })), noIndex: false }; + return { at: null, hits: [], ambiguous: exact.map(h => ({ rel: h.rel, line: h.line })), noIndex: false, asked: false }; } const def = exact[0]!; const model = projectModels.getByRel(def.rel); - if (!model || model.isDisposed()) return { at: null, hits: [], ambiguous: [], noIndex: false }; + if (!model || model.isDisposed()) return { at: null, hits: [], ambiguous: [], noIndex: false, asked: false }; const ts: any = (monaco.languages as any).typescript; - if (!ts?.getTypeScriptWorker) return { at: { rel: def.rel, line: def.line, name: def.name }, hits: [], ambiguous: [], noIndex: false }; + if (!ts?.getTypeScriptWorker) return { at: { rel: def.rel, line: def.line, name: def.name }, hits: [], ambiguous: [], noIndex: false, asked: false }; const offset = model.getOffsetAt({ lineNumber: def.line, column: def.column }); const hits: RefHit[] = []; + let asked = false; try { const getWorker = await ts.getTypeScriptWorker(); const client = await getWorker(model.uri); const refs: any[] = await client.getReferencesAtPosition(model.uri.toString(), offset); + asked = true; // 물어보는 데 성공했다 — 이제 "없다" 고 말해도 된다 for (const r of refs ?? []) { const rel = projectModels.relFor(String(r?.fileName ?? "")); if (!rel) continue; @@ -193,5 +198,5 @@ export async function findReferences(name: string, inFile?: string): Promise ({ location: { uri, range: { start: { line } } } }); + +describe("bpKey", () => { + it("같은 목록은 같은 키", () => { + expect(bpKey([bp("a.ts", 1)])).toBe(bpKey([bp("a.ts", 1)])); + }); + + // 이것 때문에 이 함수가 있다. 개수로 견주면 이 판을 놓치고, 확장은 옛 목록을 + // 그대로 믿은 채 엉뚱한 줄에 표시를 남긴다. + it("하나 끄고 하나 켜면 다른 키다(개수는 같다)", () => { + const before = [bp("a.ts", 1), bp("a.ts", 2)]; + const after = [bp("a.ts", 1), bp("a.ts", 3)]; + expect(before.length).toBe(after.length); + expect(bpKey(before)).not.toBe(bpKey(after)); + }); + + it("파일이 다르면 다른 키", () => { + expect(bpKey([bp("a.ts", 1)])).not.toBe(bpKey([bp("b.ts", 1)])); + }); + + it("순서가 달라도 같은 목록이면 같은 키", () => { + expect(bpKey([bp("a.ts", 2), bp("a.ts", 1)])).toBe(bpKey([bp("a.ts", 1), bp("a.ts", 2)])); + }); + + it("빈 목록", () => { + expect(bpKey([])).toBe(""); + expect(bpKey([])).not.toBe(bpKey([bp("a.ts", 1)])); + }); + + it("깨진 항목에 터지지 않는다", () => { + expect(() => bpKey([null, undefined, {}, { location: {} }] as any)).not.toThrow(); + }); +}); diff --git a/ide/src/engine/bpKey.ts b/ide/src/engine/bpKey.ts new file mode 100644 index 0000000..cc998f5 --- /dev/null +++ b/ide/src/engine/bpKey.ts @@ -0,0 +1,15 @@ +// 중단점 목록의 지문. +// +// 개수로 견주다가 놓친 자리가 있었다 — 하나 끄고 하나 켜면 길이가 같아서 "안 바뀌었다" +// 가 되고, 확장은 옛 목록을 그대로 믿은 채 엉뚱한 줄에 표시를 남긴다. +// 자리로 키를 만들면 그 판이 걸린다. + +export function bpKey(list: readonly unknown[]): string { + return (list ?? []) + .map(b => { + const loc = (b as any)?.location; + return String(loc?.uri ?? "") + ":" + String(loc?.range?.start?.line ?? ""); + }) + .sort() + .join("|"); +} diff --git a/ide/src/ext/vscodeShim.ts b/ide/src/ext/vscodeShim.ts index c815133..dcfe74b 100644 --- a/ide/src/ext/vscodeShim.ts +++ b/ide/src/ext/vscodeShim.ts @@ -14,6 +14,7 @@ import { parseViews, containerTitle, normalizeTreeItem, type ViewDecl, type Tree import { collectEdits, groupByFile, sortForApply, hasOverlap, normalizeAction } from "./workspaceEdit"; import { createDecoType, applyDecos, disposeAllDecos, type DecoTypeHandle } from "./decoStore"; import { planFileOps, deletedBy, badPath, type FileOp } from "./fileOps"; +import { bpKey } from "../engine/bpKey"; import { setShimDocSource } from "./extHost"; /** 지금 살아 있는 파일 감시자들. 확장을 다시 읽으면 disposeShimRegistrations 가 비운다. */ @@ -248,7 +249,9 @@ export function setDebugState(next: { active: any | null; breakpoints: any[] }): debugState = next; if (!wasActive && next.active) { debugEvents.sessionStarted.fire(next.active); debugEvents.activeChanged.fire(next.active); } else if (wasActive && !next.active) { debugEvents.sessionEnded.fire(wasActive); debugEvents.activeChanged.fire(undefined); } - if (prevBps.length !== next.breakpoints.length) { + // 개수로 견주면 하나 끄고 하나 켠 판을 놓친다 — 확장은 옛 목록을 그대로 믿는다. + // 자리로 만든 키를 견준다. + if (bpKey(prevBps) !== bpKey(next.breakpoints)) { debugEvents.breakpointsChanged.fire({ added: next.breakpoints, removed: [], changed: [] }); } }