diff --git a/ls/ls.go b/ls/ls.go index 1c4c48eda2..096c76cab5 100644 --- a/ls/ls.go +++ b/ls/ls.go @@ -7,7 +7,7 @@ package tmls import ( "context" "encoding/json" - "fmt" + stdfmt "fmt" "os" "path/filepath" "sort" @@ -18,6 +18,7 @@ import ( "github.com/terramate-io/terramate/config" "github.com/terramate-io/terramate/errors" "github.com/terramate-io/terramate/hcl" + "github.com/terramate-io/terramate/hcl/fmt" "go.lsp.dev/jsonrpc2" lsp "go.lsp.dev/protocol" "go.lsp.dev/uri" @@ -68,6 +69,7 @@ func (s *Server) buildHandlers() { lsp.MethodTextDocumentDidChange: s.handleDocumentChange, lsp.MethodTextDocumentDidSave: s.handleDocumentSaved, lsp.MethodTextDocumentCompletion: s.handleCompletion, + lsp.MethodTextDocumentFormatting: s.handleFormatting, // commands MethodExecuteCommand: s.handleExecuteCommand, @@ -116,7 +118,8 @@ func (s *Server) handleInitialize( s.workspace = string(uri.New(params.RootURI).Filename()) err := reply(ctx, lsp.InitializeResult{ Capabilities: lsp.ServerCapabilities{ - CompletionProvider: &lsp.CompletionOptions{}, + DocumentFormattingProvider: true, + CompletionProvider: &lsp.CompletionOptions{}, // if we support `goto` definition. DefinitionProvider: false, @@ -197,7 +200,7 @@ func (s *Server) handleDocumentChange( } if len(params.ContentChanges) != 1 { - err := fmt.Errorf("expected content changes = 1, got = %d", len(params.ContentChanges)) + err := stdfmt.Errorf("expected content changes = 1, got = %d", len(params.ContentChanges)) log.Error().Err(err).Send() return err } @@ -300,6 +303,52 @@ func (s *Server) handleCompletion( return reply(ctx, nil, nil) } +func (s *Server) handleFormatting( + ctx context.Context, + reply jsonrpc2.Replier, + r jsonrpc2.Request, + log zerolog.Logger, +) error { + var params lsp.CompletionParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + log.Error().Err(err).Msg("failed to unmarshal params") + return jsonrpc2.ErrParse + } + log.Debug().Str("params", string(r.Params())) + fname := params.TextDocument.URI.Filename() + content, err := os.ReadFile(fname) + if err != nil { + log.Error().Err(err).Msg("failed to read file for formatting") + return reply(ctx, nil, err) + } + formatted, err := fmt.Format(string(content), fname) + if err != nil { + log.Error().Err(err).Msg("failed to format file") + return reply(ctx, nil, err) + } + log.Info().Msgf("formatted:'%s'", formatted) + oldlines := strings.Split(string(content), "\n") + return reply(ctx, []lsp.TextEdit{ + // remove old content + { + NewText: "", + Range: lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: 0, + }, + End: lsp.Position{ + Line: uint32(len(oldlines) - 1), + Character: uint32(len(oldlines[len(oldlines)-1])), + }, + }, + }, + { + NewText: formatted, + }, + }, nil) +} + func (s *Server) sendDiagnostics(ctx context.Context, uri lsp.URI, diags []lsp.Diagnostic) { err := s.conn.Notify(ctx, lsp.MethodTextDocumentPublishDiagnostics, lsp.PublishDiagnosticsParams{ URI: uri, diff --git a/ls/ls_test.go b/ls/ls_test.go index f62d67fcc6..95aeb9d794 100644 --- a/ls/ls_test.go +++ b/ls/ls_test.go @@ -6,6 +6,7 @@ package tmls_test import ( "encoding/json" "fmt" + "path" "path/filepath" "sort" "testing" @@ -45,6 +46,42 @@ func TestDocumentOpen(t *testing.T) { params.URI.Filename()) } +func TestDocumentFormat(t *testing.T) { + t.Parallel() + f := lstest.Setup(t) + + stack := f.Sandbox.CreateStack("stack") + f.Editor.CheckInitialize(f.Sandbox.RootDir()) + + fileContent := " stack {\n }" + stack.CreateFile(stackpkg.DefaultFilename, fileContent) + gotEdits, err := f.Editor.Format(path.Join(stack.RelPath(), stackpkg.DefaultFilename)) + assert.NoError(t, err) + + want := []lsp.TextEdit{ + { + NewText: "", + Range: lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: 0, + }, + End: lsp.Position{ + Line: 1, + Character: 3, + }, + }, + }, + { + NewText: "stack {\n}", + }, + } + + if diff := cmp.Diff(gotEdits, want); diff != "" { + t.Fatalf(diff) + } +} + func TestDocumentOpenWithoutRootConfig(t *testing.T) { t.Parallel() f := lstest.SetupNoRootConfig(t) diff --git a/test/ls/editor.go b/test/ls/editor.go index 9b72142fc9..6a2f11362c 100644 --- a/test/ls/editor.go +++ b/test/ls/editor.go @@ -128,6 +128,20 @@ func (e *Editor) Change(path, content string) { assert.NoError(t, err, "call %q", lsp.MethodTextDocumentDidChange) } +func (e *Editor) Format(path string) ([]lsp.TextEdit, error) { + t := e.t + t.Helper() + abspath := filepath.Join(e.sandbox.RootDir(), path) + var edits []lsp.TextEdit + _, err := e.call(lsp.MethodTextDocumentFormatting, lsp.DocumentFormattingParams{ + TextDocument: lsp.TextDocumentIdentifier{ + URI: uri.File(abspath), + }, + }, &edits) + + return edits, err +} + // Command invokes the provided command in the LSP server. func (e *Editor) Command(cmd lsp.ExecuteCommandParams) (interface{}, error) { t := e.t @@ -150,6 +164,7 @@ func DefaultInitializeResult() lsp.InitializeResult { "openClose": true, "save": map[string]interface{}{}, }, + DocumentFormattingProvider: true, }, } }