Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unique command names #279

Merged
merged 10 commits into from
Oct 30, 2020
20 changes: 20 additions & 0 deletions docs/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ of the target platform (e.g. `\` on Windows, or `/` on Unix),
symlinks are followed, trailing slashes automatically removed,
and `~` is replaced with your home directory.

## `commandPrefix`

Some clients such as VS Code keep a global registry of commands published by language
servers, and the names must be unique, even between terraform-ls instances. Setting
this allows multiple servers to run side by side, albeit the client is now responsible
for routing commands to the correct server. Users should not need to worry about
this, the frontend client extension should manage it.

The prefix will be applied to the front of the command name, which already contains
a `terraform-ls` prefix.

`commandPrefix.terraform-ls.commandName`

Or if left empty

`terraform-ls.commandName`

This setting should be deprecated once the language server supports multiple workspaces,
as this arises in VS code because a server instance is started per VS Code workspace.

## How to pass settings

The server expects static settings to be passed as part of LSP `initialize` call,
Expand Down
23 changes: 23 additions & 0 deletions internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var (
ctxRootModuleWalker = &contextKey{"root module walker"}
ctxRootModuleLoader = &contextKey{"root module loader"}
ctxRootDir = &contextKey{"root directory"}
ctxCommandPrefix = &contextKey{"command prefix"}
ctxDiags = &contextKey{"diagnostics"}
)

Expand Down Expand Up @@ -190,6 +191,28 @@ func RootDirectory(ctx context.Context) (string, bool) {
return *rootDir, true
}

func WithCommandPrefix(ctx context.Context, prefix *string) context.Context {
return context.WithValue(ctx, ctxCommandPrefix, prefix)
}

func SetCommandPrefix(ctx context.Context, prefix string) error {
commandPrefix, ok := ctx.Value(ctxCommandPrefix).(*string)
if !ok {
return missingContextErr(ctxCommandPrefix)
}

*commandPrefix = prefix
return nil
}

func CommandPrefix(ctx context.Context) (string, bool) {
commandPrefix, ok := ctx.Value(ctxCommandPrefix).(*string)
if !ok {
return "", false
}
return *commandPrefix, true
}

func WithRootModuleWalker(ctx context.Context, w *rootmodule.Walker) context.Context {
return context.WithValue(ctx, ctxRootModuleWalker, w)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package settings

import (
"fmt"

"github.com/mitchellh/mapstructure"
)

type Options struct {
// RootModulePaths describes a list of absolute paths to root modules
RootModulePaths []string `mapstructure:"rootModulePaths"`
ExcludeModulePaths []string `mapstructure:"excludeModulePaths"`
CommandPrefix string `mapstructure:"commandPrefix"`

// TODO: Need to check for conflict with CLI flags
// TerraformExecPath string
Expand Down
29 changes: 24 additions & 5 deletions langserver/handlers/execute_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,45 @@ import (
"strings"

"github.com/creachadair/jrpc2/code"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
lsp "github.com/sourcegraph/go-lsp"
)

type executeCommandHandler func(context.Context, commandArgs) (interface{}, error)
type executeCommandHandlers map[string]executeCommandHandler

const langServerPrefix = "terraform-ls."

var handlers = executeCommandHandlers{
"rootmodules": executeCommandRootModulesHandler,
prefix("rootmodules"): executeCommandRootModulesHandler,
}

func prefix(name string) string {
appilon marked this conversation as resolved.
Show resolved Hide resolved
return langServerPrefix + name
}

func (h executeCommandHandlers) Names() []string {
var names []string
func (h executeCommandHandlers) Names(commandPrefix string) (names []string) {
if commandPrefix != "" {
commandPrefix += "."
}
for name := range h {
names = append(names, name)
names = append(names, commandPrefix+name)
}
return names
}

func (h executeCommandHandlers) Get(name, commandPrefix string) (executeCommandHandler, bool) {
if commandPrefix != "" {
commandPrefix += "."
}
name = strings.TrimPrefix(name, commandPrefix)
handler, ok := h[name]
return handler, ok
}

func (lh *logHandler) WorkspaceExecuteCommand(ctx context.Context, params lsp.ExecuteCommandParams) (interface{}, error) {
handler, ok := handlers[params.Command]
commandPrefix, _ := lsctx.CommandPrefix(ctx)
handler, ok := handlers.Get(params.Command, commandPrefix)
if !ok {
return nil, fmt.Errorf("%w: command handler not found for %q", code.MethodNotFound.Err(), params.Command)
}
Expand Down
20 changes: 10 additions & 10 deletions langserver/handlers/execute_command_rootmodules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestLangServer_workspaceExecuteCommand_rootmodules_argumentError(t *testing
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
"processId": 12345
}`, tmpDir.URI())})
ls.Notify(t, &langserver.CallRequest{
Method: "initialized",
Expand All @@ -50,9 +50,9 @@ func TestLangServer_workspaceExecuteCommand_rootmodules_argumentError(t *testing

ls.CallAndExpectError(t, &langserver.CallRequest{
Method: "workspace/executeCommand",
ReqParams: `{
"command": "rootmodules"
}`}, code.InvalidParams.Err())
ReqParams: fmt.Sprintf(`{
"command": %q
}`, prefix("rootmodules"))}, code.InvalidParams.Err())
}

func TestLangServer_workspaceExecuteCommand_rootmodules_basic(t *testing.T) {
Expand All @@ -75,7 +75,7 @@ func TestLangServer_workspaceExecuteCommand_rootmodules_basic(t *testing.T) {
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
"processId": 12345
}`, tmpDir.URI())})
ls.Notify(t, &langserver.CallRequest{
Method: "initialized",
Expand All @@ -95,9 +95,9 @@ func TestLangServer_workspaceExecuteCommand_rootmodules_basic(t *testing.T) {
ls.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "workspace/executeCommand",
ReqParams: fmt.Sprintf(`{
"command": "rootmodules",
"command": %q,
"arguments": ["uri=%s"]
}`, testFileURI)}, fmt.Sprintf(`{
}`, prefix("rootmodules"), testFileURI)}, fmt.Sprintf(`{
"jsonrpc": "2.0",
"id": 3,
"result": {
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestLangServer_workspaceExecuteCommand_rootmodules_multiple(t *testing.T) {
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
"processId": 12345
}`, root.URI())})
ls.Notify(t, &langserver.CallRequest{
Method: "initialized",
Expand All @@ -158,9 +158,9 @@ func TestLangServer_workspaceExecuteCommand_rootmodules_multiple(t *testing.T) {
ls.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "workspace/executeCommand",
ReqParams: fmt.Sprintf(`{
"command": "rootmodules",
"command": %q,
"arguments": ["uri=%s"]
}`, module.URI())}, fmt.Sprintf(`{
}`, prefix("rootmodules"), module.URI())}, fmt.Sprintf(`{
"jsonrpc": "2.0",
"id": 2,
"result": {
Expand Down
76 changes: 55 additions & 21 deletions langserver/handlers/handlers_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handlers

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -16,29 +17,38 @@ import (
"github.com/stretchr/testify/mock"
)

const initializeResponse = `{
"jsonrpc": "2.0",
"id": 1,
"result": {
"capabilities": {
"textDocumentSync": {
"openClose": true,
"change": 2
},
"completionProvider": {},
"documentSymbolProvider":true,
"documentFormattingProvider":true,
"executeCommandProvider": {
"commands": ["rootmodules"]
func initializeResponse(t *testing.T, commandPrefix string) string {
jsonArray, err := json.Marshal(handlers.Names(commandPrefix))
if err != nil {
t.Fatal(err)
}

return fmt.Sprintf(`{
"jsonrpc": "2.0",
"id": 1,
"result": {
"capabilities": {
"textDocumentSync": {
"openClose": true,
"change": 2
},
"completionProvider": {},
"documentSymbolProvider":true,
"documentFormattingProvider":true,
"executeCommandProvider": {
"commands": %s
}
}
}
}
}`
}`, string(jsonArray))
}

func TestInitalizeAndShutdown(t *testing.T) {
tmpDir := TempDir(t)

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
RootModules: map[string]*rootmodule.RootModuleMock{
TempDir(t).Dir(): {TfExecFactory: validTfMockCalls()},
tmpDir.Dir(): {TfExecFactory: validTfMockCalls()},
}}))
stop := ls.Start(t)
defer stop()
Expand All @@ -48,8 +58,8 @@ func TestInitalizeAndShutdown(t *testing.T) {
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
}`, TempDir(t).URI())}, initializeResponse)
"processId": 12345
}`, tmpDir.URI())}, initializeResponse(t, ""))
ls.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "shutdown", ReqParams: `{}`},
`{
Expand All @@ -59,7 +69,31 @@ func TestInitalizeAndShutdown(t *testing.T) {
}`)
}

func TestInitalizeWithCommandPrefix(t *testing.T) {
tmpDir := TempDir(t)

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
RootModules: map[string]*rootmodule.RootModuleMock{
tmpDir.Dir(): {TfExecFactory: validTfMockCalls()},
}}))
stop := ls.Start(t)
defer stop()

ls.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345,
"initializationOptions": {
"commandPrefix": "1"
}
}`, tmpDir.URI())}, initializeResponse(t, "1"))
}

func TestEOF(t *testing.T) {
tmpDir := TempDir(t)

ms := newMockSession(&MockSessionInput{
RootModules: map[string]*rootmodule.RootModuleMock{
TempDir(t).Dir(): {TfExecFactory: validTfMockCalls()},
Expand All @@ -73,8 +107,8 @@ func TestEOF(t *testing.T) {
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
}`, TempDir(t).URI())}, initializeResponse)
"processId": 12345
}`, tmpDir.URI())}, initializeResponse(t, ""))

ls.CloseClientStdout(t)

Expand Down
11 changes: 8 additions & 3 deletions langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam
},
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
ExecuteCommandProvider: &lsp.ExecuteCommandOptions{
Commands: handlers.Names(),
},
},
}

Expand Down Expand Up @@ -77,6 +74,14 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam
if err != nil {
return serverCaps, err
}

// set commandPrefix for session
lsctx.SetCommandPrefix(ctx, out.Options.CommandPrefix)
// apply prefix to executeCommand handler names
serverCaps.Capabilities.ExecuteCommandProvider = &lsp.ExecuteCommandOptions{
Commands: handlers.Names(out.Options.CommandPrefix),
}
appilon marked this conversation as resolved.
Show resolved Hide resolved

if len(out.UnusedKeys) > 0 {
jrpc2.PushNotify(ctx, "window/showMessage", &lsp.ShowMessageParams{
Type: lsp.MTWarning,
Expand Down
4 changes: 3 additions & 1 deletion langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {
diags := diagnostics.NewNotifier(svc.sessCtx, svc.logger)

rootDir := ""
commandPrefix := ""

m := map[string]rpch.Func{
"initialize": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
Expand All @@ -157,6 +158,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {
ctx = lsctx.WithWatcher(ctx, ww)
ctx = lsctx.WithRootModuleWalker(ctx, svc.walker)
ctx = lsctx.WithRootDirectory(ctx, &rootDir)
ctx = lsctx.WithCommandPrefix(ctx, &commandPrefix)
ctx = lsctx.WithRootModuleManager(ctx, svc.modMgr)
ctx = lsctx.WithRootModuleLoader(ctx, rmLoader)

Expand Down Expand Up @@ -240,7 +242,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {
return nil, err
}

ctx = lsctx.WithRootDirectory(ctx, &rootDir)
ctx = lsctx.WithCommandPrefix(ctx, &commandPrefix)
ctx = lsctx.WithRootModuleCandidateFinder(ctx, svc.modMgr)
ctx = lsctx.WithRootModuleWalker(ctx, svc.walker)

Expand Down