11package fourslash
22
33import (
4- "context"
54 "fmt"
6- "io"
75 "maps"
86 "slices"
97 "strings"
108 "testing"
119 "unicode/utf8"
1210
13- "github.com/go-json-experiment/json"
1411 "github.com/google/go-cmp/cmp"
1512 "github.com/microsoft/typescript-go/internal/bundled"
1613 "github.com/microsoft/typescript-go/internal/collections"
1714 "github.com/microsoft/typescript-go/internal/core"
1815 "github.com/microsoft/typescript-go/internal/ls"
1916 "github.com/microsoft/typescript-go/internal/ls/lsconv"
2017 "github.com/microsoft/typescript-go/internal/ls/lsutil"
21- "github.com/microsoft/typescript-go/internal/lsp"
2218 "github.com/microsoft/typescript-go/internal/lsp/lsproto"
2319 "github.com/microsoft/typescript-go/internal/project"
2420 "github.com/microsoft/typescript-go/internal/repo"
2521 "github.com/microsoft/typescript-go/internal/stringutil"
2622 "github.com/microsoft/typescript-go/internal/testutil/baseline"
2723 "github.com/microsoft/typescript-go/internal/testutil/harnessutil"
24+ "github.com/microsoft/typescript-go/internal/testutil/lsptestutil"
2825 "github.com/microsoft/typescript-go/internal/tspath"
29- "github.com/microsoft/typescript-go/internal/vfs"
3026 "github.com/microsoft/typescript-go/internal/vfs/vfstest"
3127 "gotest.tools/v3/assert"
3228)
3329
3430type FourslashTest struct {
35- server * lsp.Server
36- in * lspWriter
37- out * lspReader
38- id int32
39- vfs vfs.FS
31+ lsptestutil.TestLspServer
4032
4133 testData * TestData // !!! consolidate test files from test data and script info
4234 baselines map [string ]* strings.Builder
@@ -45,7 +37,6 @@ type FourslashTest struct {
4537 scriptInfos map [string ]* scriptInfo
4638 converters * lsconv.Converters
4739
48- userPreferences * lsutil.UserPreferences
4940 currentCaretPosition lsproto.Position
5041 lastKnownMarkerName * string
5142 activeFilename string
@@ -82,41 +73,6 @@ func (s *scriptInfo) FileName() string {
8273 return s .fileName
8374}
8475
85- type lspReader struct {
86- c <- chan * lsproto.Message
87- }
88-
89- func (r * lspReader ) Read () (* lsproto.Message , error ) {
90- msg , ok := <- r .c
91- if ! ok {
92- return nil , io .EOF
93- }
94- return msg , nil
95- }
96-
97- type lspWriter struct {
98- c chan <- * lsproto.Message
99- }
100-
101- func (w * lspWriter ) Write (msg * lsproto.Message ) error {
102- w .c <- msg
103- return nil
104- }
105-
106- func (r * lspWriter ) Close () {
107- close (r .c )
108- }
109-
110- var (
111- _ lsp.Reader = (* lspReader )(nil )
112- _ lsp.Writer = (* lspWriter )(nil )
113- )
114-
115- func newLSPPipe () (* lspReader , * lspWriter ) {
116- c := make (chan * lsproto.Message , 100 )
117- return & lspReader {c : c }, & lspWriter {c : c }
118- }
119-
12076const rootDir = "/"
12177
12278var parseCache = project.ParseCache {
@@ -146,33 +102,8 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten
146102 SkipDefaultLibCheck : core .TSTrue ,
147103 }
148104 harnessutil .SetCompilerOptionsFromTestConfig (t , testData .GlobalOptions , compilerOptions , rootDir )
149-
150- inputReader , inputWriter := newLSPPipe ()
151- outputReader , outputWriter := newLSPPipe ()
152105 fs := bundled .WrapFS (vfstest .FromMap (testfs , true /*useCaseSensitiveFileNames*/ ))
153-
154- var err strings.Builder
155- server := lsp .NewServer (& lsp.ServerOptions {
156- In : inputReader ,
157- Out : outputWriter ,
158- Err : & err ,
159-
160- Cwd : "/" ,
161- FS : fs ,
162- DefaultLibraryPath : bundled .LibPath (),
163-
164- ParseCache : & parseCache ,
165- })
166-
167- go func () {
168- defer func () {
169- outputWriter .Close ()
170- }()
171- err := server .Run (context .TODO ())
172- if err != nil {
173- t .Error ("server error:" , err )
174- }
175- }()
106+ lspTestServer := lsptestutil .NewTestLspServer (t , fs , & parseCache , compilerOptions , capabilities )
176107
177108 converters := lsconv .NewConverters (lsproto .PositionEncodingKindUTF8 , func (fileName string ) * lsconv.LSPLineMap {
178109 scriptInfo , ok := scriptInfos [fileName ]
@@ -183,28 +114,19 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten
183114 })
184115
185116 f := & FourslashTest {
186- server : server ,
187- in : inputWriter ,
188- out : outputReader ,
189- testData : & testData ,
190- userPreferences : lsutil .NewDefaultUserPreferences (), // !!! parse default preferences for fourslash case?
191- vfs : fs ,
192- scriptInfos : scriptInfos ,
193- converters : converters ,
194- baselines : make (map [string ]* strings.Builder ),
195- }
196-
197- // !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
198- // !!! replace with a proper request *after initialize*
199- f .server .SetCompilerOptionsForInferredProjects (t .Context (), compilerOptions )
200- f .initialize (t , capabilities )
117+ TestLspServer : * lspTestServer ,
118+ testData : & testData ,
119+ scriptInfos : scriptInfos ,
120+ converters : converters ,
121+ baselines : make (map [string ]* strings.Builder ),
122+ }
123+
201124 for _ , file := range testData .Files {
202125 f .openFile (t , file .fileName )
203126 }
204127 f .activeFilename = f .testData .Files [0 ].fileName
205128
206129 t .Cleanup (func () {
207- inputWriter .Close ()
208130 f .verifyBaselines (t )
209131 })
210132 return f
@@ -215,164 +137,23 @@ func getBaseFileNameFromTest(t *testing.T) string {
215137 return stringutil .LowerFirstChar (name )
216138}
217139
218- func (f * FourslashTest ) nextID () int32 {
219- id := f .id
220- f .id ++
221- return id
222- }
223-
224- func (f * FourslashTest ) initialize (t * testing.T , capabilities * lsproto.ClientCapabilities ) {
225- params := & lsproto.InitializeParams {
226- Locale : ptrTo ("en-US" ),
227- }
228- params .Capabilities = getCapabilitiesWithDefaults (capabilities )
229- // !!! check for errors?
230- sendRequest (t , f , lsproto .InitializeInfo , params )
231- sendNotification (t , f , lsproto .InitializedInfo , & lsproto.InitializedParams {})
232- }
233-
234- var (
235- ptrTrue = ptrTo (true )
236- defaultCompletionCapabilities = & lsproto.CompletionClientCapabilities {
237- CompletionItem : & lsproto.ClientCompletionItemOptions {
238- SnippetSupport : ptrTrue ,
239- CommitCharactersSupport : ptrTrue ,
240- PreselectSupport : ptrTrue ,
241- LabelDetailsSupport : ptrTrue ,
242- InsertReplaceSupport : ptrTrue ,
243- DocumentationFormat : & []lsproto.MarkupKind {lsproto .MarkupKindMarkdown , lsproto .MarkupKindPlainText },
244- },
245- CompletionList : & lsproto.CompletionListCapabilities {
246- ItemDefaults : & []string {"commitCharacters" , "editRange" },
247- },
248- }
249- defaultDefinitionCapabilities = & lsproto.DefinitionClientCapabilities {
250- LinkSupport : ptrTrue ,
251- }
252- defaultTypeDefinitionCapabilities = & lsproto.TypeDefinitionClientCapabilities {
253- LinkSupport : ptrTrue ,
254- }
255- defaultHoverCapabilities = & lsproto.HoverClientCapabilities {
256- ContentFormat : & []lsproto.MarkupKind {lsproto .MarkupKindMarkdown , lsproto .MarkupKindPlainText },
257- }
258- )
259-
260- func getCapabilitiesWithDefaults (capabilities * lsproto.ClientCapabilities ) * lsproto.ClientCapabilities {
261- var capabilitiesWithDefaults lsproto.ClientCapabilities
262- if capabilities != nil {
263- capabilitiesWithDefaults = * capabilities
264- }
265- capabilitiesWithDefaults .General = & lsproto.GeneralClientCapabilities {
266- PositionEncodings : & []lsproto.PositionEncodingKind {lsproto .PositionEncodingKindUTF8 },
267- }
268- if capabilitiesWithDefaults .TextDocument == nil {
269- capabilitiesWithDefaults .TextDocument = & lsproto.TextDocumentClientCapabilities {}
270- }
271- if capabilitiesWithDefaults .TextDocument .Completion == nil {
272- capabilitiesWithDefaults .TextDocument .Completion = defaultCompletionCapabilities
273- }
274- if capabilitiesWithDefaults .TextDocument .Diagnostic == nil {
275- capabilitiesWithDefaults .TextDocument .Diagnostic = & lsproto.DiagnosticClientCapabilities {
276- RelatedInformation : ptrTrue ,
277- TagSupport : & lsproto.ClientDiagnosticsTagOptions {
278- ValueSet : []lsproto.DiagnosticTag {
279- lsproto .DiagnosticTagUnnecessary ,
280- lsproto .DiagnosticTagDeprecated ,
281- },
282- },
283- }
284- }
285- if capabilitiesWithDefaults .Workspace == nil {
286- capabilitiesWithDefaults .Workspace = & lsproto.WorkspaceClientCapabilities {}
287- }
288- if capabilitiesWithDefaults .Workspace .Configuration == nil {
289- capabilitiesWithDefaults .Workspace .Configuration = ptrTrue
290- }
291- if capabilitiesWithDefaults .TextDocument .Definition == nil {
292- capabilitiesWithDefaults .TextDocument .Definition = defaultDefinitionCapabilities
293- }
294- if capabilitiesWithDefaults .TextDocument .TypeDefinition == nil {
295- capabilitiesWithDefaults .TextDocument .TypeDefinition = defaultTypeDefinitionCapabilities
296- }
297- if capabilitiesWithDefaults .TextDocument .Hover == nil {
298- capabilitiesWithDefaults .TextDocument .Hover = defaultHoverCapabilities
299- }
300- return & capabilitiesWithDefaults
301- }
302-
303140func sendRequest [Params , Resp any ](t * testing.T , f * FourslashTest , info lsproto.RequestInfo [Params , Resp ], params Params ) (* lsproto.Message , Resp , bool ) {
304- id := f .nextID ()
305- req := lsproto .NewRequestMessage (
306- info .Method ,
307- lsproto .NewID (lsproto.IntegerOrString {Integer : & id }),
308- params ,
309- )
310- f .writeMsg (t , req .Message ())
311- resp := f .readMsg (t )
312- if resp == nil {
313- return nil , * new (Resp ), false
314- }
315-
316- // currently, the only request that may be sent by the server during a client request is one `config` request
317- // !!! remove if `config` is handled in initialization and there are no other server-initiated requests
318- if resp .Kind == lsproto .MessageKindRequest {
319- req := resp .AsRequest ()
320- switch req .Method {
321- case lsproto .MethodWorkspaceConfiguration :
322- req := lsproto.ResponseMessage {
323- ID : req .ID ,
324- JSONRPC : req .JSONRPC ,
325- Result : []any {f .userPreferences },
326- }
327- f .writeMsg (t , req .Message ())
328- resp = f .readMsg (t )
329- default :
330- // other types of requests not yet used in fourslash; implement them if needed
331- t .Fatalf ("Unexpected request received: %s" , req .Method )
332- }
333- }
334-
335- if resp == nil {
336- return nil , * new (Resp ), false
337- }
338- result , ok := resp .AsResponse ().Result .(Resp )
339- return resp , result , ok
141+ return lsptestutil .SendRequest (t , & f .TestLspServer , info , params )
340142}
341143
342144func sendNotification [Params any ](t * testing.T , f * FourslashTest , info lsproto.NotificationInfo [Params ], params Params ) {
343- notification := lsproto .NewNotificationMessage (
344- info .Method ,
345- params ,
346- )
347- f .writeMsg (t , notification .Message ())
348- }
349-
350- func (f * FourslashTest ) writeMsg (t * testing.T , msg * lsproto.Message ) {
351- assert .NilError (t , json .MarshalWrite (io .Discard , msg ), "failed to encode message as JSON" )
352- if err := f .in .Write (msg ); err != nil {
353- t .Fatalf ("failed to write message: %v" , err )
354- }
355- }
356-
357- func (f * FourslashTest ) readMsg (t * testing.T ) * lsproto.Message {
358- // !!! filter out response by id etc
359- msg , err := f .out .Read ()
360- if err != nil {
361- t .Fatalf ("failed to read message: %v" , err )
362- }
363- assert .NilError (t , json .MarshalWrite (io .Discard , msg ), "failed to encode message as JSON" )
364- return msg
145+ lsptestutil .SendNotification (t , & f .TestLspServer , info , params )
365146}
366147
367148func (f * FourslashTest ) Configure (t * testing.T , config * lsutil.UserPreferences ) {
368- f .userPreferences = config
149+ f .TestLspServer . UserPreferences = config
369150 sendNotification (t , f , lsproto .WorkspaceDidChangeConfigurationInfo , & lsproto.DidChangeConfigurationParams {
370151 Settings : config ,
371152 })
372153}
373154
374155func (f * FourslashTest ) ConfigureWithReset (t * testing.T , config * lsutil.UserPreferences ) (reset func ()) {
375- originalConfig := f .userPreferences .Copy ()
156+ originalConfig := f .TestLspServer . UserPreferences .Copy ()
376157 f .Configure (t , config )
377158 return func () {
378159 f .Configure (t , originalConfig )
@@ -1745,7 +1526,7 @@ func (f *FourslashTest) editScript(t *testing.T, fileName string, start int, end
17451526 }
17461527
17471528 script .editContent (start , end , newText )
1748- err := f .vfs .WriteFile (fileName , script .content , false )
1529+ err := f .Vfs .WriteFile (fileName , script .content , false )
17491530 if err != nil {
17501531 panic (fmt .Sprintf ("Failed to write file %s: %v" , fileName , err ))
17511532 }
@@ -1949,7 +1730,7 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames
19491730
19501731 f .writeToBaseline ("Auto Imports" , "// === Auto Imports === \n " )
19511732
1952- fileContent , ok := f .vfs .ReadFile (f .activeFilename )
1733+ fileContent , ok := f .Vfs .ReadFile (f .activeFilename )
19531734 if ! ok {
19541735 t .Fatalf (prefix + "Failed to read file %s for auto-import baseline" , f .activeFilename )
19551736 }
0 commit comments