lang/langserver: Simple implementation of Formatting

Mainly just to have a proof-of-concept of this doing something, this is a
simple implementation of the Formatting method that just replaces the
whole buffer with the hclwrite result.

In future we should make this instead return surgical TextEdits, but we'll
need more infrastructure in place here before that is possible.
f-langserver
Martin Atkins 8 years ago committed by Radek Simko
parent fe819762b8
commit e4664a1abe
No known key found for this signature in database
GPG Key ID: 1F1C84FE689A88D7

@ -3,6 +3,12 @@ package langserver
import (
"fmt"
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/terraform/tfdiags"
)
type filesystem struct {
@ -15,12 +21,18 @@ type dir struct {
}
type file struct {
content []byte
open bool
fullPath string
content []byte
open bool
// TODO: use a piece table to track edits and flatten into content
// only when we need to produce a contiguous buffer to parse it.
// (That'll let us implement the incremental sync mode in the LSP.)
errs bool
diags tfdiags.Diagnostics
ast *hcl.File
wrAST *hclwrite.File
}
func newFilesystem() *filesystem {
@ -37,7 +49,7 @@ func (fs *filesystem) Open(u uri, s []byte) error {
return fmt.Errorf("invalid URL to open")
}
dn, fn := u.DirFilename()
fullName, dn, fn := u.PathParts()
d, ok := fs.dirs[dn]
if !ok {
d = newDir()
@ -45,7 +57,7 @@ func (fs *filesystem) Open(u uri, s []byte) error {
}
f, ok := d.files[fn]
if !ok {
f = newFile()
f = newFile(fullName)
}
f.content = s
f.open = true
@ -57,10 +69,6 @@ func (fs *filesystem) Change(u uri, s []byte) error {
fs.mu.Lock()
defer fs.mu.Unlock()
if !u.Valid() {
return fmt.Errorf("invalid URL to change")
}
f := fs.file(u)
if f == nil || !f.open {
return fmt.Errorf("file %q is not open", u)
@ -73,24 +81,50 @@ func (fs *filesystem) Close(u uri) error {
fs.mu.Lock()
defer fs.mu.Unlock()
if !u.Valid() {
return fmt.Errorf("invalid URL to close")
}
dn, fn := u.DirFilename()
f := fs.file(u)
if f == nil || !f.open {
return fmt.Errorf("file %q is not open", u)
}
_, dn, fn := u.PathParts()
delete(fs.dirs[dn].files, fn)
return nil
}
func (fs *filesystem) Format(u uri) ([]byte, error) {
fs.mu.Lock()
defer fs.mu.Unlock()
f := fs.file(u)
if f == nil || !f.open {
return nil, fmt.Errorf("file %q is not open", u)
}
s, changed := formatSource(f.content)
if changed {
f.change(s)
}
return s, nil
}
func (fs *filesystem) FileAST(u uri) *hcl.File {
fs.mu.Lock()
defer fs.mu.Unlock()
// FIXME: This method should work with non-open files too, reading them
// from disk and caching them.
f := fs.file(u)
if f == nil {
return nil
}
return f.hclAST()
}
func (fs *filesystem) file(u uri) *file {
if !u.Valid() {
return nil
}
dn, fn := u.DirFilename()
_, dn, fn := u.PathParts()
d, ok := fs.dirs[dn]
if !ok {
return nil
@ -104,10 +138,60 @@ func newDir() *dir {
}
}
func newFile() *file {
return &file{}
func newFile(fullPath string) *file {
return &file{fullPath: fullPath}
}
func (f *file) diagnostics() tfdiags.Diagnostics {
if f.diags != nil {
return f.diags
}
// FIXME: Unfortunate that we just keep re-parsing every time
// if there are no errors.
_ = f.hclAST()
return f.diags
}
func (f *file) hclAST() *hcl.File {
if f.errs {
return nil
}
if f.ast != nil {
return f.ast
}
hf, diags := hclsyntax.ParseConfig(f.content, f.fullPath, hcl.Pos{Line: 1, Column: 1})
f.diags = nil
f.diags = f.diags.Append(diags)
if diags.HasErrors() {
f.errs = true
return nil
}
f.ast = hf
return hf
}
func (f *file) hclWriteAST() *hclwrite.File {
if f.errs {
return nil
}
if f.wrAST != nil {
return f.wrAST
}
hf, diags := hclwrite.ParseConfig(f.content, f.fullPath, hcl.Pos{Line: 1, Column: 1})
f.diags = nil
f.diags = f.diags.Append(diags)
if diags.HasErrors() {
f.errs = true
return nil
}
f.wrAST = hf
return hf
}
func (f *file) change(s []byte) {
f.content = s
f.wrAST = nil
f.ast = nil
f.diags = nil
f.errs = false
}

@ -0,0 +1,21 @@
package langserver
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
)
// formatSource will apply hclwrite formatting to the given source code
// if it has valid syntax, or just return it verbatim if not. The second
// argument is true if the returned buffer might be different than the given
// buffer, or false if it's guaranteed identical (allowing the caller to skip
// invalidating caches, etc.)
func formatSource(in []byte) ([]byte, bool) {
_, diags := hclsyntax.ParseConfig(in, "", hcl.Pos{})
if diags.HasErrors() {
return in, false
}
return hclwrite.Format(in), true
}

@ -46,6 +46,7 @@ func (s *server) Initialize(context.Context, *lsp.InitializeParams) (*lsp.Initia
log.Printf("[DEBUG] langserver: Initialize")
return &lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
DocumentFormattingProvider: true,
TextDocumentSync: lsp.TextDocumentSyncOptions{
OpenClose: true,
@ -228,9 +229,38 @@ func (s *server) ColorPresentation(context.Context, *lsp.ColorPresentationParams
return nil, notImplemented("ColorPresentation")
}
func (s *server) Formatting(context.Context, *lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) {
func (s *server) Formatting(ctx context.Context, req *lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) {
log.Printf("[DEBUG] langserver: Formatting")
return nil, notImplemented("Formatting")
u := uri(req.TextDocument.URI)
new, err := s.fs.Format(u)
if err != nil {
return nil, err
}
// For now we just replace the entire file, but that means we need
// the whole file's source range.
astF := s.fs.FileAST(u)
if astF == nil {
// Shouldn't happen if formatting succeeded, but we'll accept it
// anyway to avoid a panic.
return nil, fmt.Errorf("failed to parse %q", u)
}
return []lsp.TextEdit{
{
Range: lsp.Range{
Start: lsp.Position{Line: 0, Character: 0},
// Since HCL's line numbers are 1-based but LSP is 0-based,
// this is intentionally pointing to one line _after_ the
// end of our file, so we don't need to convert the
// column number from grapheme clusters to UTF-16 code units.
// This is a cheat and we should implement incremental
// edits here rather than just overwriting the whole thing
// every time.
End: lsp.Position{Line: float64(astF.Body.MissingItemRange().End.Line), Character: 0},
},
NewText: string(new),
},
}, nil
}
func (s *server) RangeFormatting(context.Context, *lsp.DocumentRangeFormattingParams) ([]lsp.TextEdit, error) {

@ -39,9 +39,9 @@ func (u uri) Filename() string {
return filepath.Base(u.FullPath())
}
func (u uri) DirFilename() (dir, filename string) {
full := u.FullPath()
func (u uri) PathParts() (full, dir, filename string) {
full = u.FullPath()
dir = filepath.Dir(full)
filename = filepath.Base(full)
return dir, filename
return full, dir, filename
}

Loading…
Cancel
Save