@@ -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 / C a n n o t r e s o l v e m o d u l e ' ( [ ^ ' ] + ) ' /
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+ / i m p o r t \s + .* ?f r o m \s + ( [ " ' ` ] ) ( [ ^ " ' ` ] + ) \1| r e q u i r e \( \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- / i m p o r t \s + .* ?f r o m \s + [ ' " ` ] ( [ ^ ' " ` ] * ) $ /
497+ / i m p o r t \s + .* ?f r o m \s + [ ' " ` ] ( [ ^ ' " ` ] * ) $ | r e q u i r e \( \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