github.com/v2fly/tools@v0.100.0/internal/lsp/cmd/capabilities_test.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cmd 6 7 import ( 8 "context" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 "testing" 13 14 "github.com/v2fly/tools/internal/lsp" 15 "github.com/v2fly/tools/internal/lsp/cache" 16 "github.com/v2fly/tools/internal/lsp/protocol" 17 errors "golang.org/x/xerrors" 18 ) 19 20 // TestCapabilities does some minimal validation of the server's adherence to the LSP. 21 // The checks in the test are added as changes are made and errors noticed. 22 func TestCapabilities(t *testing.T) { 23 tmpDir, err := ioutil.TempDir("", "fake") 24 if err != nil { 25 t.Fatal(err) 26 } 27 tmpFile := filepath.Join(tmpDir, "fake.go") 28 if err := ioutil.WriteFile(tmpFile, []byte(""), 0775); err != nil { 29 t.Fatal(err) 30 } 31 if err := ioutil.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module fake\n\ngo 1.12\n"), 0775); err != nil { 32 t.Fatal(err) 33 } 34 defer os.RemoveAll(tmpDir) 35 36 app := New("gopls-test", tmpDir, os.Environ(), nil) 37 c := newConnection(app) 38 ctx := context.Background() 39 defer c.terminate(ctx) 40 41 params := &protocol.ParamInitialize{} 42 params.RootURI = protocol.URIFromPath(c.Client.app.wd) 43 params.Capabilities.Workspace.Configuration = true 44 45 // Send an initialize request to the server. 46 c.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), c.Client) 47 result, err := c.Server.Initialize(ctx, params) 48 if err != nil { 49 t.Fatal(err) 50 } 51 // Validate initialization result. 52 if err := validateCapabilities(result); err != nil { 53 t.Error(err) 54 } 55 // Complete initialization of server. 56 if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 57 t.Fatal(err) 58 } 59 60 // Open the file on the server side. 61 uri := protocol.URIFromPath(tmpFile) 62 if err := c.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ 63 TextDocument: protocol.TextDocumentItem{ 64 URI: uri, 65 LanguageID: "go", 66 Version: 1, 67 Text: `package main; func main() {};`, 68 }, 69 }); err != nil { 70 t.Fatal(err) 71 } 72 73 // If we are sending a full text change, the change.Range must be nil. 74 // It is not enough for the Change to be empty, as that is ambiguous. 75 if err := c.Server.DidChange(ctx, &protocol.DidChangeTextDocumentParams{ 76 TextDocument: protocol.VersionedTextDocumentIdentifier{ 77 TextDocumentIdentifier: protocol.TextDocumentIdentifier{ 78 URI: uri, 79 }, 80 Version: 2, 81 }, 82 ContentChanges: []protocol.TextDocumentContentChangeEvent{ 83 { 84 Range: nil, 85 Text: `package main; func main() { fmt.Println("") }`, 86 }, 87 }, 88 }); err != nil { 89 t.Fatal(err) 90 } 91 92 // Send a code action request to validate expected types. 93 actions, err := c.Server.CodeAction(ctx, &protocol.CodeActionParams{ 94 TextDocument: protocol.TextDocumentIdentifier{ 95 URI: uri, 96 }, 97 }) 98 if err != nil { 99 t.Fatal(err) 100 } 101 for _, action := range actions { 102 // Validate that an empty command is sent along with import organization responses. 103 if action.Kind == protocol.SourceOrganizeImports && action.Command != nil { 104 t.Errorf("unexpected command for import organization") 105 } 106 } 107 108 if err := c.Server.DidSave(ctx, &protocol.DidSaveTextDocumentParams{ 109 TextDocument: protocol.TextDocumentIdentifier{ 110 URI: uri, 111 }, 112 // LSP specifies that a file can be saved with optional text, so this field must be nil. 113 Text: nil, 114 }); err != nil { 115 t.Fatal(err) 116 } 117 118 // Send a completion request to validate expected types. 119 list, err := c.Server.Completion(ctx, &protocol.CompletionParams{ 120 TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 121 TextDocument: protocol.TextDocumentIdentifier{ 122 URI: uri, 123 }, 124 Position: protocol.Position{ 125 Line: 0, 126 Character: 28, 127 }, 128 }, 129 }) 130 if err != nil { 131 t.Fatal(err) 132 } 133 for _, item := range list.Items { 134 // All other completion items should have nil commands. 135 // An empty command will be treated as a command with the name '' by VS Code. 136 // This causes VS Code to report errors to users about invalid commands. 137 if item.Command != nil { 138 t.Errorf("unexpected command for completion item") 139 } 140 // The item's TextEdit must be a pointer, as VS Code considers TextEdits 141 // that don't contain the cursor position to be invalid. 142 var textEdit interface{} = item.TextEdit 143 if _, ok := textEdit.(*protocol.TextEdit); !ok { 144 t.Errorf("textEdit is not a *protocol.TextEdit, instead it is %T", textEdit) 145 } 146 } 147 if err := c.Server.Shutdown(ctx); err != nil { 148 t.Fatal(err) 149 } 150 if err := c.Server.Exit(ctx); err != nil { 151 t.Fatal(err) 152 } 153 } 154 155 func validateCapabilities(result *protocol.InitializeResult) error { 156 // If the client sends "false" for RenameProvider.PrepareSupport, 157 // the server must respond with a boolean. 158 if v, ok := result.Capabilities.RenameProvider.(bool); !ok { 159 return errors.Errorf("RenameProvider must be a boolean if PrepareSupport is false (got %T)", v) 160 } 161 // The same goes for CodeActionKind.ValueSet. 162 if v, ok := result.Capabilities.CodeActionProvider.(bool); !ok { 163 return errors.Errorf("CodeActionSupport must be a boolean if CodeActionKind.ValueSet has length 0 (got %T)", v) 164 } 165 return nil 166 }