Skip to content

Commit 21409d1

Browse files
committed
Factor out lsp server setup so we can use it for some unit testing
1 parent ae5d110 commit 21409d1

File tree

3 files changed

+289
-239
lines changed

3 files changed

+289
-239
lines changed

internal/fourslash/baselineutil.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedRa
157157
foundAdditionalLocation := false
158158

159159
baselineEntries := []string{}
160-
err := f.vfs.WalkDir("/", func(path string, d vfs.DirEntry, e error) error {
160+
err := f.Vfs.WalkDir("/", func(path string, d vfs.DirEntry, e error) error {
161161
if e != nil {
162162
return e
163163
}
@@ -172,7 +172,7 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedRa
172172
return nil
173173
}
174174

175-
content, ok := f.vfs.ReadFile(path)
175+
content, ok := f.Vfs.ReadFile(path)
176176
if !ok {
177177
// !!! error?
178178
return nil
@@ -198,7 +198,7 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedRa
198198
// already added the file to the baseline.
199199
if options.additionalLocation != nil && !foundAdditionalLocation {
200200
fileName := options.additionalLocation.Uri.FileName()
201-
if content, ok := f.vfs.ReadFile(fileName); ok {
201+
if content, ok := f.Vfs.ReadFile(fileName); ok {
202202
baselineEntries = append(
203203
baselineEntries,
204204
f.getBaselineContentForFile(fileName, content, []lsproto.Range{options.additionalLocation.Range}, nil, options),
@@ -212,7 +212,7 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedRa
212212
if !foundMarker && options.marker != nil {
213213
// If we didn't find the marker in any file, we need to add it.
214214
markerFileName := options.marker.FileName()
215-
if content, ok := f.vfs.ReadFile(markerFileName); ok {
215+
if content, ok := f.Vfs.ReadFile(markerFileName); ok {
216216
baselineEntries = append(baselineEntries, f.getBaselineContentForFile(markerFileName, content, nil, nil, options))
217217
}
218218
}

internal/fourslash/fourslash.go

Lines changed: 16 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,34 @@
11
package fourslash
22

33
import (
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

3430
type 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-
12076
const rootDir = "/"
12177

12278
var 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-
303140
func 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

342144
func 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

367148
func (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

374155
func (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

Comments
 (0)