diff --git a/internal/fourslash/tests/autoImportFileExcludePatterns_test.go b/internal/fourslash/tests/autoImportFileExcludePatterns_test.go new file mode 100644 index 0000000000..795a08a033 --- /dev/null +++ b/internal/fourslash/tests/autoImportFileExcludePatterns_test.go @@ -0,0 +1,78 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestAutoImportFileExcludePatterns1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /project/a.ts +export const varA = 10; + +// @Filename: /project/excluded/b.ts +export const varB = 20; + +// @Filename: /project/c.ts +varA/**/ +` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + + // With exclusion pattern - only varA from ./a should be available as auto-import + f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ + UserPreferences: &ls.UserPreferences{ + IncludeCompletionsForModuleExports: core.TSTrue, + IncludeCompletionsForImportStatements: core.TSTrue, + AutoImportFileExcludePatterns: []string{"/project/excluded/**/*"}, + }, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{"varA"}, + Excludes: []string{"varB"}, + }, + }) +} + +func TestAutoImportFileExcludePatterns2(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /project/src/a.ts +export const varA = 10; + +// @Filename: /project/tests/b.ts +export const varB = 20; + +// @Filename: /project/node_modules/c/index.ts +export const varC = 30; + +// @Filename: /project/src/main.ts +varA/**/ +` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + + // Exclude tests and node_modules directories + f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ + UserPreferences: &ls.UserPreferences{ + IncludeCompletionsForModuleExports: core.TSTrue, + IncludeCompletionsForImportStatements: core.TSTrue, + AutoImportFileExcludePatterns: []string{"/project/tests/**/*", "/project/node_modules/**/*"}, + }, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{"varA"}, + Excludes: []string{"varB", "varC"}, + }, + }) +} diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index ac168cf5da..6364b2820d 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/dlclark/regexp2" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/binder" @@ -19,6 +20,7 @@ import ( "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" ) type SymbolExportInfo struct { @@ -1328,20 +1330,69 @@ func getDefaultLikeExportNameFromDeclaration(symbol *ast.Symbol) string { return "" } +// getIsExcludedPatterns converts autoImportFileExcludePatterns to regexes +func getIsExcludedPatterns(preferences *UserPreferences, useCaseSensitiveFileNames bool) []*regexp2.Regexp { + if len(preferences.AutoImportFileExcludePatterns) == 0 { + return nil + } + + patterns := make([]*regexp2.Regexp, 0, len(preferences.AutoImportFileExcludePatterns)) + for _, spec := range preferences.AutoImportFileExcludePatterns { + // The client is expected to send rooted path specs since we don't know + // what directory a relative path is relative to. + pattern := vfs.GetPatternFromSpec(spec, "", "exclude") + if pattern != "" { + patterns = append(patterns, vfs.GetRegexFromPattern(pattern, useCaseSensitiveFileNames)) + } + } + return patterns +} + +// getIsExcluded returns a function that checks if a source file is excluded by the patterns +func getIsExcluded(excludePatterns []*regexp2.Regexp) func(*ast.SourceFile) bool { + if len(excludePatterns) == 0 { + return func(*ast.SourceFile) bool { return false } + } + + return func(sourceFile *ast.SourceFile) bool { + fileName := sourceFile.FileName() + for _, pattern := range excludePatterns { + match, err := pattern.MatchString(fileName) + if err == nil && match { + return true + } + } + // Note: TypeScript also checks for symlinks, but we're simplifying for now + // as the Go implementation doesn't appear to have symlink cache support yet + return false + } +} + +// getIsFileExcluded returns a function that checks if a source file should be excluded from auto-imports +func getIsFileExcluded(host Host, preferences *UserPreferences) func(*ast.SourceFile) bool { + if len(preferences.AutoImportFileExcludePatterns) == 0 { + return func(*ast.SourceFile) bool { return false } + } + return getIsExcluded(getIsExcludedPatterns(preferences, host.UseCaseSensitiveFileNames())) +} + func forEachExternalModuleToImportFrom( ch *checker.Checker, program *compiler.Program, preferences *UserPreferences, + useCaseSensitiveFileNames bool, // useAutoImportProvider bool, cb func(module *ast.Symbol, moduleFile *ast.SourceFile, checker *checker.Checker, isFromPackageJson bool), ) { - // !!! excludePatterns - // excludePatterns := preferences.autoImportFileExcludePatterns && getIsExcludedPatterns(preferences, useCaseSensitiveFileNames) + var excludePatterns []*regexp2.Regexp + if len(preferences.AutoImportFileExcludePatterns) > 0 { + excludePatterns = getIsExcludedPatterns(preferences, useCaseSensitiveFileNames) + } forEachExternalModule( ch, program.GetSourceFiles(), - // !!! excludePatterns, + excludePatterns, func(module *ast.Symbol, file *ast.SourceFile) { cb(module, file, ch, false) }, @@ -1370,19 +1421,33 @@ func forEachExternalModuleToImportFrom( func forEachExternalModule( ch *checker.Checker, allSourceFiles []*ast.SourceFile, - // excludePatterns []RegExp, + excludePatterns []*regexp2.Regexp, cb func(moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile), ) { - // !!! excludePatterns - // isExcluded := excludePatterns && getIsExcluded(excludePatterns, host) + isExcluded := getIsExcluded(excludePatterns) for _, ambient := range ch.GetAmbientModules() { - if !strings.Contains(ambient.Name, "*") /* && !(excludePatterns && ambient.Declarations.every(func (d){ return isExcluded(d.getSourceFile())})) */ { - cb(ambient, nil /*sourceFile*/) + if !strings.Contains(ambient.Name, "*") { + // Check if all declarations are excluded + allExcluded := true + if excludePatterns != nil && ambient.Declarations != nil { + for _, d := range ambient.Declarations { + if sf := ast.GetSourceFileOfNode(d); sf != nil && !isExcluded(sf) { + allExcluded = false + break + } + } + } else { + allExcluded = false + } + + if !allExcluded { + cb(ambient, nil /*sourceFile*/) + } } } for _, sourceFile := range allSourceFiles { - if ast.IsExternalOrCommonJSModule(sourceFile) /* && !isExcluded(sourceFile) */ { + if ast.IsExternalOrCommonJSModule(sourceFile) && !isExcluded(sourceFile) { cb(ch.GetMergedSymbol(sourceFile.Symbol), sourceFile) } } diff --git a/internal/ls/autoimportsexportinfo.go b/internal/ls/autoimportsexportinfo.go index 65de5e82ad..6ab58bb2ec 100644 --- a/internal/ls/autoimportsexportinfo.go +++ b/internal/ls/autoimportsexportinfo.go @@ -26,6 +26,7 @@ func (l *LanguageService) getExportInfos( ch, l.GetProgram(), preferences, + l.host.UseCaseSensitiveFileNames(), // /*useAutoImportProvider*/ true, func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { @@ -125,6 +126,7 @@ func (l *LanguageService) searchExportInfosForCompletions( ch, l.GetProgram(), preferences, + l.host.UseCaseSensitiveFileNames(), // /*useAutoImportProvider*/ true, func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 9527c4f7bb..39d9ae0ea7 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -15,7 +15,6 @@ import ( "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" - "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" @@ -453,7 +452,7 @@ func (l *LanguageService) getImplementationReferenceEntries(ctx context.Context, return core.FlatMap(symbolsAndEntries, func(s *SymbolAndEntries) []*referenceEntry { return s.references }) } -func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.RenameParams, prefs *ls.UserPreferences) (lsproto.WorkspaceEditOrNull, error) { +func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.RenameParams, prefs *UserPreferences) (lsproto.WorkspaceEditOrNull, error) { program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri) position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position)) node := astnav.GetTouchingPropertyName(sourceFile, position)