Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package fourslash_test

import (
"testing"

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

// The name of an object literal property that is not an identifier is completed as a string literal, which has to
// follow the quote preference. The preference is configured for the whole session, rather than per request,
// so that resolving a completion item sees the same preference as the completion request did.
func configureQuotePreference(t *testing.T, f *fourslash.FourslashTest, quotePreference lsutil.QuotePreference) {
t.Helper()
opts := f.GetOptions()
opts.QuotePreference = quotePreference
f.Configure(t, opts)
}

// requiredKey is the completion of a required property, whose name is `name` (already quoted).
func requiredKey(name string) *lsproto.CompletionItem {
return &lsproto.CompletionItem{Label: name}
}

// optionalKey is the completion of an optional property, whose name is `name` (already quoted).
func optionalKey(name string) *lsproto.CompletionItem {
return &lsproto.CompletionItem{
Label: name + "?",
InsertText: new(name),
FilterText: new(name),
SortText: new(string(ls.SortTextOptionalMember)),
}
}

func verifyObjectLiteralKeyCompletions(t *testing.T, f *fourslash.FourslashTest, items ...*lsproto.CompletionItem) {
t.Helper()
expected := make([]fourslash.CompletionsExpectedItem, len(items))
for i, item := range items {
expected[i] = item
}
f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &DefaultCommitCharacters,
EditRange: Ignored,
},
Items: &fourslash.CompletionsExpectedItems{Exact: expected},
})
}
Comment on lines +17 to +53

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have we not needed these helpers so far? seems surprising to need them now when our tests are already so extensive.


const objectPropertyNameContent = `interface Options {
"a-b": number;
"c d"?: string;
"it's"?: number;
'say "hi"'?: number;
plain: boolean;
}
const o: Options = {
/**/
};`

func TestCompletionsObjectPropertyName_quotePreferenceSingle(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `type TableName = 't1' | 't2';
type ColumnName = 'id' | 'value';
type TableColumn = ` + "`${TableName}.${ColumnName}`" + `;

type Join = {
on: Partial<Record<TableColumn, TableColumn>>;
};

const test: Join = {
on: {
/**/
}
};`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
configureQuotePreference(t, f, lsutil.QuotePreferenceSingle)
verifyObjectLiteralKeyCompletions(t, f,
optionalKey(`'t1.id'`),
optionalKey(`'t1.value'`),
optionalKey(`'t2.id'`),
optionalKey(`'t2.value'`),
)
}

func TestCompletionsObjectPropertyName_quotePreferenceSingleEscaping(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, objectPropertyNameContent)
defer done()
configureQuotePreference(t, f, lsutil.QuotePreferenceSingle)
// Resolving an item finds its symbol by name, so the detail checks that the name matches with the preference too.
spaced := optionalKey(`'c d'`)
spaced.Detail = new(`(property) Options["c d"]?: string | undefined`)
verifyObjectLiteralKeyCompletions(t, f,
requiredKey(`'a-b'`),
requiredKey(`plain`),
spaced,
optionalKey(`'it\'s'`),
optionalKey(`'say "hi"'`),
)
}

func TestCompletionsObjectPropertyName_quotePreferenceDouble(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, objectPropertyNameContent)
defer done()
configureQuotePreference(t, f, lsutil.QuotePreferenceDouble)
verifyObjectLiteralKeyCompletions(t, f,
requiredKey(`"a-b"`),
requiredKey(`plain`),
optionalKey(`"c d"`),
optionalKey(`"it's"`),
optionalKey(`"say \"hi\""`),
)
}

func TestCompletionsObjectPropertyName_quotePreferenceAuto(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @filename: /a.ts
export const a = 1;
// @filename: /b.ts
import { a } from './a';

interface Options {
"a-b"?: number;
}
const o: Options = {
/**/
};`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.GoToFile(t, "/b.ts")
configureQuotePreference(t, f, lsutil.QuotePreferenceAuto)
verifyObjectLiteralKeyCompletions(t, f, optionalKey(`'a-b'`))
}
15 changes: 9 additions & 6 deletions tsc/internal/ls/completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,7 @@ func (l *LanguageService) getCompletionData(
}
}
if objectLikeContainer.Kind == ast.KindObjectLiteralExpression && preferences.IncludeCompletionsWithObjectLiteralMethodSnippets.IsTrue() {
displayName, _ := getCompletionEntryDisplayNameForSymbol(member, nil /*origin*/, CompletionKindObjectPropertyDeclaration, false /*isJsxIdentifierExpected*/)
displayName, _ := getCompletionEntryDisplayNameForSymbol(file, preferences, member, nil /*origin*/, CompletionKindObjectPropertyDeclaration, false /*isJsxIdentifierExpected*/)
if displayName != "" {
originalSortText := core.OrElse(symbolToSortTextMap[symbolId], SortTextLocationPriority)
symbolToSortTextMap[symbolId] = ObjectLiteralPropertySortText(originalSortText, displayName)
Expand Down Expand Up @@ -1975,6 +1975,8 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
for index, symbol := range data.symbols {
origin := data.symbolToOriginInfoMap[index]
name, needsConvertPropertyAccess := getCompletionEntryDisplayNameForSymbol(
file,
preferences,
symbol,
origin,
data.completionKind,
Expand Down Expand Up @@ -2588,12 +2590,13 @@ func (l *LanguageService) collectObjectLiteralMethodSymbols(ctx context.Context,
return nil
}

preferences := l.UserPreferences()
var methods []objectLiteralMethodSymbol
for _, member := range members {
if !isObjectLiteralMethodSymbol(member) {
continue
}
displayName, _ := getCompletionEntryDisplayNameForSymbol(member, nil /*origin*/, CompletionKindObjectPropertyDeclaration, false /*isJsxIdentifierExpected*/)
displayName, _ := getCompletionEntryDisplayNameForSymbol(file, preferences, member, nil /*origin*/, CompletionKindObjectPropertyDeclaration, false /*isJsxIdentifierExpected*/)
if displayName == "" {
continue
}
Expand Down Expand Up @@ -3151,6 +3154,8 @@ func shouldIncludeSymbol(
}

func getCompletionEntryDisplayNameForSymbol(
file *ast.SourceFile,
preferences lsutil.UserPreferences,
symbol *ast.Symbol,
origin *symbolOriginInfo,
completionKind CompletionKind,
Expand Down Expand Up @@ -3193,9 +3198,7 @@ func getCompletionEntryDisplayNameForSymbol(
}
return "", false
case CompletionKindObjectPropertyDeclaration:
// TODO: microsoft/TypeScript#18169
escapedName, _ := core.StringifyJson(name, "", "")
return escapedName, false
return quote(file, preferences, name), false
case CompletionKindPropertyAccess, CompletionKindGlobal:
// For a 'this.' completion it will be in a global context, but may have a non-identifier name.
// Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547
Expand Down Expand Up @@ -5668,7 +5671,7 @@ func (l *LanguageService) getSymbolCompletionFromItemData(
// completion entry.
for index, symbol := range data.symbols {
origin := data.symbolToOriginInfoMap[index]
displayName, _ := getCompletionEntryDisplayNameForSymbol(symbol, origin, data.completionKind, data.isJsxIdentifierExpected)
displayName, _ := getCompletionEntryDisplayNameForSymbol(file, preferences, symbol, origin, data.completionKind, data.isJsxIdentifierExpected)
if displayName == itemData.Name &&
(itemData.Source == string(completionSourceClassMemberSnippet) && symbol.Flags&ast.SymbolFlagsClassMember != 0 ||
itemData.Source == string(completionSourceObjectLiteralMethodSnippet) && symbol.Flags&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod) != 0 ||
Expand Down