Module specifier completions - #2587
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements module specifier completions for the language server, fixing issue #2383 where path suggestions were missing in import/export statements. The implementation adds support for relative paths, non-relative modules, package.json exports/imports, typings, and triple-slash reference directives.
Changes:
- Added comprehensive module specifier completion logic (~1300 lines) including support for relative paths, node_modules, package.json exports/imports, path mappings, and triple-slash references
- Bypassed snapshot immutability to access real-time filesystem state for completion suggestions
- Updated test infrastructure to properly handle empty implicit first files in fourslash tests
- Fixed numerous test expectations and moved previously failing tests to passing status
Reviewed changes
Copilot reviewed 39 out of 39 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/ls/string_completions.go | Core completion logic with support for module specifiers, path mappings, package.json, and triple-slash references (~1300 new lines) |
| internal/ls/languageservice.go | Added DirectoryExists, ReadDirectory, and GetDirectories methods for completions |
| internal/ls/host.go | Extended Host interface with filesystem query methods that bypass snapshot immutability |
| internal/project/snapshot.go | Added DirectoryExists, GetDirectories, and ReadDirectory methods delegating to underlying filesystem |
| internal/ls/completions.go | Removed file extension detail logic from completion item creation |
| internal/tspath/extension.go | Added GetPossibleOriginalInputExtensionForExtension utility function |
| internal/module/resolver.go | Moved extension utility to tspath package and improved directory separator handling |
| internal/testrunner/test_case_parser.go | Fixed logic to properly handle empty implicit first files in fourslash tests |
| internal/astnav/tokens.go | Fixed isValidPrecedingNode to check JSDoc for EndOfFile tokens |
| internal/fourslash/_scripts/*.txt | Updated failing/manual test lists reflecting newly passing tests |
| internal/fourslash/tests/manual/*.go | Updated test expectations and file paths to match new completion behavior |
| internal/fourslash/tests/gen/*.go | Deleted tests for path completion features that now work correctly |
| internal/fourslash/fourslash.go | Added t.Helper() calls and improved nil completion handling |
| importNameCodeFixDefaultExport7 | ||
| importNameCodeFixOptionalImport0 | ||
| formattingFatArrowFunctions No newline at end of file | ||
| autoCloseFragment |
There was a problem hiding this comment.
This file was in a bad state so I had to dedupe + sort it again.
| } | ||
|
|
||
| func isValidPrecedingNode(node *ast.Node, sourceFile *ast.SourceFile) bool { | ||
| if node.Kind == ast.KindEndOfFile { |
There was a problem hiding this comment.
This matches a special case we already had in Strada and fixes a crash in one of the tests.
| if kindModifiers.Has(lsutil.ScriptElementKindModifierDeprecated) { | ||
| tags = &[]lsproto.CompletionItemTag{lsproto.CompletionItemTagDeprecated} | ||
| } | ||
| if kind == lsproto.CompletionItemKindFile { |
There was a problem hiding this comment.
Moved this logic to string_completions.go.
| if isPattern { | ||
| combinedLookup = strings.ReplaceAll(targetString, "*", subpath) | ||
| } | ||
| scopeContainingDirectory := tspath.EnsureTrailingDirectorySeparator(scope.PackageDirectory) |
There was a problem hiding this comment.
This fixes a crash when scopeContainingDirectory is /.
| if currentFileName != "" { | ||
| // Store result file - always save for regular tests, but skip empty implicit first file for fourslash | ||
| shouldSaveFile := currentFileContent.Len() != 0 || !options.AllowImplicitFirstFile | ||
| shouldSaveFile := !options.AllowImplicitFirstFile || currentFileContent.Len() != 0 || hasSeenFile |
There was a problem hiding this comment.
This allows us to parse empty files in tests, e.g.:
// @Filename: foo.ts
// @Filename: bar.ts
Fixes #2383
I opted to port the code as-is for now because it wasn't very clear to me how to modify the existing code to use the autoimports registry for non-relative specifier completions. In the future we might want to do this for performance or correctness reasons. It would likely require untangling the existing code, which ends up calling the same function for
getCompletionEntriesForDirectoryFragmentand reading from the file system for both relative and non-relative imports. We'd probably also have to simplify how we collect module specifier candidates to make better use of the information in the registry, e.g. if a user typed"packageFoo/bar/baz/f", right now we end up reading the directorypackageFoo/bar/bazand computing completions for that path, but we could just use all of the module specifier information collected forpackageFooand rely on the client to do the filtering for us for entries that match"packageFoo/bar/baz/f"(or do the filtering ourselves after we collect the entries).Note:
We bypass the snapshot's immutable view of files to be able to see what files and directories exist. This means there could be inconsistencies between the module specifier completions and the state of the current snapshot, but the worst that could happen here is extra or missing module specifiers in completions, so that seems fine.
Future work:
ReadDirectoryas ported from Strada that uses regexes. We'll need Rewrite matchFiles to not use regexp2, remove other regexp uses #2407 to get rid of regex use. The usage ofReadDirectoryby module specifier completions probably doesn't need full fidelity with the original Strada code, the only nice to haves I can think of are the filtering of*.min.jsfiles and maybe of paths starting with..