Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion ide/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
19 changes: 12 additions & 7 deletions ide/src/editor/symbolIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RefAnswer> {
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;
Expand All @@ -193,5 +198,5 @@ export async function findReferences(name: string, inFile?: string): Promise<Ref
}
} catch { /* 워커가 아직 모르는 파일 */ }

return { at: { rel: def.rel, line: def.line, name: def.name }, hits, ambiguous: [], noIndex: false };
return { at: { rel: def.rel, line: def.line, name: def.name }, hits, ambiguous: [], noIndex: false, asked };
}
36 changes: 36 additions & 0 deletions ide/src/engine/bpKey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect } from "vitest";
import { bpKey } from "./bpKey";

const bp = (uri: string, line: number) => ({ 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();
});
});
15 changes: 15 additions & 0 deletions ide/src/engine/bpKey.ts
Original file line number Diff line number Diff line change
@@ -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("|");
}
5 changes: 4 additions & 1 deletion ide/src/ext/vscodeShim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 가 비운다. */
Expand Down Expand Up @@ -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: [] });
}
}
Expand Down
Loading