Skip to content

Commit 69b9d16

Browse files
committed
update as to emit marker for unresolved paths
1 parent 2999264 commit 69b9d16

File tree

1 file changed

+179
-30
lines changed

1 file changed

+179
-30
lines changed

Frontend/src/lib/monaco/diagnostics-provider.ts

Lines changed: 179 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ export class MonacoDiagnosticsProvider {
6565
startLineNumber: error.line,
6666
startColumn: error.column,
6767
endLineNumber: error.line,
68-
endColumn: error.column + error.importPath.length,
68+
endColumn: Math.max(
69+
error.column + 1,
70+
error.column + error.importPath.length
71+
),
6972
message: error.message,
7073
source: "VFS Dependency Resolver",
7174
code: "dependency-error",
@@ -119,8 +122,28 @@ export class MonacoDiagnosticsProvider {
119122
// Combine all errors
120123
const allErrors = [...fileErrors, ...additionalErrors];
121124

122-
// Create markers
123-
const markers = this.createMarkersFromErrors(allErrors);
125+
// Create markers and adjust ranges to underline the actual import string
126+
const markers = this.createMarkersFromErrors(allErrors).map((m) => {
127+
const lineContent = model.getLineContent(m.startLineNumber);
128+
// Try to find the quoted import path on this line to improve underline accuracy
129+
const pathMatch = lineContent.match(/['"][^'"]+['"]/g);
130+
if (pathMatch) {
131+
const first = pathMatch.find((s) =>
132+
m.message.includes(s.replace(/['"]/g, ""))
133+
);
134+
if (first) {
135+
const startIdx = lineContent.indexOf(first);
136+
if (startIdx >= 0) {
137+
return {
138+
...m,
139+
startColumn: startIdx + 1,
140+
endColumn: startIdx + first.length + 1,
141+
} as monaco.editor.IMarkerData;
142+
}
143+
}
144+
}
145+
return m;
146+
});
124147

125148
// Set markers on the model
126149
monaco.editor.setModelMarkers(model, "vfs-dependency-resolver", markers);
@@ -294,7 +317,6 @@ export class MonacoDiagnosticsProvider {
294317
);
295318

296319
importMarkers.forEach((marker) => {
297-
// Get the import path from the marker message
298320
const messageMatch = marker.message.match(
299321
/Cannot resolve module '([^']+)'/
300322
);
@@ -321,7 +343,17 @@ export class MonacoDiagnosticsProvider {
321343
endLineNumber: marker.endLineNumber,
322344
endColumn: marker.endColumn,
323345
},
324-
text: suggestion,
346+
// Replace keeping quotes intact if range includes them; otherwise insert quoted
347+
text: /['"`]/.test(
348+
model.getValueInRange({
349+
startLineNumber: marker.startLineNumber,
350+
startColumn: marker.startColumn,
351+
endLineNumber: marker.endLineNumber,
352+
endColumn: marker.endColumn,
353+
})
354+
)
355+
? suggestion
356+
: `'${suggestion}'`,
325357
},
326358
versionId: model.getVersionId(),
327359
},
@@ -354,6 +386,74 @@ export class MonacoDiagnosticsProvider {
354386
);
355387
}
356388

389+
/**
390+
* Setup hover provider to show detailed info for unresolved imports
391+
*/
392+
public setupHoverProvider(): monaco.IDisposable {
393+
return monaco.languages.registerHoverProvider(
394+
["typescript", "javascript"],
395+
{
396+
provideHover: (model, position) => {
397+
const textUntilPosition = model.getValueInRange({
398+
startLineNumber: position.lineNumber,
399+
startColumn: 1,
400+
endLineNumber: position.lineNumber,
401+
endColumn: model.getLineMaxColumn(position.lineNumber),
402+
});
403+
404+
// Detect import path under cursor
405+
const importLineMatch = textUntilPosition.match(
406+
/import\s+.*?from\s+(["'`])([^"'`]+)\1|require\(\s*(["'`])([^"'`]+)\3\s*\)/
407+
);
408+
if (!importLineMatch) return { contents: [] };
409+
410+
const importPath = (importLineMatch[2] || importLineMatch[4]) ?? "";
411+
if (!importPath) return { contents: [] };
412+
413+
const filePath = this.uriToPath(model.uri);
414+
const depGraph = this.dependencyManager.getDependencyGraph();
415+
const err = depGraph.errors.find(
416+
(e) => e.file === filePath && e.importPath === importPath
417+
);
418+
419+
if (!err) return { contents: [] };
420+
421+
const md: monaco.IMarkdownString = {
422+
value:
423+
`$(error) ${err.message}\n\n` +
424+
(err.suggestion ? `Suggestion: \`${err.suggestion}\`\n\n` : "") +
425+
`Source: VFS Dependency Resolver`,
426+
isTrusted: true,
427+
supportThemeIcons: true,
428+
};
429+
430+
// Compute range roughly over the import path
431+
const lineContent = model.getLineContent(position.lineNumber);
432+
const quoted = lineContent.match(/(["'`])([^"'`]+)\1/);
433+
let startColumn = 1;
434+
let endColumn = 1;
435+
if (quoted) {
436+
const idx = lineContent.indexOf(quoted[0]);
437+
if (idx >= 0) {
438+
startColumn = idx + 1;
439+
endColumn = idx + quoted[0].length + 1;
440+
}
441+
}
442+
443+
return {
444+
range: new monaco.Range(
445+
position.lineNumber,
446+
startColumn,
447+
position.lineNumber,
448+
endColumn
449+
),
450+
contents: [md],
451+
};
452+
},
453+
}
454+
);
455+
}
456+
357457
/**
358458
* Get suggestion for import path
359459
*/
@@ -392,15 +492,14 @@ export class MonacoDiagnosticsProvider {
392492
endColumn: position.column,
393493
});
394494

395-
// Check if we're in an import statement
495+
// Check if we're in an import or require string
396496
const importMatch = textUntilPosition.match(
397-
/import\s+.*?from\s+['"`]([^'"`]*)$/
497+
/import\s+.*?from\s+['"`]([^'"`]*)$|require\(\s*['"`]([^'"`]*)$/
398498
);
399499
if (!importMatch) return { suggestions: [] };
400500

401-
const currentPath = importMatch[1];
501+
const currentPath = (importMatch[1] || importMatch[2] || "").trim();
402502
const filePath = this.uriToPath(model.uri);
403-
const suggestions: monaco.languages.CompletionItem[] = [];
404503

405504
// Get all available files for completion
406505
const entries = this.vfs.getAllEntries();
@@ -409,30 +508,80 @@ export class MonacoDiagnosticsProvider {
409508
return entry?.type === "file" && path !== filePath;
410509
});
411510

412-
// Generate relative path suggestions
413-
availableFiles.forEach((targetPath) => {
511+
type Candidate = { display: string; target: string; score: number };
512+
const candidates: Candidate[] = [];
513+
514+
const addIfResolvable = (proposed: string, targetPath: string) => {
515+
try {
516+
const fromDir =
517+
filePath.substring(0, filePath.lastIndexOf("/")) || "/";
518+
const resolved = new URL(proposed, `file://${fromDir}/`).pathname;
519+
const exists =
520+
this.vfs.getFile(resolved) ||
521+
this.vfs.getFile(resolved + ".ts") ||
522+
this.vfs.getFile(resolved + ".tsx") ||
523+
this.vfs.getFile(resolved + ".js") ||
524+
this.vfs.getFile(resolved + ".jsx") ||
525+
this.vfs.getFile(resolved + ".json") ||
526+
this.vfs.getFile(`${resolved}/index.ts`) ||
527+
this.vfs.getFile(`${resolved}/index.tsx`) ||
528+
this.vfs.getFile(`${resolved}/index.js`) ||
529+
this.vfs.getFile(`${resolved}/index.jsx`) ||
530+
this.vfs.getFile(`${resolved}/index.json`);
531+
if (exists) {
532+
candidates.push({
533+
display: proposed,
534+
target: targetPath,
535+
score: proposed.length,
536+
});
537+
}
538+
} catch {
539+
// ignore invalid URLs
540+
}
541+
};
542+
543+
for (const targetPath of availableFiles) {
414544
const relativePath = this.getRelativePath(filePath, targetPath);
545+
if (!relativePath.startsWith(currentPath)) continue;
546+
547+
// Primary: propose extensionless relative paths for TS/JS
548+
let proposed = relativePath;
549+
const lastDot = proposed.lastIndexOf(".");
550+
if (lastDot > proposed.lastIndexOf("/")) {
551+
const ext = proposed.substring(lastDot);
552+
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
553+
proposed = proposed.substring(0, lastDot);
554+
}
555+
}
556+
addIfResolvable(proposed, targetPath);
415557

416-
if (relativePath.startsWith(currentPath)) {
417-
const fileName = targetPath.split("/").pop() || "";
418-
419-
suggestions.push({
420-
421-
label: relativePath,
422-
kind: monaco.languages.CompletionItemKind.File,
423-
insertText: relativePath,
424-
detail: `Import from ${fileName}`,
425-
documentation: `File: ${targetPath}`,
426-
sortText: relativePath.length.toString().padStart(3, "0"),
427-
range: {
428-
startLineNumber: position.lineNumber,
429-
startColumn: position.column - currentPath.length,
430-
endLineNumber: position.lineNumber,
431-
endColumn: position.column,
432-
},
433-
});
558+
// Secondary: folder path for index.*
559+
const baseName = targetPath.split("/").pop() || "";
560+
if (baseName.startsWith("index.")) {
561+
const folderProposed = this.getRelativePath(
562+
filePath,
563+
targetPath.substring(0, targetPath.lastIndexOf("/"))
564+
);
565+
if (folderProposed.startsWith(currentPath)) {
566+
addIfResolvable(folderProposed, targetPath);
567+
}
434568
}
435-
});
569+
}
570+
571+
candidates.sort((a, b) => a.score - b.score);
572+
const suggestions = candidates.map((c, i) => ({
573+
label: c.display,
574+
kind: monaco.languages.CompletionItemKind.File,
575+
insertText: c.display,
576+
detail: `File: ${c.target}`,
577+
sortText: String(i).padStart(3, "0"),
578+
range: {
579+
startLineNumber: position.lineNumber,
580+
startColumn: position.column - currentPath.length,
581+
endLineNumber: position.lineNumber,
582+
endColumn: position.column,
583+
},
584+
}));
436585

437586
return { suggestions };
438587
},

0 commit comments

Comments
 (0)