Skip to content

Commit

Permalink
fix: parse out word for errors and clear them (#1163)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman authored Apr 3, 2024
1 parent 28cacf7 commit 82ff1bb
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 83 deletions.
19 changes: 19 additions & 0 deletions buildengine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +47,7 @@ type Engine struct {
schemaChanges *pubsub.Topic[schemaChange]
cancel func()
parallelism int
listener Listener
}

type Option func(o *Engine)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion cmd/ftl/cmd_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"errors"

"time"

"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}
62 changes: 0 additions & 62 deletions lsp/logger.go
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
97 changes: 77 additions & 20 deletions lsp/lsp.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -83,26 +100,41 @@ 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,
Message: e.Msg,
})
}

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 {
Expand All @@ -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
Expand All @@ -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
}

0 comments on commit 82ff1bb

Please sign in to comment.