Skip to content

Commit 5cb55d4

Browse files
authored
userpreferences parsing/ls config handing (#1729)
1 parent 06a7b84 commit 5cb55d4

19 files changed

+874
-286
lines changed

internal/fourslash/fourslash.go

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type FourslashTest struct {
4343
scriptInfos map[string]*scriptInfo
4444
converters *ls.Converters
4545

46+
userPreferences *ls.UserPreferences
4647
currentCaretPosition lsproto.Position
4748
lastKnownMarkerName *string
4849
activeFilename string
@@ -180,14 +181,15 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten
180181
})
181182

182183
f := &FourslashTest{
183-
server: server,
184-
in: inputWriter,
185-
out: outputReader,
186-
testData: &testData,
187-
vfs: fs,
188-
scriptInfos: scriptInfos,
189-
converters: converters,
190-
baselines: make(map[string]*strings.Builder),
184+
server: server,
185+
in: inputWriter,
186+
out: outputReader,
187+
testData: &testData,
188+
userPreferences: ls.NewDefaultUserPreferences(), // !!! parse default preferences for fourslash case?
189+
vfs: fs,
190+
scriptInfos: scriptInfos,
191+
converters: converters,
192+
baselines: make(map[string]*strings.Builder),
191193
}
192194

193195
// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
@@ -257,6 +259,12 @@ func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lspr
257259
if capabilitiesWithDefaults.TextDocument.Completion == nil {
258260
capabilitiesWithDefaults.TextDocument.Completion = defaultCompletionCapabilities
259261
}
262+
if capabilitiesWithDefaults.Workspace == nil {
263+
capabilitiesWithDefaults.Workspace = &lsproto.WorkspaceClientCapabilities{}
264+
}
265+
if capabilitiesWithDefaults.Workspace.Configuration == nil {
266+
capabilitiesWithDefaults.Workspace.Configuration = ptrTrue
267+
}
260268
return &capabilitiesWithDefaults
261269
}
262270

@@ -269,6 +277,29 @@ func sendRequest[Params, Resp any](t *testing.T, f *FourslashTest, info lsproto.
269277
)
270278
f.writeMsg(t, req.Message())
271279
resp := f.readMsg(t)
280+
if resp == nil {
281+
return nil, *new(Resp), false
282+
}
283+
284+
// currently, the only request that may be sent by the server during a client request is one `config` request
285+
// !!! remove if `config` is handled in initialization and there are no other server-initiated requests
286+
if resp.Kind == lsproto.MessageKindRequest {
287+
req := resp.AsRequest()
288+
switch req.Method {
289+
case lsproto.MethodWorkspaceConfiguration:
290+
req := lsproto.ResponseMessage{
291+
ID: req.ID,
292+
JSONRPC: req.JSONRPC,
293+
Result: []any{f.userPreferences},
294+
}
295+
f.writeMsg(t, req.Message())
296+
resp = f.readMsg(t)
297+
default:
298+
// other types of requests not yet used in fourslash; implement them if needed
299+
t.Fatalf("Unexpected request received: %s", req.Method)
300+
}
301+
}
302+
272303
if resp == nil {
273304
return nil, *new(Resp), false
274305
}
@@ -301,6 +332,21 @@ func (f *FourslashTest) readMsg(t *testing.T) *lsproto.Message {
301332
return msg
302333
}
303334

335+
func (f *FourslashTest) Configure(t *testing.T, config *ls.UserPreferences) {
336+
f.userPreferences = config
337+
sendNotification(t, f, lsproto.WorkspaceDidChangeConfigurationInfo, &lsproto.DidChangeConfigurationParams{
338+
Settings: config,
339+
})
340+
}
341+
342+
func (f *FourslashTest) ConfigureWithReset(t *testing.T, config *ls.UserPreferences) (reset func()) {
343+
originalConfig := f.userPreferences.Copy()
344+
f.Configure(t, config)
345+
return func() {
346+
f.Configure(t, originalConfig)
347+
}
348+
}
349+
304350
func (f *FourslashTest) GoToMarkerOrRange(t *testing.T, markerOrRange MarkerOrRange) {
305351
f.goToMarker(t, markerOrRange)
306352
}
@@ -568,12 +614,16 @@ func (f *FourslashTest) VerifyCompletions(t *testing.T, markerInput MarkerInput,
568614

569615
func (f *FourslashTest) verifyCompletionsWorker(t *testing.T, expected *CompletionsExpectedList) *lsproto.CompletionList {
570616
prefix := f.getCurrentPositionPrefix()
571-
list := f.getCompletions(t)
617+
var userPreferences *ls.UserPreferences
618+
if expected != nil {
619+
userPreferences = expected.UserPreferences
620+
}
621+
list := f.getCompletions(t, userPreferences)
572622
f.verifyCompletionsResult(t, list, expected, prefix)
573623
return list
574624
}
575625

576-
func (f *FourslashTest) getCompletions(t *testing.T) *lsproto.CompletionList {
626+
func (f *FourslashTest) getCompletions(t *testing.T, userPreferences *ls.UserPreferences) *lsproto.CompletionList {
577627
prefix := f.getCurrentPositionPrefix()
578628
params := &lsproto.CompletionParams{
579629
TextDocument: lsproto.TextDocumentIdentifier{
@@ -582,6 +632,10 @@ func (f *FourslashTest) getCompletions(t *testing.T) *lsproto.CompletionList {
582632
Position: f.currentCaretPosition,
583633
Context: &lsproto.CompletionContext{},
584634
}
635+
if userPreferences != nil {
636+
reset := f.ConfigureWithReset(t, userPreferences)
637+
defer reset()
638+
}
585639
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentCompletionInfo, params)
586640
if resMsg == nil {
587641
t.Fatalf(prefix+"Nil response received for completion request", f.lastKnownMarkerName)
@@ -867,11 +921,22 @@ type ApplyCodeActionFromCompletionOptions struct {
867921
Description string
868922
NewFileContent *string
869923
NewRangeContent *string
924+
UserPreferences *ls.UserPreferences
870925
}
871926

872927
func (f *FourslashTest) VerifyApplyCodeActionFromCompletion(t *testing.T, markerName *string, options *ApplyCodeActionFromCompletionOptions) {
873928
f.GoToMarker(t, *markerName)
874-
completionsList := f.getCompletions(t)
929+
var userPreferences *ls.UserPreferences
930+
if options != nil && options.UserPreferences != nil {
931+
userPreferences = options.UserPreferences
932+
} else {
933+
// Default preferences: enables auto-imports
934+
userPreferences = ls.NewDefaultUserPreferences()
935+
}
936+
937+
reset := f.ConfigureWithReset(t, userPreferences)
938+
defer reset()
939+
completionsList := f.getCompletions(t, nil) // Already configured, so we do not need to pass it in again
875940
item := core.Find(completionsList.Items, func(item *lsproto.CompletionItem) bool {
876941
if item.Label != options.Name || item.Data == nil {
877942
return false
@@ -1663,6 +1728,12 @@ func (f *FourslashTest) getCurrentPositionPrefix() string {
16631728
}
16641729

16651730
func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames []string) {
1731+
reset := f.ConfigureWithReset(t, &ls.UserPreferences{
1732+
IncludeCompletionsForModuleExports: core.TSTrue,
1733+
IncludeCompletionsForImportStatements: core.TSTrue,
1734+
})
1735+
defer reset()
1736+
16661737
for _, markerName := range markerNames {
16671738
f.GoToMarker(t, markerName)
16681739
params := &lsproto.CompletionParams{

internal/fourslash/tests/autoImportCompletion_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ a/**/
4040
},
4141
})
4242
f.BaselineAutoImportsCompletions(t, []string{""})
43+
f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{
44+
UserPreferences: &ls.UserPreferences{
45+
// completion autoimport preferences off; this tests if fourslash server communication correctly registers changes in user preferences
46+
IncludeCompletionsForModuleExports: core.TSUnknown,
47+
IncludeCompletionsForImportStatements: core.TSUnknown,
48+
},
49+
IsIncomplete: false,
50+
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
51+
CommitCharacters: &DefaultCommitCharacters,
52+
EditRange: Ignored,
53+
},
54+
Items: &fourslash.CompletionsExpectedItems{
55+
Excludes: []string{"anotherVar"},
56+
},
57+
})
4358
}
4459

4560
func TestAutoImportCompletion2(t *testing.T) {

internal/ls/autoimportfixes.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ func (ct *changeTracker) doAddExistingFix(
2727
defaultImport *Import,
2828
namedImports []*Import,
2929
// removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented
30-
preferences *UserPreferences,
3130
) {
3231
switch clause.Kind {
3332
case ast.KindObjectBindingPattern:
@@ -84,14 +83,14 @@ func (ct *changeTracker) doAddExistingFix(
8483
}
8584

8685
if len(namedImports) > 0 {
87-
specifierComparer, isSorted := getNamedImportSpecifierComparerWithDetection(importClause.Parent, preferences, sourceFile)
86+
specifierComparer, isSorted := ct.ls.getNamedImportSpecifierComparerWithDetection(importClause.Parent, sourceFile)
8887
newSpecifiers := core.Map(namedImports, func(namedImport *Import) *ast.Node {
8988
var identifier *ast.Node
9089
if namedImport.propertyName != "" {
9190
identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode()
9291
}
9392
return ct.NodeFactory.NewImportSpecifier(
94-
(!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences),
93+
(!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, ct.ls.UserPreferences()),
9594
identifier,
9695
ct.NodeFactory.NewIdentifier(namedImport.name),
9796
)
@@ -208,15 +207,15 @@ func (ct *changeTracker) newBindingElementFromNameAndPropertyName(name string, p
208207
)
209208
}
210209

211-
func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool, preferences *UserPreferences) {
210+
func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool) {
212211
var existingImportStatements []*ast.Statement
213212

214213
if imports[0].Kind == ast.KindVariableStatement {
215214
existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement)
216215
} else {
217216
existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax)
218217
}
219-
comparer, isSorted := getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences)
218+
comparer, isSorted := ct.ls.getOrganizeImportsStringComparerWithDetection(existingImportStatements)
220219
sortedNewImports := slices.Clone(imports)
221220
slices.SortFunc(sortedNewImports, func(a, b *ast.Statement) int {
222221
return compareImportsOrRequireStatements(a, b, comparer)
@@ -271,7 +270,6 @@ func (ct *changeTracker) getNewImports(
271270
namedImports []*Import,
272271
namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; }
273272
compilerOptions *core.CompilerOptions,
274-
preferences *UserPreferences,
275273
) []*ast.Statement {
276274
moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier)
277275
var statements []*ast.Statement // []AnyImportSyntax
@@ -280,7 +278,7 @@ func (ct *changeTracker) getNewImports(
280278
// even though it's not an error, it would add unnecessary runtime emit.
281279
topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) &&
282280
core.Every(namedImports, func(i *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) ||
283-
(compilerOptions.VerbatimModuleSyntax.IsTrue() || preferences.PreferTypeOnlyAutoImports) &&
281+
(compilerOptions.VerbatimModuleSyntax.IsTrue() || ct.ls.UserPreferences().PreferTypeOnlyAutoImports) &&
284282
defaultImport != nil && defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed && !core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed })
285283

286284
var defaultImportNode *ast.Node
@@ -294,7 +292,7 @@ func (ct *changeTracker) getNewImports(
294292
namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName)
295293
}
296294
return ct.NodeFactory.NewImportSpecifier(
297-
!topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences),
295+
!topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, ct.ls.UserPreferences()),
298296
namedImportPropertyName,
299297
ct.NodeFactory.NewIdentifier(namedImport.name),
300298
)
@@ -306,15 +304,15 @@ func (ct *changeTracker) getNewImports(
306304
if namespaceLikeImport.kind == ImportKindCommonJS {
307305
declaration = ct.NodeFactory.NewImportEqualsDeclaration(
308306
/*modifiers*/ nil,
309-
shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences),
307+
shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ct.ls.UserPreferences()),
310308
ct.NodeFactory.NewIdentifier(namespaceLikeImport.name),
311309
ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral),
312310
)
313311
} else {
314312
declaration = ct.NodeFactory.NewImportDeclaration(
315313
/*modifiers*/ nil,
316314
ct.NodeFactory.NewImportClause(
317-
/*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ast.KindTypeKeyword, ast.KindUnknown),
315+
/*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ct.ls.UserPreferences()), ast.KindTypeKeyword, ast.KindUnknown),
318316
/*name*/ nil,
319317
ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)),
320318
),

0 commit comments

Comments
 (0)