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

Enable opening a single Terraform file #843

Merged
merged 7 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ The following list of directories will always be ignored:
- `terraform.tfstate.d`
- `.terragrunt-cache`

## `ignoreSingleFileWarning` (`bool`)

This setting controls whether terraform-ls sends a warning about opening up a single Terraform file instead of a Terraform folder. Setting this to `true` will prevent the message being sent. The default value is `false`.

## `experimentalFeatures` (object)

This object contains inner settings used to opt into experimental features not yet ready to be on by default.
Expand Down
4 changes: 4 additions & 0 deletions internal/langserver/handlers/did_open.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ func (svc *service) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenT
return err
}

if svc.singleFileMode {
svc.stateStore.WalkerPaths.EnqueueDir(modHandle)
jpogran marked this conversation as resolved.
Show resolved Hide resolved
}

if !watcher.IsModuleWatched(mod.Path) {
err := watcher.AddModule(mod.Path)
if err != nil {
Expand Down
277 changes: 162 additions & 115 deletions internal/langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,89 +17,27 @@ import (
)

func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) (lsp.InitializeResult, error) {
serverCaps := lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
TextDocumentSync: lsp.TextDocumentSyncOptions{
OpenClose: true,
Change: lsp.Incremental,
},
CompletionProvider: lsp.CompletionOptions{
ResolveProvider: false,
TriggerCharacters: []string{".", "["},
},
CodeActionProvider: lsp.CodeActionOptions{
CodeActionKinds: ilsp.SupportedCodeActions.AsSlice(),
ResolveProvider: false,
},
DeclarationProvider: lsp.DeclarationOptions{},
DefinitionProvider: true,
CodeLensProvider: lsp.CodeLensOptions{},
ReferencesProvider: true,
HoverProvider: true,
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
WorkspaceSymbolProvider: true,
Workspace: lsp.Workspace5Gn{
WorkspaceFolders: lsp.WorkspaceFolders4Gn{
Supported: true,
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
},
},
},
}
serverCaps := initializeResult(ctx)

serverCaps.ServerInfo.Name = "terraform-ls"
version, ok := lsctx.LanguageServerVersion(ctx)
if ok {
serverCaps.ServerInfo.Version = version
out, err := settings.DecodeOptions(params.InitializationOptions)
if err != nil {
return serverCaps, err
}

clientCaps := params.Capabilities

properties := map[string]interface{}{
"experimentalCapabilities.referenceCountCodeLens": false,
"options.rootModulePaths": false,
"options.excludeModulePaths": false,
"options.commandPrefix": false,
"options.ignoreDirectoryNames": false,
"options.experimentalFeatures.validateOnSave": false,
"options.terraformExecPath": false,
"options.terraformExecTimeout": "",
"options.terraformLogFilePath": false,
"root_uri": "dir",
"lsVersion": serverCaps.ServerInfo.Version,
err = out.Options.Validate()
if err != nil {
return serverCaps, err
}

properties := mapProperties(out)
properties["lsVersion"] = serverCaps.ServerInfo.Version

clientCaps := params.Capabilities
expClientCaps := lsp.ExperimentalClientCapabilities(clientCaps.Experimental)

svc.server = jrpc2.ServerFromContext(ctx)

if tv, ok := expClientCaps.TelemetryVersion(); ok {
svc.logger.Printf("enabling telemetry (version: %d)", tv)
err := svc.setupTelemetry(tv, svc.server)
if err != nil {
svc.logger.Printf("failed to setup telemetry: %s", err)
}
svc.logger.Printf("telemetry enabled (version: %d)", tv)
}
defer svc.telemetry.SendEvent(ctx, "initialize", properties)

if params.RootURI == "" {
properties["root_uri"] = "file"
return serverCaps, fmt.Errorf("Editing a single file is not yet supported." +
" Please open a directory.")
}
if !uri.IsURIValid(string(params.RootURI)) {
properties["root_uri"] = "invalid"
return serverCaps, fmt.Errorf("URI %q is not valid", params.RootURI)
}

root := document.DirHandleFromURI(string(params.RootURI))

err := lsctx.SetRootDirectory(ctx, root.Path())
if err != nil {
return serverCaps, err
}
setupTelemetry(expClientCaps, svc, ctx, properties)

if params.ClientInfo.Name != "" {
err = ilsp.SetClientName(ctx, params.ClientInfo.Name)
Expand All @@ -108,7 +46,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
}
}

if _, ok = expClientCaps.ShowReferencesCommandId(); ok {
if _, ok := expClientCaps.ShowReferencesCommandId(); ok {
serverCaps.Capabilities.Experimental = lsp.ExperimentalServerCapabilities{
ReferenceCountCodeLens: true,
}
Expand All @@ -120,26 +58,6 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
return serverCaps, err
}

out, err := settings.DecodeOptions(params.InitializationOptions)
if err != nil {
return serverCaps, err
}

properties["options.rootModulePaths"] = len(out.Options.ModulePaths) > 0
properties["options.excludeModulePaths"] = len(out.Options.ExcludeModulePaths) > 0
properties["options.commandPrefix"] = len(out.Options.CommandPrefix) > 0
properties["options.ignoreDirectoryNames"] = len(out.Options.IgnoreDirectoryNames) > 0
properties["options.experimentalFeatures.prefillRequiredFields"] = out.Options.ExperimentalFeatures.PrefillRequiredFields
properties["options.experimentalFeatures.validateOnSave"] = out.Options.ExperimentalFeatures.ValidateOnSave
properties["options.terraformExecPath"] = len(out.Options.TerraformExecPath) > 0
properties["options.terraformExecTimeout"] = out.Options.TerraformExecTimeout
properties["options.terraformLogFilePath"] = len(out.Options.TerraformLogFilePath) > 0

err = out.Options.Validate()
if err != nil {
return serverCaps, err
}

err = svc.configureSessionDependencies(ctx, out.Options)
if err != nil {
return serverCaps, err
Expand Down Expand Up @@ -188,8 +106,137 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
})
}

if params.RootURI == "" {
svc.singleFileMode = true
properties["root_uri"] = "file"
if properties["options.ignoreSingleFileWarning"] == false {
jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{
Type: lsp.Warning,
Message: "Some capabilities may be reduced when editing a single file, but you can still open text files and edit them. We recommend opening a directory for full functionality.",
jpogran marked this conversation as resolved.
Show resolved Hide resolved
})
}
} else {
if !uri.IsURIValid(string(params.RootURI)) {
properties["root_uri"] = "invalid"
return serverCaps, fmt.Errorf("URI %q is not valid", params.RootURI)
}

err := svc.setupWalker(ctx, params, cfgOpts)
if err != nil {
return serverCaps, err
}
}

// Walkers run asynchronously so we're intentionally *not*
// passing the request context here
// Static user-provided paths take precedence over dynamic discovery
walkerCtx := context.Background()
err = svc.closedDirWalker.StartWalking(walkerCtx)
if err != nil {
return serverCaps, fmt.Errorf("failed to start closedDirWalker: %w", err)
}
err = svc.openDirWalker.StartWalking(walkerCtx)
if err != nil {
return serverCaps, fmt.Errorf("failed to start openDirWalker: %w", err)
}

return serverCaps, err
}

func setupTelemetry(expClientCaps lsp.ExpClientCapabilities, svc *service, ctx context.Context, properties map[string]interface{}) {
if tv, ok := expClientCaps.TelemetryVersion(); ok {
svc.logger.Printf("enabling telemetry (version: %d)", tv)
err := svc.setupTelemetry(tv, svc.server)
if err != nil {
svc.logger.Printf("failed to setup telemetry: %s", err)
}
svc.logger.Printf("telemetry enabled (version: %d)", tv)
}
defer svc.telemetry.SendEvent(ctx, "initialize", properties)
}

func mapProperties(out *settings.DecodedOptions) map[string]interface{} {
properties := map[string]interface{}{
"experimentalCapabilities.referenceCountCodeLens": false,
"options.ignoreSingleFileWarning": false,
"options.rootModulePaths": false,
"options.excludeModulePaths": false,
"options.commandPrefix": false,
"options.ignoreDirectoryNames": false,
"options.experimentalFeatures.validateOnSave": false,
"options.terraformExecPath": false,
"options.terraformExecTimeout": "",
"options.terraformLogFilePath": false,
"root_uri": "dir",
"lsVersion": "",
}

properties["options.rootModulePaths"] = len(out.Options.ModulePaths) > 0
properties["options.rootModulePaths"] = len(out.Options.ModulePaths) > 0
properties["options.excludeModulePaths"] = len(out.Options.ExcludeModulePaths) > 0
properties["options.commandPrefix"] = len(out.Options.CommandPrefix) > 0
properties["options.ignoreDirectoryNames"] = len(out.Options.IgnoreDirectoryNames) > 0
properties["options.experimentalFeatures.prefillRequiredFields"] = out.Options.ExperimentalFeatures.PrefillRequiredFields
properties["options.experimentalFeatures.validateOnSave"] = out.Options.ExperimentalFeatures.ValidateOnSave
properties["options.ignoreSingleFileWarning"] = out.Options.IgnoreSingleFileWarning
properties["options.terraformExecPath"] = len(out.Options.TerraformExecPath) > 0
properties["options.terraformExecTimeout"] = out.Options.TerraformExecTimeout
properties["options.terraformLogFilePath"] = len(out.Options.TerraformLogFilePath) > 0

return properties
}

func initializeResult(ctx context.Context) lsp.InitializeResult {
serverCaps := lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
TextDocumentSync: lsp.TextDocumentSyncOptions{
OpenClose: true,
Change: lsp.Incremental,
},
CompletionProvider: lsp.CompletionOptions{
ResolveProvider: false,
TriggerCharacters: []string{".", "["},
},
CodeActionProvider: lsp.CodeActionOptions{
CodeActionKinds: ilsp.SupportedCodeActions.AsSlice(),
ResolveProvider: false,
},
DeclarationProvider: lsp.DeclarationOptions{},
DefinitionProvider: true,
CodeLensProvider: lsp.CodeLensOptions{},
ReferencesProvider: true,
HoverProvider: true,
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
WorkspaceSymbolProvider: true,
Workspace: lsp.Workspace5Gn{
WorkspaceFolders: lsp.WorkspaceFolders4Gn{
Supported: true,
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
},
},
},
}

serverCaps.ServerInfo.Name = "terraform-ls"
version, ok := lsctx.LanguageServerVersion(ctx)
if ok {
serverCaps.ServerInfo.Version = version
}

return serverCaps
}

func (svc *service) setupWalker(ctx context.Context, params lsp.InitializeParams, options *settings.Options) error {
root := document.DirHandleFromURI(string(params.RootURI))

err := lsctx.SetRootDirectory(ctx, root.Path())
if err != nil {
return err
}

var excludeModulePaths []string
for _, rawPath := range cfgOpts.ExcludeModulePaths {
for _, rawPath := range options.ExcludeModulePaths {
modPath, err := resolvePath(root.Path(), rawPath)
if err != nil {
svc.logger.Printf("Ignoring excluded module path %s: %s", rawPath, err)
Expand All @@ -200,7 +247,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)

err = svc.stateStore.WalkerPaths.EnqueueDir(root)
if err != nil {
return serverCaps, err
return err
}

if len(params.WorkspaceFolders) > 0 {
Expand All @@ -219,27 +266,27 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
}
}

svc.closedDirWalker.SetIgnoreDirectoryNames(cfgOpts.IgnoreDirectoryNames)
svc.closedDirWalker.SetIgnoreDirectoryNames(options.IgnoreDirectoryNames)
svc.closedDirWalker.SetExcludeModulePaths(excludeModulePaths)
svc.openDirWalker.SetIgnoreDirectoryNames(cfgOpts.IgnoreDirectoryNames)
svc.openDirWalker.SetIgnoreDirectoryNames(options.IgnoreDirectoryNames)
svc.openDirWalker.SetExcludeModulePaths(excludeModulePaths)

// Walkers run asynchronously so we're intentionally *not*
// passing the request context here
walkerCtx := context.Background()
err = svc.closedDirWalker.StartWalking(walkerCtx)
if err != nil {
return serverCaps, fmt.Errorf("failed to start closedDirWalker: %w", err)
}
err = svc.openDirWalker.StartWalking(walkerCtx)
if err != nil {
return serverCaps, fmt.Errorf("failed to start openDirWalker: %w", err)
}

// Static user-provided paths take precedence over dynamic discovery
if len(cfgOpts.ModulePaths) > 0 {
svc.logger.Printf("Attempting to add %d static module paths", len(cfgOpts.ModulePaths))
for _, rawPath := range cfgOpts.ModulePaths {
// walkerCtx := context.Background()
// err = svc.closedDirWalker.StartWalking(walkerCtx)
// if err != nil {
// return fmt.Errorf("failed to start closedDirWalker: %w", err)
// }
// err = svc.openDirWalker.StartWalking(walkerCtx)
// if err != nil {
// return fmt.Errorf("failed to start openDirWalker: %w", err)
// }
jpogran marked this conversation as resolved.
Show resolved Hide resolved

if len(options.ModulePaths) > 0 {
svc.logger.Printf("Attempting to add %d static module paths", len(options.ModulePaths))
for _, rawPath := range options.ModulePaths {
modPath, err := resolvePath(root.Path(), rawPath)
if err != nil {
jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{
Expand All @@ -251,14 +298,14 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)

err = svc.watcher.AddModule(modPath)
if err != nil {
return serverCaps, err
return err
}
}

return serverCaps, nil
return nil
}

return serverCaps, nil
return nil
}

func resolvePath(rootDir, rawPath string) (string, error) {
Expand Down
2 changes: 2 additions & 0 deletions internal/langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type service struct {

walkerCollector *module.WalkerCollector
additionalHandlers map[string]rpch.Func

singleFileMode bool
}

var discardLogs = log.New(ioutil.Discard, "", 0)
Expand Down
2 changes: 2 additions & 0 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type Options struct {
// ExperimentalFeatures encapsulates experimental features users can opt into.
ExperimentalFeatures ExperimentalFeatures `mapstructure:"experimentalFeatures"`

IgnoreSingleFileWarning bool `mapstructure:"ignoreSingleFileWarning"`

TerraformExecPath string `mapstructure:"terraformExecPath"`
TerraformExecTimeout string `mapstructure:"terraformExecTimeout"`
TerraformLogFilePath string `mapstructure:"terraformLogFilePath"`
Expand Down