From bcd099f96586b8c146f3ebe58f4d9bd057489aec Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 21 Jan 2022 13:18:10 +0000 Subject: [PATCH 01/15] internal/document: Decouple document types --- internal/document/change.go | 83 +++++++++++ internal/document/change_test.go | 243 +++++++++++++++++++++++++++++++ internal/document/dir_handle.go | 36 +++++ internal/document/document.go | 45 ++++++ internal/document/errors.go | 31 ++++ internal/document/handle.go | 45 ++++++ internal/document/position.go | 73 ++++++++++ internal/document/range.go | 18 +++ 8 files changed, 574 insertions(+) create mode 100644 internal/document/change.go create mode 100644 internal/document/change_test.go create mode 100644 internal/document/dir_handle.go create mode 100644 internal/document/document.go create mode 100644 internal/document/errors.go create mode 100644 internal/document/handle.go create mode 100644 internal/document/position.go create mode 100644 internal/document/range.go diff --git a/internal/document/change.go b/internal/document/change.go new file mode 100644 index 00000000..235ddf0b --- /dev/null +++ b/internal/document/change.go @@ -0,0 +1,83 @@ +package document + +import ( + "bytes" + + "github.com/hashicorp/terraform-ls/internal/source" +) + +type Change interface { + Text() string + Range() *Range +} + +type Changes []Change + +func ApplyChanges(original []byte, changes Changes) ([]byte, error) { + if len(changes) == 0 { + return original, nil + } + + var buf bytes.Buffer + _, err := buf.Write(original) + if err != nil { + return nil, err + } + + for _, ch := range changes { + err := applyDocumentChange(&buf, ch) + if err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +func applyDocumentChange(buf *bytes.Buffer, change Change) error { + // if the range is nil, we assume it is full content change + if change.Range() == nil { + buf.Reset() + _, err := buf.WriteString(change.Text()) + return err + } + + lines := source.MakeSourceLines("", buf.Bytes()) + + startByte, err := ByteOffsetForPos(lines, change.Range().Start) + if err != nil { + return err + } + endByte, err := ByteOffsetForPos(lines, change.Range().End) + if err != nil { + return err + } + + diff := endByte - startByte + if diff > 0 { + buf.Grow(diff) + } + + beforeChange := make([]byte, startByte, startByte) + copy(beforeChange, buf.Bytes()) + afterBytes := buf.Bytes()[endByte:] + afterChange := make([]byte, len(afterBytes), len(afterBytes)) + copy(afterChange, afterBytes) + + buf.Reset() + + _, err = buf.Write(beforeChange) + if err != nil { + return err + } + _, err = buf.WriteString(change.Text()) + if err != nil { + return err + } + _, err = buf.Write(afterChange) + if err != nil { + return err + } + + return nil +} diff --git a/internal/document/change_test.go b/internal/document/change_test.go new file mode 100644 index 00000000..f85df887 --- /dev/null +++ b/internal/document/change_test.go @@ -0,0 +1,243 @@ +package document + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestApplyChanges_fullUpdate(t *testing.T) { + original := []byte("hello world") + + changes := []Change{ + &testChange{text: "something else"}, + } + + given, err := ApplyChanges(original, changes) + if err != nil { + t.Fatal(err) + } + + expectedText := "something else" + if diff := cmp.Diff(expectedText, string(given)); diff != "" { + t.Fatalf("content mismatch: %s", diff) + } +} + +func TestApplyChanges_partialUpdate(t *testing.T) { + testCases := []struct { + Name string + Original string + Change *testChange + Expect string + }{ + { + Name: "length grow: 4", + Original: "hello world", + Change: &testChange{ + text: "terraform", + rng: &Range{ + Start: Pos{ + Line: 0, + Column: 6, + }, + End: Pos{ + Line: 0, + Column: 11, + }, + }, + }, + Expect: "hello terraform", + }, + { + Name: "length the same", + Original: "hello world", + Change: &testChange{ + text: "earth", + rng: &Range{ + Start: Pos{ + Line: 0, + Column: 6, + }, + End: Pos{ + Line: 0, + Column: 11, + }, + }, + }, + Expect: "hello earth", + }, + { + Name: "length grow: -2", + Original: "hello world", + Change: &testChange{ + text: "HCL", + rng: &Range{ + Start: Pos{ + Line: 0, + Column: 6, + }, + End: Pos{ + Line: 0, + Column: 11, + }, + }, + }, + Expect: "hello HCL", + }, + { + Name: "zero-length range", + Original: "hello world", + Change: &testChange{ + text: "abc ", + rng: &Range{ + Start: Pos{ + Line: 0, + Column: 6, + }, + End: Pos{ + Line: 0, + Column: 6, + }, + }, + }, + Expect: "hello abc world", + }, + { + Name: "add utf-18 character", + Original: "hello world", + Change: &testChange{ + text: "๐€๐€ ", + rng: &Range{ + Start: Pos{ + Line: 0, + Column: 6, + }, + End: Pos{ + Line: 0, + Column: 6, + }, + }, + }, + Expect: "hello ๐€๐€ world", + }, + { + Name: "modify when containing utf-18 character", + Original: "hello ๐€๐€ world", + Change: &testChange{ + text: "aa๐€", + rng: &Range{ + Start: Pos{ + Line: 0, + Column: 8, + }, + End: Pos{ + Line: 0, + Column: 10, + }, + }, + }, + Expect: "hello ๐€aa๐€ world", + }, + } + + for _, tc := range testCases { + changes := []Change{tc.Change} + + given, err := ApplyChanges([]byte(tc.Original), changes) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.Expect, string(given)); diff != "" { + t.Fatalf("%s: content mismatch: %s", tc.Name, diff) + } + } +} + +func TestApplyChanges_partialUpdateMultipleChanges(t *testing.T) { + testCases := []struct { + Original string + Changes Changes + Expect string + }{ + { + Original: `variable "service_host" { + default = "blah" +} + +module "app" { + source = "./sub" + service_listeners = [ + { + hosts = [var.service_host] + listener = "" + } + ] +} +`, + Changes: Changes{ + &testChange{ + text: "\n", + rng: &Range{ + Start: Pos{Line: 8, Column: 18}, + End: Pos{Line: 8, Column: 18}, + }, + }, + &testChange{ + text: " ", + rng: &Range{ + Start: Pos{Line: 9, Column: 0}, + End: Pos{Line: 9, Column: 0}, + }, + }, + &testChange{ + text: " ", + rng: &Range{ + Start: Pos{Line: 9, Column: 6}, + End: Pos{Line: 9, Column: 6}, + }, + }, + }, + Expect: `variable "service_host" { + default = "blah" +} + +module "app" { + source = "./sub" + service_listeners = [ + { + hosts = [ + var.service_host] + listener = "" + } + ] +} +`, + }, + } + + for _, tc := range testCases { + given, err := ApplyChanges([]byte(tc.Original), tc.Changes) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.Expect, string(given)); diff != "" { + t.Fatalf("content mismatch: %s", diff) + } + } +} + +type testChange struct { + text string + rng *Range +} + +func (fc *testChange) Text() string { + return fc.text +} + +func (fc *testChange) Range() *Range { + return fc.rng +} diff --git a/internal/document/dir_handle.go b/internal/document/dir_handle.go new file mode 100644 index 00000000..ab44ec85 --- /dev/null +++ b/internal/document/dir_handle.go @@ -0,0 +1,36 @@ +package document + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform-ls/internal/uri" +) + +type DirHandle struct { + URI string +} + +func (dh DirHandle) Path() string { + return uri.MustPathFromURI(dh.URI) +} + +func DirHandleFromPath(path string) DirHandle { + path = strings.TrimSuffix(path, fmt.Sprintf("%c", os.PathSeparator)) + + return DirHandle{ + URI: uri.FromPath(path), + } +} + +func DirHandleFromURI(dirUri string) DirHandle { + // Dir URIs are usually without trailing separator already + // but we double check anyway, so we deal with the same URI + // regardless of language client differences + dirUri = strings.TrimSuffix(string(dirUri), "/") + + return DirHandle{ + URI: dirUri, + } +} diff --git a/internal/document/document.go b/internal/document/document.go new file mode 100644 index 00000000..d5f5c9b7 --- /dev/null +++ b/internal/document/document.go @@ -0,0 +1,45 @@ +package document + +import ( + "path/filepath" + "time" + + "github.com/hashicorp/terraform-ls/internal/source" +) + +type Document struct { + Dir DirHandle + Filename string + + ModTime time.Time + LanguageID string + Version int + + // Text contains the document body stored as bytes. + // It originally comes as string from the client via LSP + // but bytes are accepted by HCL and io/fs APIs, hence preferred. + Text []byte + + // Lines contains Text separated into lines to enable byte offset + // computation for any position-based operations within HCL, such as + // completion, hover, semantic token based highlighting, etc. + // and to aid in calculating diff when formatting document. + // LSP positions contain just line+column but hcl.Pos requires offset. + Lines source.Lines +} + +func (doc *Document) FullPath() string { + return filepath.Join(doc.Dir.Path(), doc.Filename) +} + +func (d *Document) Copy() *Document { + return &Document{ + Dir: DirHandle{URI: d.Dir.URI}, + Filename: d.Filename, + ModTime: d.ModTime, + LanguageID: d.LanguageID, + Version: d.Version, + Text: d.Text, + Lines: d.Lines.Copy(), + } +} diff --git a/internal/document/errors.go b/internal/document/errors.go new file mode 100644 index 00000000..b2e72bf0 --- /dev/null +++ b/internal/document/errors.go @@ -0,0 +1,31 @@ +package document + +import ( + "fmt" +) + +type InvalidPosErr struct { + Pos Pos +} + +func (e *InvalidPosErr) Error() string { + return fmt.Sprintf("invalid position: %s", e.Pos) +} + +type DocumentNotFound struct { + URI string +} + +func (e *DocumentNotFound) Error() string { + msg := "document not found" + if e.URI != "" { + return fmt.Sprintf("%s: %s", e.URI, msg) + } + + return msg +} + +func (e *DocumentNotFound) Is(err error) bool { + _, ok := err.(*DocumentNotFound) + return ok +} diff --git a/internal/document/handle.go b/internal/document/handle.go new file mode 100644 index 00000000..62ebdd2a --- /dev/null +++ b/internal/document/handle.go @@ -0,0 +1,45 @@ +package document + +import ( + "path/filepath" + "strings" + + "github.com/hashicorp/terraform-ls/internal/uri" +) + +type Handle struct { + Dir DirHandle + Filename string +} + +func HandleFromURI(docUri string) Handle { + path := uri.MustPathFromURI(docUri) + + filename := filepath.Base(path) + dirUri := strings.TrimSuffix(docUri, "/"+filename) + + return Handle{ + Dir: DirHandle{URI: dirUri}, + Filename: filename, + } +} + +func HandleFromPath(docPath string) Handle { + docUri := uri.FromPath(docPath) + + filename := filepath.Base(docPath) + dirUri := strings.TrimSuffix(docUri, "/"+filename) + + return Handle{ + Dir: DirHandle{URI: dirUri}, + Filename: filename, + } +} + +func (h Handle) FullPath() string { + return filepath.Join(h.Dir.Path(), h.Filename) +} + +func (h Handle) FullURI() string { + return h.Dir.URI + "/" + h.Filename +} diff --git a/internal/document/position.go b/internal/document/position.go new file mode 100644 index 00000000..e1fa9444 --- /dev/null +++ b/internal/document/position.go @@ -0,0 +1,73 @@ +package document + +import ( + "unicode/utf16" + "unicode/utf8" + + "github.com/apparentlymart/go-textseg/textseg" + "github.com/hashicorp/terraform-ls/internal/source" +) + +func ByteOffsetForPos(lines source.Lines, pos Pos) (int, error) { + if pos.Line+1 > len(lines) { + return 0, &InvalidPosErr{Pos: pos} + } + + return byteOffsetForLSPColumn(lines[pos.Line], pos.Column), nil +} + +// byteForLSPColumn takes an lsp.Position.Character value for the receving line +// and finds the byte offset of the start of the UTF-8 sequence that represents +// it in the overall source buffer. This is different than the byte returned +// by posForLSPColumn because it can return offsets that are partway through +// a grapheme cluster, while HCL positions always round to the nearest +// grapheme cluster. +// +// Note that even this can't produce an exact result; if the column index +// refers to the second unit of a UTF-16 surrogate pair then it is rounded +// down the first unit because UTF-8 sequences are not divisible in the same +// way. +func byteOffsetForLSPColumn(l source.Line, lspCol int) int { + if lspCol < 0 { + return l.Range.Start.Byte + } + + // Normally ASCII-only lines could be short-circuited here + // but it's not as easy to tell whether a line is ASCII-only + // based on column/byte differences as we also scan newlines + // and a single line range technically spans 2 lines. + + // If there are non-ASCII characters then we need to edge carefully + // along the line while counting UTF-16 code units in our UTF-8 buffer, + // since LSP columns are a count of UTF-16 units. + byteCt := 0 + utf16Ct := 0 + colIdx := 1 + remain := l.Bytes + for { + if len(remain) == 0 { // ran out of characters on the line, so given column is invalid + return l.Range.End.Byte + } + if utf16Ct >= lspCol { // we've found it + return l.Range.Start.Byte + byteCt + } + // Unlike our other conversion functions we're intentionally using + // individual UTF-8 sequences here rather than grapheme clusters because + // an LSP position might point into the middle of a grapheme cluster. + + adv, chBytes, _ := textseg.ScanUTF8Sequences(remain, true) + remain = remain[adv:] + byteCt += adv + colIdx++ + for len(chBytes) > 0 { + r, l := utf8.DecodeRune(chBytes) + chBytes = chBytes[l:] + c1, c2 := utf16.EncodeRune(r) + if c1 == 0xfffd && c2 == 0xfffd { + utf16Ct++ // codepoint fits in one 16-bit unit + } else { + utf16Ct += 2 // codepoint requires a surrogate pair + } + } + } +} diff --git a/internal/document/range.go b/internal/document/range.go new file mode 100644 index 00000000..1e938858 --- /dev/null +++ b/internal/document/range.go @@ -0,0 +1,18 @@ +package document + +import "fmt" + +// Range represents LSP-style range between two positions +// Positions are zero-indexed +type Range struct { + Start, End Pos +} + +// Pos represents LSP-style position (zero-indexed) +type Pos struct { + Line, Column int +} + +func (p Pos) String() string { + return fmt.Sprintf("%d:%d", p.Line, p.Column) +} From fcf0d3fb44f3fd9f4cab2139a2a02d8667418048 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 21 Jan 2022 13:18:54 +0000 Subject: [PATCH 02/15] internal/state: Decouple document state into memdb --- internal/state/dir_handle_field_indexer.go | 68 ++++++ internal/state/documents.go | 166 +++++++++++++ internal/state/documents_test.go | 258 +++++++++++++++++++++ internal/state/state.go | 25 ++ 4 files changed, 517 insertions(+) create mode 100644 internal/state/dir_handle_field_indexer.go create mode 100644 internal/state/documents.go create mode 100644 internal/state/documents_test.go diff --git a/internal/state/dir_handle_field_indexer.go b/internal/state/dir_handle_field_indexer.go new file mode 100644 index 00000000..75431ea5 --- /dev/null +++ b/internal/state/dir_handle_field_indexer.go @@ -0,0 +1,68 @@ +package state + +import ( + "bytes" + "fmt" + "reflect" + + "github.com/hashicorp/terraform-ls/internal/document" +) + +type DirHandleFieldIndexer struct { + Field string +} + +func (s *DirHandleFieldIndexer) FromObject(obj interface{}) (bool, []byte, error) { + v := reflect.ValueOf(obj) + v = reflect.Indirect(v) // Dereference the pointer if any + + fv := v.FieldByName(s.Field) + isPtr := fv.Kind() == reflect.Ptr + rawHandle := fv.Interface() + if rawHandle == nil { + return false, nil, nil + } + + dh, ok := rawHandle.(document.DirHandle) + if !ok { + return false, nil, + fmt.Errorf("field '%s' for %#v is invalid %v ", s.Field, obj, isPtr) + } + + val := dh.URI + if val == "" { + return false, nil, nil + } + + // Add the null character as a terminator + val += "\x00" + + return true, []byte(val), nil +} + +func (s *DirHandleFieldIndexer) FromArgs(args ...interface{}) ([]byte, error) { + if len(args) != 1 { + return nil, fmt.Errorf("must provide only a single argument") + } + if args[0] == nil { + return nil, nil + } + arg, ok := args[0].(document.DirHandle) + if !ok { + return nil, fmt.Errorf("argument must be a DirHandle: %#v", args[0]) + } + + val := arg.URI + // Add the null character as a terminator + val += "\x00" + + return []byte(val), nil +} + +func (s *DirHandleFieldIndexer) PrefixFromArgs(args ...interface{}) ([]byte, error) { + idx, err := s.FromArgs(args...) + if err != nil { + return nil, err + } + return bytes.TrimSuffix(idx, []byte("\x00")), nil +} diff --git a/internal/state/documents.go b/internal/state/documents.go new file mode 100644 index 00000000..1407fca8 --- /dev/null +++ b/internal/state/documents.go @@ -0,0 +1,166 @@ +package state + +import ( + "log" + "time" + + "github.com/hashicorp/go-memdb" + "github.com/hashicorp/terraform-ls/internal/document" + "github.com/hashicorp/terraform-ls/internal/source" +) + +type DocumentStore struct { + db *memdb.MemDB + tableName string + logger *log.Logger + + // TimeProvider provides current time (for mocking time.Now in tests) + TimeProvider func() time.Time +} + +func (s *DocumentStore) OpenDocument(dh document.Handle, langId string, version int, text []byte) error { + txn := s.db.Txn(true) + defer txn.Abort() + + // TODO: Introduce Exists method to Txn? + obj, err := txn.First(s.tableName, "id", dh.Dir, dh.Filename) + if err != nil { + return err + } + if obj != nil { + return &AlreadyExistsError{ + Idx: dh.FullURI(), + } + } + + doc := &document.Document{ + Dir: dh.Dir, + Filename: dh.Filename, + ModTime: s.TimeProvider(), + LanguageID: langId, + Version: version, + Text: text, + Lines: source.MakeSourceLines(dh.Filename, text), + } + + err = txn.Insert(s.tableName, doc) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +func (s *DocumentStore) UpdateDocument(dh document.Handle, newText []byte, newVersion int) error { + txn := s.db.Txn(true) + defer txn.Abort() + + doc, err := copyDocument(txn, dh) + if err != nil { + return err + } + + doc.Text = newText + doc.Lines = source.MakeSourceLines(dh.Filename, newText) + doc.Version = newVersion + doc.ModTime = s.TimeProvider() + + err = txn.Insert(s.tableName, doc) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +func copyDocument(txn *memdb.Txn, dh document.Handle) (*document.Document, error) { + doc, err := getDocument(txn, dh) + if err != nil { + return nil, err + } + + return doc.Copy(), nil +} + +func (s *DocumentStore) GetDocument(dh document.Handle) (*document.Document, error) { + txn := s.db.Txn(false) + return getDocument(txn, dh) +} + +func getDocument(txn *memdb.Txn, dh document.Handle) (*document.Document, error) { + obj, err := txn.First(documentsTableName, "id", dh.Dir, dh.Filename) + if err != nil { + return nil, err + } + if obj == nil { + return nil, &document.DocumentNotFound{ + URI: dh.FullURI(), + } + } + return obj.(*document.Document), nil +} + +func (s *DocumentStore) CloseDocument(dh document.Handle) error { + txn := s.db.Txn(true) + defer txn.Abort() + + obj, err := txn.First(s.tableName, "id", dh.Dir, dh.Filename) + if err != nil { + return err + } + + if obj == nil { + // already removed + return &document.DocumentNotFound{ + URI: dh.FullURI(), + } + } + + _, err = txn.DeleteAll(s.tableName, "id", dh.Dir, dh.Filename) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +func (s *DocumentStore) ListDocumentsInDir(dirHandle document.DirHandle) ([]*document.Document, error) { + txn := s.db.Txn(false) + it, err := txn.Get(s.tableName, "id_prefix", dirHandle) + if err != nil { + return nil, err + } + + docs := make([]*document.Document, 0) + for item := it.Next(); item != nil; item = it.Next() { + doc := item.(*document.Document) + docs = append(docs, doc) + } + + return docs, nil +} + +func (s *DocumentStore) IsDocumentOpen(dh document.Handle) (bool, error) { + txn := s.db.Txn(false) + + obj, err := txn.First(s.tableName, "id", dh.Dir, dh.Filename) + if err != nil { + return false, err + } + + return obj != nil, nil +} + +func (s *DocumentStore) HasOpenDocuments(dirHandle document.DirHandle) (bool, error) { + txn := s.db.Txn(false) + + obj, err := txn.First(s.tableName, "id_prefix", dirHandle) + if err != nil { + return false, err + } + + return obj != nil, nil +} diff --git a/internal/state/documents_test.go b/internal/state/documents_test.go new file mode 100644 index 00000000..d38e5f2c --- /dev/null +++ b/internal/state/documents_test.go @@ -0,0 +1,258 @@ +package state + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-ls/internal/document" + "github.com/hashicorp/terraform-ls/internal/source" +) + +func TestDocumentStore_UpdateDocument_notFound(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + testHandle := document.HandleFromURI("file:///not/found.tf") + err = s.DocumentStore.UpdateDocument(testHandle, []byte{}, 2) + expectedErr := &document.DocumentNotFound{URI: testHandle.FullURI()} + if err == nil { + t.Fatalf("Expected error: %s", expectedErr) + } + if err.Error() != expectedErr.Error() { + t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", + expectedErr, err) + } +} + +func TestDocumentStore_CloseDocument_notFound(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + testHandle := document.HandleFromURI("file:///not/found.tf") + err = s.DocumentStore.CloseDocument(testHandle) + + expectedErr := &document.DocumentNotFound{URI: testHandle.FullURI()} + if err == nil { + t.Fatalf("Expected error: %s", expectedErr) + } + if err.Error() != expectedErr.Error() { + t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", + expectedErr, err) + } +} + +func TestDocumentStore_UpdateDocument_emptyText(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + testHandle := document.HandleFromURI("file:///dir/test.tf") + + err = s.DocumentStore.OpenDocument(testHandle, "terraform", 0, []byte("foo")) + if err != nil { + t.Fatal(err) + } + + err = s.DocumentStore.UpdateDocument(testHandle, []byte{}, 1) + if err != nil { + t.Fatal(err) + } +} + +func TestDocumentStore_UpdateDocument_basic(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + testHandle := document.HandleFromURI("file:///dir/test.tf") + + err = s.DocumentStore.OpenDocument(testHandle, "terraform", 0, []byte("foo")) + if err != nil { + t.Fatal(err) + } + + err = s.DocumentStore.UpdateDocument(testHandle, []byte("barx"), 1) + if err != nil { + t.Fatal(err) + } +} + +func TestDocumentStore_GetDocument_basic(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + s.DocumentStore.TimeProvider = testTimeProvider + + testHandle := document.HandleFromURI("file:///dir/test.tf") + err = s.DocumentStore.OpenDocument(testHandle, "terraform", 0, []byte("foobar")) + if err != nil { + t.Fatal(err) + } + + doc, err := s.DocumentStore.GetDocument(testHandle) + if err != nil { + t.Fatal(err) + } + + text := []byte("foobar") + expectedDocument := &document.Document{ + Dir: testHandle.Dir, + Filename: testHandle.Filename, + ModTime: testTimeProvider(), + LanguageID: "terraform", + Version: 0, + Text: text, + Lines: source.MakeSourceLines(testHandle.Filename, text), + } + if diff := cmp.Diff(expectedDocument, doc); diff != "" { + t.Fatalf("File doesn't match: %s", diff) + } +} + +func TestDocumentStore_GetDocument_notFound(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + testHandle := document.HandleFromURI("file:///not/found.tf") + _, err = s.DocumentStore.GetDocument(testHandle) + + expectedErr := &document.DocumentNotFound{URI: testHandle.FullURI()} + if err == nil { + t.Fatalf("Expected error: %s", expectedErr) + } + if err.Error() != expectedErr.Error() { + t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", + expectedErr, err) + } +} + +func TestDocumentStore_ListDocumentsInDir(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + s.DocumentStore.TimeProvider = testTimeProvider + + testHandle1 := document.HandleFromURI("file:///dir/test1.tf") + err = s.DocumentStore.OpenDocument(testHandle1, "terraform", 0, []byte("foobar")) + if err != nil { + t.Fatal(err) + } + + testHandle2 := document.HandleFromURI("file:///dir/test2.tf") + err = s.DocumentStore.OpenDocument(testHandle2, "terraform", 0, []byte("foobar")) + if err != nil { + t.Fatal(err) + } + + dirHandle := document.DirHandleFromURI("file:///dir") + docs, err := s.DocumentStore.ListDocumentsInDir(dirHandle) + if err != nil { + t.Fatal(err) + } + + expectedDocs := []*document.Document{ + { + Dir: dirHandle, + Filename: "test1.tf", + ModTime: testTimeProvider(), + LanguageID: "terraform", + Version: 0, + Text: []byte("foobar"), + Lines: source.MakeSourceLines("test1.tf", []byte("foobar")), + }, + { + Dir: dirHandle, + Filename: "test2.tf", + ModTime: testTimeProvider(), + LanguageID: "terraform", + Version: 0, + Text: []byte("foobar"), + Lines: source.MakeSourceLines("test2.tf", []byte("foobar")), + }, + } + if diff := cmp.Diff(expectedDocs, docs); diff != "" { + t.Fatalf("unexpected docs: %s", diff) + } +} + +func TestDocumentStore_IsDocumentOpen(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + s.DocumentStore.TimeProvider = testTimeProvider + + testHandle1 := document.HandleFromURI("file:///dir/test1.tf") + err = s.DocumentStore.OpenDocument(testHandle1, "terraform", 0, []byte("foobar")) + if err != nil { + t.Fatal(err) + } + + isOpen, err := s.DocumentStore.IsDocumentOpen(testHandle1) + if err != nil { + t.Fatal(err) + } + if !isOpen { + t.Fatal("expected first document to be open") + } + + testHandle2 := document.HandleFromURI("file:///dir/test2.tf") + isOpen, err = s.DocumentStore.IsDocumentOpen(testHandle2) + if err != nil { + t.Fatal(err) + } + if isOpen { + t.Fatal("expected second document not to be open") + } +} + +func TestDocumentStore_HasOpenDocuments(t *testing.T) { + s, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + s.DocumentStore.TimeProvider = testTimeProvider + + testHandle1 := document.HandleFromURI("file:///dir/test1.tf") + err = s.DocumentStore.OpenDocument(testHandle1, "terraform", 0, []byte("foobar")) + if err != nil { + t.Fatal(err) + } + + dirHandle := document.DirHandleFromURI("file:///dir") + hasOpenDocs, err := s.DocumentStore.HasOpenDocuments(dirHandle) + if err != nil { + t.Fatal(err) + } + if !hasOpenDocs { + t.Fatal("expected to find open documents") + } + + secondDirHandle := document.DirHandleFromURI("file:///dir-x") + hasOpenDocs, err = s.DocumentStore.HasOpenDocuments(secondDirHandle) + if err != nil { + t.Fatal(err) + } + if hasOpenDocs { + t.Fatal("expected to find no open documents") + } + +} + +func testTimeProvider() time.Time { + return time.Date(2017, 1, 16, 0, 0, 0, 0, time.UTC) +} diff --git a/internal/state/state.go b/internal/state/state.go index 199063eb..8c3c7850 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -3,6 +3,7 @@ package state import ( "io/ioutil" "log" + "time" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-version" @@ -12,6 +13,7 @@ import ( ) const ( + documentsTableName = "documents" moduleTableName = "module" moduleIdsTableName = "module_ids" providerSchemaTableName = "provider_schema" @@ -20,6 +22,21 @@ const ( var dbSchema = &memdb.DBSchema{ Tables: map[string]*memdb.TableSchema{ + documentsTableName: { + Name: documentsTableName, + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &memdb.CompoundIndex{ + Indexes: []memdb.Indexer{ + &DirHandleFieldIndexer{Field: "Dir"}, + &memdb.StringFieldIndex{Field: "Filename"}, + }, + }, + }, + }, + }, moduleTableName: { Name: moduleTableName, Indexes: map[string]*memdb.IndexSchema{ @@ -71,6 +88,7 @@ var dbSchema = &memdb.DBSchema{ } type StateStore struct { + DocumentStore *DocumentStore Modules *ModuleStore ProviderSchemas *ProviderSchemaStore @@ -113,6 +131,12 @@ func NewStateStore() (*StateStore, error) { return &StateStore{ db: db, + DocumentStore: &DocumentStore{ + db: db, + tableName: documentsTableName, + logger: defaultLogger, + TimeProvider: time.Now, + }, Modules: &ModuleStore{ db: db, ChangeHooks: make(ModuleChangeHooks, 0), @@ -128,6 +152,7 @@ func NewStateStore() (*StateStore, error) { } func (s *StateStore) SetLogger(logger *log.Logger) { + s.DocumentStore.logger = logger s.Modules.logger = logger s.ProviderSchemas.logger = logger } From c440238626f20c5bfe7778e616b5e756cc9983fb Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 21 Jan 2022 15:30:42 +0000 Subject: [PATCH 03/15] internal/filesystem: reduce impl to simple OS FS + interface for DocumentStore Previously filesystem package had two major use cases, to offer a unified io/fs.FS interface for e.g. parsing *.tf or *.tfvars, which was implemented mostly via external library (spf13/afero). Secondly it also provided a direct full access to the "in-memory layer" of the filesystem for RPC handlers (e.g. didOpen, didChange, didClose, ...). These use cases rarely overlap throughout the codebase and so this lead to unnecessary imports of the `filesystem` package in places where we only needed either the OS-level FS or in-mem FS, but almost never both. This decoupling allows us to import `filesystem` or `state.DocumentStore` separately. Also, as we no longer need the in-mem backend of afero, it makes more sense to just reimplement the small part of the 3rd party library instead. --- internal/filesystem/doc.go | 7 - internal/filesystem/document.go | 84 ++-- internal/filesystem/document_metadata.go | 62 --- internal/filesystem/document_test.go | 312 -------------- internal/filesystem/errors.go | 37 -- internal/filesystem/filesystem.go | 246 +++-------- internal/filesystem/filesystem_metadata.go | 116 ----- internal/filesystem/filesystem_test.go | 466 +++++---------------- internal/filesystem/inmem.go | 79 ++++ internal/filesystem/os_fs.go | 24 ++ internal/filesystem/position.go | 73 ---- internal/filesystem/range.go | 18 - internal/filesystem/types.go | 58 --- 13 files changed, 279 insertions(+), 1303 deletions(-) delete mode 100644 internal/filesystem/doc.go delete mode 100644 internal/filesystem/document_metadata.go delete mode 100644 internal/filesystem/document_test.go delete mode 100644 internal/filesystem/errors.go delete mode 100644 internal/filesystem/filesystem_metadata.go create mode 100644 internal/filesystem/inmem.go create mode 100644 internal/filesystem/os_fs.go delete mode 100644 internal/filesystem/position.go delete mode 100644 internal/filesystem/range.go delete mode 100644 internal/filesystem/types.go diff --git a/internal/filesystem/doc.go b/internal/filesystem/doc.go deleted file mode 100644 index 783a3393..00000000 --- a/internal/filesystem/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package filesystem implements a virtual filesystem which reflects -// the needs of both the language server and the HCL parser. -// -// - creates in-memory files based on data received from the language client -// - allows updating in-memory files via diffs received from the language client -// - maintains file metadata (e.g. version, or whether it's open by the client) -package filesystem diff --git a/internal/filesystem/document.go b/internal/filesystem/document.go index 8749430e..a336dc78 100644 --- a/internal/filesystem/document.go +++ b/internal/filesystem/document.go @@ -1,73 +1,43 @@ package filesystem import ( - "bytes" - "path/filepath" + "io/fs" - "github.com/hashicorp/terraform-ls/internal/source" - "github.com/hashicorp/terraform-ls/internal/uri" - "github.com/spf13/afero" + "github.com/hashicorp/terraform-ls/internal/document" ) -type document struct { - meta *documentMetadata - fs afero.Fs -} - -func (d *document) Text() ([]byte, error) { - return afero.ReadFile(d.fs, d.meta.dh.FullPath()) -} - -func (d *document) FullPath() string { - return d.meta.dh.FullPath() -} - -func (d *document) Dir() string { - return filepath.Dir(d.meta.dh.FullPath()) -} - -func (d *document) Filename() string { - return filepath.Base(d.meta.dh.FullPath()) -} - -func (d *document) URI() string { - return uri.FromPath(d.meta.dh.FullPath()) +func documentAsFile(doc *document.Document) fs.File { + return inMemFile{ + bytes: doc.Text, + info: documentAsFileInfo(doc), + } } -func (d *document) Lines() source.Lines { - return d.meta.Lines() +func documentAsFileInfo(doc *document.Document) fs.FileInfo { + return inMemFileInfo{ + name: doc.Filename, + size: len(doc.Text), + modTime: doc.ModTime, + mode: 0o755, + isDir: false, + } } -func (d *document) Version() int { - return d.meta.Version() -} +func documentsAsDirEntries(docs []*document.Document) []fs.DirEntry { + entries := make([]fs.DirEntry, len(docs)) -func (d *document) LanguageID() string { - return d.meta.langId -} + for i, doc := range docs { + entries[i] = documentAsDirEntry(doc) + } -func (d *document) IsOpen() bool { - return d.meta.IsOpen() + return entries } -func (d *document) Equal(doc *document) bool { - if d.URI() != doc.URI() { - return false - } - if d.IsOpen() != doc.IsOpen() { - return false - } - if d.Version() != doc.Version() { - return false - } - - leftB, err := d.Text() - if err != nil { - return false - } - rightB, err := doc.Text() - if err != nil { - return false +func documentAsDirEntry(doc *document.Document) fs.DirEntry { + return inMemDirEntry{ + name: doc.Filename, + isDir: false, + typ: 0, + info: documentAsFileInfo(doc), } - return bytes.Equal(leftB, rightB) } diff --git a/internal/filesystem/document_metadata.go b/internal/filesystem/document_metadata.go deleted file mode 100644 index b20c9ac6..00000000 --- a/internal/filesystem/document_metadata.go +++ /dev/null @@ -1,62 +0,0 @@ -package filesystem - -import ( - "sync" - - "github.com/hashicorp/terraform-ls/internal/source" -) - -type documentMetadata struct { - dh DocumentHandler - - mu *sync.RWMutex - isOpen bool - version int - langId string - lines source.Lines -} - -func NewDocumentMetadata(dh DocumentHandler, langId string, content []byte) *documentMetadata { - return &documentMetadata{ - dh: dh, - mu: &sync.RWMutex{}, - langId: langId, - lines: source.MakeSourceLines(dh.Filename(), content), - } -} - -func (d *documentMetadata) setOpen(isOpen bool) { - d.mu.Lock() - defer d.mu.Unlock() - d.isOpen = isOpen -} - -func (d *documentMetadata) setVersion(version int) { - d.mu.Lock() - defer d.mu.Unlock() - d.version = version -} - -func (d *documentMetadata) updateLines(content []byte) { - d.mu.Lock() - defer d.mu.Unlock() - d.lines = source.MakeSourceLines(d.dh.Filename(), content) -} - -func (d *documentMetadata) Lines() source.Lines { - d.mu.RLock() - defer d.mu.RUnlock() - return d.lines -} - -func (d *documentMetadata) Version() int { - d.mu.RLock() - defer d.mu.RUnlock() - return d.version -} - -func (d *documentMetadata) IsOpen() bool { - d.mu.RLock() - defer d.mu.RUnlock() - return d.isOpen -} diff --git a/internal/filesystem/document_test.go b/internal/filesystem/document_test.go deleted file mode 100644 index 0f2595c2..00000000 --- a/internal/filesystem/document_test.go +++ /dev/null @@ -1,312 +0,0 @@ -package filesystem - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/spf13/afero" -) - -func TestFile_ApplyChange_fullUpdate(t *testing.T) { - fs := testDocumentStorage() - dh := &testHandler{uri: "file:///test.tf"} - - err := fs.CreateAndOpenDocument(dh, "test", []byte("hello world")) - if err != nil { - t.Fatal(err) - } - - changes := []DocumentChange{ - &testChange{text: "something else"}, - } - err = fs.ChangeDocument(dh, changes) - if err != nil { - t.Fatal(err) - } - - doc, err := fs.GetDocument(dh) - if err != nil { - t.Fatal(err) - } - given, err := doc.Text() - if err != nil { - t.Fatal(err) - } - - expectedText := "something else" - if diff := cmp.Diff(expectedText, string(given)); diff != "" { - t.Fatalf("content mismatch: %s", diff) - } -} - -func TestFile_ApplyChange_partialUpdate(t *testing.T) { - testData := []struct { - Name string - Content string - FileChange *testChange - Expect string - }{ - { - Name: "length grow: 4", - Content: "hello world", - FileChange: &testChange{ - text: "terraform", - rng: &Range{ - Start: Pos{ - Line: 0, - Column: 6, - }, - End: Pos{ - Line: 0, - Column: 11, - }, - }, - }, - Expect: "hello terraform", - }, - { - Name: "length the same", - Content: "hello world", - FileChange: &testChange{ - text: "earth", - rng: &Range{ - Start: Pos{ - Line: 0, - Column: 6, - }, - End: Pos{ - Line: 0, - Column: 11, - }, - }, - }, - Expect: "hello earth", - }, - { - Name: "length grow: -2", - Content: "hello world", - FileChange: &testChange{ - text: "HCL", - rng: &Range{ - Start: Pos{ - Line: 0, - Column: 6, - }, - End: Pos{ - Line: 0, - Column: 11, - }, - }, - }, - Expect: "hello HCL", - }, - { - Name: "zero-length range", - Content: "hello world", - FileChange: &testChange{ - text: "abc ", - rng: &Range{ - Start: Pos{ - Line: 0, - Column: 6, - }, - End: Pos{ - Line: 0, - Column: 6, - }, - }, - }, - Expect: "hello abc world", - }, - { - Name: "add utf-18 character", - Content: "hello world", - FileChange: &testChange{ - text: "๐€๐€ ", - rng: &Range{ - Start: Pos{ - Line: 0, - Column: 6, - }, - End: Pos{ - Line: 0, - Column: 6, - }, - }, - }, - Expect: "hello ๐€๐€ world", - }, - { - Name: "modify when containing utf-18 character", - Content: "hello ๐€๐€ world", - FileChange: &testChange{ - text: "aa๐€", - rng: &Range{ - Start: Pos{ - Line: 0, - Column: 8, - }, - End: Pos{ - Line: 0, - Column: 10, - }, - }, - }, - Expect: "hello ๐€aa๐€ world", - }, - } - - for _, v := range testData { - fs := testDocumentStorage() - dh := &testHandler{uri: "file:///test.tf"} - - err := fs.CreateAndOpenDocument(dh, "test", []byte(v.Content)) - if err != nil { - t.Fatal(err) - } - - changes := []DocumentChange{v.FileChange} - err = fs.ChangeDocument(dh, changes) - if err != nil { - t.Fatal(err) - } - - doc, err := fs.GetDocument(dh) - if err != nil { - t.Fatal(err) - } - - text, err := doc.Text() - if err != nil { - t.Fatal(err) - } - - if diff := cmp.Diff(v.Expect, string(text)); diff != "" { - t.Fatalf("%s: content mismatch: %s", v.Name, diff) - } - } -} - -func TestFile_ApplyChange_partialUpdateMultipleChanges(t *testing.T) { - testData := []struct { - Content string - FileChanges DocumentChanges - Expect string - }{ - { - Content: `variable "service_host" { - default = "blah" -} - -module "app" { - source = "./sub" - service_listeners = [ - { - hosts = [var.service_host] - listener = "" - } - ] -} -`, - FileChanges: DocumentChanges{ - &testChange{ - text: "\n", - rng: &Range{ - Start: Pos{Line: 8, Column: 18}, - End: Pos{Line: 8, Column: 18}, - }, - }, - &testChange{ - text: " ", - rng: &Range{ - Start: Pos{Line: 9, Column: 0}, - End: Pos{Line: 9, Column: 0}, - }, - }, - &testChange{ - text: " ", - rng: &Range{ - Start: Pos{Line: 9, Column: 6}, - End: Pos{Line: 9, Column: 6}, - }, - }, - }, - Expect: `variable "service_host" { - default = "blah" -} - -module "app" { - source = "./sub" - service_listeners = [ - { - hosts = [ - var.service_host] - listener = "" - } - ] -} -`, - }, - } - - for _, v := range testData { - fs := testDocumentStorage() - dh := &testHandler{uri: "file:///test.tf"} - - err := fs.CreateAndOpenDocument(dh, "test", []byte(v.Content)) - if err != nil { - t.Fatal(err) - } - - changes := v.FileChanges - err = fs.ChangeDocument(dh, changes) - if err != nil { - t.Fatal(err) - } - - doc, err := fs.GetDocument(dh) - if err != nil { - t.Fatal(err) - } - - text, err := doc.Text() - if err != nil { - t.Fatal(err) - } - - if diff := cmp.Diff(v.Expect, string(text)); diff != "" { - t.Fatalf("content mismatch: %s", diff) - } - } -} - -func testDocument(t *testing.T, dh DocumentHandler, meta *documentMetadata, b []byte) Document { - fs := afero.NewMemMapFs() - f, err := fs.Create(dh.FullPath()) - if err != nil { - t.Fatal(err) - } - defer f.Close() - _, err = f.Write(b) - if err != nil { - t.Fatal(err) - } - - return &document{ - meta: meta, - fs: fs, - } -} - -type testChange struct { - text string - rng *Range -} - -func (fc *testChange) Text() string { - return fc.text -} - -func (fc *testChange) Range() *Range { - return fc.rng -} diff --git a/internal/filesystem/errors.go b/internal/filesystem/errors.go deleted file mode 100644 index 4faa55ac..00000000 --- a/internal/filesystem/errors.go +++ /dev/null @@ -1,37 +0,0 @@ -package filesystem - -import ( - "fmt" -) - -type DocumentNotOpenErr struct { - DocumentHandler DocumentHandler -} - -func (e *DocumentNotOpenErr) Error() string { - return fmt.Sprintf("document is not open: %s", e.DocumentHandler.URI()) -} - -type MetadataAlreadyExistsErr struct { - DocumentHandler DocumentHandler -} - -func (e *MetadataAlreadyExistsErr) Error() string { - return fmt.Sprintf("document metadata already exists: %s", e.DocumentHandler.URI()) -} - -type UnknownDocumentErr struct { - DocumentHandler DocumentHandler -} - -func (e *UnknownDocumentErr) Error() string { - return fmt.Sprintf("unknown document: %s", e.DocumentHandler.URI()) -} - -type InvalidPosErr struct { - Pos Pos -} - -func (e *InvalidPosErr) Error() string { - return fmt.Sprintf("invalid position: %s", e.Pos) -} diff --git a/internal/filesystem/filesystem.go b/internal/filesystem/filesystem.go index 4838df9b..a8810c5f 100644 --- a/internal/filesystem/filesystem.go +++ b/internal/filesystem/filesystem.go @@ -1,225 +1,71 @@ package filesystem import ( - "bytes" + "errors" "fmt" "io/fs" "io/ioutil" "log" "os" - "sync" - "github.com/hashicorp/terraform-ls/internal/source" - "github.com/spf13/afero" + "github.com/hashicorp/terraform-ls/internal/document" ) -type fsystem struct { - memFs afero.Fs - osFs afero.Fs - - docMeta map[string]*documentMetadata - docMetaMu *sync.RWMutex +// Filesystem provides io/fs.FS compatible two-layer read-only filesystem +// with preferred source being DocumentStore and native OS FS acting as fallback. +// +// This allows for reading files in a directory while reflecting unsaved changes. +type Filesystem struct { + osFs osFs + docStore DocumentStore logger *log.Logger } -func NewFilesystem() *fsystem { - return &fsystem{ - memFs: afero.NewMemMapFs(), - osFs: afero.NewReadOnlyFs(afero.NewOsFs()), - docMeta: make(map[string]*documentMetadata, 0), - docMetaMu: &sync.RWMutex{}, - logger: log.New(ioutil.Discard, "", 0), - } -} - -func (fs *fsystem) SetLogger(logger *log.Logger) { - fs.logger = logger +type DocumentStore interface { + GetDocument(document.Handle) (*document.Document, error) + ListDocumentsInDir(document.DirHandle) ([]*document.Document, error) } -func (fs *fsystem) CreateDocument(dh DocumentHandler, langId string, text []byte) error { - _, err := fs.memFs.Stat(dh.Dir()) - if err != nil { - if os.IsNotExist(err) { - err := fs.memFs.MkdirAll(dh.Dir(), 0755) - if err != nil { - return fmt.Errorf("failed to create parent dir: %w", err) - } - } else { - return err - } +func NewFilesystem(docStore DocumentStore) *Filesystem { + return &Filesystem{ + osFs: osFs{}, + docStore: docStore, + logger: log.New(ioutil.Discard, "", 0), } - - f, err := fs.memFs.Create(dh.FullPath()) - if err != nil { - return err - } - _, err = f.Write(text) - if err != nil { - return err - } - - return fs.createDocumentMetadata(dh, langId, text) } -func (fs *fsystem) CreateAndOpenDocument(dh DocumentHandler, langId string, text []byte) error { - err := fs.CreateDocument(dh, langId, text) - if err != nil { - return err - } - - return fs.markDocumentAsOpen(dh) +func (fs *Filesystem) SetLogger(logger *log.Logger) { + fs.logger = logger } -func (fs *fsystem) ChangeDocument(dh VersionedDocumentHandler, changes DocumentChanges) error { - if len(changes) == 0 { - return nil - } - - isOpen, err := fs.isDocumentOpen(dh) - if err != nil { - return err - } - - if !isOpen { - return &DocumentNotOpenErr{dh} - } - - f, err := fs.memFs.OpenFile(dh.FullPath(), os.O_RDWR, 0700) - if err != nil { - return err - } - defer f.Close() - - var buf bytes.Buffer - _, err = buf.ReadFrom(f) +func (fs *Filesystem) ReadFile(name string) ([]byte, error) { + doc, err := fs.docStore.GetDocument(document.HandleFromPath(name)) if err != nil { - return err - } - - for _, ch := range changes { - err := fs.applyDocumentChange(&buf, ch) - if err != nil { - return err + if errors.Is(err, &document.DocumentNotFound{}) { + return fs.osFs.ReadFile(name) } - } - - err = f.Truncate(0) - if err != nil { - return err - } - _, err = f.Seek(0, 0) - if err != nil { - return err - } - _, err = f.Write(buf.Bytes()) - if err != nil { - return err - } - - return fs.updateDocumentMetadataLines(dh, buf.Bytes()) -} - -func (fs *fsystem) applyDocumentChange(buf *bytes.Buffer, change DocumentChange) error { - // if the range is nil, we assume it is full content change - if change.Range() == nil { - buf.Reset() - _, err := buf.WriteString(change.Text()) - return err - } - - lines := source.MakeSourceLines("", buf.Bytes()) - - startByte, err := ByteOffsetForPos(lines, change.Range().Start) - if err != nil { - return err - } - endByte, err := ByteOffsetForPos(lines, change.Range().End) - if err != nil { - return err - } - - diff := endByte - startByte - if diff > 0 { - buf.Grow(diff) - } - - beforeChange := make([]byte, startByte, startByte) - copy(beforeChange, buf.Bytes()) - afterBytes := buf.Bytes()[endByte:] - afterChange := make([]byte, len(afterBytes), len(afterBytes)) - copy(afterChange, afterBytes) - - buf.Reset() - - _, err = buf.Write(beforeChange) - if err != nil { - return err - } - _, err = buf.WriteString(change.Text()) - if err != nil { - return err - } - _, err = buf.Write(afterChange) - if err != nil { - return err - } - - return nil -} - -func (fs *fsystem) CloseAndRemoveDocument(dh DocumentHandler) error { - isOpen, err := fs.isDocumentOpen(dh) - if err != nil { - return err - } - - if !isOpen { - return &DocumentNotOpenErr{dh} - } - - err = fs.memFs.Remove(dh.FullPath()) - if err != nil { - return err - } - - return fs.removeDocumentMetadata(dh) -} - -func (fs *fsystem) GetDocument(dh DocumentHandler) (Document, error) { - dm, err := fs.getDocumentMetadata(dh) - if err != nil { return nil, err } - return &document{ - meta: dm, - fs: fs.memFs, - }, nil + return []byte(doc.Text), err } -func (fs *fsystem) ReadFile(name string) ([]byte, error) { - b, err := afero.ReadFile(fs.memFs, name) - if err != nil && os.IsNotExist(err) { - return afero.ReadFile(fs.osFs, name) +func (fs *Filesystem) ReadDir(name string) ([]fs.DirEntry, error) { + dirHandle := document.DirHandleFromPath(name) + docList, err := fs.docStore.ListDocumentsInDir(dirHandle) + if err != nil { + return nil, fmt.Errorf("doc FS: %w", err) } - return b, err -} - -func (fsys *fsystem) ReadDir(name string) ([]fs.DirEntry, error) { - memList, err := afero.NewIOFS(fsys.memFs).ReadDir(name) - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("memory FS: %w", err) - } - osList, err := afero.NewIOFS(fsys.osFs).ReadDir(name) + osList, err := fs.osFs.ReadDir(name) if err != nil && !os.IsNotExist(err) { return nil, fmt.Errorf("OS FS: %w", err) } - list := memList + list := documentsAsDirEntries(docList) for _, osEntry := range osList { - if fileIsInList(list, osEntry) { + if entryIsInList(list, osEntry) { continue } list = append(list, osEntry) @@ -228,7 +74,7 @@ func (fsys *fsystem) ReadDir(name string) ([]fs.DirEntry, error) { return list, nil } -func fileIsInList(list []fs.DirEntry, entry fs.DirEntry) bool { +func entryIsInList(list []fs.DirEntry, entry fs.DirEntry) bool { for _, di := range list { if di.Name() == entry.Name() { return true @@ -237,20 +83,26 @@ func fileIsInList(list []fs.DirEntry, entry fs.DirEntry) bool { return false } -func (fs *fsystem) Open(name string) (fs.File, error) { - f, err := fs.memFs.Open(name) - if err != nil && os.IsNotExist(err) { - return fs.osFs.Open(name) +func (fs *Filesystem) Open(name string) (fs.File, error) { + doc, err := fs.docStore.GetDocument(document.HandleFromPath(name)) + if err != nil { + if errors.Is(err, &document.DocumentNotFound{}) { + return fs.osFs.Open(name) + } + return nil, err } - return f, err + return documentAsFile(doc), err } -func (fs *fsystem) Stat(name string) (os.FileInfo, error) { - fi, err := fs.memFs.Stat(name) - if err != nil && os.IsNotExist(err) { - return fs.osFs.Stat(name) +func (fs *Filesystem) Stat(name string) (os.FileInfo, error) { + doc, err := fs.docStore.GetDocument(document.HandleFromPath(name)) + if err != nil { + if errors.Is(err, &document.DocumentNotFound{}) { + return fs.osFs.Stat(name) + } + return nil, err } - return fi, err + return documentAsFileInfo(doc), err } diff --git a/internal/filesystem/filesystem_metadata.go b/internal/filesystem/filesystem_metadata.go deleted file mode 100644 index a81ba1c5..00000000 --- a/internal/filesystem/filesystem_metadata.go +++ /dev/null @@ -1,116 +0,0 @@ -package filesystem - -import ( - "path/filepath" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-ls/internal/uri" -) - -func (fs *fsystem) markDocumentAsOpen(dh DocumentHandler) error { - if !fs.documentMetadataExists(dh) { - return &UnknownDocumentErr{dh} - } - - fs.docMetaMu.Lock() - defer fs.docMetaMu.Unlock() - - fs.docMeta[dh.URI()].setOpen(true) - return nil -} - -func (fs *fsystem) HasOpenFiles(dirPath string) (bool, error) { - files, err := fs.ReadDir(dirPath) - if err != nil { - return false, err - } - - fs.docMetaMu.RLock() - defer fs.docMetaMu.RUnlock() - - for _, fi := range files { - u := uri.FromPath(filepath.Join(dirPath, fi.Name())) - dm, ok := fs.docMeta[u] - if ok && dm.IsOpen() { - return true, nil - } - } - - return false, nil -} - -func (fs *fsystem) createDocumentMetadata(dh DocumentHandler, langId string, text []byte) error { - if fs.documentMetadataExists(dh) { - return &MetadataAlreadyExistsErr{dh} - } - - fs.docMetaMu.Lock() - defer fs.docMetaMu.Unlock() - - fs.docMeta[dh.URI()] = NewDocumentMetadata(dh, langId, text) - return nil -} - -func (fs *fsystem) removeDocumentMetadata(dh DocumentHandler) error { - if !fs.documentMetadataExists(dh) { - return nil - } - - fs.docMetaMu.Lock() - defer fs.docMetaMu.Unlock() - - delete(fs.docMeta, dh.URI()) - return nil -} - -func (fs *fsystem) documentMetadataExists(dh DocumentHandler) bool { - fs.docMetaMu.RLock() - defer fs.docMetaMu.RUnlock() - - _, ok := fs.docMeta[dh.URI()] - return ok -} - -func (fs *fsystem) isDocumentOpen(dh DocumentHandler) (bool, error) { - fs.docMetaMu.RLock() - defer fs.docMetaMu.RUnlock() - - dm, ok := fs.docMeta[dh.URI()] - if !ok { - return false, &UnknownDocumentErr{dh} - } - - return dm.isOpen, nil -} - -func (fs *fsystem) updateDocumentMetadataLines(dh VersionedDocumentHandler, b []byte) error { - if !fs.documentMetadataExists(dh) { - return &UnknownDocumentErr{dh} - } - - fs.docMetaMu.Lock() - defer fs.docMetaMu.Unlock() - - fs.docMeta[dh.URI()].updateLines(b) - fs.docMeta[dh.URI()].setVersion(dh.Version()) - - return nil -} - -func (fs *fsystem) getDocumentMetadata(dh DocumentHandler) (*documentMetadata, error) { - fs.docMetaMu.RLock() - defer fs.docMetaMu.RUnlock() - - dm, ok := fs.docMeta[dh.URI()] - if !ok { - return nil, &UnknownDocumentErr{dh} - } - - return dm, nil -} - -// HCL column and line indexes start from 1, therefore if the any index -// contains 0, we assume it is an undefined range -func rangeIsNil(r hcl.Range) bool { - return r.End.Column == 0 && r.End.Line == 0 -} diff --git a/internal/filesystem/filesystem_test.go b/internal/filesystem/filesystem_test.go index 4ca718b2..5e49aff2 100644 --- a/internal/filesystem/filesystem_test.go +++ b/internal/filesystem/filesystem_test.go @@ -9,162 +9,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-ls/internal/uri" + "github.com/hashicorp/terraform-ls/internal/document" ) -func TestFilesystem_Change_notOpen(t *testing.T) { - fs := testDocumentStorage() - - var changes DocumentChanges - changes = append(changes, &testChange{}) - h := &testHandler{uri: "file:///doesnotexist"} - - err := fs.ChangeDocument(h, changes) - - expectedErr := &UnknownDocumentErr{h} - if err == nil { - t.Fatalf("Expected error: %s", expectedErr) - } - if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", - expectedErr, err) - } -} - -func TestFilesystem_Change_closed(t *testing.T) { - fs := testDocumentStorage() - - fh := &testHandler{uri: "file:///doesnotexist"} - fs.CreateAndOpenDocument(fh, "test", []byte{}) - err := fs.CloseAndRemoveDocument(fh) - if err != nil { - t.Fatal(err) - } - - var changes DocumentChanges - changes = append(changes, &testChange{}) - err = fs.ChangeDocument(fh, changes) - - expectedErr := &UnknownDocumentErr{fh} - if err == nil { - t.Fatalf("Expected error: %s", expectedErr) - } - if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", - expectedErr, err) - } -} - -func TestFilesystem_Remove_unknown(t *testing.T) { - fs := testDocumentStorage() - - fh := &testHandler{uri: "file:///doesnotexist"} - fs.CreateAndOpenDocument(fh, "test", []byte{}) - err := fs.CloseAndRemoveDocument(fh) - if err != nil { - t.Fatal(err) - } - - err = fs.CloseAndRemoveDocument(fh) - - expectedErr := &UnknownDocumentErr{fh} - if err == nil { - t.Fatalf("Expected error: %s", expectedErr) - } - if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", - expectedErr, err) - } -} - -func TestFilesystem_Close_closed(t *testing.T) { - fs := testDocumentStorage() - - fh := &testHandler{uri: "file:///isnotopen"} - fs.CreateDocument(fh, "test", []byte{}) - err := fs.CloseAndRemoveDocument(fh) - expectedErr := &DocumentNotOpenErr{fh} - if err == nil { - t.Fatalf("Expected error: %s", expectedErr) - } - if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", - expectedErr, err) - } -} - -func TestFilesystem_Change_noChanges(t *testing.T) { - fs := testDocumentStorage() - - fh := &testHandler{uri: "file:///test.tf"} - fs.CreateAndOpenDocument(fh, "test", []byte{}) - - var changes DocumentChanges - err := fs.ChangeDocument(fh, changes) - if err != nil { - t.Fatal(err) - } -} - -func TestFilesystem_Change_multipleChanges(t *testing.T) { - fs := testDocumentStorage() - - fh := &testHandler{uri: "file:///test.tf"} - fs.CreateAndOpenDocument(fh, "test", []byte{}) - - var changes DocumentChanges - changes = append(changes, &testChange{text: "ahoy"}) - changes = append(changes, &testChange{text: ""}) - changes = append(changes, &testChange{text: "quick brown fox jumped over\nthe lazy dog"}) - changes = append(changes, &testChange{text: "bye"}) - - err := fs.ChangeDocument(fh, changes) - if err != nil { - t.Fatal(err) - } -} - -func TestFilesystem_GetDocument_success(t *testing.T) { - fs := testDocumentStorage() - - dh := &testHandler{uri: "file:///test.tf"} - err := fs.CreateAndOpenDocument(dh, "test", []byte("hello world")) - if err != nil { - t.Fatal(err) - } - - f, err := fs.GetDocument(dh) - if err != nil { - t.Fatal(err) - } - - b := []byte("hello world") - meta := NewDocumentMetadata(dh, "test", b) - meta.isOpen = true - expectedFile := testDocument(t, dh, meta, b) - if diff := cmp.Diff(expectedFile, f); diff != "" { - t.Fatalf("File doesn't match: %s", diff) - } -} - -func TestFilesystem_GetDocument_unknownDocument(t *testing.T) { - fs := testDocumentStorage() - - fh := &testHandler{uri: "file:///test.tf"} - _, err := fs.GetDocument(fh) - - expectedErr := &UnknownDocumentErr{fh} - if err == nil { - t.Fatalf("Expected error: %s", expectedErr) - } - if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error.\nexpected: %#v\ngiven: %#v", - expectedErr, err) - } -} - func TestFilesystem_ReadFile_osOnly(t *testing.T) { - tmpDir := TempDir(t) + tmpDir := t.TempDir() f, err := os.Create(filepath.Join(tmpDir, "testfile")) if err != nil { t.Fatal(err) @@ -176,7 +25,7 @@ func TestFilesystem_ReadFile_osOnly(t *testing.T) { t.Fatal(err) } - fs := NewFilesystem() + fs := NewFilesystem(testDocumentStore{}) b, err := fs.ReadFile(filepath.Join(tmpDir, "testfile")) if err != nil { t.Fatal(err) @@ -197,14 +46,20 @@ func TestFilesystem_ReadFile_osOnly(t *testing.T) { } func TestFilesystem_ReadFile_memOnly(t *testing.T) { - fs := NewFilesystem() - fh := &testHandler{uri: "file:///tmp/test.tf"} + testHandle := document.HandleFromURI("file:///tmp/test.tf") content := "test content" - err := fs.CreateDocument(fh, "test", []byte(content)) - if err != nil { - t.Fatal(err) - } - b, err := fs.ReadFile(fh.FullPath()) + + fs := NewFilesystem(testDocumentStore{ + testHandle: &document.Document{ + Dir: testHandle.Dir, + Filename: testHandle.Filename, + LanguageID: "terraform", + Version: 0, + Text: []byte(content), + }, + }) + + b, err := fs.ReadFile(testHandle.FullPath()) if err != nil { t.Fatal(err) } @@ -224,7 +79,7 @@ func TestFilesystem_ReadFile_memOnly(t *testing.T) { } func TestFilesystem_ReadFile_memAndOs(t *testing.T) { - tmpDir := TempDir(t) + tmpDir := t.TempDir() testPath := filepath.Join(tmpDir, "testfile") f, err := os.Create(testPath) @@ -238,16 +93,19 @@ func TestFilesystem_ReadFile_memAndOs(t *testing.T) { t.Fatal(err) } - fs := NewFilesystem() - - fh := testHandlerFromPath(testPath) + testHandle := document.HandleFromPath(testPath) memContent := "in-mem content" - err = fs.CreateDocument(fh, "test", []byte(memContent)) - if err != nil { - t.Fatal(err) - } + fs := NewFilesystem(testDocumentStore{ + testHandle: &document.Document{ + Dir: testHandle.Dir, + Filename: testHandle.Filename, + LanguageID: "terraform", + Version: 0, + Text: []byte(memContent), + }, + }) - b, err := fs.ReadFile(fh.FullPath()) + b, err := fs.ReadFile(testPath) if err != nil { t.Fatal(err) } @@ -266,8 +124,8 @@ func TestFilesystem_ReadFile_memAndOs(t *testing.T) { } } -func TestFilesystem_ReadDir(t *testing.T) { - tmpDir := TempDir(t) +func TestFilesystem_ReadDir_memAndOs(t *testing.T) { + tmpDir := t.TempDir() f, err := os.Create(filepath.Join(tmpDir, "osfile")) if err != nil { @@ -275,13 +133,16 @@ func TestFilesystem_ReadDir(t *testing.T) { } defer f.Close() - fs := NewFilesystem() - - fh := testHandlerFromPath(filepath.Join(tmpDir, "memfile")) - err = fs.CreateDocument(fh, "test", []byte("test")) - if err != nil { - t.Fatal(err) - } + testHandle := document.HandleFromPath(filepath.Join(tmpDir, "memfile")) + fs := NewFilesystem(testDocumentStore{ + testHandle: &document.Document{ + Dir: testHandle.Dir, + Filename: testHandle.Filename, + LanguageID: "terraform", + Version: 0, + Text: []byte("test"), + }, + }) fis, err := fs.ReadDir(tmpDir) if err != nil { @@ -296,15 +157,18 @@ func TestFilesystem_ReadDir(t *testing.T) { } func TestFilesystem_ReadDir_memFsOnly(t *testing.T) { - fs := NewFilesystem() - tmpDir := t.TempDir() - fh := testHandlerFromPath(filepath.Join(tmpDir, "memfile")) - err := fs.CreateDocument(fh, "test", []byte("test")) - if err != nil { - t.Fatal(err) - } + testHandle := document.HandleFromPath(filepath.Join(tmpDir, "memfile")) + fs := NewFilesystem(testDocumentStore{ + testHandle: &document.Document{ + Dir: testHandle.Dir, + Filename: testHandle.Filename, + LanguageID: "terraform", + Version: 0, + Text: []byte("test"), + }, + }) fis, err := fs.ReadDir(tmpDir) if err != nil { @@ -318,16 +182,9 @@ func TestFilesystem_ReadDir_memFsOnly(t *testing.T) { } } -func namesFromFileInfos(entries []fs.DirEntry) []string { - names := make([]string, len(entries), len(entries)) - for i, entry := range entries { - names[i] = entry.Name() - } - return names -} - func TestFilesystem_Open_osOnly(t *testing.T) { - tmpDir := TempDir(t) + tmpDir := t.TempDir() + f, err := os.Create(filepath.Join(tmpDir, "testfile")) if err != nil { t.Fatal(err) @@ -339,7 +196,7 @@ func TestFilesystem_Open_osOnly(t *testing.T) { t.Fatal(err) } - fs := NewFilesystem() + fs := NewFilesystem(testDocumentStore{}) f1, err := fs.Open(filepath.Join(tmpDir, "testfile")) if err != nil { t.Fatal(err) @@ -358,17 +215,22 @@ func TestFilesystem_Open_osOnly(t *testing.T) { } func TestFilesystem_Open_memOnly(t *testing.T) { - fs := NewFilesystem() - tmpDir := TempDir(t) - testPath := filepath.Join(tmpDir, "test.tf") - fh := testHandlerFromPath(testPath) + tmpDir := t.TempDir() - content := "test content" - err := fs.CreateDocument(fh, "test", []byte(content)) - if err != nil { - t.Fatal(err) - } - f1, err := fs.Open(fh.FullPath()) + path := filepath.Join(tmpDir, "test.tf") + testHandle := document.HandleFromPath(path) + + fs := NewFilesystem(testDocumentStore{ + testHandle: &document.Document{ + Dir: testHandle.Dir, + Filename: testHandle.Filename, + LanguageID: "terraform", + Version: 0, + Text: []byte("test"), + }, + }) + + f1, err := fs.Open(path) if err != nil { t.Fatal(err) } @@ -386,7 +248,7 @@ func TestFilesystem_Open_memOnly(t *testing.T) { } func TestFilesystem_Open_memAndOs(t *testing.T) { - tmpDir := TempDir(t) + tmpDir := t.TempDir() testPath := filepath.Join(tmpDir, "testfile") f, err := os.Create(testPath) @@ -400,16 +262,20 @@ func TestFilesystem_Open_memAndOs(t *testing.T) { t.Fatal(err) } - fs := NewFilesystem() - - fh := testHandlerFromPath(testPath) + testHandle := document.HandleFromPath(testPath) memContent := "in-mem content" - err = fs.CreateDocument(fh, "test", []byte(memContent)) - if err != nil { - t.Fatal(err) - } - f1, err := fs.Open(fh.FullPath()) + fs := NewFilesystem(testDocumentStore{ + testHandle: &document.Document{ + Dir: testHandle.Dir, + Filename: testHandle.Filename, + LanguageID: "terraform", + Version: 0, + Text: []byte(memContent), + }, + }) + + f1, err := fs.Open(testPath) if err != nil { t.Fatal(err) } @@ -433,170 +299,38 @@ func TestFilesystem_Open_memAndOs(t *testing.T) { } } -func TestFilesystem_Create_memOnly(t *testing.T) { - fs := NewFilesystem() - tmpDir := TempDir(t) - testPath := filepath.Join(tmpDir, "test.tf") - fh := testHandlerFromPath(testPath) - - content := "test content" - err := fs.CreateDocument(fh, "test", []byte(content)) - if err != nil { - t.Fatal(err) - } - - infos, err := fs.ReadDir(tmpDir) - if err != nil { - t.Fatal(err) - } - - expectedFis := []string{"test.tf"} - names := namesFromFileInfos(infos) - if diff := cmp.Diff(expectedFis, names); diff != "" { - t.Fatalf("file list mismatch: %s", diff) +func namesFromFileInfos(entries []fs.DirEntry) []string { + names := make([]string, len(entries), len(entries)) + for i, entry := range entries { + names[i] = entry.Name() } + return names } -func TestFilesystem_CreateDocument_missingParentDir(t *testing.T) { - fs := NewFilesystem() +type testDocumentStore map[document.Handle]*document.Document - tmpDir := t.TempDir() - testPath := filepath.Join(tmpDir, "foo", "bar", "test.tf") - fh := testHandlerFromPath(testPath) - - err := fs.CreateDocument(fh, "test", []byte("test")) - if err != nil { - t.Fatal(err) - } - - fooPath := filepath.Join(tmpDir, "foo") - fi, err := fs.memFs.Stat(fooPath) - if err != nil { - t.Fatal(err) - } - if !fi.IsDir() { - t.Fatalf("expected %q to be a dir", fooPath) - } - - barPath := filepath.Join(tmpDir, "foo", "bar") - fi, err = fs.memFs.Stat(barPath) - if err != nil { - t.Fatal(err) - } - if !fi.IsDir() { - t.Fatalf("expected %q to be a dir", barPath) +func (ds testDocumentStore) GetDocument(dh document.Handle) (*document.Document, error) { + doc, ok := ds[dh] + if !ok { + return nil, &document.DocumentNotFound{URI: dh.FullURI()} } + return doc, nil } -func TestFilesystem_HasOpenFiles(t *testing.T) { - fs := NewFilesystem() - - tmpDir := t.TempDir() +func (ds testDocumentStore) ListDocumentsInDir(dirHandle document.DirHandle) ([]*document.Document, error) { + docs := make([]*document.Document, 0) - notOpenHandler := filepath.Join(tmpDir, "not-open.tf") - noFh := testHandlerFromPath(notOpenHandler) - err := fs.CreateDocument(noFh, "test", []byte("test1")) - if err != nil { - t.Fatal(err) - } - - of, err := fs.HasOpenFiles(tmpDir) - if err != nil { - t.Fatal(err) - } - if of { - t.Fatalf("expected no open files for %s", tmpDir) - } - - openHandler := filepath.Join(tmpDir, "open.tf") - ofh := testHandlerFromPath(openHandler) - err = fs.CreateAndOpenDocument(ofh, "test", []byte("test2")) - if err != nil { - t.Fatal(err) - } - - of, err = fs.HasOpenFiles(tmpDir) - if err != nil { - t.Fatal(err) - } - if !of { - t.Fatalf("expected open files for %s", tmpDir) - } - - err = fs.CloseAndRemoveDocument(ofh) - if err != nil { - t.Fatal(err) - } - - of, err = fs.HasOpenFiles(tmpDir) - if err != nil { - t.Fatal(err) - } - if of { - t.Fatalf("expected no open files for %s", tmpDir) - } -} - -func TempDir(t *testing.T) string { - tmpDir := filepath.Join(os.TempDir(), "terraform-ls", t.Name()) - - err := os.MkdirAll(tmpDir, 0755) - if err != nil { - if os.IsExist(err) { - return tmpDir + for dh, doc := range ds { + if dh.Dir == dirHandle { + docs = append(docs, doc) } - t.Fatal(err) } - t.Cleanup(func() { - err := os.RemoveAll(tmpDir) - if err != nil { - t.Fatal(err) - } - }) - - return tmpDir -} - -func testHandlerFromPath(path string) DocumentHandler { - return &testHandler{uri: uri.FromPath(path), fullPath: path} -} - -type testHandler struct { - uri string - fullPath string -} - -func (fh *testHandler) URI() string { - return fh.uri -} - -func (fh *testHandler) FullPath() string { - return fh.fullPath -} - -func (fh *testHandler) Dir() string { - return "" -} - -func (fh *testHandler) Filename() string { - return "" -} - -func (fh *testHandler) IsOpen() bool { - return false -} - -func (fh *testHandler) Version() int { - return 0 -} - -func (fh *testHandler) LanguageID() string { - return "terraform" + return docs, nil } -func testDocumentStorage() DocumentStorage { - fs := NewFilesystem() +func testFilesystem(docStore DocumentStore) *Filesystem { + fs := NewFilesystem(docStore) fs.logger = testLogger() return fs } diff --git a/internal/filesystem/inmem.go b/internal/filesystem/inmem.go new file mode 100644 index 00000000..baa010c8 --- /dev/null +++ b/internal/filesystem/inmem.go @@ -0,0 +1,79 @@ +package filesystem + +import ( + "bytes" + "io/fs" + "time" +) + +type inMemFile struct { + bytes []byte + info fs.FileInfo +} + +func (f inMemFile) Read(b []byte) (int, error) { + return bytes.NewBuffer(f.bytes).Read(b) +} + +func (f inMemFile) Stat() (fs.FileInfo, error) { + return f.info, nil +} + +func (f inMemFile) Close() error { + return nil +} + +type inMemFileInfo struct { + name string + size int + mode fs.FileMode + modTime time.Time + isDir bool +} + +func (fi inMemFileInfo) Name() string { + return fi.name +} + +func (fi inMemFileInfo) Size() int64 { + return int64(fi.size) +} + +func (fi inMemFileInfo) Mode() fs.FileMode { + return fi.mode +} + +func (fi inMemFileInfo) ModTime() time.Time { + return fi.modTime +} + +func (fi inMemFileInfo) IsDir() bool { + return fi.isDir +} + +func (fi inMemFileInfo) Sys() interface{} { + return nil +} + +type inMemDirEntry struct { + name string + isDir bool + typ fs.FileMode + info fs.FileInfo +} + +func (de inMemDirEntry) Name() string { + return de.name +} + +func (de inMemDirEntry) IsDir() bool { + return de.isDir +} + +func (de inMemDirEntry) Type() fs.FileMode { + return de.typ +} + +func (de inMemDirEntry) Info() (fs.FileInfo, error) { + return de.info, nil +} diff --git a/internal/filesystem/os_fs.go b/internal/filesystem/os_fs.go new file mode 100644 index 00000000..b6fb5709 --- /dev/null +++ b/internal/filesystem/os_fs.go @@ -0,0 +1,24 @@ +package filesystem + +import ( + "io/fs" + "os" +) + +type osFs struct{} + +func (osfs osFs) Open(name string) (fs.File, error) { + return os.Open(name) +} + +func (osfs osFs) Stat(name string) (fs.FileInfo, error) { + return os.Stat(name) +} + +func (osfs osFs) ReadDir(name string) ([]fs.DirEntry, error) { + return os.ReadDir(name) +} + +func (osfs osFs) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} diff --git a/internal/filesystem/position.go b/internal/filesystem/position.go deleted file mode 100644 index fd8df268..00000000 --- a/internal/filesystem/position.go +++ /dev/null @@ -1,73 +0,0 @@ -package filesystem - -import ( - "unicode/utf16" - "unicode/utf8" - - "github.com/apparentlymart/go-textseg/textseg" - "github.com/hashicorp/terraform-ls/internal/source" -) - -func ByteOffsetForPos(lines source.Lines, pos Pos) (int, error) { - if pos.Line+1 > len(lines) { - return 0, &InvalidPosErr{Pos: pos} - } - - return byteOffsetForLSPColumn(lines[pos.Line], pos.Column), nil -} - -// byteForLSPColumn takes an lsp.Position.Character value for the receving line -// and finds the byte offset of the start of the UTF-8 sequence that represents -// it in the overall source buffer. This is different than the byte returned -// by posForLSPColumn because it can return offsets that are partway through -// a grapheme cluster, while HCL positions always round to the nearest -// grapheme cluster. -// -// Note that even this can't produce an exact result; if the column index -// refers to the second unit of a UTF-16 surrogate pair then it is rounded -// down the first unit because UTF-8 sequences are not divisible in the same -// way. -func byteOffsetForLSPColumn(l source.Line, lspCol int) int { - if lspCol < 0 { - return l.Range().Start.Byte - } - - // Normally ASCII-only lines could be short-circuited here - // but it's not as easy to tell whether a line is ASCII-only - // based on column/byte differences as we also scan newlines - // and a single line range technically spans 2 lines. - - // If there are non-ASCII characters then we need to edge carefully - // along the line while counting UTF-16 code units in our UTF-8 buffer, - // since LSP columns are a count of UTF-16 units. - byteCt := 0 - utf16Ct := 0 - colIdx := 1 - remain := l.Bytes() - for { - if len(remain) == 0 { // ran out of characters on the line, so given column is invalid - return l.Range().End.Byte - } - if utf16Ct >= lspCol { // we've found it - return l.Range().Start.Byte + byteCt - } - // Unlike our other conversion functions we're intentionally using - // individual UTF-8 sequences here rather than grapheme clusters because - // an LSP position might point into the middle of a grapheme cluster. - - adv, chBytes, _ := textseg.ScanUTF8Sequences(remain, true) - remain = remain[adv:] - byteCt += adv - colIdx++ - for len(chBytes) > 0 { - r, l := utf8.DecodeRune(chBytes) - chBytes = chBytes[l:] - c1, c2 := utf16.EncodeRune(r) - if c1 == 0xfffd && c2 == 0xfffd { - utf16Ct++ // codepoint fits in one 16-bit unit - } else { - utf16Ct += 2 // codepoint requires a surrogate pair - } - } - } -} diff --git a/internal/filesystem/range.go b/internal/filesystem/range.go deleted file mode 100644 index 11192990..00000000 --- a/internal/filesystem/range.go +++ /dev/null @@ -1,18 +0,0 @@ -package filesystem - -import "fmt" - -// Range represents LSP-style range between two positions -// Positions are zero-indexed -type Range struct { - Start, End Pos -} - -// Pos represents LSP-style position (zero-indexed) -type Pos struct { - Line, Column int -} - -func (p Pos) String() string { - return fmt.Sprintf("%d:%d", p.Line, p.Column) -} diff --git a/internal/filesystem/types.go b/internal/filesystem/types.go deleted file mode 100644 index a36f36de..00000000 --- a/internal/filesystem/types.go +++ /dev/null @@ -1,58 +0,0 @@ -package filesystem - -import ( - "io/fs" - "log" - "os" - - "github.com/hashicorp/terraform-ls/internal/source" -) - -type Document interface { - DocumentHandler - Text() ([]byte, error) - Lines() source.Lines - LanguageID() string - Version() int -} - -type DocumentHandler interface { - URI() string - FullPath() string - Dir() string - Filename() string -} - -type VersionedDocumentHandler interface { - DocumentHandler - Version() int -} - -type DocumentChange interface { - Text() string - Range() *Range -} - -type DocumentChanges []DocumentChange - -type DocumentStorage interface { - // LS-specific methods - CreateDocument(DocumentHandler, string, []byte) error - CreateAndOpenDocument(DocumentHandler, string, []byte) error - GetDocument(DocumentHandler) (Document, error) - CloseAndRemoveDocument(DocumentHandler) error - ChangeDocument(VersionedDocumentHandler, DocumentChanges) error - HasOpenFiles(path string) (bool, error) -} - -type Filesystem interface { - DocumentStorage - - SetLogger(*log.Logger) - - // direct FS methods - ReadFile(name string) ([]byte, error) - ReadDir(name string) ([]fs.DirEntry, error) - Open(name string) (fs.File, error) - Stat(name string) (os.FileInfo, error) -} From 9c938a2f981d79c3a55a1f7e4aa0edb05eceb3f9 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 25 Jan 2022 11:45:12 +0000 Subject: [PATCH 04/15] internal/hcl: Update references --- internal/hcl/diff.go | 44 +++++++++++++++++++-------------------- internal/hcl/diff_test.go | 20 +++++++++--------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/internal/hcl/diff.go b/internal/hcl/diff.go index 5f56b315..64a1ecab 100644 --- a/internal/hcl/diff.go +++ b/internal/hcl/diff.go @@ -2,7 +2,7 @@ package hcl import ( "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/source" "github.com/pmezard/go-difflib/difflib" ) @@ -17,17 +17,17 @@ func (ch *fileChange) Text() string { return ch.newText } -func (ch *fileChange) Range() *filesystem.Range { +func (ch *fileChange) Range() *document.Range { if ch.rng == nil { return nil } - return &filesystem.Range{ - Start: filesystem.Pos{ + return &document.Range{ + Start: document.Pos{ Line: ch.rng.Start.Line - 1, Column: ch.rng.Start.Column - 1, }, - End: filesystem.Pos{ + End: document.Pos{ Line: ch.rng.End.Line - 1, Column: ch.rng.End.Column - 1, }, @@ -42,23 +42,23 @@ const ( ) // Diff calculates difference between Document's content -// and after byte sequence and returns it as filesystem.DocumentChanges -func Diff(f filesystem.DocumentHandler, before, after []byte) filesystem.DocumentChanges { - return diffLines(f.Filename(), - source.MakeSourceLines(f.Filename(), before), - source.MakeSourceLines(f.Filename(), after)) +// and after byte sequence and returns it as document.Changes +func Diff(f document.Handle, before, after []byte) document.Changes { + return diffLines(f.Filename, + source.MakeSourceLines(f.Filename, before), + source.MakeSourceLines(f.Filename, after)) } // diffLines calculates difference between two source.Lines -// and returns them as filesystem.DocumentChanges -func diffLines(filename string, beforeLines, afterLines source.Lines) filesystem.DocumentChanges { +// and returns them as document.Changes +func diffLines(filename string, beforeLines, afterLines source.Lines) document.Changes { context := 3 m := difflib.NewMatcher( source.StringLines(beforeLines), source.StringLines(afterLines)) - changes := make(filesystem.DocumentChanges, 0) + changes := make(document.Changes, 0) for _, group := range m.GetGroupedOpCodes(context) { for _, c := range group { @@ -75,15 +75,15 @@ func diffLines(filename string, beforeLines, afterLines source.Lines) filesystem for i, line := range beforeLines[beforeStart:beforeEnd] { if i == 0 { - lr := line.Range() + lr := line.Range rng = &lr continue } - rng.End = line.Range().End + rng.End = line.Range.End } for _, line := range afterLines[afterStart:afterEnd] { - newBytes = append(newBytes, line.Bytes()...) + newBytes = append(newBytes, line.Bytes...) } changes = append(changes, &fileChange{ @@ -97,11 +97,11 @@ func diffLines(filename string, beforeLines, afterLines source.Lines) filesystem var deleteRng *hcl.Range for i, line := range beforeLines[beforeStart:beforeEnd] { if i == 0 { - lr := line.Range() + lr := line.Range deleteRng = &lr continue } - deleteRng.End = line.Range().End + deleteRng.End = line.Range.End } changes = append(changes, &fileChange{ newText: "", @@ -120,21 +120,21 @@ func diffLines(filename string, beforeLines, afterLines source.Lines) filesystem if beforeStart == beforeEnd { line := beforeLines[beforeStart] - insertRng = line.Range().Ptr() + insertRng = line.Range.Ptr() } else { for i, line := range beforeLines[beforeStart:beforeEnd] { if i == 0 { - insertRng = line.Range().Ptr() + insertRng = line.Range.Ptr() continue } - insertRng.End = line.Range().End + insertRng.End = line.Range.End } } var newBytes []byte for _, line := range afterLines[afterStart:afterEnd] { - newBytes = append(newBytes, line.Bytes()...) + newBytes = append(newBytes, line.Bytes...) } changes = append(changes, &fileChange{ diff --git a/internal/hcl/diff_test.go b/internal/hcl/diff_test.go index 56f6180b..d3cd0ff4 100644 --- a/internal/hcl/diff_test.go +++ b/internal/hcl/diff_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/source" "github.com/pmezard/go-difflib/difflib" ) @@ -16,7 +16,7 @@ func TestDiff(t *testing.T) { testCases := []struct { name string beforeCfg, afterCfg string - expectedChanges filesystem.DocumentChanges + expectedChanges document.Changes }{ { "no-op", @@ -26,7 +26,7 @@ ccc`, `aaa bbb ccc`, - filesystem.DocumentChanges{}, + document.Changes{}, }, { "two separate lines replaced", @@ -46,7 +46,7 @@ ccc`, s = 3 } }`, - filesystem.DocumentChanges{ + document.Changes{ &fileChange{ newText: ` "key" = "value" `, @@ -87,7 +87,7 @@ ccc`, s = 3 } }`, - filesystem.DocumentChanges{ + document.Changes{ &fileChange{ newText: "", rng: &hcl.Range{ @@ -106,7 +106,7 @@ ccc`, `resource "aws_vpc" "name" { }`, - filesystem.DocumentChanges{ + document.Changes{ &fileChange{ newText: "\n", rng: &hcl.Range{ @@ -122,7 +122,7 @@ ccc`, `resource "aws_vpc" "name" {}`, `resource "aws_vpc" "name" { }`, - filesystem.DocumentChanges{ + document.Changes{ &fileChange{ newText: `resource "aws_vpc" "name" { }`, @@ -143,7 +143,7 @@ ccc`, } `, - filesystem.DocumentChanges{ + document.Changes{ &fileChange{ newText: "\n", rng: &hcl.Range{ @@ -166,7 +166,7 @@ ccc`, attr2 = "two" attr3 = "three" }`, - filesystem.DocumentChanges{ + document.Changes{ &fileChange{ newText: ` attr2 = "two" `, @@ -183,7 +183,7 @@ ccc`, ``, ` `, - filesystem.DocumentChanges{ + document.Changes{ &fileChange{ newText: "\n", rng: &hcl.Range{ From 5a5fe6a622d2d4c8ed31c58245840470aa3a01e0 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 25 Jan 2022 11:45:21 +0000 Subject: [PATCH 05/15] internal/lsp: Update references --- internal/lsp/file.go | 71 ------------------ internal/lsp/file_change.go | 16 ++-- internal/lsp/file_handler.go | 91 ++--------------------- internal/lsp/file_handler_test.go | 19 ----- internal/lsp/file_handler_unix_test.go | 54 -------------- internal/lsp/file_handler_windows_test.go | 42 ----------- internal/lsp/position.go | 48 +++--------- internal/lsp/range.go | 22 +++--- internal/lsp/text_edits.go | 6 +- internal/lsp/token_encoder.go | 2 +- 10 files changed, 38 insertions(+), 333 deletions(-) delete mode 100644 internal/lsp/file.go delete mode 100644 internal/lsp/file_handler_test.go delete mode 100644 internal/lsp/file_handler_unix_test.go delete mode 100644 internal/lsp/file_handler_windows_test.go diff --git a/internal/lsp/file.go b/internal/lsp/file.go deleted file mode 100644 index 978b7823..00000000 --- a/internal/lsp/file.go +++ /dev/null @@ -1,71 +0,0 @@ -package lsp - -import ( - lsp "github.com/hashicorp/terraform-ls/internal/protocol" - "github.com/hashicorp/terraform-ls/internal/source" -) - -type File interface { - URI() string - FullPath() string - Dir() string - Filename() string - Lines() source.Lines - LanguageID() string -} - -type file struct { - fh *fileHandler - ls source.Lines - text []byte - version int - languageID string -} - -func (f *file) URI() string { - return f.fh.URI() -} - -func (f *file) FullPath() string { - return f.fh.FullPath() -} - -func (f *file) Dir() string { - return f.fh.Dir() -} - -func (f *file) Filename() string { - return f.fh.Filename() -} - -func (f *file) Text() []byte { - return f.text -} - -func (f *file) Lines() source.Lines { - return f.lines() -} - -func (f *file) LanguageID() string { - return f.languageID -} - -func (f *file) lines() source.Lines { - if f.ls == nil { - f.ls = source.MakeSourceLines(f.fh.Filename(), f.text) - } - return f.ls -} - -func (f *file) Version() int { - return f.version -} - -func FileFromDocumentItem(doc lsp.TextDocumentItem) *file { - return &file{ - fh: FileHandlerFromDocumentURI(doc.URI), - text: []byte(doc.Text), - version: int(doc.Version), - languageID: doc.LanguageID, - } -} diff --git a/internal/lsp/file_change.go b/internal/lsp/file_change.go index 3bd8b703..48bce779 100644 --- a/internal/lsp/file_change.go +++ b/internal/lsp/file_change.go @@ -1,35 +1,35 @@ package lsp import ( - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) type contentChange struct { text string - rng *filesystem.Range + rng *document.Range } -func ContentChange(chEvent lsp.TextDocumentContentChangeEvent) filesystem.DocumentChange { +func ContentChange(chEvent lsp.TextDocumentContentChangeEvent) document.Change { return &contentChange{ text: chEvent.Text, - rng: lspRangeToFsRange(chEvent.Range), + rng: lspRangeToDocRange(chEvent.Range), } } -func DocumentChanges(events []lsp.TextDocumentContentChangeEvent, f File) (filesystem.DocumentChanges, error) { - changes := make(filesystem.DocumentChanges, len(events)) +func DocumentChanges(events []lsp.TextDocumentContentChangeEvent) document.Changes { + changes := make(document.Changes, len(events)) for i, event := range events { ch := ContentChange(event) changes[i] = ch } - return changes, nil + return changes } func (fc *contentChange) Text() string { return fc.text } -func (fc *contentChange) Range() *filesystem.Range { +func (fc *contentChange) Range() *document.Range { return fc.rng } diff --git a/internal/lsp/file_handler.go b/internal/lsp/file_handler.go index e57c8984..8e84169d 100644 --- a/internal/lsp/file_handler.go +++ b/internal/lsp/file_handler.go @@ -1,95 +1,14 @@ package lsp import ( - "path/filepath" - "strings" - + "github.com/hashicorp/terraform-ls/internal/document" lsp "github.com/hashicorp/terraform-ls/internal/protocol" - "github.com/hashicorp/terraform-ls/internal/uri" ) -func FileHandlerFromDocumentURI(docUri lsp.DocumentURI) *fileHandler { - return &fileHandler{uri: string(docUri)} -} - -func FileHandlerFromDirURI(dirUri lsp.DocumentURI) *fileHandler { - // Dir URIs are usually without trailing separator already - // but we do sanity check anyway, so we deal with the same URI - // regardless of language client differences - uri := strings.TrimSuffix(string(dirUri), "/") - return &fileHandler{uri: uri, isDir: true} -} - -type FileHandler interface { - Valid() bool - Dir() string - IsDir() bool - Filename() string - DocumentURI() lsp.DocumentURI - URI() string -} - -type fileHandler struct { - uri string - isDir bool -} - -func (fh *fileHandler) Valid() bool { - return uri.IsURIValid(fh.uri) -} - -func (fh *fileHandler) IsDir() bool { - return fh.isDir -} - -func (fh *fileHandler) Dir() string { - if fh.isDir { - return fh.FullPath() - } - - path := filepath.Dir(fh.FullPath()) - return path -} - -func (fh *fileHandler) Filename() string { - return filepath.Base(fh.FullPath()) +func HandleFromDocumentURI(docUri lsp.DocumentURI) document.Handle { + return document.HandleFromURI(string(docUri)) } -func (fh *fileHandler) FullPath() string { - return uri.MustPathFromURI(fh.uri) -} - -func (fh *fileHandler) DocumentURI() lsp.DocumentURI { - return lsp.DocumentURI(fh.uri) -} - -func (fh *fileHandler) URI() string { - return string(fh.uri) -} - -type versionedFileHandler struct { - fileHandler - v int -} - -func VersionedFileHandler(doc lsp.VersionedTextDocumentIdentifier) *versionedFileHandler { - return &versionedFileHandler{ - fileHandler: fileHandler{uri: string(doc.URI)}, - v: int(doc.Version), - } -} - -func (fh *versionedFileHandler) Version() int { - return fh.v -} - -func FileHandlerFromPath(path string) *fileHandler { - return &fileHandler{uri: uri.FromPath(path)} -} - -func FileHandlerFromDirPath(dirPath string) *fileHandler { - // Dir URIs are usually without trailing separator in LSP - dirPath = filepath.Clean(dirPath) - - return &fileHandler{uri: uri.FromPath(dirPath), isDir: true} +func DirHandleFromDirURI(dirUri lsp.DocumentURI) document.DirHandle { + return document.DirHandleFromURI(string(dirUri)) } diff --git a/internal/lsp/file_handler_test.go b/internal/lsp/file_handler_test.go deleted file mode 100644 index b96b2fa7..00000000 --- a/internal/lsp/file_handler_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package lsp - -import ( - "testing" - - lsp "github.com/hashicorp/terraform-ls/internal/protocol" -) - -var ( - validUnixPath = "file:///valid/path/to/file.tf" -) - -func TestFileHandler_invalid(t *testing.T) { - path := "invalidpath" - fh := FileHandlerFromDocumentURI(lsp.DocumentURI(path)) - if fh.Valid() { - t.Fatalf("Expected %q to be invalid", path) - } -} diff --git a/internal/lsp/file_handler_unix_test.go b/internal/lsp/file_handler_unix_test.go deleted file mode 100644 index d7b718ca..00000000 --- a/internal/lsp/file_handler_unix_test.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build !windows -// +build !windows - -package lsp - -import ( - "testing" - - lsp "github.com/hashicorp/terraform-ls/internal/protocol" -) - -func TestFileHandler_valid_unix(t *testing.T) { - fh := FileHandlerFromDocumentURI(lsp.DocumentURI(validUnixPath)) - if !fh.Valid() { - t.Fatalf("Expected %q to be valid", validUnixPath) - } - - expectedDir := "/valid/path/to" - if fh.Dir() != expectedDir { - t.Fatalf("Expected dir: %q, given: %q", - expectedDir, fh.Dir()) - } - - expectedFilename := "file.tf" - if fh.Filename() != expectedFilename { - t.Fatalf("Expected filename: %q, given: %q", - expectedFilename, fh.Filename()) - } - - expectedFullPath := "/valid/path/to/file.tf" - if fh.FullPath() != expectedFullPath { - t.Fatalf("Expected full path: %q, given: %q", - expectedFullPath, fh.FullPath()) - } - - if fh.URI() != validUnixPath { - t.Fatalf("Expected document URI: %q, given: %q", - validUnixPath, fh.URI()) - } -} - -func TestFileHandler_valid_unixDir(t *testing.T) { - uri := "file:///valid/path/to" - fh := FileHandlerFromDirURI(lsp.DocumentURI(uri)) - if !fh.Valid() { - t.Fatalf("Expected %q to be valid", uri) - } - - expectedDir := "/valid/path/to" - if fh.Dir() != expectedDir { - t.Fatalf("Expected dir: %q, given: %q", - expectedDir, fh.Dir()) - } -} diff --git a/internal/lsp/file_handler_windows_test.go b/internal/lsp/file_handler_windows_test.go deleted file mode 100644 index 90b71ad0..00000000 --- a/internal/lsp/file_handler_windows_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package lsp - -import ( - "testing" - - lsp "github.com/hashicorp/terraform-ls/internal/protocol" -) - -var ( - validWindowsPath = "file:///C:/Users/With%20Space/tf-test/file.tf" -) - -func TestFileHandler_valid_windows(t *testing.T) { - path := "file:///C:/Users/With%20Space/tf-test/file.tf" - fh := FileHandlerFromDocumentURI(lsp.DocumentURI(path)) - if !fh.Valid() { - t.Fatalf("Expected %q to be valid", path) - } - - expectedDir := `C:\Users\With Space\tf-test` - if fh.Dir() != expectedDir { - t.Fatalf("Expected dir: %q, given: %q", - expectedDir, fh.Dir()) - } - - expectedFilename := "file.tf" - if fh.Filename() != expectedFilename { - t.Fatalf("Expected filename: %q, given: %q", - expectedFilename, fh.Filename()) - } - - expectedFullPath := `C:\Users\With Space\tf-test\file.tf` - if fh.FullPath() != expectedFullPath { - t.Fatalf("Expected full path: %q, given: %q", - expectedFullPath, fh.FullPath()) - } - - if fh.URI() != validWindowsPath { - t.Fatalf("Expected document URI: %q, given: %q", - validWindowsPath, fh.URI()) - } -} diff --git a/internal/lsp/position.go b/internal/lsp/position.go index 9b61b090..83d17ab7 100644 --- a/internal/lsp/position.go +++ b/internal/lsp/position.go @@ -2,53 +2,25 @@ package lsp import ( "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) -type filePosition struct { - fh *fileHandler - pos hcl.Pos -} - -func (p *filePosition) Position() hcl.Pos { - return p.pos -} - -func (p *filePosition) URI() string { - return p.fh.URI() -} - -func (p *filePosition) FullPath() string { - return p.fh.FullPath() -} - -func (p *filePosition) Dir() string { - return p.fh.Dir() -} - -func (p *filePosition) Filename() string { - return p.fh.Filename() -} - -func FilePositionFromDocumentPosition(params lsp.TextDocumentPositionParams, f File) (*filePosition, error) { - byteOffset, err := filesystem.ByteOffsetForPos(f.Lines(), lspPosToFsPos(params.Position)) +func HCLPositionFromLspPosition(pos lsp.Position, doc *document.Document) (hcl.Pos, error) { + byteOffset, err := document.ByteOffsetForPos(doc.Lines, lspPosToDocumentPos(pos)) if err != nil { - return nil, err + return hcl.Pos{}, err } - return &filePosition{ - fh: FileHandlerFromDocumentURI(params.TextDocument.URI), - pos: hcl.Pos{ - Line: int(params.Position.Line) + 1, - Column: int(params.Position.Character) + 1, - Byte: byteOffset, - }, + return hcl.Pos{ + Line: int(pos.Line) + 1, + Column: int(pos.Character) + 1, + Byte: byteOffset, }, nil } -func lspPosToFsPos(pos lsp.Position) filesystem.Pos { - return filesystem.Pos{ +func lspPosToDocumentPos(pos lsp.Position) document.Pos { + return document.Pos{ Line: int(pos.Line), Column: int(pos.Character), } diff --git a/internal/lsp/range.go b/internal/lsp/range.go index e4e16ebc..708b7ec0 100644 --- a/internal/lsp/range.go +++ b/internal/lsp/range.go @@ -2,38 +2,38 @@ package lsp import ( "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) -func fsRangeToLSP(fsRng *filesystem.Range) lsp.Range { - if fsRng == nil { +func documentRangeToLSP(docRng *document.Range) lsp.Range { + if docRng == nil { return lsp.Range{} } return lsp.Range{ Start: lsp.Position{ - Character: uint32(fsRng.Start.Column), - Line: uint32(fsRng.Start.Line), + Character: uint32(docRng.Start.Column), + Line: uint32(docRng.Start.Line), }, End: lsp.Position{ - Character: uint32(fsRng.End.Column), - Line: uint32(fsRng.End.Line), + Character: uint32(docRng.End.Column), + Line: uint32(docRng.End.Line), }, } } -func lspRangeToFsRange(rng *lsp.Range) *filesystem.Range { +func lspRangeToDocRange(rng *lsp.Range) *document.Range { if rng == nil { return nil } - return &filesystem.Range{ - Start: filesystem.Pos{ + return &document.Range{ + Start: document.Pos{ Line: int(rng.Start.Line), Column: int(rng.Start.Character), }, - End: filesystem.Pos{ + End: document.Pos{ Line: int(rng.End.Line), Column: int(rng.End.Character), }, diff --git a/internal/lsp/text_edits.go b/internal/lsp/text_edits.go index f83b4362..e3255778 100644 --- a/internal/lsp/text_edits.go +++ b/internal/lsp/text_edits.go @@ -2,16 +2,16 @@ package lsp import ( "github.com/hashicorp/hcl-lang/lang" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) -func TextEditsFromDocumentChanges(changes filesystem.DocumentChanges) []lsp.TextEdit { +func TextEditsFromDocumentChanges(changes document.Changes) []lsp.TextEdit { edits := make([]lsp.TextEdit, len(changes)) for i, change := range changes { edits[i] = lsp.TextEdit{ - Range: fsRangeToLSP(change.Range()), + Range: documentRangeToLSP(change.Range()), NewText: change.Text(), } } diff --git a/internal/lsp/token_encoder.go b/internal/lsp/token_encoder.go index 1e3d8918..6136f7a9 100644 --- a/internal/lsp/token_encoder.go +++ b/internal/lsp/token_encoder.go @@ -121,7 +121,7 @@ func (te *TokenEncoder) encodeTokenOfIndex(i int) []uint32 { deltaStartChar = token.Range.Start.Column - 1 - previousStartChar } - lineBytes := bytes.TrimRight(te.Lines[tokenLine].Bytes(), "\n\r") + lineBytes := bytes.TrimRight(te.Lines[tokenLine].Bytes, "\n\r") length := len(lineBytes) if tokenLine == token.Range.End.Line-1 { From 5040cbce42dd974bd0878665c13aa527e606345c Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 25 Jan 2022 11:43:05 +0000 Subject: [PATCH 06/15] internal/source: Update references --- internal/source/source.go | 38 +++++++++++++++++++------------------- internal/source/types.go | 15 ++++++++------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/internal/source/source.go b/internal/source/source.go index 79bd6381..9e7fd56a 100644 --- a/internal/source/source.go +++ b/internal/source/source.go @@ -6,21 +6,21 @@ import ( "github.com/hashicorp/hcl/v2" ) -type sourceLine struct { - content []byte - rng hcl.Range -} +type Line struct { + // Bytes returns the line byte inc. any trailing end-of-line markers + Bytes []byte -// Range returns range of the line bytes inc. any trailing end-of-line markers -// The range will span across two lines in most cases -// (other than last line without trailing new line) -func (l sourceLine) Range() hcl.Range { - return l.rng + // Range returns range of the line bytes inc. any trailing end-of-line markers + // The range will span across two lines in most cases + // (other than last line without trailing new line) + Range hcl.Range } -// Bytes returns the line byte inc. any trailing end-of-line markers -func (l sourceLine) Bytes() []byte { - return l.content +func (l Line) Copy() Line { + return Line{ + Bytes: l.Bytes, + Range: l.Range, + } } func MakeSourceLines(filename string, s []byte) []Line { @@ -33,17 +33,17 @@ func MakeSourceLines(filename string, s []byte) []Line { } sc := hcl.NewRangeScanner(s, filename, scanLines) for sc.Scan() { - ret = append(ret, sourceLine{ - content: sc.Bytes(), - rng: sc.Range(), + ret = append(ret, Line{ + Bytes: sc.Bytes(), + Range: sc.Range(), }) lastRng = sc.Range() } // Account for the last (virtual) user-percieved line - ret = append(ret, sourceLine{ - content: []byte{}, - rng: hcl.Range{ + ret = append(ret, Line{ + Bytes: []byte{}, + Range: hcl.Range{ Filename: lastRng.Filename, Start: lastRng.End, End: lastRng.End, @@ -76,7 +76,7 @@ func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { func StringLines(lines Lines) []string { strLines := make([]string, len(lines)) for i, l := range lines { - strLines[i] = string(l.Bytes()) + strLines[i] = string(l.Bytes) } return strLines } diff --git a/internal/source/types.go b/internal/source/types.go index 98d7edda..f9997a85 100644 --- a/internal/source/types.go +++ b/internal/source/types.go @@ -1,12 +1,13 @@ package source -import ( - "github.com/hashicorp/hcl/v2" -) - type Lines []Line -type Line interface { - Range() hcl.Range - Bytes() []byte +func (l Lines) Copy() Lines { + newLines := make(Lines, len(l)) + + for i, line := range l { + newLines[i] = line.Copy() + } + + return newLines } From 1b112db398cd79b9c4cdd6b193237b57e03809e7 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 24 Jan 2022 12:13:11 +0000 Subject: [PATCH 07/15] internal/terraform: update references --- internal/terraform/datadir/datadir.go | 5 +- internal/terraform/datadir/module_manifest.go | 4 +- .../terraform/datadir/plugin_lock_file.go | 5 +- internal/terraform/module/module_loader.go | 14 ++++-- .../terraform/module/module_loader_test.go | 4 +- internal/terraform/module/module_manager.go | 15 +++--- .../terraform/module/module_manager_mock.go | 5 +- .../terraform/module/module_manager_test.go | 17 ++++--- internal/terraform/module/module_ops.go | 7 ++- internal/terraform/module/module_ops_queue.go | 14 +++--- .../terraform/module/module_ops_queue_test.go | 47 +++++++++++-------- internal/terraform/module/types.go | 18 +++++-- internal/terraform/module/walker.go | 8 ++-- internal/terraform/module/walker_mock.go | 8 ++-- internal/terraform/module/walker_queue.go | 15 +++--- internal/terraform/module/watcher.go | 7 +-- internal/terraform/module/watcher_mock.go | 4 +- internal/terraform/module/watcher_test.go | 18 +++---- 18 files changed, 121 insertions(+), 94 deletions(-) diff --git a/internal/terraform/datadir/datadir.go b/internal/terraform/datadir/datadir.go index 8948e3b8..fce62f64 100644 --- a/internal/terraform/datadir/datadir.go +++ b/internal/terraform/datadir/datadir.go @@ -1,10 +1,9 @@ package datadir import ( + "io/fs" "path/filepath" "strings" - - "github.com/hashicorp/terraform-ls/internal/filesystem" ) type DataDir struct { @@ -54,7 +53,7 @@ func ModulePath(filePath string) (string, bool) { return "", false } -func WalkDataDirOfModule(fs filesystem.Filesystem, modPath string) *DataDir { +func WalkDataDirOfModule(fs fs.StatFS, modPath string) *DataDir { dir := &DataDir{} path, ok := ModuleManifestFilePath(fs, modPath) diff --git a/internal/terraform/datadir/module_manifest.go b/internal/terraform/datadir/module_manifest.go index c707b967..953503c6 100644 --- a/internal/terraform/datadir/module_manifest.go +++ b/internal/terraform/datadir/module_manifest.go @@ -3,16 +3,16 @@ package datadir import ( "encoding/json" "fmt" + "io/fs" "io/ioutil" "path/filepath" "strings" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/pathcmp" ) -func ModuleManifestFilePath(fs filesystem.Filesystem, modulePath string) (string, bool) { +func ModuleManifestFilePath(fs fs.StatFS, modulePath string) (string, bool) { manifestPath := filepath.Join( append([]string{modulePath}, manifestPathElements...)...) diff --git a/internal/terraform/datadir/plugin_lock_file.go b/internal/terraform/datadir/plugin_lock_file.go index 3e315548..4bc73d52 100644 --- a/internal/terraform/datadir/plugin_lock_file.go +++ b/internal/terraform/datadir/plugin_lock_file.go @@ -1,12 +1,11 @@ package datadir import ( + "io/fs" "path/filepath" - - "github.com/hashicorp/terraform-ls/internal/filesystem" ) -func PluginLockFilePath(fs filesystem.Filesystem, modPath string) (string, bool) { +func PluginLockFilePath(fs fs.StatFS, modPath string) (string, bool) { for _, pathElems := range pluginLockFilePathElements { fullPath := filepath.Join(append([]string{modPath}, pathElems...)...) fi, err := fs.Stat(fullPath) diff --git a/internal/terraform/module/module_loader.go b/internal/terraform/module/module_loader.go index a0349e3f..9960528a 100644 --- a/internal/terraform/module/module_loader.go +++ b/internal/terraform/module/module_loader.go @@ -6,7 +6,7 @@ import ( "sync/atomic" "time" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/exec" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" @@ -20,7 +20,8 @@ type moduleLoader struct { tfExecOpts *exec.ExecutorOpts opsToDispatch chan ModuleOperation - fs filesystem.Filesystem + fs ReadOnlyFS + docStore DocumentStore modStore *state.ModuleStore schemaStore *state.ProviderSchemaStore @@ -28,10 +29,12 @@ type moduleLoader struct { prioLoadingCount *int64 } -func newModuleLoader(fs filesystem.Filesystem, modStore *state.ModuleStore, schemaStore *state.ProviderSchemaStore) *moduleLoader { +func newModuleLoader(fs ReadOnlyFS, docStore DocumentStore, + modStore *state.ModuleStore, schemaStore *state.ProviderSchemaStore) *moduleLoader { + plc, lc := int64(0), int64(0) ml := &moduleLoader{ - queue: newModuleOpsQueue(fs), + queue: newModuleOpsQueue(docStore), logger: defaultLogger, nonPrioParallelism: 1, prioParallelism: 1, @@ -39,6 +42,7 @@ func newModuleLoader(fs filesystem.Filesystem, modStore *state.ModuleStore, sche loadingCount: &lc, prioLoadingCount: &plc, fs: fs, + docStore: docStore, modStore: modStore, schemaStore: schemaStore, } @@ -66,7 +70,7 @@ func (ml *moduleLoader) run(ctx context.Context) { return } - hasOpenFiles, _ := ml.fs.HasOpenFiles(nextOp.ModulePath) + hasOpenFiles, _ := ml.docStore.HasOpenDocuments(document.DirHandleFromPath(nextOp.ModulePath)) if hasOpenFiles && ml.prioCapacity() > 0 { atomic.AddInt64(ml.prioLoadingCount, 1) diff --git a/internal/terraform/module/module_loader_test.go b/internal/terraform/module/module_loader_test.go index 9b54235d..9c771dd9 100644 --- a/internal/terraform/module/module_loader_test.go +++ b/internal/terraform/module/module_loader_test.go @@ -22,9 +22,9 @@ func TestModuleLoader_referenceCollection(t *testing.T) { if err != nil { t.Fatal(err) } - fs := filesystem.NewFilesystem() + fs := filesystem.NewFilesystem(ss.DocumentStore) - ml := newModuleLoader(fs, ss.Modules, ss.ProviderSchemas) + ml := newModuleLoader(fs, ss.DocumentStore, ss.Modules, ss.ProviderSchemas) ml.logger = testLogger() testData, err := filepath.Abs("testdata") diff --git a/internal/terraform/module/module_manager.go b/internal/terraform/module/module_manager.go index 9c3c951f..11edeff8 100644 --- a/internal/terraform/module/module_manager.go +++ b/internal/terraform/module/module_manager.go @@ -5,14 +5,13 @@ import ( "log" "path/filepath" - "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/state" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" tfmodule "github.com/hashicorp/terraform-schema/module" ) type moduleManager struct { - fs filesystem.Filesystem + fs ReadOnlyFS moduleStore *state.ModuleStore schemaStore *state.ProviderSchemaStore @@ -22,8 +21,8 @@ type moduleManager struct { logger *log.Logger } -func NewModuleManager(ctx context.Context, fs filesystem.Filesystem, ms *state.ModuleStore, pss *state.ProviderSchemaStore) ModuleManager { - mm := newModuleManager(fs, ms, pss) +func NewModuleManager(ctx context.Context, fs ReadOnlyFS, ds DocumentStore, ms *state.ModuleStore, pss *state.ProviderSchemaStore) ModuleManager { + mm := newModuleManager(fs, ds, ms, pss) ctx, cancelFunc := context.WithCancel(ctx) mm.cancelFunc = cancelFunc @@ -32,8 +31,8 @@ func NewModuleManager(ctx context.Context, fs filesystem.Filesystem, ms *state.M return mm } -func NewSyncModuleManager(ctx context.Context, fs filesystem.Filesystem, ms *state.ModuleStore, pss *state.ProviderSchemaStore) ModuleManager { - mm := newModuleManager(fs, ms, pss) +func NewSyncModuleManager(ctx context.Context, fs ReadOnlyFS, ds DocumentStore, ms *state.ModuleStore, pss *state.ProviderSchemaStore) ModuleManager { + mm := newModuleManager(fs, ds, ms, pss) ctx, cancelFunc := context.WithCancel(ctx) mm.cancelFunc = cancelFunc @@ -44,13 +43,13 @@ func NewSyncModuleManager(ctx context.Context, fs filesystem.Filesystem, ms *sta return mm } -func newModuleManager(fs filesystem.Filesystem, ms *state.ModuleStore, pss *state.ProviderSchemaStore) *moduleManager { +func newModuleManager(fs ReadOnlyFS, ds DocumentStore, ms *state.ModuleStore, pss *state.ProviderSchemaStore) *moduleManager { mm := &moduleManager{ fs: fs, moduleStore: ms, schemaStore: pss, logger: defaultLogger, - loader: newModuleLoader(fs, ms, pss), + loader: newModuleLoader(fs, ds, ms, pss), } return mm } diff --git a/internal/terraform/module/module_manager_mock.go b/internal/terraform/module/module_manager_mock.go index 687b530a..4ae81736 100644 --- a/internal/terraform/module/module_manager_mock.go +++ b/internal/terraform/module/module_manager_mock.go @@ -6,7 +6,6 @@ import ( "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" @@ -26,7 +25,7 @@ func NewModuleManagerMock(input *ModuleManagerMockInput) ModuleManagerFactory { tfCalls = input.TerraformCalls } - return func(ctx context.Context, fs filesystem.Filesystem, ms *state.ModuleStore, pss *state.ProviderSchemaStore) ModuleManager { + return func(ctx context.Context, fs ReadOnlyFS, ds DocumentStore, ms *state.ModuleStore, pss *state.ProviderSchemaStore) ModuleManager { if tfCalls != nil { ctx = exec.WithExecutorFactory(ctx, exec.NewMockExecutor(tfCalls)) ctx = exec.WithExecutorOpts(ctx, &exec.ExecutorOpts{ @@ -34,7 +33,7 @@ func NewModuleManagerMock(input *ModuleManagerMockInput) ModuleManagerFactory { }) } - mm := NewSyncModuleManager(ctx, fs, ms, pss) + mm := NewSyncModuleManager(ctx, fs, ds, ms, pss) if logger != nil { mm.SetLogger(logger) diff --git a/internal/terraform/module/module_manager_test.go b/internal/terraform/module/module_manager_test.go index 338aaea9..11f24d9a 100644 --- a/internal/terraform/module/module_manager_test.go +++ b/internal/terraform/module/module_manager_test.go @@ -336,21 +336,24 @@ func TestModuleManager_ModuleCandidatesByPath(t *testing.T) { base := filepath.Base(tc.walkerRoot) t.Run(fmt.Sprintf("%d-%s/%s", i, tc.name, base), func(t *testing.T) { ctx := context.Background() - fs := filesystem.NewFilesystem() + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + + fs := filesystem.NewFilesystem(ss.DocumentStore) mmock := NewModuleManagerMock(&ModuleManagerMockInput{ Logger: testLogger(), TerraformCalls: &exec.TerraformMockCalls{ AnyWorkDir: validTfMockCalls(tc.totalModuleCount), }, }) - ss, err := state.NewStateStore() - if err != nil { - t.Fatal(err) - } - mm := mmock(ctx, fs, ss.Modules, ss.ProviderSchemas) + + mm := mmock(ctx, fs, ss.DocumentStore, ss.Modules, ss.ProviderSchemas) t.Cleanup(mm.CancelLoading) - w := SyncWalker(fs, mm) + w := SyncWalker(fs, ss.DocumentStore, mm) w.SetLogger(testLogger()) w.EnqueuePath(tc.walkerRoot) err = w.StartWalking(ctx) diff --git a/internal/terraform/module/module_ops.go b/internal/terraform/module/module_ops.go index 8e5f4471..4f367adf 100644 --- a/internal/terraform/module/module_ops.go +++ b/internal/terraform/module/module_ops.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/go-version" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/terraform-ls/internal/decoder" - "github.com/hashicorp/terraform-ls/internal/filesystem" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/datadir" @@ -151,7 +150,7 @@ func ObtainSchema(ctx context.Context, modStore *state.ModuleStore, schemaStore return modStore.UpdateInstalledProviders(modPath, installedProviders) } -func ParseModuleConfiguration(fs filesystem.Filesystem, modStore *state.ModuleStore, modPath string) error { +func ParseModuleConfiguration(fs ReadOnlyFS, modStore *state.ModuleStore, modPath string) error { err := modStore.SetModuleParsingState(modPath, op.OpStateLoading) if err != nil { return err @@ -172,7 +171,7 @@ func ParseModuleConfiguration(fs filesystem.Filesystem, modStore *state.ModuleSt return err } -func ParseVariables(fs filesystem.Filesystem, modStore *state.ModuleStore, modPath string) error { +func ParseVariables(fs ReadOnlyFS, modStore *state.ModuleStore, modPath string) error { err := modStore.SetVarsParsingState(modPath, op.OpStateLoading) if err != nil { return err @@ -193,7 +192,7 @@ func ParseVariables(fs filesystem.Filesystem, modStore *state.ModuleStore, modPa return err } -func ParseModuleManifest(fs filesystem.Filesystem, modStore *state.ModuleStore, modPath string) error { +func ParseModuleManifest(fs ReadOnlyFS, modStore *state.ModuleStore, modPath string) error { err := modStore.SetModManifestState(modPath, op.OpStateLoading) if err != nil { return err diff --git a/internal/terraform/module/module_ops_queue.go b/internal/terraform/module/module_ops_queue.go index 7f418b1f..51a8d9c5 100644 --- a/internal/terraform/module/module_ops_queue.go +++ b/internal/terraform/module/module_ops_queue.go @@ -4,7 +4,7 @@ import ( "container/heap" "sync" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" ) type moduleOpsQueue struct { @@ -12,11 +12,11 @@ type moduleOpsQueue struct { mu *sync.Mutex } -func newModuleOpsQueue(fs filesystem.Filesystem) moduleOpsQueue { +func newModuleOpsQueue(ds DocumentStore) moduleOpsQueue { q := moduleOpsQueue{ q: &queue{ ops: make([]ModuleOperation, 0), - fs: fs, + ds: ds, }, mu: &sync.Mutex{}, } @@ -67,7 +67,7 @@ func (q *moduleOpsQueue) Len() int { type queue struct { ops []ModuleOperation - fs filesystem.Filesystem + ds DocumentStore } var _ heap.Interface = &queue{} @@ -100,10 +100,12 @@ func (q *queue) Less(i, j int) bool { func (q *queue) moduleOperationLess(aModOp, bModOp ModuleOperation) bool { leftOpen, rightOpen := 0, 0 - if hasOpenFiles, _ := q.fs.HasOpenFiles(aModOp.ModulePath); hasOpenFiles { + aModHandle := document.DirHandleFromPath(aModOp.ModulePath) + if hasOpenFiles, _ := q.ds.HasOpenDocuments(aModHandle); hasOpenFiles { leftOpen = 1 } - if hasOpenFiles, _ := q.fs.HasOpenFiles(bModOp.ModulePath); hasOpenFiles { + bModHandle := document.DirHandleFromPath(bModOp.ModulePath) + if hasOpenFiles, _ := q.ds.HasOpenDocuments(bModHandle); hasOpenFiles { rightOpen = 1 } diff --git a/internal/terraform/module/module_ops_queue_test.go b/internal/terraform/module/module_ops_queue_test.go index 946daffe..39cbd7d7 100644 --- a/internal/terraform/module/module_ops_queue_test.go +++ b/internal/terraform/module/module_ops_queue_test.go @@ -5,18 +5,19 @@ import ( "path/filepath" "testing" - "github.com/hashicorp/terraform-ls/internal/filesystem" - ilsp "github.com/hashicorp/terraform-ls/internal/lsp" - "github.com/hashicorp/terraform-ls/internal/protocol" + "github.com/hashicorp/terraform-ls/internal/document" + "github.com/hashicorp/terraform-ls/internal/state" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" - "github.com/hashicorp/terraform-ls/internal/uri" ) func TestModuleOpsQueue_modulePriority(t *testing.T) { - fs := filesystem.NewFilesystem() - fs.SetLogger(testLogger()) + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + ss.SetLogger(testLogger()) - mq := newModuleOpsQueue(fs) + mq := newModuleOpsQueue(ss.DocumentStore) dir := t.TempDir() t.Cleanup(func() { @@ -25,22 +26,23 @@ func TestModuleOpsQueue_modulePriority(t *testing.T) { ops := []ModuleOperation{ NewModuleOperation( - closedModPath(t, fs, dir, "alpha"), + closedModPath(t, dir, "alpha"), op.OpTypeGetTerraformVersion, ), NewModuleOperation( - openModAtPath(t, fs, dir, "beta"), + openModAtPath(t, ss.DocumentStore, dir, "beta"), op.OpTypeGetTerraformVersion, ), NewModuleOperation( - openModAtPath(t, fs, dir, "gamma"), + openModAtPath(t, ss.DocumentStore, dir, "gamma"), op.OpTypeGetTerraformVersion, ), NewModuleOperation( - closedModPath(t, fs, dir, "delta"), + closedModPath(t, dir, "delta"), op.OpTypeGetTerraformVersion, ), } + t.Logf("total operations: %d", len(ops)) for _, op := range ops { mq.PushOp(op) @@ -67,24 +69,31 @@ func TestModuleOpsQueue_modulePriority(t *testing.T) { } } -func closedModPath(t *testing.T, fs filesystem.Filesystem, dir, modName string) string { +func closedModPath(t *testing.T, dir, modName string) string { modPath := filepath.Join(dir, modName) - docPath := filepath.Join(modPath, "main.tf") - dh := ilsp.FileHandlerFromDocumentURI(protocol.DocumentURI(uri.FromPath(docPath))) - err := fs.CreateDocument(dh, "test", []byte{}) + err := os.Mkdir(modPath, 0o755) + if err != nil { + t.Fatal(err) + } + + path := filepath.Join(modPath, "main.tf") + + f, err := os.Create(path) if err != nil { t.Fatal(err) } + defer f.Close() return modPath } -func openModAtPath(t *testing.T, fs filesystem.Filesystem, dir, modName string) string { +func openModAtPath(t *testing.T, ds *state.DocumentStore, dir, modName string) string { modPath := filepath.Join(dir, modName) - docPath := filepath.Join(modPath, "main.tf") - dh := ilsp.FileHandlerFromDocumentURI(protocol.DocumentURI(uri.FromPath(docPath))) - err := fs.CreateAndOpenDocument(dh, "test", []byte{}) + + dh := document.HandleFromPath(filepath.Join(modPath, "main.tf")) + + err := ds.OpenDocument(dh, "test", 0, []byte{}) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/module/types.go b/internal/terraform/module/types.go index ec284e0e..b6b78844 100644 --- a/internal/terraform/module/types.go +++ b/internal/terraform/module/types.go @@ -2,9 +2,10 @@ package module import ( "context" + "io/fs" "log" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/state" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" tfmodule "github.com/hashicorp/terraform-schema/module" @@ -44,9 +45,9 @@ type Module *state.Module type ModuleFactory func(string) (Module, error) -type ModuleManagerFactory func(context.Context, filesystem.Filesystem, *state.ModuleStore, *state.ProviderSchemaStore) ModuleManager +type ModuleManagerFactory func(context.Context, ReadOnlyFS, DocumentStore, *state.ModuleStore, *state.ProviderSchemaStore) ModuleManager -type WalkerFactory func(filesystem.Filesystem, ModuleManager) *Walker +type WalkerFactory func(fs.StatFS, DocumentStore, ModuleManager) *Walker type Watcher interface { Start() error @@ -56,3 +57,14 @@ type Watcher interface { RemoveModule(string) error IsModuleWatched(string) bool } + +type ReadOnlyFS interface { + fs.FS + ReadDir(name string) ([]fs.DirEntry, error) + ReadFile(name string) ([]byte, error) + Stat(name string) (fs.FileInfo, error) +} + +type DocumentStore interface { + HasOpenDocuments(dirHandle document.DirHandle) (bool, error) +} diff --git a/internal/terraform/module/walker.go b/internal/terraform/module/walker.go index 2616a12f..7109cb63 100644 --- a/internal/terraform/module/walker.go +++ b/internal/terraform/module/walker.go @@ -4,6 +4,7 @@ import ( "container/heap" "context" "fmt" + "io/fs" "io/ioutil" "log" "os" @@ -11,7 +12,6 @@ import ( "sync" "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/terraform/datadir" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" ) @@ -35,7 +35,7 @@ var ( type pathToWatch struct{} type Walker struct { - fs filesystem.Filesystem + fs fs.StatFS modMgr ModuleManager watcher Watcher logger *log.Logger @@ -59,13 +59,13 @@ type Walker struct { // until a path is consumed const queueCap = 50 -func NewWalker(fs filesystem.Filesystem, modMgr ModuleManager) *Walker { +func NewWalker(fs fs.StatFS, ds DocumentStore, modMgr ModuleManager) *Walker { return &Walker{ fs: fs, modMgr: modMgr, logger: discardLogger, walkingMu: &sync.RWMutex{}, - queue: newWalkerQueue(fs), + queue: newWalkerQueue(ds), queueMu: &sync.Mutex{}, pushChan: make(chan struct{}, queueCap), doneCh: make(chan struct{}, 0), diff --git a/internal/terraform/module/walker_mock.go b/internal/terraform/module/walker_mock.go index bebdb453..8577ed54 100644 --- a/internal/terraform/module/walker_mock.go +++ b/internal/terraform/module/walker_mock.go @@ -1,9 +1,11 @@ package module -import "github.com/hashicorp/terraform-ls/internal/filesystem" +import ( + "io/fs" +) -func SyncWalker(fs filesystem.Filesystem, modMgr ModuleManager) *Walker { - w := NewWalker(fs, modMgr) +func SyncWalker(fs fs.StatFS, ds DocumentStore, modMgr ModuleManager) *Walker { + w := NewWalker(fs, ds, modMgr) w.sync = true return w } diff --git a/internal/terraform/module/walker_queue.go b/internal/terraform/module/walker_queue.go index 18bcb007..f622f4a8 100644 --- a/internal/terraform/module/walker_queue.go +++ b/internal/terraform/module/walker_queue.go @@ -3,21 +3,21 @@ package module import ( "container/heap" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" ) type walkerQueue struct { paths []string - fs filesystem.Filesystem + ds DocumentStore } var _ heap.Interface = &walkerQueue{} -func newWalkerQueue(fs filesystem.Filesystem) *walkerQueue { +func newWalkerQueue(ds DocumentStore) *walkerQueue { wq := &walkerQueue{ paths: make([]string, 0), - fs: fs, + ds: ds, } heap.Init(wq) return wq @@ -74,10 +74,13 @@ func (q *walkerQueue) Less(i, j int) bool { func (q *walkerQueue) moduleOperationLess(leftModPath, rightModPath string) bool { leftOpen, rightOpen := 0, 0 - if hasOpenFiles, _ := q.fs.HasOpenFiles(leftModPath); hasOpenFiles { + leftMod := document.DirHandleFromPath(leftModPath) + if hasOpenFiles, _ := q.ds.HasOpenDocuments(leftMod); hasOpenFiles { leftOpen = 1 } - if hasOpenFiles, _ := q.fs.HasOpenFiles(rightModPath); hasOpenFiles { + + rightMod := document.DirHandleFromPath(rightModPath) + if hasOpenFiles, _ := q.ds.HasOpenDocuments(rightMod); hasOpenFiles { rightOpen = 1 } diff --git a/internal/terraform/module/watcher.go b/internal/terraform/module/watcher.go index 670e9107..f4d560f7 100644 --- a/internal/terraform/module/watcher.go +++ b/internal/terraform/module/watcher.go @@ -8,7 +8,6 @@ import ( "path/filepath" "github.com/fsnotify/fsnotify" - "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/pathcmp" "github.com/hashicorp/terraform-ls/internal/terraform/datadir" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" @@ -19,7 +18,6 @@ import ( // (rather than just events that may not be changing any bytes) type watcher struct { fw *fsnotify.Watcher - fs filesystem.Filesystem modMgr ModuleManager modules []*watchedModule logger *log.Logger @@ -28,7 +26,7 @@ type watcher struct { cancelFunc context.CancelFunc } -type WatcherFactory func(filesystem.Filesystem, ModuleManager) (Watcher, error) +type WatcherFactory func(ModuleManager) (Watcher, error) type watchedModule struct { Path string @@ -36,7 +34,7 @@ type watchedModule struct { Watchable *datadir.WatchablePaths } -func NewWatcher(fs filesystem.Filesystem, modMgr ModuleManager) (Watcher, error) { +func NewWatcher(modMgr ModuleManager) (Watcher, error) { fw, err := fsnotify.NewWatcher() if err != nil { return nil, err @@ -44,7 +42,6 @@ func NewWatcher(fs filesystem.Filesystem, modMgr ModuleManager) (Watcher, error) return &watcher{ fw: fw, - fs: fs, modMgr: modMgr, logger: defaultLogger, modules: make([]*watchedModule, 0), diff --git a/internal/terraform/module/watcher_mock.go b/internal/terraform/module/watcher_mock.go index 01f0398f..1938e2c1 100644 --- a/internal/terraform/module/watcher_mock.go +++ b/internal/terraform/module/watcher_mock.go @@ -2,12 +2,10 @@ package module import ( "log" - - "github.com/hashicorp/terraform-ls/internal/filesystem" ) func MockWatcher() WatcherFactory { - return func(filesystem.Filesystem, ModuleManager) (Watcher, error) { + return func(ModuleManager) (Watcher, error) { return &mockWatcher{}, nil } } diff --git a/internal/terraform/module/watcher_test.go b/internal/terraform/module/watcher_test.go index 2a70d7ea..babe4c54 100644 --- a/internal/terraform/module/watcher_test.go +++ b/internal/terraform/module/watcher_test.go @@ -21,10 +21,15 @@ import ( ) func TestWatcher_initFromScratch(t *testing.T) { - fs := filesystem.NewFilesystem() + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + + fs := filesystem.NewFilesystem(ss.DocumentStore) modPath := filepath.Join(t.TempDir(), "module") - err := os.Mkdir(modPath, 0755) + err = os.Mkdir(modPath, 0755) if err != nil { t.Fatal(err) } @@ -66,13 +71,10 @@ func TestWatcher_initFromScratch(t *testing.T) { }, }) ctx := context.Background() - ss, err := state.NewStateStore() - if err != nil { - t.Fatal(err) - } - modMgr := mmm(ctx, fs, ss.Modules, ss.ProviderSchemas) - w, err := NewWatcher(fs, modMgr) + modMgr := mmm(ctx, fs, ss.DocumentStore, ss.Modules, ss.ProviderSchemas) + + w, err := NewWatcher(modMgr) if err != nil { t.Fatal(err) } From 03b0b98de4f7a97cdf7d8a77ddf44fe2789f4ced Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 25 Jan 2022 11:44:01 +0000 Subject: [PATCH 08/15] internal/*: Update RPC handlers + custom cmds --- internal/cmd/completion_command.go | 42 ++++++------ internal/cmd/inspect_module_command.go | 12 ++-- internal/context/context.go | 15 ---- .../handlers/cancel_request_test.go | 2 +- internal/langserver/handlers/code_action.go | 35 ++++------ .../langserver/handlers/code_action_test.go | 28 ++++---- internal/langserver/handlers/code_lens.go | 16 ++--- .../langserver/handlers/code_lens_test.go | 32 ++++----- internal/langserver/handlers/command/init.go | 9 ++- .../langserver/handlers/command/modules.go | 15 ++-- .../langserver/handlers/command/validate.go | 9 ++- internal/langserver/handlers/complete.go | 14 ++-- internal/langserver/handlers/complete_test.go | 50 +++++++------- internal/langserver/handlers/did_change.go | 25 ++++--- .../langserver/handlers/did_change_test.go | 33 ++++----- .../did_change_workspace_folders_test.go | 6 +- internal/langserver/handlers/did_close.go | 17 +---- internal/langserver/handlers/did_open.go | 17 ++--- internal/langserver/handlers/did_open_test.go | 30 ++++---- internal/langserver/handlers/did_save.go | 5 +- internal/langserver/handlers/document_link.go | 13 ++-- .../langserver/handlers/document_link_test.go | 10 +-- .../handlers/execute_command_init_test.go | 20 +++--- .../handlers/execute_command_modules_test.go | 42 ++++++------ .../handlers/execute_command_test.go | 8 +-- .../handlers/execute_command_validate_test.go | 6 +- internal/langserver/handlers/formatting.go | 29 +++----- .../langserver/handlers/formatting_test.go | 26 +++---- .../langserver/handlers/go_to_ref_target.go | 17 ++--- .../handlers/go_to_ref_target_test.go | 68 +++++++++---------- internal/langserver/handlers/handlers_test.go | 18 ++--- internal/langserver/handlers/hover.go | 15 ++-- internal/langserver/handlers/hover_test.go | 24 +++---- internal/langserver/handlers/initialize.go | 18 ++--- .../langserver/handlers/initialize_test.go | 22 +++--- internal/langserver/handlers/references.go | 17 ++--- .../langserver/handlers/references_test.go | 24 +++---- .../langserver/handlers/semantic_tokens.go | 14 ++-- .../handlers/semantic_tokens_test.go | 32 ++++----- internal/langserver/handlers/service.go | 48 +++++-------- .../langserver/handlers/session_mock_test.go | 8 --- internal/langserver/handlers/shutdown_test.go | 4 +- internal/langserver/handlers/symbols.go | 11 +-- internal/langserver/handlers/symbols_test.go | 10 +-- .../handlers/workspace_symbol_test.go | 16 ++--- 45 files changed, 411 insertions(+), 521 deletions(-) diff --git a/internal/cmd/completion_command.go b/internal/cmd/completion_command.go index 2a6b167c..7beb2e28 100644 --- a/internal/cmd/completion_command.go +++ b/internal/cmd/completion_command.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/terraform-ls/internal/decoder" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/logging" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" @@ -63,8 +64,6 @@ func (c *CompletionCommand) Run(args []string) int { return 1 } - fh := ilsp.FileHandlerFromPath(path) - parts := strings.Split(c.atPos, ":") if len(parts) != 2 { c.Ui.Error(fmt.Sprintf("Error parsing at-pos argument: %q (expected line:col format)", c.atPos)) @@ -84,56 +83,57 @@ func (c *CompletionCommand) Run(args []string) int { logger := logging.NewLogger(os.Stderr) - fs := filesystem.NewFilesystem() - fs.SetLogger(logger) - fs.CreateAndOpenDocument(fh, "terraform", content) - - doc, err := fs.GetDocument(fh) + ss, err := state.NewStateStore() if err != nil { c.Ui.Error(err.Error()) return 1 } - fPos, err := ilsp.FilePositionFromDocumentPosition(lsp.TextDocumentPositionParams{ - TextDocument: lsp.TextDocumentIdentifier{ - URI: fh.DocumentURI(), - }, - Position: lspPos, - }, doc) + dh := document.HandleFromPath(path) + err = ss.DocumentStore.OpenDocument(dh, "terraform", 0, content) if err != nil { c.Ui.Error(err.Error()) return 1 } - ctx := context.Background() - ss, err := state.NewStateStore() + fs := filesystem.NewFilesystem(ss.DocumentStore) + fs.SetLogger(logger) + + doc, err := ss.DocumentStore.GetDocument(dh) if err != nil { c.Ui.Error(err.Error()) return 1 } - modMgr := module.NewSyncModuleManager(ctx, fs, ss.Modules, ss.ProviderSchemas) - _, err = modMgr.AddModule(fh.Dir()) + pos, err := ilsp.HCLPositionFromLspPosition(lspPos, doc) if err != nil { c.Ui.Error(err.Error()) return 1 } - pos := fPos.Position() + ctx := context.Background() + + modMgr := module.NewSyncModuleManager(ctx, fs, ss.DocumentStore, ss.Modules, ss.ProviderSchemas) + + _, err = modMgr.AddModule(dh.Dir.Path()) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } d, err := decoder.NewDecoder(ctx, &decoder.PathReader{ ModuleReader: ss.Modules, SchemaReader: ss.ProviderSchemas, }).Path(lang.Path{ - Path: doc.Dir(), - LanguageID: doc.LanguageID(), + Path: doc.Dir.Path(), + LanguageID: doc.LanguageID, }) if err != nil { c.Ui.Error(err.Error()) return 1 } - candidates, err := d.CandidatesAtPos(doc.Filename(), pos) + candidates, err := d.CandidatesAtPos(doc.Filename, pos) if err != nil { c.Ui.Error(fmt.Sprintf("failed to find candidates: %s", err.Error())) return 1 diff --git a/internal/cmd/inspect_module_command.go b/internal/cmd/inspect_module_command.go index 4c1b3b13..94988e4e 100644 --- a/internal/cmd/inspect_module_command.go +++ b/internal/cmd/inspect_module_command.go @@ -84,17 +84,19 @@ func (c *InspectModuleCommand) inspect(rootPath string) error { return fmt.Errorf("expected %s to be a directory", rootPath) } - fs := filesystem.NewFilesystem() - - ctx := context.Background() ss, err := state.NewStateStore() if err != nil { return err } - modMgr := module.NewSyncModuleManager(ctx, fs, ss.Modules, ss.ProviderSchemas) + + fs := filesystem.NewFilesystem(ss.DocumentStore) + + ctx := context.Background() + + modMgr := module.NewSyncModuleManager(ctx, fs, ss.DocumentStore, ss.Modules, ss.ProviderSchemas) modMgr.SetLogger(c.logger) - walker := module.SyncWalker(fs, modMgr) + walker := module.SyncWalker(fs, ss.DocumentStore, modMgr) walker.SetLogger(c.logger) ctx, cancel := ictx.WithSignalCancel(context.Background(), diff --git a/internal/context/context.go b/internal/context/context.go index 1dabe369..4b7ebfee 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/langserver/diagnostics" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/settings" @@ -20,7 +19,6 @@ func (k *contextKey) String() string { } var ( - ctxDs = &contextKey{"document storage"} ctxTfExecPath = &contextKey{"terraform executable path"} ctxTfExecLogPath = &contextKey{"terraform executor log path"} ctxTfExecTimeout = &contextKey{"terraform execution timeout"} @@ -40,19 +38,6 @@ func missingContextErr(ctxKey *contextKey) *MissingContextErr { return &MissingContextErr{ctxKey} } -func WithDocumentStorage(ctx context.Context, fs filesystem.DocumentStorage) context.Context { - return context.WithValue(ctx, ctxDs, fs) -} - -func DocumentStorage(ctx context.Context) (filesystem.DocumentStorage, error) { - fs, ok := ctx.Value(ctxDs).(filesystem.DocumentStorage) - if !ok { - return nil, missingContextErr(ctxDs) - } - - return fs, nil -} - func WithTerraformExecLogPath(ctx context.Context, path string) context.Context { return context.WithValue(ctx, ctxTfExecLogPath, path) } diff --git a/internal/langserver/handlers/cancel_request_test.go b/internal/langserver/handlers/cancel_request_test.go index 618215f0..0b0987b2 100644 --- a/internal/langserver/handlers/cancel_request_test.go +++ b/internal/langserver/handlers/cancel_request_test.go @@ -53,7 +53,7 @@ func TestLangServer_cancelRequest(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", diff --git a/internal/langserver/handlers/code_action.go b/internal/langserver/handlers/code_action.go index c517459a..b83c570c 100644 --- a/internal/langserver/handlers/code_action.go +++ b/internal/langserver/handlers/code_action.go @@ -4,35 +4,34 @@ import ( "context" "fmt" - lsctx "github.com/hashicorp/terraform-ls/internal/context" "github.com/hashicorp/terraform-ls/internal/langserver/errors" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/terraform/module" ) -func (h *logHandler) TextDocumentCodeAction(ctx context.Context, params lsp.CodeActionParams) []lsp.CodeAction { - ca, err := h.textDocumentCodeAction(ctx, params) +func (svc *service) TextDocumentCodeAction(ctx context.Context, params lsp.CodeActionParams) []lsp.CodeAction { + ca, err := svc.textDocumentCodeAction(ctx, params) if err != nil { - h.logger.Printf("code action failed: %s", err) + svc.logger.Printf("code action failed: %s", err) } return ca } -func (h *logHandler) textDocumentCodeAction(ctx context.Context, params lsp.CodeActionParams) ([]lsp.CodeAction, error) { +func (svc *service) textDocumentCodeAction(ctx context.Context, params lsp.CodeActionParams) ([]lsp.CodeAction, error) { var ca []lsp.CodeAction // For action definitions, refer to https://code.visualstudio.com/api/references/vscode-api#CodeActionKind // We only support format type code actions at the moment, and do not want to format without the client asking for // them, so exit early here if nothing is requested. if len(params.Context.Only) == 0 { - h.logger.Printf("No code action requested, exiting") + svc.logger.Printf("No code action requested, exiting") return ca, nil } for _, o := range params.Context.Only { - h.logger.Printf("Code actions requested: %q", o) + svc.logger.Printf("Code actions requested: %q", o) } wantedCodeActions := ilsp.SupportedCodeActions.Only(params.Context.Only) @@ -41,19 +40,11 @@ func (h *logHandler) textDocumentCodeAction(ctx context.Context, params lsp.Code params.TextDocument.URI, params.Context.Only) } - h.logger.Printf("Code actions supported: %v", wantedCodeActions) + svc.logger.Printf("Code actions supported: %v", wantedCodeActions) - fh := ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return ca, err - } - file, err := fs.GetDocument(fh) - if err != nil { - return ca, err - } - original, err := file.Text() + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return ca, err } @@ -61,14 +52,14 @@ func (h *logHandler) textDocumentCodeAction(ctx context.Context, params lsp.Code for action := range wantedCodeActions { switch action { case ilsp.SourceFormatAllTerraform: - tfExec, err := module.TerraformExecutorForModule(ctx, fh.Dir()) + tfExec, err := module.TerraformExecutorForModule(ctx, dh.Dir.Path()) if err != nil { return ca, errors.EnrichTfExecError(err) } - h.logger.Printf("Formatting document via %q", tfExec.GetExecPath()) + svc.logger.Printf("Formatting document via %q", tfExec.GetExecPath()) - edits, err := formatDocument(ctx, tfExec, original, file) + edits, err := formatDocument(ctx, tfExec, doc.Text, dh) if err != nil { return ca, err } @@ -78,7 +69,7 @@ func (h *logHandler) textDocumentCodeAction(ctx context.Context, params lsp.Code Kind: action, Edit: lsp.WorkspaceEdit{ Changes: map[string][]lsp.TextEdit{ - string(fh.URI()): edits, + string(dh.FullURI()): edits, }, }, }) diff --git a/internal/langserver/handlers/code_action_test.go b/internal/langserver/handlers/code_action_test.go index f81d54c0..3b460f32 100644 --- a/internal/langserver/handlers/code_action_test.go +++ b/internal/langserver/handlers/code_action_test.go @@ -25,7 +25,7 @@ func TestLangServer_codeActionWithoutInitialization(t *testing.T) { "text": "provider \"github\" {}", "uri": "%s/main.tf" } - }`, TempDir(t).URI())}, session.SessionNotInitialized.Err()) + }`, TempDir(t).URI)}, session.SessionNotInitialized.Err()) } func TestLangServer_codeAction_basic(t *testing.T) { @@ -34,7 +34,7 @@ func TestLangServer_codeAction_basic(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -78,7 +78,7 @@ func TestLangServer_codeAction_basic(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -92,7 +92,7 @@ func TestLangServer_codeAction_basic(t *testing.T) { "text": "provider \"test\" {\n\n }\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/codeAction", ReqParams: fmt.Sprintf(`{ @@ -102,7 +102,7 @@ func TestLangServer_codeAction_basic(t *testing.T) { "end": { "line": 1, "character": 0 } }, "context": { "diagnostics": [], "only": ["source.formatAll.terraform"] } - }`, tmpDir.URI())}, fmt.Sprintf(`{ + }`, tmpDir.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -143,7 +143,7 @@ func TestLangServer_codeAction_basic(t *testing.T) { } } ] - }`, tmpDir.URI())) + }`, tmpDir.URI)) } func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { @@ -165,7 +165,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { "end": { "line": 1, "character": 0 } }, "context": { "diagnostics": [], "only": [""] } - }`, tmpDir.URI())}, + }`, tmpDir.URI)}, want: `{ "jsonrpc": "2.0", "id": 3, @@ -183,7 +183,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { "end": { "line": 1, "character": 0 } }, "context": { "diagnostics": [], "only": ["source.formatAll.terraform"] } - }`, tmpDir.URI())}, + }`, tmpDir.URI)}, want: fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, @@ -219,7 +219,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { } } ] - }`, tmpDir.URI()), + }`, tmpDir.URI), }, { name: "source.fixAll and source.formatAll.terraform code action requested", @@ -232,7 +232,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { "end": { "line": 1, "character": 0 } }, "context": { "diagnostics": [], "only": ["source.fixAll", "source.formatAll.terraform"] } - }`, tmpDir.URI()), + }`, tmpDir.URI), }, want: fmt.Sprintf(`{ "jsonrpc": "2.0", @@ -263,7 +263,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { } } ] - }`, tmpDir.URI()), + }`, tmpDir.URI), }, } @@ -272,7 +272,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -316,7 +316,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 123456 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -330,7 +330,7 @@ func TestLangServer_codeAction_no_code_action_requested(t *testing.T) { "text": "provider \"test\" {\n\n }\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, tt.request, tt.want) }) diff --git a/internal/langserver/handlers/code_lens.go b/internal/langserver/handlers/code_lens.go index ed1f50f4..03bc1deb 100644 --- a/internal/langserver/handlers/code_lens.go +++ b/internal/langserver/handlers/code_lens.go @@ -4,7 +4,6 @@ import ( "context" "github.com/hashicorp/hcl-lang/lang" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) @@ -12,23 +11,18 @@ import ( func (svc *service) TextDocumentCodeLens(ctx context.Context, params lsp.CodeLensParams) ([]lsp.CodeLens, error) { list := make([]lsp.CodeLens, 0) - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return list, err - } - - fh := ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI) - doc, err := fs.GetDocument(fh) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return list, err } path := lang.Path{ - Path: doc.Dir(), - LanguageID: doc.LanguageID(), + Path: doc.Dir.Path(), + LanguageID: doc.LanguageID, } - lenses, err := svc.decoder.CodeLensesForFile(ctx, path, doc.Filename()) + lenses, err := svc.decoder.CodeLensesForFile(ctx, path, doc.Filename) if err != nil { return nil, err } diff --git a/internal/langserver/handlers/code_lens_test.go b/internal/langserver/handlers/code_lens_test.go index 1c252c43..61e26178 100644 --- a/internal/langserver/handlers/code_lens_test.go +++ b/internal/langserver/handlers/code_lens_test.go @@ -9,9 +9,9 @@ import ( "github.com/hashicorp/go-version" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" "github.com/hashicorp/terraform-ls/internal/langserver/session" - "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) @@ -27,12 +27,12 @@ func TestCodeLens_withoutInitialization(t *testing.T) { "textDocument": { "uri": "%s/main.tf" } - }`, TempDir(t).URI())}, session.SessionNotInitialized.Err()) + }`, TempDir(t).URI)}, session.SessionNotInitialized.Err()) } func TestCodeLens_withoutOptIn(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -50,7 +50,7 @@ func TestCodeLens_withoutOptIn(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -64,14 +64,14 @@ func TestCodeLens_withoutOptIn(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/codeLens", ReqParams: fmt.Sprintf(`{ "textDocument": { "uri": "%s/main.tf" } - }`, TempDir(t).URI()), + }`, TempDir(t).URI), }, `{ "jsonrpc": "2.0", "id": 3, @@ -81,7 +81,7 @@ func TestCodeLens_withoutOptIn(t *testing.T) { func TestCodeLens_referenceCount(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -92,7 +92,7 @@ func TestCodeLens_referenceCount(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -139,7 +139,7 @@ func TestCodeLens_referenceCount(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -158,14 +158,14 @@ func TestCodeLens_referenceCount(t *testing.T) { output "test" { value = var.test } -`, tmpDir.URI())}) +`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/codeLens", ReqParams: fmt.Sprintf(`{ "textDocument": { "uri": "%s/main.tf" } - }`, TempDir(t).URI()), + }`, TempDir(t).URI), }, `{ "jsonrpc": "2.0", "id": 3, @@ -207,8 +207,8 @@ func TestCodeLens_referenceCount_crossModule(t *testing.T) { submodPath := filepath.Join(rootModPath, "application") - rootModUri := lsp.FileHandlerFromDirPath(rootModPath) - submodUri := lsp.FileHandlerFromDirPath(submodPath) + rootModUri := document.DirHandleFromPath(rootModPath) + submodUri := document.DirHandleFromPath(submodPath) var testSchema tfjson.ProviderSchemas err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -236,7 +236,7 @@ func TestCodeLens_referenceCount_crossModule(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, rootModUri.URI())}) + }`, rootModUri.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -261,7 +261,7 @@ variable "app_prefix" { variable "instances" { type = number } -`, submodUri.URI())}) +`, submodUri.URI)}) // TODO remove once we support synchronous dependent tasks // See https://github.com/hashicorp/terraform-ls/issues/719 time.Sleep(2 * time.Second) @@ -271,7 +271,7 @@ variable "instances" { "textDocument": { "uri": "%s/main.tf" } - }`, submodUri.URI()), + }`, submodUri.URI), }, `{ "jsonrpc": "2.0", "id": 3, diff --git a/internal/langserver/handlers/command/init.go b/internal/langserver/handlers/command/init.go index 3ac3b8e9..c7b78955 100644 --- a/internal/langserver/handlers/command/init.go +++ b/internal/langserver/handlers/command/init.go @@ -6,11 +6,10 @@ import ( "github.com/creachadair/jrpc2/code" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver/cmd" "github.com/hashicorp/terraform-ls/internal/langserver/errors" "github.com/hashicorp/terraform-ls/internal/langserver/progress" - ilsp "github.com/hashicorp/terraform-ls/internal/lsp" - lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/terraform/module" "github.com/hashicorp/terraform-ls/internal/uri" ) @@ -25,17 +24,17 @@ func TerraformInitHandler(ctx context.Context, args cmd.CommandArgs) (interface{ return nil, fmt.Errorf("URI %q is not valid", dirUri) } - dh := ilsp.FileHandlerFromDirURI(lsp.DocumentURI(dirUri)) + dirHandle := document.DirHandleFromURI(dirUri) modMgr, err := lsctx.ModuleManager(ctx) if err != nil { return nil, err } - mod, err := modMgr.ModuleByPath(dh.Dir()) + mod, err := modMgr.ModuleByPath(dirHandle.Path()) if err != nil { if module.IsModuleNotFound(err) { - mod, err = modMgr.AddModule(dh.Dir()) + mod, err = modMgr.AddModule(dirHandle.Path()) if err != nil { return nil, err } diff --git a/internal/langserver/handlers/command/modules.go b/internal/langserver/handlers/command/modules.go index 09fadc01..8de43a59 100644 --- a/internal/langserver/handlers/command/modules.go +++ b/internal/langserver/handlers/command/modules.go @@ -8,9 +8,8 @@ import ( "github.com/creachadair/jrpc2/code" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver/cmd" - ilsp "github.com/hashicorp/terraform-ls/internal/lsp" - lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/terraform/module" "github.com/hashicorp/terraform-ls/internal/uri" ) @@ -34,12 +33,12 @@ func ModulesHandler(ctx context.Context, args cmd.CommandArgs) (interface{}, err return nil, err } - fileUri, ok := args.GetString("uri") - if !ok || fileUri == "" { + docUri, ok := args.GetString("uri") + if !ok || docUri == "" { return nil, fmt.Errorf("%w: expected module uri argument to be set", code.InvalidParams.Err()) } - fh := ilsp.FileHandlerFromDocumentURI(lsp.DocumentURI(fileUri)) + dh := document.HandleFromURI(docUri) modMgr, err := lsctx.ModuleManager(ctx) if err != nil { @@ -49,14 +48,14 @@ func ModulesHandler(ctx context.Context, args cmd.CommandArgs) (interface{}, err doneLoading := !walker.IsWalking() var sources []module.SchemaSource - sources, err = modMgr.SchemaSourcesForModule(fh.Dir()) + sources, err = modMgr.SchemaSourcesForModule(dh.Dir.Path()) if err != nil { if module.IsModuleNotFound(err) { - _, err := modMgr.AddModule(fh.Dir()) + _, err := modMgr.AddModule(dh.Dir.Path()) if err != nil { return nil, err } - sources, err = modMgr.SchemaSourcesForModule(fh.Dir()) + sources, err = modMgr.SchemaSourcesForModule(dh.Dir.Path()) if err != nil { return nil, err } diff --git a/internal/langserver/handlers/command/validate.go b/internal/langserver/handlers/command/validate.go index 02469b84..2a9d857a 100644 --- a/internal/langserver/handlers/command/validate.go +++ b/internal/langserver/handlers/command/validate.go @@ -6,12 +6,11 @@ import ( "github.com/creachadair/jrpc2/code" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver/cmd" "github.com/hashicorp/terraform-ls/internal/langserver/diagnostics" "github.com/hashicorp/terraform-ls/internal/langserver/errors" "github.com/hashicorp/terraform-ls/internal/langserver/progress" - ilsp "github.com/hashicorp/terraform-ls/internal/lsp" - lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/terraform/module" "github.com/hashicorp/terraform-ls/internal/uri" ) @@ -26,17 +25,17 @@ func TerraformValidateHandler(ctx context.Context, args cmd.CommandArgs) (interf return nil, fmt.Errorf("URI %q is not valid", dirUri) } - dh := ilsp.FileHandlerFromDirURI(lsp.DocumentURI(dirUri)) + dirHandle := document.DirHandleFromURI(dirUri) modMgr, err := lsctx.ModuleManager(ctx) if err != nil { return nil, err } - mod, err := modMgr.ModuleByPath(dh.Dir()) + mod, err := modMgr.ModuleByPath(dirHandle.Path()) if err != nil { if module.IsModuleNotFound(err) { - mod, err = modMgr.AddModule(dh.Dir()) + mod, err = modMgr.AddModule(dirHandle.Path()) if err != nil { return nil, err } diff --git a/internal/langserver/handlers/complete.go b/internal/langserver/handlers/complete.go index e86010d8..44433846 100644 --- a/internal/langserver/handlers/complete.go +++ b/internal/langserver/handlers/complete.go @@ -11,17 +11,13 @@ import ( func (svc *service) TextDocumentComplete(ctx context.Context, params lsp.CompletionParams) (lsp.CompletionList, error) { var list lsp.CompletionList - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return list, err - } - cc, err := ilsp.ClientCapabilities(ctx) if err != nil { return list, err } - doc, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return list, err } @@ -38,13 +34,13 @@ func (svc *service) TextDocumentComplete(ctx context.Context, params lsp.Complet d.PrefillRequiredFields = expFeatures.PrefillRequiredFields - fPos, err := ilsp.FilePositionFromDocumentPosition(params.TextDocumentPositionParams, doc) + pos, err := ilsp.HCLPositionFromLspPosition(params.TextDocumentPositionParams.Position, doc) if err != nil { return list, err } - svc.logger.Printf("Looking for candidates at %q -> %#v", doc.Filename(), fPos.Position()) - candidates, err := d.CandidatesAtPos(doc.Filename(), fPos.Position()) + svc.logger.Printf("Looking for candidates at %q -> %#v", doc.Filename, pos) + candidates, err := d.CandidatesAtPos(doc.Filename, pos) svc.logger.Printf("received candidates: %#v", candidates) return ilsp.ToCompletionList(candidates, cc.TextDocument), err } diff --git a/internal/langserver/handlers/complete_test.go b/internal/langserver/handlers/complete_test.go index 2324e13b..21bfcdd8 100644 --- a/internal/langserver/handlers/complete_test.go +++ b/internal/langserver/handlers/complete_test.go @@ -37,12 +37,12 @@ func TestModuleCompletion_withoutInitialization(t *testing.T) { "character": 0, "line": 1 } - }`, TempDir(t).URI())}, session.SessionNotInitialized.Err()) + }`, TempDir(t).URI)}, session.SessionNotInitialized.Err()) } func TestModuleCompletion_withValidData(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -53,7 +53,7 @@ func TestModuleCompletion_withValidData(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -96,7 +96,7 @@ func TestModuleCompletion_withValidData(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -110,7 +110,7 @@ func TestModuleCompletion_withValidData(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/completion", @@ -122,7 +122,7 @@ func TestModuleCompletion_withValidData(t *testing.T) { "character": 0, "line": 1 } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": { @@ -240,7 +240,7 @@ func TestModuleCompletion_withValidData(t *testing.T) { func TestModuleCompletion_withValidDataAndSnippets(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -251,7 +251,7 @@ func TestModuleCompletion_withValidDataAndSnippets(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -302,7 +302,7 @@ func TestModuleCompletion_withValidDataAndSnippets(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -316,7 +316,7 @@ func TestModuleCompletion_withValidDataAndSnippets(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/completion", @@ -328,7 +328,7 @@ func TestModuleCompletion_withValidDataAndSnippets(t *testing.T) { "character": 0, "line": 1 } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": { @@ -537,7 +537,7 @@ var testModuleSchemaOutput = `{ func TestVarsCompletion_withValidData(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -548,7 +548,7 @@ func TestVarsCompletion_withValidData(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -591,7 +591,7 @@ func TestVarsCompletion_withValidData(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -605,7 +605,7 @@ func TestVarsCompletion_withValidData(t *testing.T) { "text": "variable \"test\" {\n type=string\n}\n", "uri": "%s/variables.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Call(t, &langserver.CallRequest{ Method: "textDocument/didOpen", ReqParams: fmt.Sprintf(`{ @@ -614,7 +614,7 @@ func TestVarsCompletion_withValidData(t *testing.T) { "languageId": "terraform-vars", "uri": "%s/terraform.tfvars" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/completion", @@ -626,7 +626,7 @@ func TestVarsCompletion_withValidData(t *testing.T) { "character": 0, "line": 0 } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 4, "result": { @@ -651,7 +651,7 @@ func TestVarsCompletion_withValidData(t *testing.T) { func TestCompletion_moduleWithValidData(t *testing.T) { tmpDir := TempDir(t) - writeContentToFile(t, filepath.Join(tmpDir.Dir(), "submodule", "main.tf"), `variable "testvar" { + writeContentToFile(t, filepath.Join(tmpDir.Path(), "submodule", "main.tf"), `variable "testvar" { type = string } @@ -668,7 +668,7 @@ output "test" { } ` - writeContentToFile(t, filepath.Join(tmpDir.Dir(), "main.tf"), mainCfg) + writeContentToFile(t, filepath.Join(tmpDir.Path(), "main.tf"), mainCfg) mainCfg = `module "refname" { source = "./submodule" @@ -679,7 +679,7 @@ output "test" { } ` - tfExec := tfExecutor(t, tmpDir.Dir(), "1.0.2") + tfExec := tfExecutor(t, tmpDir.Path(), "1.0.2") err := tfExec.Get(context.Background()) if err != nil { t.Fatal(err) @@ -694,7 +694,7 @@ output "test" { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -737,7 +737,7 @@ output "test" { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -751,7 +751,7 @@ output "test" { "text": %q, "uri": "%s/main.tf" } - }`, mainCfg, tmpDir.URI())}) + }`, mainCfg, tmpDir.URI)}) // TODO remove once we support synchronous dependent tasks // See https://github.com/hashicorp/terraform-ls/issues/719 @@ -767,7 +767,7 @@ output "test" { "character": 0, "line": 2 } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": { @@ -849,7 +849,7 @@ output "test" { "character": 25, "line": 6 } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 4, "result": { diff --git a/internal/langserver/handlers/did_change.go b/internal/langserver/handlers/did_change.go index dc1f770d..3e9b07b7 100644 --- a/internal/langserver/handlers/did_change.go +++ b/internal/langserver/handlers/did_change.go @@ -5,12 +5,13 @@ import ( "fmt" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/document" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" ) -func TextDocumentDidChange(ctx context.Context, params lsp.DidChangeTextDocumentParams) error { +func (svc *service) TextDocumentDidChange(ctx context.Context, params lsp.DidChangeTextDocumentParams) error { p := lsp.DidChangeTextDocumentParams{ TextDocument: lsp.VersionedTextDocumentIdentifier{ TextDocumentIdentifier: lsp.TextDocumentIdentifier{ @@ -21,30 +22,28 @@ func TextDocumentDidChange(ctx context.Context, params lsp.DidChangeTextDocument ContentChanges: params.ContentChanges, } - fs, err := lsctx.DocumentStorage(ctx) + dh := ilsp.HandleFromDocumentURI(p.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return err } - fh := ilsp.VersionedFileHandler(p.TextDocument) - f, err := fs.GetDocument(fh) - if err != nil { - return err - } + newVersion := int(p.TextDocument.Version) // Versions don't have to be consecutive, but they must be increasing - if int(p.TextDocument.Version) <= f.Version() { - fs.CloseAndRemoveDocument(fh) + if newVersion <= doc.Version { + svc.stateStore.DocumentStore.CloseDocument(dh) return fmt.Errorf("Old version (%d) received, current version is %d. "+ "Unable to update %s. This is likely a bug, please report it.", - int(p.TextDocument.Version), f.Version(), p.TextDocument.URI) + newVersion, doc.Version, p.TextDocument.URI) } - changes, err := ilsp.DocumentChanges(params.ContentChanges, f) + changes := ilsp.DocumentChanges(params.ContentChanges) + newText, err := document.ApplyChanges(doc.Text, changes) if err != nil { return err } - err = fs.ChangeDocument(fh, changes) + err = svc.stateStore.DocumentStore.UpdateDocument(dh, newText, newVersion) if err != nil { return err } @@ -54,7 +53,7 @@ func TextDocumentDidChange(ctx context.Context, params lsp.DidChangeTextDocument return err } - mod, err := modMgr.ModuleByPath(fh.Dir()) + mod, err := modMgr.ModuleByPath(dh.Dir.Path()) if err != nil { return err } diff --git a/internal/langserver/handlers/did_change_test.go b/internal/langserver/handlers/did_change_test.go index d1316095..b2633b8c 100644 --- a/internal/langserver/handlers/did_change_test.go +++ b/internal/langserver/handlers/did_change_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" - "github.com/hashicorp/terraform-ls/internal/lsp" + "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) @@ -16,15 +16,18 @@ import ( func TestLangServer_didChange_sequenceOfPartialChanges(t *testing.T) { tmpDir := TempDir(t) - fs := filesystem.NewFilesystem() + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, - Filesystem: fs, + StateStore: ss, })) stop := ls.Start(t) defer stop() @@ -35,7 +38,7 @@ func TestLangServer_didChange_sequenceOfPartialChanges(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -64,7 +67,7 @@ module "app" { "uri": "%s/main.tf", "text": %q } -}`, TempDir(t).URI(), originalText)}) +}`, TempDir(t).URI, originalText)}) ls.Call(t, &langserver.CallRequest{ Method: "textDocument/didChange", ReqParams: fmt.Sprintf(`{ @@ -102,7 +105,7 @@ module "app" { } } ] -}`, TempDir(t).URI())}) +}`, TempDir(t).URI)}) ls.Call(t, &langserver.CallRequest{ Method: "textDocument/didChange", ReqParams: fmt.Sprintf(`{ @@ -126,17 +129,15 @@ module "app" { } } ] -}`, TempDir(t).URI())}) +}`, TempDir(t).URI)}) - path := filepath.Join(TempDir(t).Dir(), "main.tf") - doc, err := fs.GetDocument(lsp.FileHandlerFromPath(path)) - if err != nil { - t.Fatal(err) - } - text, err := doc.Text() + path := filepath.Join(TempDir(t).Path(), "main.tf") + dh := document.HandleFromPath(path) + doc, err := ss.DocumentStore.GetDocument(dh) if err != nil { t.Fatal(err) } + expectedText := `variable "service_host" { default = "blah" } @@ -153,7 +154,7 @@ module "app" { } ` - if diff := cmp.Diff(expectedText, string(text)); diff != "" { + if diff := cmp.Diff(expectedText, string(doc.Text)); diff != "" { t.Fatalf("unexpected text: %s", diff) } } diff --git a/internal/langserver/handlers/did_change_workspace_folders_test.go b/internal/langserver/handlers/did_change_workspace_folders_test.go index 2efec9f1..470be69e 100644 --- a/internal/langserver/handlers/did_change_workspace_folders_test.go +++ b/internal/langserver/handlers/did_change_workspace_folders_test.go @@ -14,7 +14,7 @@ func TestDidChangeWorkspaceFolders(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - rootDir.Dir(): validTfMockCalls(), + rootDir.Path(): validTfMockCalls(), }, }, })) @@ -33,7 +33,7 @@ func TestDidChangeWorkspaceFolders(t *testing.T) { "name": "first" } ] - }`, rootDir.URI(), rootDir.URI())}) + }`, rootDir.URI, rootDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -49,5 +49,5 @@ func TestDidChangeWorkspaceFolders(t *testing.T) { {"uri": %q, "name": "first"} ] } - }`, rootDir.URI(), rootDir.URI())}) + }`, rootDir.URI, rootDir.URI)}) } diff --git a/internal/langserver/handlers/did_close.go b/internal/langserver/handlers/did_close.go index 8b4bb52d..6a0d55dc 100644 --- a/internal/langserver/handlers/did_close.go +++ b/internal/langserver/handlers/did_close.go @@ -3,22 +3,11 @@ package handlers import ( "context" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) -func TextDocumentDidClose(ctx context.Context, params lsp.DidCloseTextDocumentParams) error { - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return err - } - - fh := ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI) - err = fs.CloseAndRemoveDocument(fh) - if err != nil { - return err - } - - return nil +func (svc *service) TextDocumentDidClose(ctx context.Context, params lsp.DidCloseTextDocumentParams) error { + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + return svc.stateStore.DocumentStore.CloseDocument(dh) } diff --git a/internal/langserver/handlers/did_open.go b/internal/langserver/handlers/did_open.go index b8e6f3e4..d04be177 100644 --- a/internal/langserver/handlers/did_open.go +++ b/internal/langserver/handlers/did_open.go @@ -10,14 +10,11 @@ import ( op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" ) -func (lh *logHandler) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentParams) error { - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return err - } +func (svc *service) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentParams) error { + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) - f := ilsp.FileFromDocumentItem(params.TextDocument) - err = fs.CreateAndOpenDocument(f, f.LanguageID(), f.Text()) + err := svc.stateStore.DocumentStore.OpenDocument(dh, params.TextDocument.LanguageID, + int(params.TextDocument.Version), []byte(params.TextDocument.Text)) if err != nil { return err } @@ -29,10 +26,10 @@ func (lh *logHandler) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpe var mod module.Module - mod, err = modMgr.ModuleByPath(f.Dir()) + mod, err = modMgr.ModuleByPath(dh.Dir.Path()) if err != nil { if module.IsModuleNotFound(err) { - mod, err = modMgr.AddModule(f.Dir()) + mod, err = modMgr.AddModule(dh.Dir.Path()) if err != nil { return err } @@ -41,7 +38,7 @@ func (lh *logHandler) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpe } } - lh.logger.Printf("opened module: %s", mod.Path) + svc.logger.Printf("opened module: %s", mod.Path) // We reparse because the file being opened may not match // (originally parsed) content on the disk diff --git a/internal/langserver/handlers/did_open_test.go b/internal/langserver/handlers/did_open_test.go index eaee9ae4..87ede225 100644 --- a/internal/langserver/handlers/did_open_test.go +++ b/internal/langserver/handlers/did_open_test.go @@ -6,10 +6,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" "github.com/hashicorp/terraform-ls/internal/langserver/session" - "github.com/hashicorp/terraform-ls/internal/lsp" + "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) @@ -28,20 +28,24 @@ func TestLangServer_didOpenWithoutInitialization(t *testing.T) { "text": "provider \"github\" {}", "uri": "%s/main.tf" } - }`, TempDir(t).URI())}, session.SessionNotInitialized.Err()) + }`, TempDir(t).URI)}, session.SessionNotInitialized.Err()) } func TestLangServer_didOpenLanguageIdStored(t *testing.T) { tmpDir := TempDir(t) - fs := filesystem.NewFilesystem() + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, - Filesystem: fs, + StateStore: ss, })) stop := ls.Start(t) defer stop() @@ -52,7 +56,7 @@ func TestLangServer_didOpenLanguageIdStored(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -71,21 +75,21 @@ func TestLangServer_didOpenLanguageIdStored(t *testing.T) { "uri": "%s/main.tf", "text": %q } -}`, TempDir(t).URI(), originalText)}) - path := filepath.Join(TempDir(t).Dir(), "main.tf") - doc, err := fs.GetDocument(lsp.FileHandlerFromPath(path)) +}`, TempDir(t).URI, originalText)}) + path := filepath.Join(TempDir(t).Path(), "main.tf") + dh := document.HandleFromPath(path) + doc, err := ss.DocumentStore.GetDocument(dh) if err != nil { t.Fatal(err) } - languageID := doc.LanguageID() - if diff := cmp.Diff(languageID, string("terraform")); diff != "" { + if diff := cmp.Diff(doc.LanguageID, string("terraform")); diff != "" { t.Fatalf("unexpected languageID: %s", diff) } fullPath := doc.FullPath() if diff := cmp.Diff(fullPath, string(path)); diff != "" { t.Fatalf("unexpected fullPath: %s", diff) } - version := doc.Version() + version := doc.Version if diff := cmp.Diff(version, int(0)); diff != "" { t.Fatalf("unexpected version: %s", diff) } diff --git a/internal/langserver/handlers/did_save.go b/internal/langserver/handlers/did_save.go index 184e2537..a5842a8e 100644 --- a/internal/langserver/handlers/did_save.go +++ b/internal/langserver/handlers/did_save.go @@ -19,11 +19,10 @@ func (lh *logHandler) TextDocumentDidSave(ctx context.Context, params lsp.DidSav return nil } - fh := ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI) - dh := ilsp.FileHandlerFromDirPath(fh.Dir()) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) _, err = command.TerraformValidateHandler(ctx, cmd.CommandArgs{ - "uri": dh.URI(), + "uri": dh.Dir.URI, }) return err diff --git a/internal/langserver/handlers/document_link.go b/internal/langserver/handlers/document_link.go index 87cf48b2..43bcedb9 100644 --- a/internal/langserver/handlers/document_link.go +++ b/internal/langserver/handlers/document_link.go @@ -3,28 +3,23 @@ package handlers import ( "context" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) func (svc *service) TextDocumentLink(ctx context.Context, params lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) { - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return nil, err - } - cc, err := ilsp.ClientCapabilities(ctx) if err != nil { return nil, err } - doc, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return nil, err } - if doc.LanguageID() != ilsp.Terraform.String() { + if doc.LanguageID != ilsp.Terraform.String() { return nil, nil } @@ -33,7 +28,7 @@ func (svc *service) TextDocumentLink(ctx context.Context, params lsp.DocumentLin return nil, err } - links, err := d.LinksInFile(doc.Filename()) + links, err := d.LinksInFile(doc.Filename) if err != nil { return nil, err } diff --git a/internal/langserver/handlers/document_link_test.go b/internal/langserver/handlers/document_link_test.go index 9ee62f1e..719a599a 100644 --- a/internal/langserver/handlers/document_link_test.go +++ b/internal/langserver/handlers/document_link_test.go @@ -14,7 +14,7 @@ import ( func TestDocumentLink_withValidData(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -25,7 +25,7 @@ func TestDocumentLink_withValidData(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -68,7 +68,7 @@ func TestDocumentLink_withValidData(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -82,7 +82,7 @@ func TestDocumentLink_withValidData(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/documentLink", @@ -90,7 +90,7 @@ func TestDocumentLink_withValidData(t *testing.T) { "textDocument": { "uri": "%s/main.tf" } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": [ diff --git a/internal/langserver/handlers/execute_command_init_test.go b/internal/langserver/handlers/execute_command_init_test.go index ec4074bb..8e6b8d3c 100644 --- a/internal/langserver/handlers/execute_command_init_test.go +++ b/internal/langserver/handlers/execute_command_init_test.go @@ -15,12 +15,12 @@ import ( func TestLangServer_workspaceExecuteCommand_init_argumentError(t *testing.T) { tmpDir := TempDir(t) - testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI()) + testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -33,7 +33,7 @@ func TestLangServer_workspaceExecuteCommand_init_argumentError(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -58,7 +58,7 @@ func TestLangServer_workspaceExecuteCommand_init_argumentError(t *testing.T) { func TestLangServer_workspaceExecuteCommand_init_basic(t *testing.T) { tmpDir := TempDir(t) - testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI()) + testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI) tfMockCalls := []*mock.Call{ { @@ -95,7 +95,7 @@ func TestLangServer_workspaceExecuteCommand_init_basic(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): tfMockCalls, + tmpDir.Path(): tfMockCalls, }, }, })) @@ -108,7 +108,7 @@ func TestLangServer_workspaceExecuteCommand_init_basic(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -129,7 +129,7 @@ func TestLangServer_workspaceExecuteCommand_init_basic(t *testing.T) { ReqParams: fmt.Sprintf(`{ "command": %q, "arguments": ["uri=%s"] - }`, cmd.Name("terraform.init"), tmpDir.URI())}, `{ + }`, cmd.Name("terraform.init"), tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": null @@ -138,7 +138,7 @@ func TestLangServer_workspaceExecuteCommand_init_basic(t *testing.T) { func TestLangServer_workspaceExecuteCommand_init_error(t *testing.T) { tmpDir := TempDir(t) - testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI()) + testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI) tfMockCalls := []*mock.Call{ { @@ -175,7 +175,7 @@ func TestLangServer_workspaceExecuteCommand_init_error(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): tfMockCalls, + tmpDir.Path(): tfMockCalls, }, }, })) @@ -188,7 +188,7 @@ func TestLangServer_workspaceExecuteCommand_init_error(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", diff --git a/internal/langserver/handlers/execute_command_modules_test.go b/internal/langserver/handlers/execute_command_modules_test.go index 88707142..ee8e8269 100644 --- a/internal/langserver/handlers/execute_command_modules_test.go +++ b/internal/langserver/handlers/execute_command_modules_test.go @@ -7,22 +7,22 @@ import ( "testing" "github.com/creachadair/jrpc2/code" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" "github.com/hashicorp/terraform-ls/internal/langserver/cmd" - "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) func TestLangServer_workspaceExecuteCommand_modules_argumentError(t *testing.T) { tmpDir := TempDir(t) - testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI()) - InitPluginCache(t, tmpDir.Dir()) + testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI) + InitPluginCache(t, tmpDir.Path()) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -35,7 +35,7 @@ func TestLangServer_workspaceExecuteCommand_modules_argumentError(t *testing.T) "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -60,13 +60,13 @@ func TestLangServer_workspaceExecuteCommand_modules_argumentError(t *testing.T) func TestLangServer_workspaceExecuteCommand_modules_basic(t *testing.T) { tmpDir := TempDir(t) - testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI()) - InitPluginCache(t, tmpDir.Dir()) + testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI) + InitPluginCache(t, tmpDir.Path()) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -79,7 +79,7 @@ func TestLangServer_workspaceExecuteCommand_modules_basic(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -113,7 +113,7 @@ func TestLangServer_workspaceExecuteCommand_modules_basic(t *testing.T) { } ] } - }`, tmpDir.URI(), t.Name())) + }`, tmpDir.URI, t.Name())) } func TestLangServer_workspaceExecuteCommand_modules_multiple(t *testing.T) { @@ -129,19 +129,19 @@ func TestLangServer_workspaceExecuteCommand_modules_multiple(t *testing.T) { t.Fatal(err) } - root := lsp.FileHandlerFromDirPath(filepath.Join(testData, "main-module-multienv")) - mod := lsp.FileHandlerFromDirPath(filepath.Join(testData, "main-module-multienv", "main", "main.tf")) + root := document.DirHandleFromPath(filepath.Join(testData, "main-module-multienv")) + mod := document.DirHandleFromPath(filepath.Join(testData, "main-module-multienv", "main", "main.tf")) - dev := lsp.FileHandlerFromDirPath(filepath.Join(testData, "main-module-multienv", "env", "dev")) - staging := lsp.FileHandlerFromDirPath(filepath.Join(testData, "main-module-multienv", "env", "staging")) - prod := lsp.FileHandlerFromDirPath(filepath.Join(testData, "main-module-multienv", "env", "prod")) + dev := document.DirHandleFromPath(filepath.Join(testData, "main-module-multienv", "env", "dev")) + staging := document.DirHandleFromPath(filepath.Join(testData, "main-module-multienv", "env", "staging")) + prod := document.DirHandleFromPath(filepath.Join(testData, "main-module-multienv", "env", "prod")) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - dev.Dir(): validTfMockCalls(), - staging.Dir(): validTfMockCalls(), - prod.Dir(): validTfMockCalls(), + dev.Path(): validTfMockCalls(), + staging.Path(): validTfMockCalls(), + prod.Path(): validTfMockCalls(), }, }, })) @@ -154,7 +154,7 @@ func TestLangServer_workspaceExecuteCommand_modules_multiple(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, root.URI())}) + }`, root.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -167,7 +167,7 @@ func TestLangServer_workspaceExecuteCommand_modules_multiple(t *testing.T) { ReqParams: fmt.Sprintf(`{ "command": %q, "arguments": ["uri=%s"] - }`, cmd.Name("rootmodules"), mod.URI())}, fmt.Sprintf(`{ + }`, cmd.Name("rootmodules"), mod.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 2, "result": { @@ -188,5 +188,5 @@ func TestLangServer_workspaceExecuteCommand_modules_multiple(t *testing.T) { } ] } - }`, dev.URI(), filepath.Join("env", "dev"), prod.URI(), filepath.Join("env", "prod"), staging.URI(), filepath.Join("env", "staging"))) + }`, dev.URI, filepath.Join("env", "dev"), prod.URI, filepath.Join("env", "prod"), staging.URI, filepath.Join("env", "staging"))) } diff --git a/internal/langserver/handlers/execute_command_test.go b/internal/langserver/handlers/execute_command_test.go index 7d7d24be..6592fcd8 100644 --- a/internal/langserver/handlers/execute_command_test.go +++ b/internal/langserver/handlers/execute_command_test.go @@ -12,14 +12,14 @@ import ( func TestLangServer_workspaceExecuteCommand_noCommandHandlerError(t *testing.T) { tmpDir := TempDir(t) - testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI()) + testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -32,7 +32,7 @@ func TestLangServer_workspaceExecuteCommand_noCommandHandlerError(t *testing.T) "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", diff --git a/internal/langserver/handlers/execute_command_validate_test.go b/internal/langserver/handlers/execute_command_validate_test.go index c6cfeaeb..fd97059f 100644 --- a/internal/langserver/handlers/execute_command_validate_test.go +++ b/internal/langserver/handlers/execute_command_validate_test.go @@ -13,12 +13,12 @@ import ( func TestLangServer_workspaceExecuteCommand_validate_argumentError(t *testing.T) { tmpDir := TempDir(t) - testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI()) + testFileURI := fmt.Sprintf("%s/main.tf", tmpDir.URI) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -31,7 +31,7 @@ func TestLangServer_workspaceExecuteCommand_validate_argumentError(t *testing.T) "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", diff --git a/internal/langserver/handlers/formatting.go b/internal/langserver/handlers/formatting.go index 2c6a1d7b..6d163291 100644 --- a/internal/langserver/handlers/formatting.go +++ b/internal/langserver/handlers/formatting.go @@ -3,8 +3,7 @@ package handlers import ( "context" - lsctx "github.com/hashicorp/terraform-ls/internal/context" - "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/hcl" "github.com/hashicorp/terraform-ls/internal/langserver/errors" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" @@ -13,34 +12,24 @@ import ( "github.com/hashicorp/terraform-ls/internal/terraform/module" ) -func (h *logHandler) TextDocumentFormatting(ctx context.Context, params lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) { +func (svc *service) TextDocumentFormatting(ctx context.Context, params lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) { var edits []lsp.TextEdit - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return edits, err - } - - fh := ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) - tfExec, err := module.TerraformExecutorForModule(ctx, fh.Dir()) + tfExec, err := module.TerraformExecutorForModule(ctx, dh.Dir.Path()) if err != nil { return edits, errors.EnrichTfExecError(err) } - file, err := fs.GetDocument(fh) - if err != nil { - return edits, err - } - - original, err := file.Text() + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return edits, err } - h.logger.Printf("formatting document via %q", tfExec.GetExecPath()) + svc.logger.Printf("formatting document via %q", tfExec.GetExecPath()) - edits, err = formatDocument(ctx, tfExec, original, file) + edits, err = formatDocument(ctx, tfExec, doc.Text, dh) if err != nil { return edits, err } @@ -48,7 +37,7 @@ func (h *logHandler) TextDocumentFormatting(ctx context.Context, params lsp.Docu return edits, nil } -func formatDocument(ctx context.Context, tfExec exec.TerraformExecutor, original []byte, file filesystem.Document) ([]lsp.TextEdit, error) { +func formatDocument(ctx context.Context, tfExec exec.TerraformExecutor, original []byte, dh document.Handle) ([]lsp.TextEdit, error) { var edits []lsp.TextEdit formatted, err := tfExec.Format(ctx, original) @@ -56,7 +45,7 @@ func formatDocument(ctx context.Context, tfExec exec.TerraformExecutor, original return edits, err } - changes := hcl.Diff(file, original, formatted) + changes := hcl.Diff(dh, original, formatted) return ilsp.TextEditsFromDocumentChanges(changes), nil } diff --git a/internal/langserver/handlers/formatting_test.go b/internal/langserver/handlers/formatting_test.go index d54c3fd2..d9e9bcba 100644 --- a/internal/langserver/handlers/formatting_test.go +++ b/internal/langserver/handlers/formatting_test.go @@ -27,7 +27,7 @@ func TestLangServer_formattingWithoutInitialization(t *testing.T) { "text": "provider \"github\" {}", "uri": "%s/main.tf" } - }`, TempDir(t).URI())}, session.SessionNotInitialized.Err()) + }`, TempDir(t).URI)}, session.SessionNotInitialized.Err()) } func TestLangServer_formatting_basic(t *testing.T) { @@ -36,7 +36,7 @@ func TestLangServer_formatting_basic(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -81,7 +81,7 @@ func TestLangServer_formatting_basic(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -95,14 +95,14 @@ func TestLangServer_formatting_basic(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/formatting", ReqParams: fmt.Sprintf(`{ "textDocument": { "uri": "%s/main.tf" } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -122,7 +122,7 @@ func TestLangServer_formatting_oldVersion(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -167,7 +167,7 @@ func TestLangServer_formatting_oldVersion(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -181,14 +181,14 @@ func TestLangServer_formatting_oldVersion(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectError(t, &langserver.CallRequest{ Method: "textDocument/formatting", ReqParams: fmt.Sprintf(`{ "textDocument": { "uri": "%s/main.tf" } - }`, tmpDir.URI())}, code.SystemError.Err()) + }`, tmpDir.URI)}, code.SystemError.Err()) } func TestLangServer_formatting_variables(t *testing.T) { @@ -197,7 +197,7 @@ func TestLangServer_formatting_variables(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -242,7 +242,7 @@ func TestLangServer_formatting_variables(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -256,14 +256,14 @@ func TestLangServer_formatting_variables(t *testing.T) { "text": "test = \"dev\"", "uri": "%s/terraform.tfvars" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/formatting", ReqParams: fmt.Sprintf(`{ "textDocument": { "uri": "%s/terraform.tfvars" } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": [ diff --git a/internal/langserver/handlers/go_to_ref_target.go b/internal/langserver/handlers/go_to_ref_target.go index fb20862c..e71f7b7b 100644 --- a/internal/langserver/handlers/go_to_ref_target.go +++ b/internal/langserver/handlers/go_to_ref_target.go @@ -5,7 +5,6 @@ import ( "github.com/hashicorp/hcl-lang/decoder" "github.com/hashicorp/hcl-lang/lang" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) @@ -39,25 +38,21 @@ func (svc *service) GoToDeclaration(ctx context.Context, params lsp.TextDocument } func (svc *service) goToReferenceTarget(ctx context.Context, params lsp.TextDocumentPositionParams) (decoder.ReferenceTargets, error) { - fs, err := lsctx.DocumentStorage(ctx) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return nil, err } - doc, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) - if err != nil { - return nil, err - } - - fPos, err := ilsp.FilePositionFromDocumentPosition(params, doc) + pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc) if err != nil { return nil, err } path := lang.Path{ - Path: doc.Dir(), - LanguageID: doc.LanguageID(), + Path: doc.Dir.Path(), + LanguageID: doc.LanguageID, } - return svc.decoder.ReferenceTargetsForOriginAtPos(path, doc.Filename(), fPos.Position()) + return svc.decoder.ReferenceTargetsForOriginAtPos(path, doc.Filename, pos) } diff --git a/internal/langserver/handlers/go_to_ref_target_test.go b/internal/langserver/handlers/go_to_ref_target_test.go index c52abbc3..498df034 100644 --- a/internal/langserver/handlers/go_to_ref_target_test.go +++ b/internal/langserver/handlers/go_to_ref_target_test.go @@ -9,8 +9,8 @@ import ( "github.com/hashicorp/go-version" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" - "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) @@ -21,7 +21,7 @@ func TestDefinition_basic(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -54,7 +54,7 @@ func TestDefinition_basic(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -74,7 +74,7 @@ output "foo" { }`)+`, "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/definition", ReqParams: fmt.Sprintf(`{ @@ -85,7 +85,7 @@ output "foo" { "line": 4, "character": 13 } - }`, tmpDir.URI())}, fmt.Sprintf(`{ + }`, tmpDir.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [{ @@ -101,12 +101,12 @@ output "foo" { } } }] - }`, tmpDir.URI())) + }`, tmpDir.URI)) } func TestDefinition_withLinkToDefLessBlock(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -117,7 +117,7 @@ func TestDefinition_withLinkToDefLessBlock(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -167,7 +167,7 @@ func TestDefinition_withLinkToDefLessBlock(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -191,7 +191,7 @@ output "foo" { }`)+`, "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/definition", ReqParams: fmt.Sprintf(`{ @@ -202,7 +202,7 @@ output "foo" { "line": 8, "character": 35 } - }`, tmpDir.URI())}, fmt.Sprintf(`{ + }`, tmpDir.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -240,12 +240,12 @@ output "foo" { } } ] - }`, tmpDir.URI())) + }`, tmpDir.URI)) } func TestDefinition_withLinkToDefBlock(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -256,7 +256,7 @@ func TestDefinition_withLinkToDefBlock(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -306,7 +306,7 @@ func TestDefinition_withLinkToDefBlock(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -330,7 +330,7 @@ output "foo" { }`)+`, "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/definition", ReqParams: fmt.Sprintf(`{ @@ -341,7 +341,7 @@ output "foo" { "line": 8, "character": 30 } - }`, tmpDir.URI())}, fmt.Sprintf(`{ + }`, tmpDir.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -379,7 +379,7 @@ output "foo" { } } ] - }`, tmpDir.URI())) + }`, tmpDir.URI)) } func TestDefinition_moduleInputToVariable(t *testing.T) { @@ -387,7 +387,7 @@ func TestDefinition_moduleInputToVariable(t *testing.T) { if err != nil { t.Fatal(err) } - modUri := lsp.FileHandlerFromDirPath(modPath) + modHandle := document.DirHandleFromPath(modPath) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ @@ -405,7 +405,7 @@ func TestDefinition_moduleInputToVariable(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, modUri.URI())}) + }`, modHandle.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -426,7 +426,7 @@ func TestDefinition_moduleInputToVariable(t *testing.T) { `)+`, "uri": "%s/main.tf" } - }`, modUri.URI())}) + }`, modHandle.URI)}) // TODO remove once we support synchronous dependent tasks // See https://github.com/hashicorp/terraform-ls/issues/719 time.Sleep(2 * time.Second) @@ -440,7 +440,7 @@ func TestDefinition_moduleInputToVariable(t *testing.T) { "line": 2, "character": 6 } - }`, modUri.URI())}, fmt.Sprintf(`{ + }`, modHandle.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -458,7 +458,7 @@ func TestDefinition_moduleInputToVariable(t *testing.T) { } } ] - }`, modUri.URI())) + }`, modHandle.URI)) } func TestDeclaration_basic(t *testing.T) { @@ -467,7 +467,7 @@ func TestDeclaration_basic(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -500,7 +500,7 @@ func TestDeclaration_basic(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -520,7 +520,7 @@ output "foo" { }`)+`, "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/declaration", ReqParams: fmt.Sprintf(`{ @@ -531,7 +531,7 @@ output "foo" { "line": 4, "character": 13 } - }`, tmpDir.URI())}, fmt.Sprintf(`{ + }`, tmpDir.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [{ @@ -547,12 +547,12 @@ output "foo" { } } }] - }`, tmpDir.URI())) + }`, tmpDir.URI)) } func TestDeclaration_withLinkSupport(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -563,7 +563,7 @@ func TestDeclaration_withLinkSupport(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -613,7 +613,7 @@ func TestDeclaration_withLinkSupport(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -637,7 +637,7 @@ output "foo" { }`)+`, "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/declaration", ReqParams: fmt.Sprintf(`{ @@ -648,7 +648,7 @@ output "foo" { "line": 8, "character": 35 } - }`, tmpDir.URI())}, fmt.Sprintf(`{ + }`, tmpDir.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -686,5 +686,5 @@ output "foo" { } } ] - }`, tmpDir.URI())) + }`, tmpDir.URI)) } diff --git a/internal/langserver/handlers/handlers_test.go b/internal/langserver/handlers/handlers_test.go index 2c7f603f..4877accb 100644 --- a/internal/langserver/handlers/handlers_test.go +++ b/internal/langserver/handlers/handlers_test.go @@ -10,8 +10,8 @@ import ( "github.com/hashicorp/go-version" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" - "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) @@ -83,7 +83,7 @@ func TestInitalizeAndShutdown(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -96,7 +96,7 @@ func TestInitalizeAndShutdown(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}, initializeResponse(t, "")) + }`, tmpDir.URI)}, initializeResponse(t, "")) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "shutdown", ReqParams: `{}`}, `{ @@ -112,7 +112,7 @@ func TestInitalizeWithCommandPrefix(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -128,7 +128,7 @@ func TestInitalizeWithCommandPrefix(t *testing.T) { "initializationOptions": { "commandPrefix": "1" } - }`, tmpDir.URI())}, initializeResponse(t, "1")) + }`, tmpDir.URI)}, initializeResponse(t, "1")) } func TestEOF(t *testing.T) { @@ -137,7 +137,7 @@ func TestEOF(t *testing.T) { ms := newMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, }) @@ -151,7 +151,7 @@ func TestEOF(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}, initializeResponse(t, "")) + }`, tmpDir.URI)}, initializeResponse(t, "")) ls.CloseClientStdout(t) @@ -219,7 +219,7 @@ func validTfMockCalls() []*mock.Call { // โ””โ”€โ”€ c // // The returned filehandler is the parent tmp dir -func TempDir(t *testing.T, nested ...string) lsp.FileHandler { +func TempDir(t *testing.T, nested ...string) document.DirHandle { tmpDir := filepath.Join(os.TempDir(), "terraform-ls", t.Name()) err := os.MkdirAll(tmpDir, 0755) if err != nil && !os.IsExist(err) { @@ -239,7 +239,7 @@ func TempDir(t *testing.T, nested ...string) lsp.FileHandler { } } - return lsp.FileHandlerFromDirPath(tmpDir) + return document.DirHandleFromPath(tmpDir) } func InitPluginCache(t *testing.T, dir string) { diff --git a/internal/langserver/handlers/hover.go b/internal/langserver/handlers/hover.go index a163e0f7..bbf58898 100644 --- a/internal/langserver/handlers/hover.go +++ b/internal/langserver/handlers/hover.go @@ -3,23 +3,18 @@ package handlers import ( "context" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) func (svc *service) TextDocumentHover(ctx context.Context, params lsp.TextDocumentPositionParams) (*lsp.Hover, error) { - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return nil, err - } - cc, err := ilsp.ClientCapabilities(ctx) if err != nil { return nil, err } - doc, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return nil, err } @@ -29,13 +24,13 @@ func (svc *service) TextDocumentHover(ctx context.Context, params lsp.TextDocume return nil, err } - fPos, err := ilsp.FilePositionFromDocumentPosition(params, doc) + pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc) if err != nil { return nil, err } - svc.logger.Printf("Looking for hover data at %q -> %#v", doc.Filename(), fPos.Position()) - hoverData, err := d.HoverAtPos(doc.Filename(), fPos.Position()) + svc.logger.Printf("Looking for hover data at %q -> %#v", doc.Filename, pos) + hoverData, err := d.HoverAtPos(doc.Filename, pos) svc.logger.Printf("received hover data: %#v", hoverData) if err != nil { return nil, err diff --git a/internal/langserver/handlers/hover_test.go b/internal/langserver/handlers/hover_test.go index 7dbc0610..2adad1bd 100644 --- a/internal/langserver/handlers/hover_test.go +++ b/internal/langserver/handlers/hover_test.go @@ -28,12 +28,12 @@ func TestHover_withoutInitialization(t *testing.T) { "character": 0, "line": 1 } - }`, TempDir(t).URI())}, session.SessionNotInitialized.Err()) + }`, TempDir(t).URI)}, session.SessionNotInitialized.Err()) } func TestHover_withValidData(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -44,7 +44,7 @@ func TestHover_withValidData(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -88,7 +88,7 @@ func TestHover_withValidData(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -102,7 +102,7 @@ func TestHover_withValidData(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/hover", @@ -114,7 +114,7 @@ func TestHover_withValidData(t *testing.T) { "character": 3, "line": 0 } - }`, TempDir(t).URI())}, `{ + }`, TempDir(t).URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": { @@ -132,7 +132,7 @@ func TestHover_withValidData(t *testing.T) { func TestVarsHover_withValidData(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -143,7 +143,7 @@ func TestVarsHover_withValidData(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -186,7 +186,7 @@ func TestVarsHover_withValidData(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -200,7 +200,7 @@ func TestVarsHover_withValidData(t *testing.T) { "text": "variable \"test\" {\n type=string\n sensitive=true}\n", "uri": "%s/variables.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Call(t, &langserver.CallRequest{ Method: "textDocument/didOpen", ReqParams: fmt.Sprintf(`{ @@ -210,7 +210,7 @@ func TestVarsHover_withValidData(t *testing.T) { "text": "test = \"dev\"\n", "uri": "%s/terraform.tfvars" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/hover", @@ -222,7 +222,7 @@ func TestVarsHover_withValidData(t *testing.T) { "character": 3, "line": 0 } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 4, "result": { diff --git a/internal/langserver/handlers/initialize.go b/internal/langserver/handlers/initialize.go index b836675e..ab72ef5d 100644 --- a/internal/langserver/handlers/initialize.go +++ b/internal/langserver/handlers/initialize.go @@ -8,9 +8,11 @@ import ( "github.com/creachadair/jrpc2" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/document" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/settings" + "github.com/hashicorp/terraform-ls/internal/uri" "github.com/mitchellh/go-homedir" ) @@ -82,19 +84,19 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) } defer svc.telemetry.SendEvent(ctx, "initialize", properties) - fh := ilsp.FileHandlerFromDirURI(params.RootURI) - if fh.URI() == "" || !fh.IsDir() { + if params.RootURI == "" { properties["root_uri"] = "file" return serverCaps, fmt.Errorf("Editing a single file is not yet supported." + " Please open a directory.") } - if !fh.Valid() { + if !uri.IsURIValid(string(params.RootURI)) { properties["root_uri"] = "invalid" return serverCaps, fmt.Errorf("URI %q is not valid", params.RootURI) } - rootDir := fh.FullPath() - err := lsctx.SetRootDirectory(ctx, rootDir) + root := document.DirHandleFromURI(string(params.RootURI)) + + err := lsctx.SetRootDirectory(ctx, root.Path()) if err != nil { return serverCaps, err } @@ -188,7 +190,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) var excludeModulePaths []string for _, rawPath := range cfgOpts.ExcludeModulePaths { - modPath, err := resolvePath(rootDir, rawPath) + modPath, err := resolvePath(root.Path(), rawPath) if err != nil { svc.logger.Printf("Ignoring excluded module path %s: %s", rawPath, err) continue @@ -198,7 +200,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) svc.walker.SetIgnoreDirectoryNames(cfgOpts.IgnoreDirectoryNames) svc.walker.SetExcludeModulePaths(excludeModulePaths) - svc.walker.EnqueuePath(fh.Dir()) + svc.walker.EnqueuePath(root.Path()) // Walker runs asynchronously so we're intentionally *not* // passing the request context here @@ -231,7 +233,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) if len(cfgOpts.ModulePaths) > 0 { svc.logger.Printf("Attempting to add %d static module paths", len(cfgOpts.ModulePaths)) for _, rawPath := range cfgOpts.ModulePaths { - modPath, err := resolvePath(rootDir, rawPath) + modPath, err := resolvePath(root.Path(), rawPath) if err != nil { jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ Type: lsp.Warning, diff --git a/internal/langserver/handlers/initialize_test.go b/internal/langserver/handlers/initialize_test.go index 6d4d0e20..7c8e5e1f 100644 --- a/internal/langserver/handlers/initialize_test.go +++ b/internal/langserver/handlers/initialize_test.go @@ -17,7 +17,7 @@ func TestInitialize_twice(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -30,14 +30,14 @@ func TestInitialize_twice(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.CallAndExpectError(t, &langserver.CallRequest{ Method: "initialize", ReqParams: fmt.Sprintf(`{ "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, TempDir(t).URI())}, code.SystemError.Err()) + }`, TempDir(t).URI)}, code.SystemError.Err()) } func TestInitialize_withIncompatibleTerraformVersion(t *testing.T) { @@ -45,7 +45,7 @@ func TestInitialize_withIncompatibleTerraformVersion(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -70,7 +70,7 @@ func TestInitialize_withIncompatibleTerraformVersion(t *testing.T) { "capabilities": {}, "processId": 12345, "rootUri": %q - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) } func TestInitialize_withInvalidRootURI(t *testing.T) { @@ -78,7 +78,7 @@ func TestInitialize_withInvalidRootURI(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -99,7 +99,7 @@ func TestInitialize_multipleFolders(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - rootDir.Dir(): validTfMockCalls(), + rootDir.Path(): validTfMockCalls(), }, }, })) @@ -118,13 +118,13 @@ func TestInitialize_multipleFolders(t *testing.T) { "name": "root" } ] - }`, rootDir.URI(), rootDir.URI())}) + }`, rootDir.URI, rootDir.URI)}) } func TestInitialize_ignoreDirectoryNames(t *testing.T) { tmpDir := TempDir(t, "plugin", "ignore") - pluginDir := filepath.Join(tmpDir.Dir(), "plugin") - emptyDir := filepath.Join(tmpDir.Dir(), "ignore") + pluginDir := filepath.Join(tmpDir.Path(), "plugin") + emptyDir := filepath.Join(tmpDir.Path(), "ignore") InitPluginCache(t, pluginDir) InitPluginCache(t, emptyDir) @@ -157,5 +157,5 @@ func TestInitialize_ignoreDirectoryNames(t *testing.T) { "initializationOptions": { "ignoreDirectoryNames": [%q] } - }`, tmpDir.URI(), "ignore")}) + }`, tmpDir.URI, "ignore")}) } diff --git a/internal/langserver/handlers/references.go b/internal/langserver/handlers/references.go index 16c37b68..2a081420 100644 --- a/internal/langserver/handlers/references.go +++ b/internal/langserver/handlers/references.go @@ -4,7 +4,6 @@ import ( "context" "github.com/hashicorp/hcl-lang/lang" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) @@ -12,27 +11,23 @@ import ( func (svc *service) References(ctx context.Context, params lsp.ReferenceParams) ([]lsp.Location, error) { list := make([]lsp.Location, 0) - fs, err := lsctx.DocumentStorage(ctx) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return list, err } - doc, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) - if err != nil { - return list, err - } - - fPos, err := ilsp.FilePositionFromDocumentPosition(params.TextDocumentPositionParams, doc) + pos, err := ilsp.HCLPositionFromLspPosition(params.TextDocumentPositionParams.Position, doc) if err != nil { return list, err } path := lang.Path{ - Path: doc.Dir(), - LanguageID: doc.LanguageID(), + Path: doc.Dir.Path(), + LanguageID: doc.LanguageID, } - origins := svc.decoder.ReferenceOriginsTargetingPos(path, doc.Filename(), fPos.Position()) + origins := svc.decoder.ReferenceOriginsTargetingPos(path, doc.Filename, pos) return ilsp.RefOriginsToLocations(origins), nil } diff --git a/internal/langserver/handlers/references_test.go b/internal/langserver/handlers/references_test.go index 051499f4..e3961b8e 100644 --- a/internal/langserver/handlers/references_test.go +++ b/internal/langserver/handlers/references_test.go @@ -7,8 +7,8 @@ import ( "time" "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" - "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) @@ -19,7 +19,7 @@ func TestReferences_basic(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -56,7 +56,7 @@ func TestReferences_basic(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -76,7 +76,7 @@ output "foo" { }`)+`, "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/references", ReqParams: fmt.Sprintf(`{ @@ -87,7 +87,7 @@ output "foo" { "line": 0, "character": 2 } - }`, tmpDir.URI())}, fmt.Sprintf(`{ + }`, tmpDir.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -118,7 +118,7 @@ output "foo" { } } ] - }`, tmpDir.URI(), tmpDir.URI())) + }`, tmpDir.URI, tmpDir.URI)) } func TestReferences_variableToModuleInput(t *testing.T) { @@ -129,8 +129,8 @@ func TestReferences_variableToModuleInput(t *testing.T) { submodPath := filepath.Join(rootModPath, "application") - rootModUri := lsp.FileHandlerFromDirPath(rootModPath) - submodUri := lsp.FileHandlerFromDirPath(submodPath) + rootHandle := document.DirHandleFromPath(rootModPath) + subHandle := document.DirHandleFromPath(submodPath) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ @@ -152,7 +152,7 @@ func TestReferences_variableToModuleInput(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, rootModUri.URI())}) + }`, rootHandle.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -178,7 +178,7 @@ variable "instances" { `)+`, "uri": "%s/main.tf" } - }`, submodUri.URI())}) + }`, subHandle.URI)}) // TODO remove once we support synchronous dependent tasks // See https://github.com/hashicorp/terraform-ls/issues/719 time.Sleep(2 * time.Second) @@ -192,7 +192,7 @@ variable "instances" { "line": 0, "character": 5 } - }`, submodUri.URI())}, fmt.Sprintf(`{ + }`, subHandle.URI)}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, "result": [ @@ -210,5 +210,5 @@ variable "instances" { } } ] - }`, rootModUri.URI())) + }`, rootHandle.URI)) } diff --git a/internal/langserver/handlers/semantic_tokens.go b/internal/langserver/handlers/semantic_tokens.go index 7ede9662..b63cf30a 100644 --- a/internal/langserver/handlers/semantic_tokens.go +++ b/internal/langserver/handlers/semantic_tokens.go @@ -4,7 +4,6 @@ import ( "context" "github.com/creachadair/jrpc2/code" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) @@ -28,13 +27,8 @@ func (svc *service) TextDocumentSemanticTokensFull(ctx context.Context, params l return tks, code.MethodNotFound.Err() } - ds, err := lsctx.DocumentStorage(ctx) - if err != nil { - return tks, err - } - - fh := ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI) - doc, err := ds.GetDocument(fh) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return tks, err } @@ -44,13 +38,13 @@ func (svc *service) TextDocumentSemanticTokensFull(ctx context.Context, params l return tks, err } - tokens, err := d.SemanticTokensInFile(doc.Filename()) + tokens, err := d.SemanticTokensInFile(doc.Filename) if err != nil { return tks, err } te := &ilsp.TokenEncoder{ - Lines: doc.Lines(), + Lines: doc.Lines, Tokens: tokens, ClientCaps: cc.TextDocument.SemanticTokens, } diff --git a/internal/langserver/handlers/semantic_tokens_test.go b/internal/langserver/handlers/semantic_tokens_test.go index 06557f1a..d86e073c 100644 --- a/internal/langserver/handlers/semantic_tokens_test.go +++ b/internal/langserver/handlers/semantic_tokens_test.go @@ -14,7 +14,7 @@ import ( func TestSemanticTokensFull(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -25,7 +25,7 @@ func TestSemanticTokensFull(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -85,7 +85,7 @@ func TestSemanticTokensFull(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -99,7 +99,7 @@ func TestSemanticTokensFull(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/semanticTokens/full", @@ -107,7 +107,7 @@ func TestSemanticTokensFull(t *testing.T) { "textDocument": { "uri": "%s/main.tf" } - }`, TempDir(t).URI())}, `{ + }`, TempDir(t).URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": { @@ -121,7 +121,7 @@ func TestSemanticTokensFull(t *testing.T) { func TestSemanticTokensFull_clientSupportsDelta(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -132,7 +132,7 @@ func TestSemanticTokensFull_clientSupportsDelta(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -194,7 +194,7 @@ func TestSemanticTokensFull_clientSupportsDelta(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -208,7 +208,7 @@ func TestSemanticTokensFull_clientSupportsDelta(t *testing.T) { "text": "provider \"test\" {\n\n}\n", "uri": "%s/main.tf" } - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/semanticTokens/full", @@ -216,7 +216,7 @@ func TestSemanticTokensFull_clientSupportsDelta(t *testing.T) { "textDocument": { "uri": "%s/main.tf" } - }`, TempDir(t).URI())}, `{ + }`, TempDir(t).URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": { @@ -230,7 +230,7 @@ func TestSemanticTokensFull_clientSupportsDelta(t *testing.T) { func TestVarsSemanticTokensFull(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) var testSchema tfjson.ProviderSchemas err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) @@ -241,7 +241,7 @@ func TestVarsSemanticTokensFull(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): { + tmpDir.Path(): { { Method: "Version", Repeatability: 1, @@ -301,7 +301,7 @@ func TestVarsSemanticTokensFull(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -315,7 +315,7 @@ func TestVarsSemanticTokensFull(t *testing.T) { "text": "variable \"test\" {\n type=string\n}\n", "uri": "%s/variables.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Call(t, &langserver.CallRequest{ Method: "textDocument/didOpen", ReqParams: fmt.Sprintf(`{ @@ -325,7 +325,7 @@ func TestVarsSemanticTokensFull(t *testing.T) { "text": "test = \"dev\"\n", "uri": "%s/terraform.tfvars" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/semanticTokens/full", @@ -333,7 +333,7 @@ func TestVarsSemanticTokensFull(t *testing.T) { "textDocument": { "uri": "%s/terraform.tfvars" } - }`, TempDir(t).URI())}, `{ + }`, TempDir(t).URI)}, `{ "jsonrpc": "2.0", "id": 4, "result": { diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index 544d4d2a..326c8cf9 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/hcl-lang/lang" lsctx "github.com/hashicorp/terraform-ls/internal/context" idecoder "github.com/hashicorp/terraform-ls/internal/decoder" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/langserver/diagnostics" "github.com/hashicorp/terraform-ls/internal/langserver/session" @@ -37,7 +38,7 @@ type service struct { sessCtx context.Context stopSession context.CancelFunc - fs filesystem.Filesystem + fs *filesystem.Filesystem modStore *state.ModuleStore schemaStore *state.ProviderSchemaStore watcher module.Watcher @@ -61,13 +62,11 @@ type service struct { var discardLogs = log.New(ioutil.Discard, "", 0) func NewSession(srvCtx context.Context) session.Session { - fs := filesystem.NewFilesystem() d := &discovery.Discovery{} sessCtx, stopSession := context.WithCancel(srvCtx) return &service{ logger: discardLogs, - fs: fs, srvCtx: srvCtx, sessCtx: sessCtx, stopSession: stopSession, @@ -97,7 +96,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } svc.telemetry = &telemetry.NoopSender{Logger: svc.logger} - svc.fs.SetLogger(svc.logger) lh := LogHandler(svc.logger) cc := &lsp.ClientCapabilities{} @@ -140,27 +138,24 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { if err != nil { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = lsctx.WithModuleManager(ctx, svc.modMgr) - return handle(ctx, req, TextDocumentDidChange) + return handle(ctx, req, svc.TextDocumentDidChange) }, "textDocument/didOpen": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() if err != nil { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = lsctx.WithModuleManager(ctx, svc.modMgr) ctx = lsctx.WithWatcher(ctx, svc.watcher) - return handle(ctx, req, lh.TextDocumentDidOpen) + return handle(ctx, req, svc.TextDocumentDidOpen) }, "textDocument/didClose": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() if err != nil { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) - return handle(ctx, req, TextDocumentDidClose) + return handle(ctx, req, svc.TextDocumentDidClose) }, "textDocument/documentSymbol": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() @@ -168,7 +163,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) return handle(ctx, req, svc.TextDocumentSymbol) @@ -179,7 +173,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) ctx = ilsp.ContextWithClientName(ctx, &clientName) @@ -191,7 +184,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) return handle(ctx, req, svc.GoToDeclaration) @@ -202,7 +194,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) return handle(ctx, req, svc.GoToDefinition) @@ -213,7 +204,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) ctx = lsctx.WithExperimentalFeatures(ctx, &expFeatures) @@ -225,7 +215,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) ctx = ilsp.ContextWithClientName(ctx, &clientName) @@ -238,11 +227,10 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } ctx = ilsp.WithClientCapabilities(ctx, cc) - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = exec.WithExecutorOpts(ctx, svc.tfExecOpts) ctx = exec.WithExecutorFactory(ctx, svc.tfExecFactory) - return handle(ctx, req, lh.TextDocumentCodeAction) + return handle(ctx, req, svc.TextDocumentCodeAction) }, "textDocument/codeLens": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() @@ -251,7 +239,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } ctx = ilsp.WithClientCapabilities(ctx, cc) - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) return handle(ctx, req, svc.TextDocumentCodeLens) }, @@ -261,11 +248,10 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = exec.WithExecutorOpts(ctx, svc.tfExecOpts) ctx = exec.WithExecutorFactory(ctx, svc.tfExecFactory) - return handle(ctx, req, lh.TextDocumentFormatting) + return handle(ctx, req, svc.TextDocumentFormatting) }, "textDocument/semanticTokens/full": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() @@ -273,7 +259,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) return handle(ctx, req, svc.TextDocumentSemanticTokensFull) @@ -309,8 +294,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) - return handle(ctx, req, svc.References) }, "workspace/executeCommand": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { @@ -337,7 +320,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) return handle(ctx, req, svc.WorkspaceSymbol) @@ -347,7 +329,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { if err != nil { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) svc.shutdown() return handle(ctx, req, Shutdown) }, @@ -459,6 +440,9 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s svc.modStore = svc.stateStore.Modules svc.schemaStore = svc.stateStore.ProviderSchemas + svc.fs = filesystem.NewFilesystem(svc.stateStore.DocumentStore) + svc.fs.SetLogger(svc.logger) + svc.decoder = idecoder.NewDecoder(ctx, &idecoder.PathReader{ ModuleReader: svc.modStore, SchemaReader: svc.schemaStore, @@ -469,13 +453,13 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s return err } - svc.modMgr = svc.newModuleManager(svc.sessCtx, svc.fs, svc.stateStore.Modules, svc.stateStore.ProviderSchemas) + svc.modMgr = svc.newModuleManager(svc.sessCtx, svc.fs, svc.stateStore.DocumentStore, svc.stateStore.Modules, svc.stateStore.ProviderSchemas) svc.modMgr.SetLogger(svc.logger) - svc.walker = svc.newWalker(svc.fs, svc.modMgr) + svc.walker = svc.newWalker(svc.fs, svc.stateStore.DocumentStore, svc.modMgr) svc.walker.SetLogger(svc.logger) - ww, err := svc.newWatcher(svc.fs, svc.modMgr) + ww, err := svc.newWatcher(svc.modMgr) if err != nil { return err } @@ -556,9 +540,9 @@ func handle(ctx context.Context, req *jrpc2.Request, fn interface{}) (interface{ return result, err } -func (svc *service) decoderForDocument(ctx context.Context, doc filesystem.Document) (*decoder.PathDecoder, error) { +func (svc *service) decoderForDocument(ctx context.Context, doc *document.Document) (*decoder.PathDecoder, error) { return svc.decoder.Path(lang.Path{ - Path: doc.Dir(), - LanguageID: doc.LanguageID(), + Path: doc.Dir.Path(), + LanguageID: doc.LanguageID, }) } diff --git a/internal/langserver/handlers/session_mock_test.go b/internal/langserver/handlers/session_mock_test.go index a84c05c7..2b871584 100644 --- a/internal/langserver/handlers/session_mock_test.go +++ b/internal/langserver/handlers/session_mock_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/creachadair/jrpc2/handler" - "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/langserver/session" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/discovery" @@ -18,7 +17,6 @@ import ( ) type MockSessionInput struct { - Filesystem filesystem.Filesystem TerraformCalls *exec.TerraformMockCalls AdditionalHandlers map[string]handler.Func StateStore *state.StateStore @@ -43,14 +41,9 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session { } } - var fs filesystem.Filesystem - fs = filesystem.NewFilesystem() var handlers map[string]handler.Func var stateStore *state.StateStore if ms.mockInput != nil { - if ms.mockInput.Filesystem != nil { - fs = ms.mockInput.Filesystem - } stateStore = ms.mockInput.StateStore handlers = ms.mockInput.AdditionalHandlers } @@ -69,7 +62,6 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session { srvCtx: srvCtx, sessCtx: sessCtx, stopSession: ms.stop, - fs: fs, newModuleManager: module.NewModuleManagerMock(input), newWatcher: module.MockWatcher(), newWalker: module.SyncWalker, diff --git a/internal/langserver/handlers/shutdown_test.go b/internal/langserver/handlers/shutdown_test.go index 95948090..35ee02c2 100644 --- a/internal/langserver/handlers/shutdown_test.go +++ b/internal/langserver/handlers/shutdown_test.go @@ -15,7 +15,7 @@ func TestShutdown_twice(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -28,7 +28,7 @@ func TestShutdown_twice(t *testing.T) { "capabilities": {}, "rootUri": %q, "processId": 12345 - }`, TempDir(t).URI())}) + }`, TempDir(t).URI)}) ls.Call(t, &langserver.CallRequest{ Method: "shutdown", ReqParams: `{}`}) diff --git a/internal/langserver/handlers/symbols.go b/internal/langserver/handlers/symbols.go index a87df97d..cc7c106d 100644 --- a/internal/langserver/handlers/symbols.go +++ b/internal/langserver/handlers/symbols.go @@ -3,7 +3,6 @@ package handlers import ( "context" - lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) @@ -11,17 +10,13 @@ import ( func (svc *service) TextDocumentSymbol(ctx context.Context, params lsp.DocumentSymbolParams) ([]lsp.DocumentSymbol, error) { var symbols []lsp.DocumentSymbol - fs, err := lsctx.DocumentStorage(ctx) - if err != nil { - return symbols, err - } - cc, err := ilsp.ClientCapabilities(ctx) if err != nil { return symbols, err } - doc, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) + dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI) + doc, err := svc.stateStore.DocumentStore.GetDocument(dh) if err != nil { return symbols, err } @@ -31,7 +26,7 @@ func (svc *service) TextDocumentSymbol(ctx context.Context, params lsp.DocumentS return symbols, err } - sbs, err := d.SymbolsInFile(doc.Filename()) + sbs, err := d.SymbolsInFile(doc.Filename) if err != nil { return symbols, err } diff --git a/internal/langserver/handlers/symbols_test.go b/internal/langserver/handlers/symbols_test.go index 32c00f91..203d6996 100644 --- a/internal/langserver/handlers/symbols_test.go +++ b/internal/langserver/handlers/symbols_test.go @@ -11,12 +11,12 @@ import ( func TestLangServer_symbols_basic(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -46,7 +46,7 @@ func TestLangServer_symbols_basic(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -60,7 +60,7 @@ func TestLangServer_symbols_basic(t *testing.T) { "text": "provider \"github\" {}", "uri": "%s/main.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/documentSymbol", @@ -68,7 +68,7 @@ func TestLangServer_symbols_basic(t *testing.T) { "textDocument": { "uri": "%s/main.tf" } - }`, tmpDir.URI())}, `{ + }`, tmpDir.URI)}, `{ "jsonrpc": "2.0", "id": 3, "result": [ diff --git a/internal/langserver/handlers/workspace_symbol_test.go b/internal/langserver/handlers/workspace_symbol_test.go index f9d1b806..efa0a54b 100644 --- a/internal/langserver/handlers/workspace_symbol_test.go +++ b/internal/langserver/handlers/workspace_symbol_test.go @@ -11,12 +11,12 @@ import ( func TestLangServer_workspace_symbol_basic(t *testing.T) { tmpDir := TempDir(t) - InitPluginCache(t, tmpDir.Dir()) + InitPluginCache(t, tmpDir.Path()) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): validTfMockCalls(), + tmpDir.Path(): validTfMockCalls(), }, }, })) @@ -44,7 +44,7 @@ func TestLangServer_workspace_symbol_basic(t *testing.T) { }, "rootUri": %q, "processId": 12345 - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", ReqParams: "{}", @@ -58,7 +58,7 @@ func TestLangServer_workspace_symbol_basic(t *testing.T) { "text": "provider \"github\" {}", "uri": "%s/first.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Call(t, &langserver.CallRequest{ Method: "textDocument/didOpen", ReqParams: fmt.Sprintf(`{ @@ -68,7 +68,7 @@ func TestLangServer_workspace_symbol_basic(t *testing.T) { "text": "provider \"google\" {}", "uri": "%s/second.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.Call(t, &langserver.CallRequest{ Method: "textDocument/didOpen", ReqParams: fmt.Sprintf(`{ @@ -78,7 +78,7 @@ func TestLangServer_workspace_symbol_basic(t *testing.T) { "text": "myblock \"custom\" {}", "uri": "%s/blah/third.tf" } - }`, tmpDir.URI())}) + }`, tmpDir.URI)}) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "workspace/symbol", @@ -122,7 +122,7 @@ func TestLangServer_workspace_symbol_basic(t *testing.T) { } } ] - }`, tmpDir.URI(), tmpDir.URI(), tmpDir.URI())) + }`, tmpDir.URI, tmpDir.URI, tmpDir.URI)) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "workspace/symbol", @@ -144,5 +144,5 @@ func TestLangServer_workspace_symbol_basic(t *testing.T) { } } ] - }`, tmpDir.URI())) + }`, tmpDir.URI)) } From 215ebe58ccbf37b331bc9913026fd0467a938b16 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 25 Jan 2022 11:58:04 +0000 Subject: [PATCH 09/15] replace last occurence of afero with osFs --- go.mod | 2 +- internal/terraform/parser/module_test.go | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index feea2276..445d2ba0 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 - github.com/spf13/afero v1.8.1 + github.com/spf13/afero v1.8.1 // indirect github.com/stretchr/testify v1.7.0 github.com/vektra/mockery/v2 v2.10.0 github.com/zclconf/go-cty v1.10.0 diff --git a/internal/terraform/parser/module_test.go b/internal/terraform/parser/module_test.go index 40ed985a..ac087ce1 100644 --- a/internal/terraform/parser/module_test.go +++ b/internal/terraform/parser/module_test.go @@ -2,13 +2,14 @@ package parser import ( "fmt" + "io/fs" + "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform-ls/internal/terraform/ast" - "github.com/spf13/afero" ) func TestParseModuleFiles(t *testing.T) { @@ -82,7 +83,7 @@ func TestParseModuleFiles(t *testing.T) { }, } - fs := afero.NewIOFS(afero.NewOsFs()) + fs := osFs{} for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.dirName), func(t *testing.T) { @@ -112,3 +113,21 @@ func mapKeys(mf ast.ModFiles) map[string]struct{} { } return m } + +type osFs struct{} + +func (osfs osFs) Open(name string) (fs.File, error) { + return os.Open(name) +} + +func (osfs osFs) Stat(name string) (fs.FileInfo, error) { + return os.Stat(name) +} + +func (osfs osFs) ReadDir(name string) ([]fs.DirEntry, error) { + return os.ReadDir(name) +} + +func (osfs osFs) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} From 17c332491439dfb58b9bf7337d4f190dac3905af Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Thu, 17 Feb 2022 13:53:19 +0000 Subject: [PATCH 10/15] internal/document: add tests and comments to Handle+DirHandle --- internal/document/dir_handle.go | 20 ++- internal/document/dir_handle_test.go | 37 ++++++ internal/document/handle.go | 14 ++ internal/document/handle_test.go | 188 +++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 internal/document/dir_handle_test.go create mode 100644 internal/document/handle_test.go diff --git a/internal/document/dir_handle.go b/internal/document/dir_handle.go index ab44ec85..4e254850 100644 --- a/internal/document/dir_handle.go +++ b/internal/document/dir_handle.go @@ -8,6 +8,10 @@ import ( "github.com/hashicorp/terraform-ls/internal/uri" ) +// DirHandle represents a directory location +// +// This may be received via LSP from the client (as URI) +// or constructed from a file path on OS FS. type DirHandle struct { URI string } @@ -16,14 +20,24 @@ func (dh DirHandle) Path() string { return uri.MustPathFromURI(dh.URI) } -func DirHandleFromPath(path string) DirHandle { - path = strings.TrimSuffix(path, fmt.Sprintf("%c", os.PathSeparator)) +// DirHandleFromPath creates a DirHandle from a given path. +// +// dirPath is expected to be a directory path (rather than document). +// It is however outside the scope of the function to verify +// this is actually the case or whether the directory exists. +func DirHandleFromPath(dirPath string) DirHandle { + dirPath = strings.TrimSuffix(dirPath, fmt.Sprintf("%c", os.PathSeparator)) return DirHandle{ - URI: uri.FromPath(path), + URI: uri.FromPath(dirPath), } } +// DirHandleFromURI creates a DirHandle from a given URI. +// +// dirUri is expected to be a directory URI (rather than document). +// It is however outside the scope of the function to verify +// this is actually the case or whether the directory exists. func DirHandleFromURI(dirUri string) DirHandle { // Dir URIs are usually without trailing separator already // but we double check anyway, so we deal with the same URI diff --git a/internal/document/dir_handle_test.go b/internal/document/dir_handle_test.go new file mode 100644 index 00000000..baba0ce7 --- /dev/null +++ b/internal/document/dir_handle_test.go @@ -0,0 +1,37 @@ +package document + +import ( + "fmt" + "testing" +) + +func TestDirHandleFromURI(t *testing.T) { + type testCase struct { + RawURI string + ExpectedHandle DirHandle + } + + testCases := []testCase{ + { + RawURI: "file:///random/path", + ExpectedHandle: DirHandle{ + URI: "file:///random/path", + }, + }, + { + RawURI: "file:///C:/random/path", + ExpectedHandle: DirHandle{ + URI: "file:///C:/random/path", + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + h := DirHandleFromURI(tc.RawURI) + if h != tc.ExpectedHandle { + t.Fatalf("expected handle: %#v, given: %#v", tc.ExpectedHandle, h) + } + }) + } +} diff --git a/internal/document/handle.go b/internal/document/handle.go index 62ebdd2a..dfdb40de 100644 --- a/internal/document/handle.go +++ b/internal/document/handle.go @@ -7,11 +7,20 @@ import ( "github.com/hashicorp/terraform-ls/internal/uri" ) +// Handle represents a document location +// +// This may be received via LSP from the client (as URI) +// or constructed from a file path on OS FS. type Handle struct { Dir DirHandle Filename string } +// HandleFromURI creates a Handle from a given URI. +// +// docURI is expected to be a document URI (rather than dir). +// It is however outside the scope of the function to verify +// this is actually the case or whether the file exists. func HandleFromURI(docUri string) Handle { path := uri.MustPathFromURI(docUri) @@ -24,6 +33,11 @@ func HandleFromURI(docUri string) Handle { } } +// HandleFromPath creates a Handle from a given path. +// +// docPath is expected to be a document path (rather than dir). +// It is however outside the scope of the function to verify +// this is actually the case or whether the file exists. func HandleFromPath(docPath string) Handle { docUri := uri.FromPath(docPath) diff --git a/internal/document/handle_test.go b/internal/document/handle_test.go new file mode 100644 index 00000000..9612c850 --- /dev/null +++ b/internal/document/handle_test.go @@ -0,0 +1,188 @@ +package document + +import ( + "fmt" + "runtime" + "testing" +) + +func TestHandleFromURI(t *testing.T) { + testCases := []struct { + RawURI string + ExpectedHandle Handle + }{ + { + RawURI: "file:///random/path/to/config.tf", + ExpectedHandle: Handle{ + Dir: DirHandle{URI: "file:///random/path/to"}, + Filename: "config.tf", + }, + }, + { + RawURI: "file:///C:/random/path/to/config.tf", + ExpectedHandle: Handle{ + Dir: DirHandle{URI: "file:///C:/random/path/to"}, + Filename: "config.tf", + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + h := HandleFromURI(tc.RawURI) + if h != tc.ExpectedHandle { + t.Fatalf("expected handle: %#v, given: %#v", tc.ExpectedHandle, h) + } + }) + } +} + +func TestHandleFromPath(t *testing.T) { + type testCase struct { + RawURI string + ExpectedHandle Handle + } + + testCases := []testCase{} + if runtime.GOOS == "windows" { + testCases = []testCase{ + { + RawURI: `C:\random\path\to\config.tf`, + ExpectedHandle: Handle{ + Dir: DirHandle{URI: "file:///C:/random/path/to"}, + Filename: "config.tf", + }, + }, + } + } else { + testCases = []testCase{ + { + RawURI: "/random/path/to/config.tf", + ExpectedHandle: Handle{ + Dir: DirHandle{URI: "file:///random/path/to"}, + Filename: "config.tf", + }, + }, + } + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + h := HandleFromPath(tc.RawURI) + if h != tc.ExpectedHandle { + t.Fatalf("expected handle: %#v, given: %#v", tc.ExpectedHandle, h) + } + }) + } +} + +func TestDirHandleFromPath(t *testing.T) { + type testCase struct { + RawURI string + ExpectedHandle DirHandle + } + + testCases := []testCase{} + if runtime.GOOS == "windows" { + testCases = []testCase{ + { + RawURI: `C:\random\path\to`, + ExpectedHandle: DirHandle{URI: "file:///C:/random/path/to"}, + }, + { + RawURI: `C:\random\path\to\`, + ExpectedHandle: DirHandle{URI: "file:///C:/random/path/to"}, + }, + } + } else { + testCases = []testCase{ + { + RawURI: "/random/path/to", + ExpectedHandle: DirHandle{URI: "file:///random/path/to"}, + }, + { + RawURI: "/random/path/to/", + ExpectedHandle: DirHandle{URI: "file:///random/path/to"}, + }, + } + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + h := DirHandleFromPath(tc.RawURI) + if h != tc.ExpectedHandle { + t.Fatalf("expected handle: %#v, given: %#v", tc.ExpectedHandle, h) + } + }) + } +} + +func TestHandle_FullURI(t *testing.T) { + type testCase struct { + Handle Handle + ExpectedURI string + } + + testCases := []testCase{ + { + Handle: Handle{ + Dir: DirHandle{URI: "file:///C:/random/path/to"}, + Filename: "config.tf", + }, + ExpectedURI: "file:///C:/random/path/to/config.tf", + }, + { + Handle: Handle{ + Dir: DirHandle{URI: "file:///random/path/to"}, + Filename: "config.tf", + }, + ExpectedURI: "file:///random/path/to/config.tf", + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + if tc.ExpectedURI != tc.Handle.FullURI() { + t.Fatalf("expected URI: %#v, given: %#v", tc.ExpectedURI, tc.Handle.FullURI()) + } + }) + } +} + +func TestHandle_FullPath(t *testing.T) { + type testCase struct { + Handle Handle + ExpectedPath string + } + + testCases := []testCase{} + if runtime.GOOS == "windows" { + testCases = []testCase{ + { + Handle: Handle{ + Dir: DirHandle{URI: "file:///C:/random/path/to"}, + Filename: "config.tf", + }, + ExpectedPath: `C:\random\path\to\config.tf`, + }, + } + } else { + testCases = []testCase{ + { + Handle: Handle{ + Dir: DirHandle{URI: "file:///random/path/to"}, + Filename: "config.tf", + }, + ExpectedPath: "/random/path/to/config.tf", + }, + } + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + if tc.ExpectedPath != tc.Handle.FullPath() { + t.Fatalf("expected path: %#v, given: %#v", tc.ExpectedPath, tc.Handle.FullPath()) + } + }) + } +} From 942456f4343a7330215120a3025cbd1d21e84a05 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Thu, 17 Feb 2022 21:01:05 +0000 Subject: [PATCH 11/15] internal/uri: refactor & add comments --- internal/uri/uri.go | 111 +++++++++++++++++++++++++---- internal/uri/uri_test.go | 116 +++++++++++++++++++++++++++++++ internal/uri/uri_unix.go | 27 ------- internal/uri/uri_unix_test.go | 49 ------------- internal/uri/uri_windows.go | 36 ---------- internal/uri/uri_windows_test.go | 50 ------------- 6 files changed, 213 insertions(+), 176 deletions(-) delete mode 100644 internal/uri/uri_unix.go delete mode 100644 internal/uri/uri_unix_test.go delete mode 100644 internal/uri/uri_windows.go delete mode 100644 internal/uri/uri_windows_test.go diff --git a/internal/uri/uri.go b/internal/uri/uri.go index 0cb3c897..0fcffcba 100644 --- a/internal/uri/uri.go +++ b/internal/uri/uri.go @@ -3,20 +3,55 @@ package uri import ( "fmt" "net/url" + "os" "path/filepath" + "strings" + "unicode" ) -func FromPath(path string) string { - p := filepath.ToSlash(path) - p = wrapPath(p) +// FromPath creates a URI from OS-specific path per RFC 8089 "file" URI Scheme +func FromPath(rawPath string) string { + // Cleaning up path trims any trailing separator + // which then (in the context of URI below) complies + // with RFC 3986 ยง 6.2.4 which is relevant in LSP. + path := filepath.Clean(rawPath) + + // Convert any OS-specific separators to '/' + path = filepath.ToSlash(path) + + volume := filepath.VolumeName(rawPath) + if isWindowsDriveVolume(volume) { + // Per RFC 8089 (Appendix F. Collected Nonstandard Rules) + // file-absolute = "/" drive-letter path-absolute + // i.e. paths with drive-letters (such as C:) are prepended + // with an additional slash. + path = "/" + path + } u := &url.URL{ Scheme: "file", - Path: p, + Path: path, } + + // Ensure that String() returns uniform escaped path at all times + escapedPath := u.EscapedPath() + if escapedPath != path { + u.RawPath = escapedPath + } + return u.String() } +// isWindowsDriveVolume returns true if the volume name has a drive letter. +// For example: C:\example. +func isWindowsDriveVolume(path string) bool { + if len(path) < 2 { + return false + } + return unicode.IsLetter(rune(path[0])) && path[1] == ':' +} + +// IsURIValid checks whether uri is a valid URI per RFC 8089 func IsURIValid(uri string) bool { _, err := parseUri(uri) if err != nil { @@ -26,24 +61,72 @@ func IsURIValid(uri string) bool { return true } -func mustParseUri(uri string) string { - u, err := parseUri(uri) +// PathFromURI extracts OS-specific path from an RFC 8089 "file" URI Scheme +func PathFromURI(rawUri string) (string, error) { + uri, err := parseUri(rawUri) + if err != nil { + return "", err + } + + // Convert '/' to any OS-specific separators + osPath := filepath.FromSlash(uri.Path) + + // Upstream net/url parser prefers consistency and reusability + // (e.g. in HTTP servers) which complies with + // the Comparison Ladder as defined in ยง 6.2 of RFC 3968. + // https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 + // + // Cleaning up path trims any trailing separator + // which then still complies with RFC 3986 per ยง 6.2.4 + // which is relevant in LSP. + osPath = filepath.Clean(osPath) + + trimmedOsPath := trimLeftPathSeparator(osPath) + if strings.HasSuffix(filepath.VolumeName(trimmedOsPath), ":") { + // Per RFC 8089 (Appendix F. Collected Nonstandard Rules) + // file-absolute = "/" drive-letter path-absolute + // i.e. paths with drive-letters (such as C:) are preprended + // with an additional slash (which we converted to OS separator above) + // which we trim here. + // See also https://github.com/golang/go/issues/6027 + osPath = trimmedOsPath + } + + return osPath, nil +} + +func trimLeftPathSeparator(s string) string { + return strings.TrimLeftFunc(s, func(r rune) bool { + return r == os.PathSeparator + }) +} + +func MustPathFromURI(uri string) string { + osPath, err := PathFromURI(uri) if err != nil { panic(fmt.Sprintf("invalid URI: %s", uri)) } - return u + return osPath } -func parseUri(uri string) (string, error) { - u, err := url.ParseRequestURI(uri) +func parseUri(rawUri string) (*url.URL, error) { + uri, err := url.ParseRequestURI(rawUri) if err != nil { - return "", err + return nil, err } - if u.Scheme != "file" { - return "", fmt.Errorf("unexpected scheme %q in URI %q", - u.Scheme, uri) + if uri.Scheme != "file" { + return nil, fmt.Errorf("unexpected scheme %q in URI %q", + uri.Scheme, rawUri) } - return url.PathUnescape(u.Path) + // Upstream net/url parser prefers consistency and reusability + // (e.g. in HTTP servers) which complies with + // the Comparison Ladder as defined in ยง 6.2 of RFC 3968. + // https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 + // Here we essentially just implement ยง 6.2.4 + // as it is relevant in LSP (which uses the file scheme). + uri.Path = strings.TrimSuffix(uri.Path, "/") + + return uri, nil } diff --git a/internal/uri/uri_test.go b/internal/uri/uri_test.go index b9af80d6..fe761fa0 100644 --- a/internal/uri/uri_test.go +++ b/internal/uri/uri_test.go @@ -1,6 +1,8 @@ package uri import ( + "fmt" + "runtime" "testing" ) @@ -10,3 +12,117 @@ func TestIsURIValid_invalid(t *testing.T) { t.Fatalf("Expected %q to be invalid", uri) } } + +func TestFromPath(t *testing.T) { + type testCase struct { + RawPath string + ExpectedURI string + } + testCases := []testCase{} + + if runtime.GOOS == "windows" { + // windows + testCases = []testCase{ + { + RawPath: `C:\Users\With Space\file.tf`, + ExpectedURI: "file:///C:/Users/With%20Space/file.tf", + }, + { + RawPath: `C:\Users\WithoutSpace\file.tf`, + ExpectedURI: "file:///C:/Users/WithoutSpace/file.tf", + }, + { + RawPath: `C:\Users\TrailingSeparator\`, + ExpectedURI: "file:///C:/Users/TrailingSeparator", + }, + } + } else { + // unix + testCases = []testCase{ + { + RawPath: "/random/path/with space", + ExpectedURI: "file:///random/path/with%20space", + }, + { + RawPath: "/random/path", + ExpectedURI: "file:///random/path", + }, + { + RawPath: `/path/with/trailing-separator/`, + ExpectedURI: "file:///path/with/trailing-separator", + }, + } + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + uri := FromPath(tc.RawPath) + if uri != tc.ExpectedURI { + t.Fatalf("URI doesn't match.\nExpected: %q\nGiven: %q", + tc.ExpectedURI, uri) + } + }) + } +} + +func TestPathFromURI(t *testing.T) { + type testCase struct { + URI string + ExpectedPath string + } + testCases := []testCase{} + + if runtime.GOOS == "windows" { + // windows + testCases = []testCase{ + { + URI: "file:///C:/Users/With%20Space/tf-test/file.tf", + ExpectedPath: `C:\Users\With Space\tf-test\file.tf`, + }, + { + URI: "file:///C:/Users/With%20Space/tf-test", + ExpectedPath: `C:\Users\With Space\tf-test`, + }, + // Ensure URI with trailing slash is trimmed per RFC 3986 ยง 6.2.4 + { + URI: "file:///C:/Users/Test/tf-test/", + ExpectedPath: `C:\Users\Test\tf-test`, + }, + } + } else { + // unix + testCases = []testCase{ + { + URI: "file:///valid/path/to/file.tf", + ExpectedPath: "/valid/path/to/file.tf", + }, + { + URI: "file:///valid/path/to", + ExpectedPath: "/valid/path/to", + }, + + // Ensure URI with trailing slash is trimmed per RFC 3986 ยง 6.2.4 + { + URI: "file:///random/dir/", + ExpectedPath: "/random/dir", + }, + } + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + if !IsURIValid(tc.URI) { + t.Fatalf("Expected %q to be valid", tc.URI) + } + + path, err := PathFromURI(tc.URI) + if err != nil { + t.Fatal(err) + } + if path != tc.ExpectedPath { + t.Fatalf("Expected full path: %q, given: %q", + tc.ExpectedPath, path) + } + }) + } +} diff --git a/internal/uri/uri_unix.go b/internal/uri/uri_unix.go deleted file mode 100644 index 6d20b394..00000000 --- a/internal/uri/uri_unix.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build !windows -// +build !windows - -package uri - -import ( - "path/filepath" -) - -// wrapPath is no-op for unix-style paths -func wrapPath(path string) string { - return path -} - -func PathFromURI(uri string) (string, error) { - p, err := parseUri(uri) - if err != nil { - return "", err - } - - return filepath.FromSlash(p), nil -} - -func MustPathFromURI(uri string) string { - p := mustParseUri(uri) - return filepath.FromSlash(p) -} diff --git a/internal/uri/uri_unix_test.go b/internal/uri/uri_unix_test.go deleted file mode 100644 index 17e41f5c..00000000 --- a/internal/uri/uri_unix_test.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build !windows -// +build !windows - -package uri - -import ( - "testing" -) - -func TestURIFromPath(t *testing.T) { - path := "/random/path" - uri := FromPath(path) - - expectedURI := "file:///random/path" - if uri != expectedURI { - t.Fatalf("URI doesn't match.\nExpected: %q\nGiven: %q", - expectedURI, uri) - } -} - -func TestPathFromURI_valid_unixFile(t *testing.T) { - uri := "file:///valid/path/to/file.tf" - if !IsURIValid(uri) { - t.Fatalf("Expected %q to be valid", uri) - } - - expectedFullPath := "/valid/path/to/file.tf" - path, err := PathFromURI(uri) - if err != nil { - t.Fatal(err) - } - if path != expectedFullPath { - t.Fatalf("Expected full path: %q, given: %q", - expectedFullPath, path) - } -} - -func TestPathFromURI_valid_unixDir(t *testing.T) { - uri := "file:///valid/path/to" - expectedDir := "/valid/path/to" - path, err := PathFromURI(uri) - if err != nil { - t.Fatal(err) - } - if path != expectedDir { - t.Fatalf("Expected dir: %q, given: %q", - expectedDir, path) - } -} diff --git a/internal/uri/uri_windows.go b/internal/uri/uri_windows.go deleted file mode 100644 index db084e2a..00000000 --- a/internal/uri/uri_windows.go +++ /dev/null @@ -1,36 +0,0 @@ -package uri - -import ( - "path/filepath" - "strings" -) - -// wrapPath prepends Windows-style paths (C:\path) -// with an additional slash to account for an empty hostname -// in a valid file-scheme URI per RFC 8089 -func wrapPath(path string) string { - return "/" + path -} - -func PathFromURI(uri string) (string, error) { - p, err := parseUri(uri) - if err != nil { - return "", err - } - - p = strings.TrimPrefix(p, "/") - - return filepath.FromSlash(p), nil -} - -// MustPathFromURI on Windows strips the leading '/' -// which occurs in Windows-style paths (e.g. file:///C:/) -// as url.URL methods don't account for that -// (see golang/go#6027). -func MustPathFromURI(uri string) string { - p := mustParseUri(uri) - - p = strings.TrimPrefix(p, "/") - - return filepath.FromSlash(p) -} diff --git a/internal/uri/uri_windows_test.go b/internal/uri/uri_windows_test.go deleted file mode 100644 index 76f069ba..00000000 --- a/internal/uri/uri_windows_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package uri - -import ( - "testing" -) - -func TestFromPath(t *testing.T) { - path := `C:\Users\With Space\file.tf` - uri := FromPath(path) - - expectedURI := "file:///C:/Users/With%20Space/file.tf" - if uri != expectedURI { - t.Fatalf("URI doesn't match.\nExpected: %q\nGiven: %q", - expectedURI, uri) - } -} - -func TestPathFromURI_valid_windowsFile(t *testing.T) { - uri := "file:///C:/Users/With%20Space/tf-test/file.tf" - if !IsURIValid(uri) { - t.Fatalf("Expected %q to be valid", uri) - } - - expectedPath := `C:\Users\With Space\tf-test\file.tf` - path, err := PathFromURI(uri) - if err != nil { - t.Fatal(err) - } - if path != expectedPath { - t.Fatalf("Expected full path: %q, given: %q", - expectedPath, path) - } -} - -func TestPathFromURI_valid_windowsDir(t *testing.T) { - uri := "file:///C:/Users/With%20Space/tf-test" - if !IsURIValid(uri) { - t.Fatalf("Expected %q to be valid", uri) - } - - expectedPath := `C:\Users\With Space\tf-test` - path, err := PathFromURI(uri) - if err != nil { - t.Fatal(err) - } - if path != expectedPath { - t.Fatalf("Expected full path: %q, given: %q", - expectedPath, path) - } -} From 162e3a8a3cb8287a8312e05813ca1a8b8141ea5c Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 18 Feb 2022 13:34:35 +0000 Subject: [PATCH 12/15] internal/uri: introduce MustParseURI --- internal/uri/uri.go | 13 +++++++++++++ internal/uri/uri_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/internal/uri/uri.go b/internal/uri/uri.go index 0fcffcba..59824198 100644 --- a/internal/uri/uri.go +++ b/internal/uri/uri.go @@ -95,6 +95,19 @@ func PathFromURI(rawUri string) (string, error) { return osPath, nil } +// MustParseURI returns a normalized RFC 8089 URI. +// It will panic if rawUri is invalid. +// +// Use IsURIValid for checking validity upfront. +func MustParseURI(rawUri string) string { + uri, err := parseUri(rawUri) + if err != nil { + panic(fmt.Sprintf("invalid URI: %s", uri)) + } + + return uri.String() +} + func trimLeftPathSeparator(s string) string { return strings.TrimLeftFunc(s, func(r rune) bool { return r == os.PathSeparator diff --git a/internal/uri/uri_test.go b/internal/uri/uri_test.go index fe761fa0..4f42a1e8 100644 --- a/internal/uri/uri_test.go +++ b/internal/uri/uri_test.go @@ -126,3 +126,30 @@ func TestPathFromURI(t *testing.T) { }) } } + +func TestMustParseURI(t *testing.T) { + type testCase struct { + RawURI string + ExpectedURI string + } + + testCases := []testCase{ + { + RawURI: "file:///C:/Users/With Space/tf-test/file.tf", + ExpectedURI: "file:///C:/Users/With%20Space/tf-test/file.tf", + }, + { + RawURI: "file:///C:/Users/With%20Space/tf-test/file.tf", + ExpectedURI: "file:///C:/Users/With%20Space/tf-test/file.tf", + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + uri := MustParseURI(tc.RawURI) + if tc.ExpectedURI != uri { + t.Fatalf("Expected %q, given %q", tc.ExpectedURI, uri) + } + }) + } +} From 2bb1bf379dd3dc44d80cd50e9521f9484cbaa604 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 21 Feb 2022 11:42:21 +0000 Subject: [PATCH 13/15] internal/uri: account for VSCode's over-escaping of colon --- internal/uri/uri.go | 12 ++++++++++++ internal/uri/uri_test.go | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/internal/uri/uri.go b/internal/uri/uri.go index 59824198..7543ac75 100644 --- a/internal/uri/uri.go +++ b/internal/uri/uri.go @@ -141,5 +141,17 @@ func parseUri(rawUri string) (*url.URL, error) { // as it is relevant in LSP (which uses the file scheme). uri.Path = strings.TrimSuffix(uri.Path, "/") + // Upstream net/url parser (correctly) escapes only + // non-ASCII characters as per ยง 2.1 of RFC 3986. + // https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 + // Unfortunately VSCode effectively violates that section + // by escaping ASCII characters such as colon. + // See https://github.com/microsoft/vscode/issues/75027 + // + // To account for this we reset RawPath which would + // otherwise be used by String() to effectively enforce + // clean re-escaping of the (unescaped) Path. + uri.RawPath = "" + return uri, nil } diff --git a/internal/uri/uri_test.go b/internal/uri/uri_test.go index 4f42a1e8..96cb319e 100644 --- a/internal/uri/uri_test.go +++ b/internal/uri/uri_test.go @@ -88,6 +88,11 @@ func TestPathFromURI(t *testing.T) { URI: "file:///C:/Users/Test/tf-test/", ExpectedPath: `C:\Users\Test\tf-test`, }, + // Ensure over-escaped colon (which may come from VS Code) is normalized + { + URI: "file:///C%3A/Users/With%20Space/tf-test", + ExpectedPath: `C:\Users\With Space\tf-test`, + }, } } else { // unix @@ -142,6 +147,11 @@ func TestMustParseURI(t *testing.T) { RawURI: "file:///C:/Users/With%20Space/tf-test/file.tf", ExpectedURI: "file:///C:/Users/With%20Space/tf-test/file.tf", }, + // Ensure over-escaped colon (which may come from VS Code) is normalized + { + RawURI: "file:///C%3A/Users/With%20Space/tf-test/file.tf", + ExpectedURI: "file:///C:/Users/With%20Space/tf-test/file.tf", + }, } for i, tc := range testCases { From 5af49bee1f9b6da86ced6ac45513a43a89117809 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 18 Feb 2022 13:30:19 +0000 Subject: [PATCH 14/15] internal/document: clean up handle logic & normalize URIs before saving --- internal/document/dir_handle.go | 12 ++---------- internal/document/dir_handle_test.go | 6 ++++++ internal/document/handle.go | 17 +++++++---------- internal/document/handle_test.go | 7 +++++++ 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/internal/document/dir_handle.go b/internal/document/dir_handle.go index 4e254850..46c738df 100644 --- a/internal/document/dir_handle.go +++ b/internal/document/dir_handle.go @@ -1,10 +1,6 @@ package document import ( - "fmt" - "os" - "strings" - "github.com/hashicorp/terraform-ls/internal/uri" ) @@ -26,8 +22,6 @@ func (dh DirHandle) Path() string { // It is however outside the scope of the function to verify // this is actually the case or whether the directory exists. func DirHandleFromPath(dirPath string) DirHandle { - dirPath = strings.TrimSuffix(dirPath, fmt.Sprintf("%c", os.PathSeparator)) - return DirHandle{ URI: uri.FromPath(dirPath), } @@ -39,10 +33,8 @@ func DirHandleFromPath(dirPath string) DirHandle { // It is however outside the scope of the function to verify // this is actually the case or whether the directory exists. func DirHandleFromURI(dirUri string) DirHandle { - // Dir URIs are usually without trailing separator already - // but we double check anyway, so we deal with the same URI - // regardless of language client differences - dirUri = strings.TrimSuffix(string(dirUri), "/") + // Normalize the raw URI to account for any escaping differences + dirUri = uri.MustParseURI(dirUri) return DirHandle{ URI: dirUri, diff --git a/internal/document/dir_handle_test.go b/internal/document/dir_handle_test.go index baba0ce7..77480193 100644 --- a/internal/document/dir_handle_test.go +++ b/internal/document/dir_handle_test.go @@ -24,6 +24,12 @@ func TestDirHandleFromURI(t *testing.T) { URI: "file:///C:/random/path", }, }, + { + RawURI: "file:///C%3A/random/path", + ExpectedHandle: DirHandle{ + URI: "file:///C:/random/path", + }, + }, } for i, tc := range testCases { diff --git a/internal/document/handle.go b/internal/document/handle.go index dfdb40de..5623b533 100644 --- a/internal/document/handle.go +++ b/internal/document/handle.go @@ -1,10 +1,11 @@ package document import ( + "fmt" + "os" + "path" "path/filepath" "strings" - - "github.com/hashicorp/terraform-ls/internal/uri" ) // Handle represents a document location @@ -22,13 +23,11 @@ type Handle struct { // It is however outside the scope of the function to verify // this is actually the case or whether the file exists. func HandleFromURI(docUri string) Handle { - path := uri.MustPathFromURI(docUri) - - filename := filepath.Base(path) + filename := path.Base(docUri) dirUri := strings.TrimSuffix(docUri, "/"+filename) return Handle{ - Dir: DirHandle{URI: dirUri}, + Dir: DirHandleFromURI(dirUri), Filename: filename, } } @@ -39,13 +38,11 @@ func HandleFromURI(docUri string) Handle { // It is however outside the scope of the function to verify // this is actually the case or whether the file exists. func HandleFromPath(docPath string) Handle { - docUri := uri.FromPath(docPath) - filename := filepath.Base(docPath) - dirUri := strings.TrimSuffix(docUri, "/"+filename) + dirPath := strings.TrimSuffix(docPath, fmt.Sprintf("%c%s", os.PathSeparator, filename)) return Handle{ - Dir: DirHandle{URI: dirUri}, + Dir: DirHandleFromPath(dirPath), Filename: filename, } } diff --git a/internal/document/handle_test.go b/internal/document/handle_test.go index 9612c850..0115c39a 100644 --- a/internal/document/handle_test.go +++ b/internal/document/handle_test.go @@ -25,6 +25,13 @@ func TestHandleFromURI(t *testing.T) { Filename: "config.tf", }, }, + { + RawURI: "file:///C%3A/random/path/to/config.tf", + ExpectedHandle: Handle{ + Dir: DirHandle{URI: "file:///C:/random/path/to"}, + Filename: "config.tf", + }, + }, } for i, tc := range testCases { From 1aac0858b8e4fb0c9fb02b611bb4158c9de1e351 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 21 Feb 2022 15:50:13 +0000 Subject: [PATCH 15/15] internal/uri: account for drive-letter normalization in VSCode --- .../handlers/did_change_workspace_folders.go | 13 ++------ internal/langserver/handlers/initialize.go | 2 +- internal/uri/uri.go | 30 +++++++++++++++++++ internal/uri/uri_test.go | 15 ++++++++++ 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/internal/langserver/handlers/did_change_workspace_folders.go b/internal/langserver/handlers/did_change_workspace_folders.go index 4683e9ed..17d6504f 100644 --- a/internal/langserver/handlers/did_change_workspace_folders.go +++ b/internal/langserver/handlers/did_change_workspace_folders.go @@ -27,7 +27,7 @@ func (lh *logHandler) DidChangeWorkspaceFolders(ctx context.Context, params lsp. } for _, removed := range params.Event.Removed { - modPath, err := pathFromDocumentURI(removed.URI) + modPath, err := uri.PathFromURI(removed.URI) if err != nil { jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ Type: lsp.Warning, @@ -55,7 +55,7 @@ func (lh *logHandler) DidChangeWorkspaceFolders(ctx context.Context, params lsp. } for _, added := range params.Event.Added { - modPath, err := pathFromDocumentURI(added.URI) + modPath, err := uri.PathFromURI(added.URI) if err != nil { jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ Type: lsp.Warning, @@ -75,12 +75,3 @@ func (lh *logHandler) DidChangeWorkspaceFolders(ctx context.Context, params lsp. return nil } - -func pathFromDocumentURI(docUri string) (string, error) { - rawPath, err := uri.PathFromURI(docUri) - if err != nil { - return "", err - } - - return cleanupPath(rawPath) -} diff --git a/internal/langserver/handlers/initialize.go b/internal/langserver/handlers/initialize.go index ab72ef5d..1d957b67 100644 --- a/internal/langserver/handlers/initialize.go +++ b/internal/langserver/handlers/initialize.go @@ -215,7 +215,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) if len(params.WorkspaceFolders) > 0 { for _, folderPath := range params.WorkspaceFolders { - modPath, err := pathFromDocumentURI(folderPath.URI) + modPath, err := uri.PathFromURI(folderPath.URI) if err != nil { jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ Type: lsp.Warning, diff --git a/internal/uri/uri.go b/internal/uri/uri.go index 7543ac75..8e1f386d 100644 --- a/internal/uri/uri.go +++ b/internal/uri/uri.go @@ -21,6 +21,14 @@ func FromPath(rawPath string) string { volume := filepath.VolumeName(rawPath) if isWindowsDriveVolume(volume) { + // VSCode normalizes drive letters for unknown reasons. + // See https://github.com/microsoft/vscode/issues/42159#issuecomment-360533151 + // While it is a relatively safe assumption that letters are + // case insensitive, this doesn't seem to be documented anywhere. + // + // We just account for VSCode's past decisions here. + path = strings.ToUpper(string(path[0])) + path[1:] + // Per RFC 8089 (Appendix F. Collected Nonstandard Rules) // file-absolute = "/" drive-letter path-absolute // i.e. paths with drive-letters (such as C:) are prepended @@ -153,5 +161,27 @@ func parseUri(rawUri string) (*url.URL, error) { // clean re-escaping of the (unescaped) Path. uri.RawPath = "" + // The upstream net/url parser (correctly) does not interpret Path + // within URI based on the filesystem or OS where they may (or may not) + // be pointing. + // VSCode normalizes drive letters for unknown reasons. + // See https://github.com/microsoft/vscode/issues/42159#issuecomment-360533151 + // While it is a relatively safe assumption that letters are + // case insensitive, this doesn't seem to be documented anywhere. + // + // We just account for VSCode's past decisions here. + if isLikelyWindowsDriveURIPath(uri.Path) { + uri.Path = string(uri.Path[0]) + strings.ToUpper(string(uri.Path[1])) + uri.Path[2:] + } + return uri, nil } + +// isLikelyWindowsDrivePath returns true if the URI path is of the form used by +// Windows URIs. We check if the URI path has a drive prefix (e.g. "/C:") +func isLikelyWindowsDriveURIPath(uriPath string) bool { + if len(uriPath) < 4 { + return false + } + return uriPath[0] == '/' && unicode.IsLetter(rune(uriPath[1])) && uriPath[2] == ':' +} diff --git a/internal/uri/uri_test.go b/internal/uri/uri_test.go index 96cb319e..a3b3386d 100644 --- a/internal/uri/uri_test.go +++ b/internal/uri/uri_test.go @@ -35,6 +35,11 @@ func TestFromPath(t *testing.T) { RawPath: `C:\Users\TrailingSeparator\`, ExpectedURI: "file:///C:/Users/TrailingSeparator", }, + // Ensure any-cased drive letter (which may come from VS Code) is uppercased + { + RawPath: `c:\test`, + ExpectedURI: "file:///C:/test", + }, } } else { // unix @@ -93,6 +98,11 @@ func TestPathFromURI(t *testing.T) { URI: "file:///C%3A/Users/With%20Space/tf-test", ExpectedPath: `C:\Users\With Space\tf-test`, }, + // Ensure any-cased drive letter (which may come from VS Code) is uppercased + { + URI: "file:///c:/tf-test", + ExpectedPath: `C:\tf-test`, + }, } } else { // unix @@ -152,6 +162,11 @@ func TestMustParseURI(t *testing.T) { RawURI: "file:///C%3A/Users/With%20Space/tf-test/file.tf", ExpectedURI: "file:///C:/Users/With%20Space/tf-test/file.tf", }, + // Ensure any-cased drive letter (which may come from VS Code) is uppercased + { + RawURI: "file:///c:/tf-test", + ExpectedURI: "file:///C:/tf-test", + }, } for i, tc := range testCases {