From 82ff1bb7c3da8b4da27bba93667f6b7bb5ebc692 Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 3 Apr 2024 15:50:09 -0700 Subject: [PATCH] fix: parse out word for errors and clear them (#1163) https://github.com/TBD54566975/ftl/assets/51647/eddab587-ae5b-4786-99dd-92434b38d729 --- buildengine/engine.go | 19 +++++++++ cmd/ftl/cmd_dev.go | 10 ++++- lsp/logger.go | 62 --------------------------- lsp/lsp.go | 97 ++++++++++++++++++++++++++++++++++--------- 4 files changed, 105 insertions(+), 83 deletions(-) diff --git a/buildengine/engine.go b/buildengine/engine.go index 8babbb0b48..1ba9a5e95f 100644 --- a/buildengine/engine.go +++ b/buildengine/engine.go @@ -29,6 +29,14 @@ type schemaChange struct { *schema.Module } +type Listener interface { + OnBuildStarted(project Project) +} + +type BuildStartedListenerFunc func(project Project) + +func (b BuildStartedListenerFunc) OnBuildStarted(project Project) { b(project) } + // Engine for building a set of modules. type Engine struct { client ftlv1connect.ControllerServiceClient @@ -39,6 +47,7 @@ type Engine struct { schemaChanges *pubsub.Topic[schemaChange] cancel func() parallelism int + listener Listener } type Option func(o *Engine) @@ -49,6 +58,13 @@ func Parallelism(n int) Option { } } +// WithListener sets the event listener for the Engine. +func WithListener(listener Listener) Option { + return func(o *Engine) { + o.listener = listener + } +} + // New constructs a new [Engine]. // // Completely offline builds are possible if the full dependency graph is @@ -457,6 +473,9 @@ func (e *Engine) build(ctx context.Context, key ProjectKey, builtModules map[str } sch := &schema.Schema{Modules: maps.Values(combined)} + if e.listener != nil { + e.listener.OnBuildStarted(project) + } err := Build(ctx, sch, project) if err != nil { return err diff --git a/cmd/ftl/cmd_dev.go b/cmd/ftl/cmd_dev.go index 3ee3bd94d6..4ba152c48b 100644 --- a/cmd/ftl/cmd_dev.go +++ b/cmd/ftl/cmd_dev.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "time" "golang.org/x/sync/errgroup" @@ -60,14 +61,17 @@ func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error return err } + opts := []buildengine.Option{buildengine.Parallelism(d.Parallelism)} if d.RunLsp { d.languageServer = lsp.NewServer(ctx) + opts = append(opts, buildengine.WithListener(buildengine.BuildStartedListenerFunc(d.OnBuildStarted))) ctx = log.ContextWithLogger(ctx, log.FromContext(ctx).AddSink(lsp.NewLogSink(d.languageServer))) g.Go(func() error { return d.languageServer.Run() }) } - engine, err := buildengine.New(ctx, client, d.Dirs, d.External, buildengine.Parallelism(d.Parallelism)) + + engine, err := buildengine.New(ctx, client, d.Dirs, d.External, opts...) if err != nil { return err } @@ -76,3 +80,7 @@ func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error return g.Wait() } + +func (d *devCmd) OnBuildStarted(project buildengine.Project) { + d.languageServer.BuildStarted(project.Config().Dir) +} diff --git a/lsp/logger.go b/lsp/logger.go index 380eed2283..1c696735b3 100644 --- a/lsp/logger.go +++ b/lsp/logger.go @@ -1,71 +1,9 @@ package lsp import ( - "fmt" - - "github.com/tliron/commonlog" - "github.com/TBD54566975/ftl/internal/log" ) -// GLSPLogger is a custom logger for the language server. -type GLSPLogger struct { - commonlog.Logger -} - -func (l *GLSPLogger) Log(entry log.Entry) { - l.Logger.Log(toGLSPLevel(entry.Level), 10, entry.Message, entry.Attributes) -} - -func (l *GLSPLogger) Logf(level log.Level, format string, args ...interface{}) { - l.Log(log.Entry{Level: level, Message: fmt.Sprintf(format, args...)}) -} -func (l *GLSPLogger) Tracef(format string, args ...interface{}) { - l.Log(log.Entry{Level: log.Trace, Message: fmt.Sprintf(format, args...)}) -} - -func (l *GLSPLogger) Debugf(format string, args ...interface{}) { - l.Log(log.Entry{Level: log.Debug, Message: fmt.Sprintf(format, args...)}) -} - -func (l *GLSPLogger) Infof(format string, args ...interface{}) { - l.Log(log.Entry{Level: log.Info, Message: fmt.Sprintf(format, args...)}) -} - -func (l *GLSPLogger) Warnf(format string, args ...interface{}) { - l.Log(log.Entry{Level: log.Warn, Message: fmt.Sprintf(format, args...)}) -} - -func (l *GLSPLogger) Errorf(err error, format string, args ...interface{}) { - if err == nil { - return - } - l.Log(log.Entry{Level: log.Error, Message: fmt.Sprintf(format, args...) + ": " + err.Error(), Error: err}) -} - -var _ log.Interface = (*GLSPLogger)(nil) - -func NewGLSPLogger(log commonlog.Logger) *GLSPLogger { - return &GLSPLogger{log} -} - -func toGLSPLevel(l log.Level) commonlog.Level { - switch l { - case log.Trace: - return commonlog.Debug - case log.Debug: - return commonlog.Debug - case log.Info: - return commonlog.Info - case log.Warn: - return commonlog.Warning - case log.Error: - return commonlog.Error - default: - return commonlog.Debug - } -} - type LogSink struct { server *Server } diff --git a/lsp/lsp.go b/lsp/lsp.go index 2e052215b8..74d1b6fc08 100644 --- a/lsp/lsp.go +++ b/lsp/lsp.go @@ -1,11 +1,15 @@ package lsp import ( + "bufio" "context" "errors" "fmt" + "os" "strings" + "unicode" + "github.com/puzpuzpuz/xsync/v3" _ "github.com/tliron/commonlog/simple" "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" @@ -22,10 +26,10 @@ const lsName = "ftl-language-server" // Server is a language server. type Server struct { server *glspServer.Server - glspLogger *GLSPLogger glspContext *glsp.Context handler protocol.Handler logger log.Logger + diagnostics *xsync.MapOf[protocol.DocumentUri, []protocol.Diagnostic] } // NewServer creates a new language server. @@ -38,9 +42,9 @@ func NewServer(ctx context.Context) *Server { } s := glspServer.NewServer(&handler, lsName, false) server := &Server{ - server: s, - glspLogger: NewGLSPLogger(s.Log), - logger: *log.FromContext(ctx), + server: s, + logger: *log.FromContext(ctx).Scope("lsp"), + diagnostics: xsync.NewMapOf[protocol.DocumentUri, []protocol.Diagnostic](), } handler.Initialize = server.initialize() return server @@ -56,6 +60,19 @@ func (s *Server) Run() error { type errSet map[string]schema.Error +// BuildStarted clears diagnostics for the given directory. New errors will arrive later if they still exist. +func (s *Server) BuildStarted(dir string) { + dirURI := "file://" + dir + + s.diagnostics.Range(func(uri protocol.DocumentUri, diagnostics []protocol.Diagnostic) bool { + if strings.HasPrefix(uri, dirURI) { + s.diagnostics.Delete(uri) + s.publishDiagnostics(uri, []protocol.Diagnostic{}) + } + return true + }) +} + // Post sends diagnostics to the client. err must be joined schema.Errors. func (s *Server) post(err error) { errByFilename := make(map[string]errSet) @@ -83,10 +100,18 @@ func publishErrors(errByFilename map[string]errSet, s *Server) { pp := e.Pos sourceName := "ftl" severity := protocol.DiagnosticSeverityError + + length, err := getLineOrWordLength(filename, pp.Line, pp.Column, false) + if err != nil { + s.logger.Errorf(err, "Failed to get line or word length") + continue + } + endColumn := pp.Column + length + diagnostics = append(diagnostics, protocol.Diagnostic{ Range: protocol.Range{ Start: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column - 1)}, - End: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column + 10 - 1)}, + End: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(endColumn - 1)}, }, Severity: &severity, Source: &sourceName, @@ -94,15 +119,22 @@ func publishErrors(errByFilename map[string]errSet, s *Server) { }) } - if s.glspContext == nil { - return - } + uri := "file://" + filename + s.diagnostics.Store(uri, diagnostics) + s.publishDiagnostics(uri, diagnostics) + } +} - go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ - URI: "file://" + filename, - Diagnostics: diagnostics, - }) +func (s *Server) publishDiagnostics(uri protocol.DocumentUri, diagnostics []protocol.Diagnostic) { + s.logger.Debugf("Publishing diagnostics for %s\n", uri) + if s.glspContext == nil { + return } + + go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ + URI: uri, + Diagnostics: diagnostics, + }) } func (s *Server) initialize() protocol.InitializeFunc { @@ -123,14 +155,6 @@ func (s *Server) initialize() protocol.InitializeFunc { }, nil } } -func (s *Server) clearDiagnosticsOfDocument(uri protocol.DocumentUri) { - go func() { - go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ - URI: uri, - Diagnostics: []protocol.Diagnostic{}, - }) - }() -} func initialized(context *glsp.Context, params *protocol.InitializedParams) error { return nil @@ -149,3 +173,36 @@ func setTrace(context *glsp.Context, params *protocol.SetTraceParams) error { protocol.SetTraceValue(params.Value) return nil } + +// getLineOrWordLength returns the length of the line or the length of the word starting at the given column. +// If wholeLine is true, it returns the length of the entire line. +// If wholeLine is false, it returns the length of the word starting at the column. +func getLineOrWordLength(filePath string, lineNum, column int, wholeLine bool) (int, error) { + file, err := os.Open(filePath) + if err != nil { + return 0, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + currentLine := 1 + for scanner.Scan() { + if currentLine == lineNum { + lineText := scanner.Text() + if wholeLine { + return len(lineText), nil + } + start := column - 1 + end := start + for end < len(lineText) && !unicode.IsSpace(rune(lineText[end])) { + end++ + } + return end - start, nil + } + currentLine++ + } + if err := scanner.Err(); err != nil { + return 0, err + } + return 0, os.ErrNotExist +}