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

LS state & performance refactoring #1667

Merged
merged 18 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .copywrite.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ project {
# files or folders should be ignored
header_ignore = [
"**/testdata/**",
"**/testdata-initialize/**",
".github/ISSUE_TEMPLATE/**",
".changes/**",
"internal/schemas/gen-workspace/**",
"internal/schemas/tf-plugin-cache/**",
]
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/hashicorp/terraform-exec v0.21.0
github.com/hashicorp/terraform-json v0.22.1
github.com/hashicorp/terraform-registry-address v0.2.3
github.com/hashicorp/terraform-schema v0.0.0-20240527093557-661c6794495e
github.com/hashicorp/terraform-schema v0.0.0-20240607143625-26a6f401ff0c
github.com/mcuadros/go-defaults v1.2.0
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
github.com/mitchellh/cli v1.1.5
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/terraform-schema v0.0.0-20240527093557-661c6794495e h1:H8s/5oVHR+jlMILG4qbG4OycOr+8piyGUOpL+kqx24k=
github.com/hashicorp/terraform-schema v0.0.0-20240527093557-661c6794495e/go.mod h1:lLCq9hyDL4yO7tcAu0Qj7MIwpw3StgB/DVcJM9r1ymA=
github.com/hashicorp/terraform-schema v0.0.0-20240607143625-26a6f401ff0c h1:+ku2UJbLniAXN+WHNpmDosYOlCe6IcwuuJv2kmZli9U=
github.com/hashicorp/terraform-schema v0.0.0-20240607143625-26a6f401ff0c/go.mod h1:lLCq9hyDL4yO7tcAu0Qj7MIwpw3StgB/DVcJM9r1ymA=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo=
Expand Down
76 changes: 0 additions & 76 deletions internal/decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,88 +7,12 @@ import (
"context"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform-ls/internal/codelens"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
"github.com/hashicorp/terraform-ls/internal/state"
"github.com/hashicorp/terraform-ls/internal/terraform/ast"
"github.com/hashicorp/terraform-ls/internal/utm"
tfschema "github.com/hashicorp/terraform-schema/schema"
)

func modulePathContext(mod *state.Module, schemaReader state.SchemaReader, modReader ModuleReader) (*decoder.PathContext, error) {
schema, err := schemaForModule(mod, schemaReader, modReader)
if err != nil {
return nil, err
}
functions, err := functionsForModule(mod, schemaReader)
if err != nil {
return nil, err
}

pathCtx := &decoder.PathContext{
Schema: schema,
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File, 0),
Functions: functions,
Validators: moduleValidators,
}

for _, origin := range mod.RefOrigins {
if ast.IsModuleFilename(origin.OriginRange().Filename) {
pathCtx.ReferenceOrigins = append(pathCtx.ReferenceOrigins, origin)
}
}
for _, target := range mod.RefTargets {
if target.RangePtr != nil && ast.IsModuleFilename(target.RangePtr.Filename) {
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
} else if target.RangePtr == nil {
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
}
}

for name, f := range mod.ParsedModuleFiles {
pathCtx.Files[name.String()] = f
}

return pathCtx, nil
}

func varsPathContext(mod *state.Module) (*decoder.PathContext, error) {
schema, err := tfschema.SchemaForVariables(mod.Meta.Variables, mod.Path)
if err != nil {
return nil, err
}

pathCtx := &decoder.PathContext{
Schema: schema,
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File),
}

if len(mod.ParsedModuleFiles) > 0 {
// Only validate if this is actually a module
// as we may come across standalone tfvars files
// for which we have no context.
pathCtx.Validators = varsValidators
}

for _, origin := range mod.VarsRefOrigins {
if ast.IsVarsFilename(origin.OriginRange().Filename) {
pathCtx.ReferenceOrigins = append(pathCtx.ReferenceOrigins, origin)
}
}

for name, f := range mod.ParsedVarsFiles {
pathCtx.Files[name.String()] = f
}
return pathCtx, nil
}

func DecoderContext(ctx context.Context) decoder.DecoderContext {
dCtx := decoder.NewDecoderContext()
dCtx.UtmSource = utm.UtmSource
Expand Down
58 changes: 13 additions & 45 deletions internal/decoder/path_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,34 @@ import (
"context"
"fmt"

"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
"github.com/hashicorp/terraform-ls/internal/state"
tfaddr "github.com/hashicorp/terraform-registry-address"
tfmod "github.com/hashicorp/terraform-schema/module"
"github.com/hashicorp/terraform-schema/registry"
)

type ModuleReader interface {
ModuleByPath(modPath string) (*state.Module, error)
List() ([]*state.Module, error)
ModuleCalls(modPath string) (tfmod.ModuleCalls, error)
LocalModuleMeta(modPath string) (*tfmod.Meta, error)
RegistryModuleMeta(addr tfaddr.Module, cons version.Constraints) (*registry.ModuleData, error)
}
type PathReaderMap map[string]decoder.PathReader

type PathReader struct {
ModuleReader ModuleReader
SchemaReader state.SchemaReader
// GlobalPathReader is a PathReader that delegates language specific PathReaders
// that usually come from features.
type GlobalPathReader struct {
PathReaderMap PathReaderMap
}

var _ decoder.PathReader = &PathReader{}
var _ decoder.PathReader = &GlobalPathReader{}

func (mr *PathReader) Paths(ctx context.Context) []lang.Path {
func (mr *GlobalPathReader) Paths(ctx context.Context) []lang.Path {
paths := make([]lang.Path, 0)

modList, err := mr.ModuleReader.List()
if err != nil {
return paths
}
for _, mod := range modList {
paths = append(paths, lang.Path{
Path: mod.Path,
LanguageID: ilsp.Terraform.String(),
})
if len(mod.ParsedVarsFiles) > 0 {
paths = append(paths, lang.Path{
Path: mod.Path,
LanguageID: ilsp.Tfvars.String(),
})
}
for _, feature := range mr.PathReaderMap {
paths = append(paths, feature.Paths(ctx)...)
}

return paths
}

func (mr *PathReader) PathContext(path lang.Path) (*decoder.PathContext, error) {
mod, err := mr.ModuleReader.ModuleByPath(path.Path)
if err != nil {
return nil, err
}

switch path.LanguageID {
case ilsp.Terraform.String():
return modulePathContext(mod, mr.SchemaReader, mr.ModuleReader)
case ilsp.Tfvars.String():
return varsPathContext(mod)
func (mr *GlobalPathReader) PathContext(path lang.Path) (*decoder.PathContext, error) {
if feature, ok := mr.PathReaderMap[path.LanguageID]; ok {
return feature.PathContext(path)
}

return nil, fmt.Errorf("unknown language ID: %q", path.LanguageID)
return nil, fmt.Errorf("no feature found for language %s", path.LanguageID)
}
99 changes: 99 additions & 0 deletions internal/eventbus/bus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package eventbus

import (
"io"
"log"
"sync"
)

const ChannelSize = 10

var discardLogger = log.New(io.Discard, "", 0)

// EventBus is a simple event bus that allows for subscribing to and publishing
// events of a specific type.
//
// It has a static list of topics. Each topic can have multiple subscribers.
// When an event is published to a topic, it is sent to all subscribers.
type EventBus struct {
logger *log.Logger

didOpenTopic *Topic[DidOpenEvent]
didChangeTopic *Topic[DidChangeEvent]
didChangeWatchedTopic *Topic[DidChangeWatchedEvent]
discoverTopic *Topic[DiscoverEvent]

manifestChangeTopic *Topic[ManifestChangeEvent]
pluginLockChangeTopic *Topic[PluginLockChangeEvent]
}

func NewEventBus() *EventBus {
return &EventBus{
logger: discardLogger,
didOpenTopic: NewTopic[DidOpenEvent](),
didChangeTopic: NewTopic[DidChangeEvent](),
didChangeWatchedTopic: NewTopic[DidChangeWatchedEvent](),
discoverTopic: NewTopic[DiscoverEvent](),
manifestChangeTopic: NewTopic[ManifestChangeEvent](),
pluginLockChangeTopic: NewTopic[PluginLockChangeEvent](),
}
}

func (eb *EventBus) SetLogger(logger *log.Logger) {
eb.logger = logger
}

// Topic represents a generic subscription topic
type Topic[T any] struct {
subscribers []Subscriber[T]
mutex sync.Mutex
}

// Subscriber represents a subscriber to a topic
type Subscriber[T any] struct {
// channel is the channel to which all events of the topic are sent
channel chan<- T

// doneChannel is an optional channel that the subscriber can use to signal
// that it is done processing the event
doneChannel <-chan struct{}
}

// NewTopic creates a new topic
func NewTopic[T any]() *Topic[T] {
return &Topic[T]{
subscribers: make([]Subscriber[T], 0),
}
}

// Subscribe adds a subscriber to a topic
func (eb *Topic[T]) Subscribe(doneChannel <-chan struct{}) <-chan T {
channel := make(chan T, ChannelSize)
eb.mutex.Lock()
defer eb.mutex.Unlock()

eb.subscribers = append(eb.subscribers, Subscriber[T]{
channel: channel,
doneChannel: doneChannel,
})
return channel
}

// Publish sends an event to all subscribers of a specific topic
func (eb *Topic[T]) Publish(event T) {
eb.mutex.Lock()
defer eb.mutex.Unlock()

for _, subscriber := range eb.subscribers {
// Send the event to the subscriber
subscriber.channel <- event

if subscriber.doneChannel != nil {
// And wait until the subscriber is done processing it
<-subscriber.doneChannel
}
}
}
31 changes: 31 additions & 0 deletions internal/eventbus/did_change.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package eventbus

import (
"context"

"github.com/hashicorp/terraform-ls/internal/document"
)

// DidChangeEvent is an event to signal that a file in directory has changed.
//
// It is usually emitted when a document is changed via a language server
// text synchronization event.
type DidChangeEvent struct {
Context context.Context

Dir document.DirHandle
LanguageID string
}

func (n *EventBus) OnDidChange(identifier string, doneChannel <-chan struct{}) <-chan DidChangeEvent {
n.logger.Printf("bus: %q subscribed to OnDidChange", identifier)
return n.didChangeTopic.Subscribe(doneChannel)
}

func (n *EventBus) DidChange(e DidChangeEvent) {
n.logger.Printf("bus: -> DidChange %s", e.Dir)
n.didChangeTopic.Publish(e)
}
34 changes: 34 additions & 0 deletions internal/eventbus/did_change_watched.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package eventbus

import (
"context"

"github.com/hashicorp/terraform-ls/internal/protocol"
)

// DidChangeWatchedEvent is the event that is emitted when a client notifies
// the language server that a directory or file was changed outside of the
// editor.
type DidChangeWatchedEvent struct {
Context context.Context

// RawPath contains an OS specific path to the file or directory that was
// changed. Usually extracted from the URI.
RawPath string
// IsDir is true if we were able to determine that the path is a directory.
IsDir bool
ChangeType protocol.FileChangeType
}

func (n *EventBus) OnDidChangeWatched(identifier string, doneChannel <-chan struct{}) <-chan DidChangeWatchedEvent {
n.logger.Printf("bus: %q subscribed to OnDidChangeWatched", identifier)
return n.didChangeWatchedTopic.Subscribe(doneChannel)
}

func (n *EventBus) DidChangeWatched(e DidChangeWatchedEvent) {
n.logger.Printf("bus: -> DidChangeWatched %s", e.RawPath)
n.didChangeWatchedTopic.Publish(e)
}
Loading