Skip to content

Commit 969dcd0

Browse files
committed
fix editor go to definition
1 parent 69dc2ed commit 969dcd0

File tree

3 files changed

+212
-44
lines changed

3 files changed

+212
-44
lines changed

src/ui/clip-visibility.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const ClipVisibilityPanel = () => {
8484
}
8585
if (matches.length === 1) {
8686
const match = matches[0]
87-
openFile(match.filePath, match.line)
87+
openFile(match.filePath, match.line, match.column)
8888
return
8989
}
9090
setMatchDialog({ label, matches })
@@ -239,10 +239,10 @@ export const ClipVisibilityPanel = () => {
239239
<button
240240
key={`${match.filePath}:${match.line}`}
241241
type="button"
242-
onClick={() => {
243-
openFile(match.filePath, match.line)
244-
setMatchDialog(null)
245-
}}
242+
onClick={() => {
243+
openFile(match.filePath, match.line, match.column)
244+
setMatchDialog(null)
245+
}}
246246
style={{
247247
padding: "8px 10px",
248248
borderRadius: 8,

src/ui/code-editor.tsx

Lines changed: 190 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ interface CodeEditorProps {
3939
}
4040

4141
type FileResource = URI;
42+
type JumpTarget = {
43+
line: number;
44+
column?: number;
45+
};
4246

4347
const decodeEscaped = (input: string) => {
4448
let result = "";
@@ -174,6 +178,64 @@ const toResourcePath = (resource: FileResource) => {
174178
return resource.path ?? resource.toString();
175179
};
176180

181+
const getLanguageId = (filePath: string) => {
182+
const normalized = filePath.toLowerCase();
183+
if (normalized.endsWith(".tsx")) return "typescriptreact";
184+
if (normalized.endsWith(".ts")) return "typescript";
185+
if (normalized.endsWith(".jsx")) return "javascriptreact";
186+
if (normalized.endsWith(".js")) return "javascript";
187+
return "typescript";
188+
};
189+
190+
const normalizeLine = (line?: number) => {
191+
if (line == null || Number.isNaN(line)) return undefined;
192+
if (line <= 0) return line + 1;
193+
return line;
194+
};
195+
196+
const normalizeColumn = (column?: number) => {
197+
if (column == null || Number.isNaN(column)) return undefined;
198+
if (column <= 0) return column + 1;
199+
return column;
200+
};
201+
202+
const extractSelection = (options: unknown): JumpTarget | null => {
203+
if (!options || typeof options !== "object") return null;
204+
const candidate = options as {
205+
selection?: unknown;
206+
range?: unknown;
207+
};
208+
const selection = (candidate.selection ?? candidate.range) as
209+
| {
210+
startLineNumber?: number;
211+
startColumn?: number;
212+
start?: { line?: number; character?: number };
213+
}
214+
| undefined;
215+
if (!selection) return null;
216+
if (typeof selection.startLineNumber === "number") {
217+
const line = normalizeLine(selection.startLineNumber);
218+
if (line == null) return null;
219+
const column = normalizeColumn(selection.startColumn);
220+
return { line, column };
221+
}
222+
if (selection.start && typeof selection.start.line === "number") {
223+
const line = normalizeLine(selection.start.line + 1);
224+
if (line == null) return null;
225+
const column = normalizeColumn((selection.start.character ?? 0) + 1);
226+
return { line, column };
227+
}
228+
return null;
229+
};
230+
231+
const logNavigationIssue = (message: string, detail?: unknown) => {
232+
if (detail) {
233+
console.warn(`[editor-nav] ${message}`, detail);
234+
} else {
235+
console.warn(`[editor-nav] ${message}`);
236+
}
237+
};
238+
177239
export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
178240
const [code, setCode] = useState<string>("");
179241
const [currentFile, setCurrentFile] = useState<string>("project.tsx");
@@ -194,9 +256,14 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
194256
const resizerRef = useRef<HTMLDivElement>(null);
195257
const { registerEditor } = useEditor();
196258
const loadIdRef = useRef(0);
197-
const pendingJumpRef = useRef<number | null>(null);
259+
const pendingJumpRef = useRef<JumpTarget | null>(null);
198260
const currentFileRef = useRef<string>(currentFile);
199-
const openFileRef = useRef<((filePath: string, line?: number) => Promise<void>) | null>(null);
261+
const openFileRef = useRef<
262+
((filePath: string, line?: number, column?: number) => Promise<void>) | null
263+
>(null);
264+
const openFileWithContentRef = useRef<
265+
((filePath: string, content: string, line?: number, column?: number) => Promise<void>) | null
266+
>(null);
200267

201268
useEffect(() => {
202269
currentFileRef.current = currentFile;
@@ -232,18 +299,37 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
232299
return { path: filePath, content: extracted ?? text };
233300
}, []);
234301

235-
const loadFile = useCallback(async (filePath: string) => {
302+
const loadFile = useCallback(async (filePath: string, contentOverride?: string) => {
236303
const loadId = loadIdRef.current + 1;
237304
loadIdRef.current = loadId;
238305
try {
239306
setIsLoading(true);
240307
setError(null);
241-
const { content, path } = await readFile(filePath);
308+
const payload =
309+
contentOverride != null
310+
? { content: contentOverride, path: filePath }
311+
: await readFile(filePath);
312+
const { content, path } = payload;
242313
if (loadId !== loadIdRef.current) return;
243314
const fileUri = toFileUri(path);
244315
setCode(content);
245316
setCurrentFile(fileUri);
246317
setIsDirty(false);
318+
const editor = editorRef.current;
319+
const monacoApi = monacoRef.current;
320+
if (editor && monacoApi) {
321+
const uri = monacoApi.Uri.parse(fileUri);
322+
let model = monacoApi.editor.getModel(uri);
323+
const languageId = getLanguageId(fileUri);
324+
if (!model) {
325+
model = monacoApi.editor.createModel(content, languageId, uri);
326+
} else if (model.getValue() !== content) {
327+
model.setValue(content);
328+
}
329+
if (editor.getModel()?.uri.toString() !== uri.toString()) {
330+
editor.setModel(model);
331+
}
332+
}
247333
} catch (err) {
248334
if (loadId !== loadIdRef.current) return;
249335
console.error("Failed to load project file:", err);
@@ -259,15 +345,17 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
259345
const editor = editorRef.current;
260346
const monaco = monacoRef.current;
261347
if (!editor || !monaco) return;
262-
editor.revealLineInCenter(line, monaco.editor.ScrollType.Smooth);
263-
editor.setPosition({ lineNumber: line, column: 1 });
348+
const model = editor.getModel();
349+
const clampedLine = model ? Math.max(1, Math.min(line, model.getLineCount())) : line;
350+
editor.revealLineInCenter(clampedLine, monaco.editor.ScrollType.Smooth);
351+
editor.setPosition({ lineNumber: clampedLine, column: 1 });
264352
editor.focus();
265353

266354
const collection = highlightRef.current;
267355
if (!collection) return;
268356
collection.set([
269357
{
270-
range: new monaco.Range(line, 1, line, 1),
358+
range: new monaco.Range(clampedLine, 1, clampedLine, 1),
271359
options: {
272360
isWholeLine: true,
273361
className: "highlight-line",
@@ -284,7 +372,43 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
284372
}, 2000);
285373
}, []);
286374

287-
const configureMonaco = useCallback((monaco: typeof import("@codingame/monaco-vscode-editor-api")) => {
375+
const revealPosition = useCallback((line: number, column?: number) => {
376+
const editor = editorRef.current;
377+
const monaco = monacoRef.current;
378+
if (!editor || !monaco) return;
379+
const model = editor.getModel();
380+
const clampedLine = model ? Math.max(1, Math.min(line, model.getLineCount())) : line;
381+
const maxColumn = model ? model.getLineMaxColumn(clampedLine) : undefined;
382+
let targetColumn = column && column > 0 ? column : 1;
383+
if (maxColumn) {
384+
targetColumn = Math.max(1, Math.min(targetColumn, maxColumn));
385+
}
386+
editor.revealLineInCenter(clampedLine, monaco.editor.ScrollType.Smooth);
387+
editor.setPosition({ lineNumber: clampedLine, column: targetColumn });
388+
editor.focus();
389+
390+
const collection = highlightRef.current;
391+
if (!collection) return;
392+
collection.set([
393+
{
394+
range: new monaco.Range(clampedLine, 1, clampedLine, 1),
395+
options: {
396+
isWholeLine: true,
397+
className: "highlight-line",
398+
glyphMarginClassName: "highlight-glyph",
399+
},
400+
},
401+
]);
402+
if (highlightTimerRef.current != null) {
403+
window.clearTimeout(highlightTimerRef.current);
404+
}
405+
highlightTimerRef.current = window.setTimeout(() => {
406+
collection.set([]);
407+
highlightTimerRef.current = null;
408+
}, 2000);
409+
}, []);
410+
411+
const configureMonaco = useCallback((monaco: typeof import("@codingame/monaco-vscode-editor-api")) => {
288412
if (monacoConfiguredRef.current) return;
289413
monacoConfiguredRef.current = true;
290414

@@ -448,13 +572,42 @@ const configureMonaco = useCallback((monaco: typeof import("@codingame/monaco-vs
448572
viewsConfig: {
449573
$type: "EditorService",
450574
openEditorFunc: async (modelRef, options) => {
451-
const target = modelRef?.object?.textEditorModel?.uri?.toString();
575+
const model = modelRef?.object;
576+
if (model && !model.isResolved()) {
577+
try {
578+
await model.resolve();
579+
} catch (error) {
580+
logNavigationIssue("Failed to resolve editor model", error);
581+
}
582+
}
583+
const textModel = model?.textEditorModel ?? null;
584+
const target = textModel?.uri?.toString();
452585
if (target) {
453-
const selection = (options as { selection?: { startLineNumber?: number } } | undefined)
454-
?.selection;
455-
const line = selection?.startLineNumber;
456-
await openFileRef.current?.(target, line);
586+
const selection = extractSelection(options);
587+
if (!selection && options) {
588+
logNavigationIssue("Missing selection for editor navigation", options);
589+
}
590+
if (textModel && editorRef.current) {
591+
editorRef.current.setModel(textModel);
592+
const content = textModel.getValue();
593+
setCode(content);
594+
setCurrentFile(target);
595+
setIsDirty(false);
596+
currentFileRef.current = target;
597+
if (selection) {
598+
revealPosition(selection.line, selection.column);
599+
}
600+
return editorRef.current;
601+
}
602+
const content = textModel?.getValue();
603+
if (content != null) {
604+
await openFileWithContentRef.current?.(target, content, selection?.line, selection?.column);
605+
} else {
606+
await openFileRef.current?.(target, selection?.line, selection?.column);
607+
}
608+
return editorRef.current ?? undefined;
457609
}
610+
logNavigationIssue("Missing target URI for editor navigation", { modelRef, options });
458611
return editorRef.current ?? undefined;
459612
},
460613
},
@@ -624,20 +777,35 @@ const configureMonaco = useCallback((monaco: typeof import("@codingame/monaco-vs
624777
}
625778
}, [revealLine]);
626779

627-
const openFile = useCallback(async (filePath: string, line?: number) => {
780+
const openFile = useCallback(async (filePath: string, line?: number, column?: number) => {
628781
if (!filePath) return;
629782
const fileUri = toFileUri(filePath);
630783
if (currentFileRef.current === fileUri) {
631-
if (line != null) revealLine(line);
784+
if (line != null) revealPosition(line, column);
632785
return;
633786
}
634-
pendingJumpRef.current = line ?? null;
787+
pendingJumpRef.current = line != null ? { line, column } : null;
635788
await loadFile(fileUri);
636-
}, [loadFile, revealLine]);
789+
}, [loadFile, revealPosition]);
790+
791+
const openFileWithContent = useCallback(
792+
async (filePath: string, content: string, line?: number, column?: number) => {
793+
if (!filePath) return;
794+
const fileUri = toFileUri(filePath);
795+
if (currentFileRef.current === fileUri) {
796+
if (line != null) revealPosition(line, column);
797+
return;
798+
}
799+
pendingJumpRef.current = line != null ? { line, column } : null;
800+
await loadFile(fileUri, content);
801+
},
802+
[loadFile, revealPosition],
803+
);
637804

638805
useEffect(() => {
639806
openFileRef.current = openFile;
640-
}, [openFile]);
807+
openFileWithContentRef.current = openFileWithContent;
808+
}, [openFile, openFileWithContent]);
641809

642810
// Load project.tsx content
643811
useEffect(() => {
@@ -663,10 +831,10 @@ const configureMonaco = useCallback((monaco: typeof import("@codingame/monaco-vs
663831

664832
useEffect(() => {
665833
if (pendingJumpRef.current == null) return;
666-
const line = pendingJumpRef.current;
834+
const { line, column } = pendingJumpRef.current;
667835
pendingJumpRef.current = null;
668-
revealLine(line);
669-
}, [code, currentFile, revealLine]);
836+
revealPosition(line, column);
837+
}, [code, currentFile, revealPosition]);
670838

671839
const handleSave = async () => {
672840
if (!code) return;
@@ -721,7 +889,7 @@ const configureMonaco = useCallback((monaco: typeof import("@codingame/monaco-vs
721889
};
722890

723891
const displayPath = resolveDisplayPath(currentFile);
724-
const languageId = displayPath.endsWith(".tsx") ? "typescriptreact" : "typescript";
892+
const languageId = getLanguageId(displayPath);
725893

726894
return (
727895
<div

src/ui/editor-context.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
import { createContext, useContext, type ReactNode, useState, useCallback } from "react";
2-
1+
import { createContext, useContext, type ReactNode, useState, useCallback } from "react";
2+
33
type EditorApi = {
4-
openFile: (filePath: string, line?: number) => void;
4+
openFile: (filePath: string, line?: number, column?: number) => void;
55
jumpToLine: (line: number) => void;
66
jumpToMatch: (needle: string) => void;
77
}
88

99
interface EditorContextValue {
10-
openFile: (filePath: string, line?: number) => void;
10+
openFile: (filePath: string, line?: number, column?: number) => void;
1111
jumpToLine: (line: number) => void;
1212
jumpToMatch: (needle: string) => void;
1313
registerEditor: (api: EditorApi) => void;
1414
}
15-
16-
const EditorContext = createContext<EditorContextValue | null>(null);
17-
15+
16+
const EditorContext = createContext<EditorContextValue | null>(null);
17+
1818
export const EditorProvider = ({ children }: { children: ReactNode }) => {
1919
const [editorApi, setEditorApi] = useState<EditorApi | null>(null);
2020

21-
const openFile = useCallback((filePath: string, line?: number) => {
22-
editorApi?.openFile(filePath, line);
21+
const openFile = useCallback((filePath: string, line?: number, column?: number) => {
22+
editorApi?.openFile(filePath, line, column);
2323
}, [editorApi]);
2424

2525
const jumpToLine = useCallback((line: number) => {
@@ -40,11 +40,11 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
4040
</EditorContext.Provider>
4141
);
4242
};
43-
44-
export const useEditor = () => {
45-
const context = useContext(EditorContext);
46-
if (!context) {
47-
throw new Error("useEditor must be used within EditorProvider");
48-
}
49-
return context;
50-
};
43+
44+
export const useEditor = () => {
45+
const context = useContext(EditorContext);
46+
if (!context) {
47+
throw new Error("useEditor must be used within EditorProvider");
48+
}
49+
return context;
50+
};

0 commit comments

Comments
 (0)