Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions tsc/internal/fourslash/tests/autoImportQuotedModuleSpecifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package fourslash_test

import (
"fmt"
"strings"
"testing"

"github.com/microsoft/TypeScript/tsc/internal/fourslash"
. "github.com/microsoft/TypeScript/tsc/internal/fourslash/tests/util"
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/testutil"
)

func TestAutoImportQuotedModuleSpecifier(t *testing.T) {
t.Parallel()
for _, quote := range []struct {
preference lsutil.QuotePreference
specifier string
}{
{lsutil.QuotePreferenceSingle, `'dep/a\'b"c'`},
{lsutil.QuotePreferenceDouble, `"dep/a'b\"c"`},
} {
for _, test := range []struct {
name string
extension string
declaration string
usage string
result string
statement string
snippet bool
}{
{
name: "named",
extension: "mts",
declaration: "export class Foo {}",
usage: "Foo/**/;",
result: "import { Foo } from %s;\n\nFoo;",
statement: "import { Foo$1 } from %s;",
snippet: true,
},
{
name: "namedNoSnippet",
extension: "mts",
declaration: "export class Foo {}",
usage: "Foo/**/;",
result: "import { Foo } from %s;\n\nFoo;",
statement: "import { Foo } from %s;",
},
{
name: "default",
extension: "mts",
declaration: "export default class Foo {}",
usage: "Foo/**/;",
result: "import Foo from %s;\n\nFoo;",
statement: "import Foo$1 from %s;",
snippet: true,
},
{
name: "require",
extension: "cjs",
declaration: "export class Foo {}",
usage: "Foo/**/;",
result: "const { Foo } = require(%s);\n\nFoo;",
},
{
name: "jsdoc",
extension: "js",
declaration: "export interface Foo {}",
usage: "/** @type {Foo/**/} */\nlet value;",
result: "/** @type {import(%s).Foo} */\nlet value;",
},
} {
t.Run(string(quote.preference)+"/"+test.name, func(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
content := fmt.Sprintf(`// @Filename: /tsconfig.json
{
"compilerOptions": {
"module": "node20",
"allowJs": true,
"checkJs": true,
"noEmit": true
}
}
// @Filename: /node_modules/dep/package.json
{
"name": "dep",
"exports": { "./a'b\"c": "./index.d.ts" }
}
// @Filename: /node_modules/dep/index.d.ts
%s
// @Filename: /fix.%s
%s
// @Filename: /completion.%s
%s
// @Filename: /all.%s
%s
// @Filename: /statement.%s
[|import F/*statement*/|]`,
test.declaration, test.extension, test.usage, test.extension,
strings.ReplaceAll(test.usage, "/**/", "/*completion*/"), test.extension,
strings.ReplaceAll(test.usage, "/**/", "/*all*/")+"\n"+
strings.ReplaceAll(strings.ReplaceAll(test.usage, "/**/", ""), "value", "other"), test.extension)
f, done := fourslash.NewFourslash(t, fourslash.GetDefaultCapabilitiesWithOptions(&fourslash.ClientCapabilitiesOptions{
CompletionItem: &lsproto.ClientCompletionItemOptions{SnippetSupport: new(test.snippet)},
}), content)
defer done()
preferences := lsutil.NewDefaultUserPreferences()
preferences.QuotePreference = quote.preference
expected := fmt.Sprintf(test.result, quote.specifier)

f.GoToMarker(t, "")
f.VerifyImportFixAtPosition(t, []string{expected}, &preferences)
reset := f.ConfigureWithReset(t, preferences)
defer reset()
f.GoToMarker(t, "all")
allExpected := expected + "\nFoo;"
if test.name == "jsdoc" {
allExpected = expected + "\n" + strings.ReplaceAll(expected, "value", "other")
}
f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{
FixID: "fixMissingImport",
NewFileContent: allExpected,
})

f.VerifyApplyCodeActionFromCompletion(t, new("completion"), &fourslash.ApplyCodeActionFromCompletionOptions{
Name: "Foo",
Source: `dep/a'b"c`,
UserPreferences: &preferences,
NewFileContent: new(expected),
})

if test.statement != "" {
statement := fmt.Sprintf(test.statement, quote.specifier)
var insertTextFormat *lsproto.InsertTextFormat
if test.snippet {
// Snippets escape backslashes independently of string literal escaping.
statement = strings.ReplaceAll(statement, `\`, `\\`)
insertTextFormat = new(lsproto.InsertTextFormatSnippet)
}
f.VerifyCompletions(t, "statement", &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &[]string{},
EditRange: Ignored,
},
Items: &fourslash.CompletionsExpectedItems{
Includes: []fourslash.CompletionsExpectedItem{
&lsproto.CompletionItem{
Label: "Foo",
InsertText: new(statement),
InsertTextFormat: insertTextFormat,
TextEdit: &lsproto.TextEditOrInsertReplaceEdit{
TextEdit: &lsproto.TextEdit{
NewText: statement,
Range: f.Ranges()[0].LSRange,
},
},
Data: &lsproto.CompletionItemData{
AutoImport: &lsproto.AutoImportFix{ModuleSpecifier: `dep/a'b"c`},
},
},
},
},
})
}
})
}
}
}
7 changes: 4 additions & 3 deletions tsc/internal/ls/autoimport/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/modulespecifiers"
"github.com/microsoft/TypeScript/tsc/internal/printer"
"github.com/microsoft/TypeScript/tsc/internal/scanner"
"github.com/microsoft/TypeScript/tsc/internal/stringutil"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
Expand Down Expand Up @@ -147,11 +148,11 @@ func addImportType(f *Fix, file *ast.SourceFile, preferences lsutil.UserPreferen
panic("UsagePosition must be set for JSDoc type import fix")
}
quotePreference := lsutil.GetQuotePreference(file, preferences)
quoteChar := "\""
quoteChar := printer.QuoteCharDoubleQuote
if quotePreference == lsutil.QuotePreferenceSingle {
quoteChar = "'"
quoteChar = printer.QuoteCharSingleQuote
}
importTypePrefix := fmt.Sprintf("import(%s%s%s).", quoteChar, f.ModuleSpecifier, quoteChar)
importTypePrefix := fmt.Sprintf("import(%c%s%c).", quoteChar, printer.EscapeString(f.ModuleSpecifier, quoteChar), quoteChar)
tracker.InsertText(file, *f.UsagePosition, importTypePrefix)
return diagnostics.Change_0_to_1.Localize(locale, f.Name, importTypePrefix+f.Name)
}
Expand Down
5 changes: 4 additions & 1 deletion tsc/internal/ls/completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2141,7 +2141,10 @@ func completionNameForLiteral(
}

func getInsertTextAndReplacementSpanForImportCompletion(fix *autoimport.Fix, importKind lsproto.ImportKind, importStatementCompletion *importStatementCompletionInfo, useSemicolons bool, file *ast.SourceFile, preferences lsutil.UserPreferences, isSnippet bool) (insertText string, replacementSpan *lsproto.Range) {
quotedModuleSpecifier := escapeSnippetText(quote(file, preferences, fix.ModuleSpecifier))
quotedModuleSpecifier := quote(file, preferences, fix.ModuleSpecifier)
if isSnippet {
quotedModuleSpecifier = escapeSnippetText(strings.ReplaceAll(quotedModuleSpecifier, `\`, `\\`))
}
tabStop := core.IfElse(isSnippet, "$1", "")
suffix := core.IfElse(useSemicolons, ";", "")
topLevelTypeOnlyText := core.IfElse(importStatementCompletion.isTopLevelTypeOnly, " "+scanner.TokenToString(ast.KindTypeKeyword)+" ", " ")
Expand Down
Loading