From eea40102637a99ac5ed206172ca3fcf439df70b1 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 6 Nov 2020 21:19:12 +0100 Subject: [PATCH 1/6] Use github.com/arduino/go-properties-orderedmap for handling properties --- go.mod | 1 + go.sum | 11 ++++++++++ handler/builder.go | 29 ++++++++++++++------------- handler/properties.go | 41 -------------------------------------- handler/properties_test.go | 33 ------------------------------ 5 files changed, 27 insertions(+), 88 deletions(-) delete mode 100644 handler/properties.go delete mode 100644 handler/properties_test.go diff --git a/go.mod b/go.mod index ad27295..20a9e06 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/bcmi-labs/arduino-language-server go 1.12 require ( + github.com/arduino/go-properties-orderedmap v1.4.0 github.com/gorilla/websocket v1.4.0 // indirect github.com/pkg/errors v0.8.1 github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1 diff --git a/go.sum b/go.sum index 386ebdb..5b03502 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,19 @@ +github.com/arduino/go-paths-helper v1.0.1 h1:utYXLM2RfFlc9qp/MJTIYp3t6ux/xM6mWjeEb/WLK4Q= +github.com/arduino/go-paths-helper v1.0.1/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck= +github.com/arduino/go-properties-orderedmap v1.4.0 h1:YEbbzPqm1gXWDM/Jaq8tlvmh09z2qeHPJTUw9/VA4Dk= +github.com/arduino/go-properties-orderedmap v1.4.0/go.mod h1:DKjD2VXY/NZmlingh4lSFMEYCVubfeArCsGPGDwb2yk= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1 h1:O1d7nVzpGmP5pGAZBSlp9TSpjNwwI0xThxhPd9oVJuU= github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1/go.mod h1:tpps84QRlOVVLYk5QpKYX8Tr289D1v/UTWDLqeguiqM= github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a h1:jTZwOlrDhmk4Ez2vhWh7kA0eKUahp1lCO2uyM4fi/Qk= github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a/go.mod h1:eESpbCslcLDs8j2D7IEdGVgul7xuk9odqDTaor30IUU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/handler/builder.go b/handler/builder.go index 170bbd2..88a5402 100644 --- a/handler/builder.go +++ b/handler/builder.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/arduino/go-properties-orderedmap" "github.com/pkg/errors" ) @@ -142,7 +143,7 @@ func generateCompileFlags(tempDir, inoPath, sourcePath, fqbn string) (string, er err = logCommandErr(globalCliPath, output, err, errMsgFilter(tempDir)) return "", err } - properties, err := readProperties(bytes.NewReader(output)) + buildProps, err := properties.LoadFromBytes(output) if err != nil { return "", errors.Wrap(err, "Error while reading build properties.") } @@ -154,7 +155,7 @@ func generateCompileFlags(tempDir, inoPath, sourcePath, fqbn string) (string, er defer outFile.Close() printer := Printer{Writer: bufio.NewWriter(outFile)} - printCompileFlags(properties, &printer, fqbn) + printCompileFlags(buildProps, &printer, fqbn) printLibraryPaths(sourcePath, &printer) printer.Flush() return flagsPath, printer.Err @@ -214,40 +215,40 @@ func copyIno2Cpp(inoCode string, cppPath string) (cppCode []byte, err error) { return } -func printCompileFlags(properties map[string]string, printer *Printer, fqbn string) { +func printCompileFlags(buildProps *properties.Map, printer *Printer, fqbn string) { if strings.Contains(fqbn, ":avr:") { printer.Println("--target=avr") } else if strings.Contains(fqbn, ":sam:") { printer.Println("--target=arm-none-eabi") } - cppFlags := expandProperty(properties, "compiler.cpp.flags") + cppFlags := buildProps.ExpandPropsInString(buildProps.Get("compiler.cpp.flags")) printer.Println(splitFlags(cppFlags)) - mcu := expandProperty(properties, "build.mcu") + mcu := buildProps.ExpandPropsInString(buildProps.Get("build.mcu")) if strings.Contains(fqbn, ":avr:") { printer.Println("-mmcu=", mcu) } else if strings.Contains(fqbn, ":sam:") { printer.Println("-mcpu=", mcu) } - fcpu := expandProperty(properties, "build.f_cpu") + fcpu := buildProps.ExpandPropsInString(buildProps.Get("build.f_cpu")) printer.Println("-DF_CPU=", fcpu) - ideVersion := expandProperty(properties, "runtime.ide.version") + ideVersion := buildProps.ExpandPropsInString(buildProps.Get("runtime.ide.version")) printer.Println("-DARDUINO=", ideVersion) - board := expandProperty(properties, "build.board") + board := buildProps.ExpandPropsInString(buildProps.Get("build.board")) printer.Println("-DARDUINO_", board) - arch := expandProperty(properties, "build.arch") + arch := buildProps.ExpandPropsInString(buildProps.Get("build.arch")) printer.Println("-DARDUINO_ARCH_", arch) if strings.Contains(fqbn, ":sam:") { - libSamFlags := expandProperty(properties, "compiler.libsam.c.flags") + libSamFlags := buildProps.ExpandPropsInString(buildProps.Get("compiler.libsam.c.flags")) printer.Println(splitFlags(libSamFlags)) } - extraFlags := expandProperty(properties, "build.extra_flags") + extraFlags := buildProps.ExpandPropsInString(buildProps.Get("build.extra_flags")) printer.Println(splitFlags(extraFlags)) - corePath := expandProperty(properties, "build.core.path") + corePath := buildProps.ExpandPropsInString(buildProps.Get("build.core.path")) printer.Println("-I", corePath) - variantPath := expandProperty(properties, "build.variant.path") + variantPath := buildProps.ExpandPropsInString(buildProps.Get("build.variant.path")) printer.Println("-I", variantPath) if strings.Contains(fqbn, ":avr:") { - avrgccPath := expandProperty(properties, "runtime.tools.avr-gcc.path") + avrgccPath := buildProps.ExpandPropsInString(buildProps.Get("runtime.tools.avr-gcc.path")) printer.Println("-I", filepath.Join(avrgccPath, "avr", "include")) } diff --git a/handler/properties.go b/handler/properties.go deleted file mode 100644 index b30de0b..0000000 --- a/handler/properties.go +++ /dev/null @@ -1,41 +0,0 @@ -package handler - -import ( - "bufio" - "io" - "strings" -) - -func readProperties(propsFile io.Reader) (map[string]string, error) { - properties := make(map[string]string) - scanner := bufio.NewScanner(propsFile) - for scanner.Scan() { - line := scanner.Text() - equalIndex := strings.Index(line, "=") - if equalIndex >= 0 { - key := strings.TrimSpace(line[:equalIndex]) - if len(key) > 0 { - value := strings.TrimSpace(line[equalIndex+1:]) - properties[key] = value - } - } - } - return properties, scanner.Err() -} - -func expandProperty(properties map[string]string, name string) string { - value := properties[name] - varStart := strings.Index(value, "{") - for varStart >= 0 { - varEnd := strings.Index(value[varStart:], "}") - if varEnd >= 0 { - referencedName := value[varStart+1 : varStart+varEnd] - expanded := expandProperty(properties, referencedName) - value = value[:varStart] + expanded + value[varStart+varEnd+1:] - varStart = strings.Index(value, "{") - } else { - varStart = -1 - } - } - return value -} diff --git a/handler/properties_test.go b/handler/properties_test.go deleted file mode 100644 index 845ddba..0000000 --- a/handler/properties_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package handler - -import ( - "reflect" - "strings" - "testing" -) - -func TestReadProperties(t *testing.T) { - properties, err := readProperties(strings.NewReader("foo=Hello\n bar = World \nbaz=!")) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(properties, map[string]string{ - "foo": "Hello", - "bar": "World", - "baz": "!", - }) { - t.Error(properties) - } -} - -func TestExpandProperty(t *testing.T) { - properties := map[string]string{ - "foo": "Hello {bar} {baz}", - "bar": "{baz} World", - "baz": "!", - } - foo := expandProperty(properties, "foo") - if foo != "Hello ! World !" { - t.Error(foo) - } -} From e9ee2675760195c97ffd697c4277d7b22cc2c0c7 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 6 Nov 2020 21:22:18 +0100 Subject: [PATCH 2/6] When logging command output add also the command line args --- handler/builder.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/handler/builder.go b/handler/builder.go index 88a5402..e182f9a 100644 --- a/handler/builder.go +++ b/handler/builder.go @@ -140,7 +140,7 @@ func generateCompileFlags(tempDir, inoPath, sourcePath, fqbn string) (string, er propertiesCmd := exec.Command(globalCliPath, cliArgs...) output, err := propertiesCmd.Output() if err != nil { - err = logCommandErr(globalCliPath, output, err, errMsgFilter(tempDir)) + err = logCommandErr(propertiesCmd, output, err, errMsgFilter(tempDir)) return "", err } buildProps, err := properties.LoadFromBytes(output) @@ -171,7 +171,7 @@ func generateTargetFile(tempDir, inoPath, cppPath, fqbn string) (cppCode []byte, preprocessCmd := exec.Command(globalCliPath, cliArgs...) cppCode, err = preprocessCmd.Output() if err != nil { - err = logCommandErr(globalCliPath, cppCode, err, errMsgFilter(tempDir)) + err = logCommandErr(preprocessCmd, cppCode, err, errMsgFilter(tempDir)) return } @@ -331,9 +331,9 @@ func splitFlags(flags string) string { return string(result) } -func logCommandErr(command string, stdout []byte, err error, filter func(string) string) error { +func logCommandErr(command *exec.Cmd, stdout []byte, err error, filter func(string) string) error { message := "" - log.Println("Command error:", command, err) + log.Println("Command error:", command.Args, err) if len(stdout) > 0 { stdoutStr := string(stdout) log.Println("------------------------------BEGIN STDOUT\n", stdoutStr, "------------------------------END STDOUT") From 3ec5d2ae9dd7ebb8d7cf4d4bf2b24b9f6923cc33 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Sun, 8 Nov 2020 11:22:55 +0100 Subject: [PATCH 3/6] updated go modules --- go.mod | 7 +++---- go.sum | 7 +++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 20a9e06..562a57e 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,7 @@ go 1.12 require ( github.com/arduino/go-properties-orderedmap v1.4.0 - github.com/gorilla/websocket v1.4.0 // indirect - github.com/pkg/errors v0.8.1 - github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1 - github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a + github.com/pkg/errors v0.9.1 + github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d + github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 ) diff --git a/go.sum b/go.sum index 5b03502..ee7e7db 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,21 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1 h1:O1d7nVzpGmP5pGAZBSlp9TSpjNwwI0xThxhPd9oVJuU= github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1/go.mod h1:tpps84QRlOVVLYk5QpKYX8Tr289D1v/UTWDLqeguiqM= +github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d h1:afLbh+ltiygTOB37ymZVwKlJwWZn+86syPTbrrOAydY= +github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d/go.mod h1:SULmZY7YNBsvNiQbrb/BEDdEJ84TGnfyUQxaHt8t8rY= github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a h1:jTZwOlrDhmk4Ez2vhWh7kA0eKUahp1lCO2uyM4fi/Qk= github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a/go.mod h1:eESpbCslcLDs8j2D7IEdGVgul7xuk9odqDTaor30IUU= +github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 h1:marA1XQDC7N870zmSFIoHZpIUduK80USeY0Rkuflgp4= +github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= From a8d0ddc9687407c60539a40c92d97979fb717e51 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Sun, 8 Nov 2020 11:24:43 +0100 Subject: [PATCH 4/6] Some refactoring wrt logging and debugging - clangd launcher has been moved into main module, removing an unneded indirection. - streams management functions now resides in their own module. - file logging has been downgraded from an object to a function --- handler/handler.go | 36 +++-------- handler/streamlog.go | 147 ------------------------------------------- main.go | 50 +++++++++++---- streams/dumper.go | 45 +++++++++++++ streams/streams.go | 33 ++++++++++ 5 files changed, 123 insertions(+), 188 deletions(-) delete mode 100644 handler/streamlog.go create mode 100644 streams/dumper.go create mode 100644 streams/streams.go diff --git a/handler/handler.go b/handler/handler.go index 4141da6..46d255b 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "log" "os" "regexp" @@ -33,19 +32,19 @@ func Setup(cliPath string, _enableLogging bool, _asyncProcessing bool) { type CLangdStarter func() (stdin io.WriteCloser, stdout io.ReadCloser, stderr io.ReadCloser) // NewInoHandler creates and configures an InoHandler. -func NewInoHandler(stdin io.ReadCloser, stdout io.WriteCloser, logStreams *StreamLogger, startClangd CLangdStarter, board Board) *InoHandler { +func NewInoHandler(stdio io.ReadWriteCloser, clangdStdio io.ReadWriteCloser, board Board) *InoHandler { handler := &InoHandler{ - clangdProc: ClangdProc{ - Start: startClangd, - Logs: logStreams, - }, data: make(map[lsp.DocumentURI]*FileData), config: BoardConfig{ SelectedBoard: board, }, } - handler.startClangd() - stdStream := jsonrpc2.NewBufferedStream(logStreams.AttachStdInOut(stdin, stdout), jsonrpc2.VSCodeObjectCodec{}) + + clangdStream := jsonrpc2.NewBufferedStream(clangdStdio, jsonrpc2.VSCodeObjectCodec{}) + clangdHandler := jsonrpc2.AsyncHandler(jsonrpc2.HandlerWithError(handler.FromClangd)) + handler.ClangdConn = jsonrpc2.NewConn(context.Background(), clangdStream, clangdHandler) + + stdStream := jsonrpc2.NewBufferedStream(stdio, jsonrpc2.VSCodeObjectCodec{}) var stdHandler jsonrpc2.Handler = jsonrpc2.HandlerWithError(handler.FromStdio) if asyncProcessing { stdHandler = AsyncHandler{ @@ -64,18 +63,11 @@ func NewInoHandler(stdin io.ReadCloser, stdout io.WriteCloser, logStreams *Strea type InoHandler struct { StdioConn *jsonrpc2.Conn ClangdConn *jsonrpc2.Conn - clangdProc ClangdProc data map[lsp.DocumentURI]*FileData config BoardConfig synchronizer Synchronizer } -// ClangdProc contains the process input / output streams for clangd. -type ClangdProc struct { - Start func() (io.WriteCloser, io.ReadCloser, io.ReadCloser) - Logs *StreamLogger -} - // FileData gathers information on a .ino source file. type FileData struct { sourceText string @@ -86,20 +78,6 @@ type FileData struct { version int } -// StartClangd starts the clangd process and connects its input / output streams. -func (handler *InoHandler) startClangd() { - clangdWrite, clangdRead, clangdErr := handler.clangdProc.Start() - if enableLogging { - go io.Copy(handler.clangdProc.Logs.ClangdErr, clangdErr) - } else { - go io.Copy(ioutil.Discard, clangdErr) - } - srw := handler.clangdProc.Logs.AttachClangdInOut(clangdRead, clangdWrite) - clangdStream := jsonrpc2.NewBufferedStream(srw, jsonrpc2.VSCodeObjectCodec{}) - clangdHandler := jsonrpc2.AsyncHandler(jsonrpc2.HandlerWithError(handler.FromClangd)) - handler.ClangdConn = jsonrpc2.NewConn(context.Background(), clangdStream, clangdHandler) -} - // StopClangd closes the connection to the clangd process. func (handler *InoHandler) StopClangd() { handler.ClangdConn.Close() diff --git a/handler/streamlog.go b/handler/streamlog.go deleted file mode 100644 index 7df9e99..0000000 --- a/handler/streamlog.go +++ /dev/null @@ -1,147 +0,0 @@ -package handler - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -// StreamLogger maintains log files for all streams involved in the language server -type StreamLogger struct { - Default io.WriteCloser - Stdin io.WriteCloser - Stdout io.WriteCloser - ClangdIn io.WriteCloser - ClangdOut io.WriteCloser - ClangdErr io.WriteCloser -} - -// Close closes all logging streams -func (s *StreamLogger) Close() (err error) { - var errs []string - for _, c := range []io.Closer{s.Default, s.Stdin, s.Stdout, s.ClangdIn, s.ClangdOut, s.ClangdErr} { - if c == nil { - continue - } - - err = c.Close() - if err != nil { - errs = append(errs, err.Error()) - } - } - if len(errs) != 0 { - return fmt.Errorf(strings.Join(errs, ", ")) - } - - return nil -} - -// AttachStdInOut attaches the stdin, stdout logger to the in/out channels -func (s *StreamLogger) AttachStdInOut(in io.ReadCloser, out io.WriteCloser) io.ReadWriteCloser { - return &streamDuplex{ - io.TeeReader(in, s.Stdin), - in, - io.MultiWriter(out, s.Stdout), - out, - } -} - -// AttachClangdInOut attaches the clangd in, out logger to the in/out channels -func (s *StreamLogger) AttachClangdInOut(in io.ReadCloser, out io.WriteCloser) io.ReadWriteCloser { - return &streamDuplex{ - io.TeeReader(in, s.ClangdIn), - in, - io.MultiWriter(out, s.ClangdOut), - out, - } -} - -type streamDuplex struct { - in io.Reader - inc io.Closer - out io.Writer - outc io.Closer -} - -func (sd *streamDuplex) Read(p []byte) (int, error) { - return sd.in.Read(p) -} - -func (sd *streamDuplex) Write(p []byte) (int, error) { - return sd.out.Write(p) -} - -func (sd *streamDuplex) Close() error { - ierr := sd.inc.Close() - oerr := sd.outc.Close() - - if ierr != nil { - return ierr - } - if oerr != nil { - return oerr - } - return nil -} - -// NewStreamLogger creates files for all stream logs. Returns an error if opening a single stream fails. -func NewStreamLogger(basepath string) (res *StreamLogger, err error) { - res = &StreamLogger{} - - res.Default, err = os.OpenFile(filepath.Join(basepath, "inols.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - res.Close() - return - } - res.Stdin, err = os.OpenFile(filepath.Join(basepath, "inols-stdin.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.Stdout, err = os.OpenFile(filepath.Join(basepath, "inols-stdout.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.ClangdIn, err = os.OpenFile(filepath.Join(basepath, "inols-clangd-in.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.ClangdOut, err = os.OpenFile(filepath.Join(basepath, "inols-clangd-out.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.ClangdErr, err = os.OpenFile(filepath.Join(basepath, "inols-clangd-err.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - - return -} - -// NewNoopLogger creates a logger that does nothing -func NewNoopLogger() (res *StreamLogger) { - noop := noopCloser{ioutil.Discard} - return &StreamLogger{ - Default: noop, - Stdin: noop, - Stdout: noop, - ClangdIn: noop, - ClangdOut: noop, - ClangdErr: noop, - } -} - -type noopCloser struct { - io.Writer -} - -func (noopCloser) Close() error { - return nil -} diff --git a/main.go b/main.go index 9980174..a97dbf1 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,10 @@ import ( "log" "os" "os/exec" + "path/filepath" "github.com/bcmi-labs/arduino-language-server/handler" + "github.com/bcmi-labs/arduino-language-server/streams" ) var clangdPath string @@ -29,29 +31,53 @@ func main() { flag.StringVar(&loggingBasePath, "logpath", ".", "Location where to write logging files to when logging is enabled") flag.Parse() - // var stdinLog, stdoutLog, clangdinLog, clangdoutLog, clangderrLog io.Writer - var logStreams *handler.StreamLogger if enableLogging { - var err error - logStreams, err = handler.NewStreamLogger(loggingBasePath) - if err != nil { - log.Fatal(err) - } - defer logStreams.Close() - - log.SetOutput(logStreams.Default) + logfile := openLogFile("inols-err.log") + defer logfile.Close() + log.SetOutput(logfile) } else { - logStreams = handler.NewNoopLogger() log.SetOutput(os.Stderr) } handler.Setup(cliPath, enableLogging, true) initialBoard := handler.Board{Fqbn: initialFqbn, Name: initialBoardName} - inoHandler := handler.NewInoHandler(os.Stdin, os.Stdout, logStreams, startClangd, initialBoard) + + clangdStdout, clangdStdin, clangdStderr := startClangd() + clangdStdio := streams.NewReadWriteCloser(clangdStdin, clangdStdout) + if enableLogging { + logfile := openLogFile("inols-clangd.log") + defer logfile.Close() + clangdStdio = streams.LogReadWriteCloserToFile(clangdStdio, logfile) + + errLogfile := openLogFile("inols-clangd-err.log") + defer errLogfile.Close() + go io.Copy(errLogfile, clangdStderr) + } + + stdio := streams.NewReadWriteCloser(os.Stdin, os.Stdout) + if enableLogging { + logfile := openLogFile("inols.log") + defer logfile.Close() + stdio = streams.LogReadWriteCloserToFile(stdio, logfile) + } + + inoHandler := handler.NewInoHandler(stdio, clangdStdio, initialBoard) defer inoHandler.StopClangd() <-inoHandler.StdioConn.DisconnectNotify() } +func openLogFile(name string) *os.File { + path := filepath.Join(loggingBasePath, name) + logfile, err := os.Create(path) + if err != nil { + log.Fatalf("Error opening log file: %s", err) + } else { + abs, _ := filepath.Abs(path) + log.Println("logging to " + abs) + } + return logfile +} + func startClangd() (clangdIn io.WriteCloser, clangdOut io.ReadCloser, clangdErr io.ReadCloser) { if enableLogging { log.Println("Starting clangd process:", clangdPath) diff --git a/streams/dumper.go b/streams/dumper.go new file mode 100644 index 0000000..1cc8359 --- /dev/null +++ b/streams/dumper.go @@ -0,0 +1,45 @@ +package streams + +import ( + "fmt" + "io" + "os" +) + +// LogReadWriteCloserToFile return a proxy for the given upstream io.ReadWriteCloser +// that forward and logs all read/write/close operations on the given file. +func LogReadWriteCloserToFile(upstream io.ReadWriteCloser, file *os.File) io.ReadWriteCloser { + return &dumper{upstream, file} +} + +type dumper struct { + upstream io.ReadWriteCloser + logfile *os.File +} + +func (d *dumper) Read(buff []byte) (int, error) { + n, err := d.upstream.Read(buff) + if err != nil { + d.logfile.Write([]byte(fmt.Sprintf("<<< Read Error: %s\n", err))) + } else { + d.logfile.Write([]byte(fmt.Sprintf("<<< Read %d bytes:\n%s\n", n, buff[:n]))) + } + return n, err +} + +func (d *dumper) Write(buff []byte) (int, error) { + n, err := d.upstream.Write(buff) + if err != nil { + _, _ = d.logfile.Write([]byte(fmt.Sprintf(">>> Write Error: %s\n", err))) + } else { + _, _ = d.logfile.Write([]byte(fmt.Sprintf(">>> Wrote %d bytes:\n%s\n", n, buff[:n]))) + } + return n, err +} + +func (d *dumper) Close() error { + err := d.upstream.Close() + _, _ = d.logfile.Write([]byte(fmt.Sprintf("--- Stream closed, err=%s\n", err))) + _ = d.logfile.Close() + return err +} diff --git a/streams/streams.go b/streams/streams.go new file mode 100644 index 0000000..10d313c --- /dev/null +++ b/streams/streams.go @@ -0,0 +1,33 @@ +package streams + +import "io" + +// NewReadWriteCloser create an io.ReadWriteCloser from given io.ReadCloser and io.WriteCloser. +func NewReadWriteCloser(in io.ReadCloser, out io.WriteCloser) io.ReadWriteCloser { + return &combinedReadWriteCloser{in, out} +} + +type combinedReadWriteCloser struct { + reader io.ReadCloser + writer io.WriteCloser +} + +func (sd *combinedReadWriteCloser) Read(p []byte) (int, error) { + return sd.reader.Read(p) +} + +func (sd *combinedReadWriteCloser) Write(p []byte) (int, error) { + return sd.writer.Write(p) +} + +func (sd *combinedReadWriteCloser) Close() error { + ierr := sd.reader.Close() + oerr := sd.writer.Close() + if ierr != nil { + return ierr + } + if oerr != nil { + return oerr + } + return nil +} From 12a1bf64e9d2ba5234fa891e4dbef302ae457647 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 9 Nov 2020 19:36:44 +0100 Subject: [PATCH 5/6] Create a source mapper object and factored textutils subroutine --- handler/handler.go | 95 +++----- handler/sourcemap.go | 204 ------------------ handler/sourcemapper/ino.go | 157 ++++++++++++++ handler/sourcemapper/ino_test.go | 137 ++++++++++++ handler/textutils/textutils.go | 97 +++++++++ .../textutils_test.go} | 70 +----- 6 files changed, 427 insertions(+), 333 deletions(-) delete mode 100644 handler/sourcemap.go create mode 100644 handler/sourcemapper/ino.go create mode 100644 handler/sourcemapper/ino_test.go create mode 100644 handler/textutils/textutils.go rename handler/{sourcemap_test.go => textutils/textutils_test.go} (69%) diff --git a/handler/handler.go b/handler/handler.go index 46d255b..77732d6 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -12,6 +12,8 @@ import ( "strings" "time" + "github.com/bcmi-labs/arduino-language-server/handler/sourcemapper" + "github.com/bcmi-labs/arduino-language-server/handler/textutils" "github.com/pkg/errors" lsp "github.com/sourcegraph/go-lsp" "github.com/sourcegraph/jsonrpc2" @@ -70,12 +72,11 @@ type InoHandler struct { // FileData gathers information on a .ino source file. type FileData struct { - sourceText string - sourceURI lsp.DocumentURI - targetURI lsp.DocumentURI - sourceLineMap map[int]int - targetLineMap map[int]int - version int + sourceText string + sourceURI lsp.DocumentURI + targetURI lsp.DocumentURI + sourceMap *sourcemapper.InoMapper + version int } // StopClangd closes the connection to the clangd process. @@ -240,13 +241,11 @@ func (handler *InoHandler) createFileData(ctx context.Context, sourceURI lsp.Doc } targetURI := pathToURI(targetPath) - sourceLineMap, targetLineMap := createSourceMaps(bytes.NewReader(targetBytes)) data := &FileData{ sourceText, sourceURI, targetURI, - sourceLineMap, - targetLineMap, + sourcemapper.CreateInoMapper(bytes.NewReader(targetBytes)), version, } handler.data[sourceURI] = data @@ -262,7 +261,7 @@ func (handler *InoHandler) updateFileData(ctx context.Context, data *FileData, c if rang == nil { newSourceText = change.Text } else { - newSourceText, err = applyTextChange(data.sourceText, *rang, change.Text) + newSourceText, err = textutils.ApplyTextChange(data.sourceText, *rang, change.Text) if err != nil { return err } @@ -277,35 +276,28 @@ func (handler *InoHandler) updateFileData(ctx context.Context, data *FileData, c } } else { // Fallback: try to apply a multi-line update - targetStartLine := data.targetLineMap[rang.Start.Line] - targetEndLine := data.targetLineMap[rang.End.Line] data.sourceText = newSourceText - updateSourceMaps(data.sourceLineMap, data.targetLineMap, rang.End.Line-rang.Start.Line, rang.Start.Line, change.Text) - rang.Start.Line = targetStartLine - rang.End.Line = targetEndLine + data.sourceMap.Update(rang.End.Line-rang.Start.Line, rang.Start.Line, change.Text) + *rang = data.sourceMap.InoToCppRange(*rang) return nil } } - sourceLineMap, targetLineMap := createSourceMaps(bytes.NewReader(targetBytes)) data.sourceText = newSourceText - data.sourceLineMap = sourceLineMap - data.targetLineMap = targetLineMap + data.sourceMap = sourcemapper.CreateInoMapper(bytes.NewReader(targetBytes)) change.Text = string(targetBytes) change.Range = nil change.RangeLength = 0 } else { // Apply an update to a single line both to the source and the target text - targetLine := data.targetLineMap[rang.Start.Line] - data.sourceText, err = applyTextChange(data.sourceText, *rang, change.Text) + data.sourceText, err = textutils.ApplyTextChange(data.sourceText, *rang, change.Text) if err != nil { return err } - updateSourceMaps(data.sourceLineMap, data.targetLineMap, 0, rang.Start.Line, change.Text) + data.sourceMap.Update(0, rang.Start.Line, change.Text) - rang.Start.Line = targetLine - rang.End.Line = targetLine + *rang = data.sourceMap.InoToCppRange(*rang) } return nil } @@ -391,7 +383,7 @@ func (handler *InoHandler) ino2cppDidChangeTextDocumentParams(ctx context.Contex func (handler *InoHandler) ino2cppTextDocumentPositionParams(params *lsp.TextDocumentPositionParams) error { handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) if data, ok := handler.data[params.TextDocument.URI]; ok { - targetLine := data.targetLineMap[params.Position.Line] + targetLine := data.sourceMap.InoToCppLine(params.Position.Line) params.Position.Line = targetLine return nil } @@ -401,12 +393,10 @@ func (handler *InoHandler) ino2cppTextDocumentPositionParams(params *lsp.TextDoc func (handler *InoHandler) ino2cppCodeActionParams(params *lsp.CodeActionParams) error { handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Range.Start.Line = data.targetLineMap[params.Range.Start.Line] - params.Range.End.Line = data.targetLineMap[params.Range.End.Line] + params.Range = data.sourceMap.InoToCppRange(params.Range) for index := range params.Context.Diagnostics { r := ¶ms.Context.Diagnostics[index].Range - r.Start.Line = data.targetLineMap[r.Start.Line] - r.End.Line = data.targetLineMap[r.End.Line] + *r = data.sourceMap.InoToCppRange(*r) } return nil } @@ -416,8 +406,7 @@ func (handler *InoHandler) ino2cppCodeActionParams(params *lsp.CodeActionParams) func (handler *InoHandler) ino2cppDocumentRangeFormattingParams(params *lsp.DocumentRangeFormattingParams) error { handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Range.Start.Line = data.targetLineMap[params.Range.Start.Line] - params.Range.End.Line = data.targetLineMap[params.Range.End.Line] + params.Range = data.sourceMap.InoToCppRange(params.Range) return nil } return unknownURI(params.TextDocument.URI) @@ -426,7 +415,7 @@ func (handler *InoHandler) ino2cppDocumentRangeFormattingParams(params *lsp.Docu func (handler *InoHandler) ino2cppDocumentOnTypeFormattingParams(params *lsp.DocumentOnTypeFormattingParams) error { handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Position.Line = data.targetLineMap[params.Position.Line] + params.Position.Line = data.sourceMap.InoToCppLine(params.Position.Line) return nil } return unknownURI(params.TextDocument.URI) @@ -435,7 +424,7 @@ func (handler *InoHandler) ino2cppDocumentOnTypeFormattingParams(params *lsp.Doc func (handler *InoHandler) ino2cppRenameParams(params *lsp.RenameParams) error { handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Position.Line = data.targetLineMap[params.Position.Line] + params.Position.Line = data.sourceMap.InoToCppLine(params.Position.Line) return nil } return unknownURI(params.TextDocument.URI) @@ -467,13 +456,9 @@ func (handler *InoHandler) ino2cppWorkspaceEdit(origEdit *lsp.WorkspaceEdit) *ls if data, ok := handler.data[lsp.DocumentURI(uri)]; ok { newValue := make([]lsp.TextEdit, len(edit)) for index := range edit { - r := edit[index].Range newValue[index] = lsp.TextEdit{ NewText: edit[index].NewText, - Range: lsp.Range{ - Start: lsp.Position{Line: data.targetLineMap[r.Start.Line], Character: r.Start.Character}, - End: lsp.Position{Line: data.targetLineMap[r.End.Line], Character: r.End.Character}, - }, + Range: data.sourceMap.InoToCppRange(edit[index].Range), } } newEdit.Changes[string(data.targetURI)] = newValue @@ -577,9 +562,7 @@ func (handler *InoHandler) cpp2inoCompletionList(list *lsp.CompletionList, uri l for _, item := range list.Items { if !strings.HasPrefix(item.InsertText, "_") { if item.TextEdit != nil { - r := &item.TextEdit.Range - r.Start.Line = data.sourceLineMap[r.Start.Line] - r.End.Line = data.sourceLineMap[r.End.Line] + item.TextEdit.Range = data.sourceMap.CppToInoRange(item.TextEdit.Range) } newItems = append(newItems, item) } @@ -592,9 +575,7 @@ func (handler *InoHandler) cpp2inoCodeAction(codeAction *CodeAction, uri lsp.Doc codeAction.Edit = handler.cpp2inoWorkspaceEdit(codeAction.Edit) if data, ok := handler.data[uri]; ok { for index := range codeAction.Diagnostics { - r := &codeAction.Diagnostics[index].Range - r.Start.Line = data.sourceLineMap[r.Start.Line] - r.End.Line = data.sourceLineMap[r.End.Line] + codeAction.Diagnostics[index].Range = data.sourceMap.CppToInoRange(codeAction.Diagnostics[index].Range) } } } @@ -614,13 +595,9 @@ func (handler *InoHandler) cpp2inoWorkspaceEdit(origEdit *lsp.WorkspaceEdit) *ls if data, ok := handler.data[lsp.DocumentURI(uri)]; ok { newValue := make([]lsp.TextEdit, len(edit)) for index := range edit { - r := edit[index].Range newValue[index] = lsp.TextEdit{ NewText: edit[index].NewText, - Range: lsp.Range{ - Start: lsp.Position{Line: data.sourceLineMap[r.Start.Line], Character: r.Start.Character}, - End: lsp.Position{Line: data.sourceLineMap[r.End.Line], Character: r.End.Character}, - }, + Range: data.sourceMap.CppToInoRange(edit[index].Range), } } newEdit.Changes[string(data.sourceURI)] = newValue @@ -635,8 +612,7 @@ func (handler *InoHandler) cpp2inoHover(hover *Hover, uri lsp.DocumentURI) { if data, ok := handler.data[uri]; ok { r := hover.Range if r != nil { - r.Start.Line = data.sourceLineMap[r.Start.Line] - r.End.Line = data.sourceLineMap[r.End.Line] + *r = data.sourceMap.CppToInoRange(*r) } } } @@ -644,22 +620,19 @@ func (handler *InoHandler) cpp2inoHover(hover *Hover, uri lsp.DocumentURI) { func (handler *InoHandler) cpp2inoLocation(location *lsp.Location) { if data, ok := handler.data[location.URI]; ok { location.URI = data.sourceURI - location.Range.Start.Line = data.sourceLineMap[location.Range.Start.Line] - location.Range.End.Line = data.sourceLineMap[location.Range.End.Line] + location.Range = data.sourceMap.CppToInoRange(location.Range) } } func (handler *InoHandler) cpp2inoDocumentHighlight(highlight *lsp.DocumentHighlight, uri lsp.DocumentURI) { if data, ok := handler.data[uri]; ok { - highlight.Range.Start.Line = data.sourceLineMap[highlight.Range.Start.Line] - highlight.Range.End.Line = data.sourceLineMap[highlight.Range.End.Line] + highlight.Range = data.sourceMap.CppToInoRange(highlight.Range) } } func (handler *InoHandler) cpp2inoTextEdit(edit *lsp.TextEdit, uri lsp.DocumentURI) { if data, ok := handler.data[uri]; ok { - edit.Range.Start.Line = data.sourceLineMap[edit.Range.Start.Line] - edit.Range.End.Line = data.sourceLineMap[edit.Range.End.Line] + edit.Range = data.sourceMap.CppToInoRange(edit.Range) } } @@ -672,8 +645,7 @@ func (handler *InoHandler) cpp2inoDocumentSymbols(origSymbols []DocumentSymbol, symbolIdx := make(map[string]*DocumentSymbol) for i := 0; i < len(origSymbols); i++ { symbol := &origSymbols[i] - symbol.Range.Start.Line = data.sourceLineMap[symbol.Range.Start.Line] - symbol.Range.End.Line = data.sourceLineMap[symbol.Range.End.Line] + symbol.Range = data.sourceMap.CppToInoRange(symbol.Range) duplicate := false other, duplicate := symbolIdx[symbol.Name] @@ -686,8 +658,7 @@ func (handler *InoHandler) cpp2inoDocumentSymbols(origSymbols []DocumentSymbol, } } - symbol.SelectionRange.Start.Line = data.sourceLineMap[symbol.SelectionRange.Start.Line] - symbol.SelectionRange.End.Line = data.sourceLineMap[symbol.SelectionRange.End.Line] + symbol.SelectionRange = data.sourceMap.CppToInoRange(symbol.SelectionRange) symbol.Children = handler.cpp2inoDocumentSymbols(symbol.Children, uri) symbolIdx[symbol.Name] = symbol } @@ -776,9 +747,9 @@ func (handler *InoHandler) cpp2inoPublishDiagnosticsParams(params *lsp.PublishDi newDiagnostics := make([]lsp.Diagnostic, 0, len(params.Diagnostics)) for index := range params.Diagnostics { r := ¶ms.Diagnostics[index].Range - if startLine, ok := data.sourceLineMap[r.Start.Line]; ok { + if startLine, ok := data.sourceMap.CppToInoLineOk(r.Start.Line); ok { r.Start.Line = startLine - r.End.Line = data.sourceLineMap[r.End.Line] + r.End.Line = data.sourceMap.CppToInoLine(r.End.Line) newDiagnostics = append(newDiagnostics, params.Diagnostics[index]) } } diff --git a/handler/sourcemap.go b/handler/sourcemap.go deleted file mode 100644 index 41840a4..0000000 --- a/handler/sourcemap.go +++ /dev/null @@ -1,204 +0,0 @@ -package handler - -import ( - "bufio" - "fmt" - "io" - "strconv" - "strings" - - lsp "github.com/sourcegraph/go-lsp" -) - -func createSourceMaps(targetFile io.Reader) (sourceLineMap, targetLineMap map[int]int) { - sourceLine := -1 - targetLine := 0 - sourceLineMap = make(map[int]int) - targetLineMap = make(map[int]int) - scanner := bufio.NewScanner(targetFile) - for scanner.Scan() { - lineStr := scanner.Text() - if strings.HasPrefix(lineStr, "#line") { - nrEnd := strings.Index(lineStr[6:], " ") - var l int - var err error - if nrEnd > 0 { - l, err = strconv.Atoi(lineStr[6 : nrEnd+6]) - } else { - l, err = strconv.Atoi(lineStr[6:]) - } - if err == nil && l > 0 { - sourceLine = l - 1 - } - } else if sourceLine >= 0 { - sourceLineMap[targetLine] = sourceLine - targetLineMap[sourceLine] = targetLine - sourceLine++ - } - targetLine++ - } - sourceLineMap[targetLine] = sourceLine - targetLineMap[sourceLine] = targetLine - return -} - -func updateSourceMaps(sourceLineMap, targetLineMap map[int]int, deletedLines, insertLine int, insertText string) { - for i := 1; i <= deletedLines; i++ { - sourceLine := insertLine + 1 - targetLine := targetLineMap[sourceLine] - - // Shift up all following lines by one and put them into a new map - newMappings := make(map[int]int) - maxSourceLine, maxTargetLine := 0, 0 - for t, s := range sourceLineMap { - if t > targetLine && s > sourceLine { - newMappings[t-1] = s - 1 - } else if s > sourceLine { - newMappings[t] = s - 1 - } else if t > targetLine { - newMappings[t-1] = s - } - if s > maxSourceLine { - maxSourceLine = s - } - if t > maxTargetLine { - maxTargetLine = t - } - } - - // Remove mappings for the deleted line - delete(sourceLineMap, maxTargetLine) - delete(targetLineMap, maxSourceLine) - - // Copy the mappings from the intermediate map - copyMappings(sourceLineMap, targetLineMap, newMappings) - } - - addedLines := strings.Count(insertText, "\n") - if addedLines > 0 { - targetLine := targetLineMap[insertLine] - - // Shift down all following lines and put them into a new map - newMappings := make(map[int]int) - for t, s := range sourceLineMap { - if t > targetLine && s > insertLine { - newMappings[t+addedLines] = s + addedLines - } else if s > insertLine { - newMappings[t] = s + addedLines - } else if t > targetLine { - newMappings[t+addedLines] = s - } - } - - // Add mappings for the added lines - for i := 1; i <= addedLines; i++ { - sourceLineMap[targetLine+i] = insertLine + i - targetLineMap[insertLine+i] = targetLine + i - } - - // Copy the mappings from the intermediate map - copyMappings(sourceLineMap, targetLineMap, newMappings) - } -} - -func copyMappings(sourceLineMap, targetLineMap, newMappings map[int]int) { - for t, s := range newMappings { - sourceLineMap[t] = s - targetLineMap[s] = t - } - for t, s := range newMappings { - // In case multiple target lines are present for a source line, use the last one - if t > targetLineMap[s] { - targetLineMap[s] = t - } - } -} - -// OutOfRangeError returned if one attempts to access text out of its range -type OutOfRangeError struct { - Type string - Max int - Req int -} - -func (oor OutOfRangeError) Error() string { - return fmt.Sprintf("%s access out of range: max=%d requested=%d", oor.Type, oor.Max, oor.Req) -} - -func applyTextChange(text string, rang lsp.Range, insertText string) (res string, err error) { - start, err := getOffset(text, rang.Start) - if err != nil { - return "", err - } - end, err := getOffset(text, rang.End) - if err != nil { - return "", err - } - - return text[:start] + insertText + text[end:], nil -} - -// getOffset computes the offset in the text expressed by the lsp.Position. -// Returns OutOfRangeError if the position is out of range. -func getOffset(text string, pos lsp.Position) (int, error) { - // Find line - lineOffset, err := getLineOffset(text, pos.Line) - if err != nil { - return -1, err - } - character := pos.Character - if character == 0 { - return lineOffset, nil - } - - // Find the character and return its offset within the text - var count = len(text[lineOffset:]) - for offset, c := range text[lineOffset:] { - if character == offset { - // We've found the character - return lineOffset + offset, nil - } - if c == '\n' { - // We've reached the end of line. LSP spec says we should default back to the line length. - // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#position - if character > offset { - return lineOffset + offset, nil - } - count = offset - break - } - } - if character > 0 { - // We've reached the end of the last line. Default to the text length (see above). - return len(text), nil - } - - // We haven't found the character in the text (character index was negative) - return -1, OutOfRangeError{"Character", count, character} -} - -// getLineOffset finds the offset/position of the beginning of a line within the text. -// For example: -// text := "foo\nfoobar\nbaz" -// getLineOffset(text, 0) == 0 -// getLineOffset(text, 1) == 4 -// getLineOffset(text, 2) == 11 -func getLineOffset(text string, line int) (int, error) { - if line == 0 { - return 0, nil - } - - // Find the line and return its offset within the text - var count int - for offset, c := range text { - if c == '\n' { - count++ - if count == line { - return offset + 1, nil - } - } - } - - // We haven't found the line in the text - return -1, OutOfRangeError{"Line", count, line} -} diff --git a/handler/sourcemapper/ino.go b/handler/sourcemapper/ino.go new file mode 100644 index 0000000..8294a9b --- /dev/null +++ b/handler/sourcemapper/ino.go @@ -0,0 +1,157 @@ +package sourcemapper + +import ( + "bufio" + "io" + "strconv" + "strings" + + "github.com/sourcegraph/go-lsp" +) + +// InoMapper is a mapping between the .ino sketch and the preprocessed .cpp file +type InoMapper struct { + toCpp map[int]int + toIno map[int]int +} + +// InoToCppLine converts a source (.ino) line into a target (.cpp) line +func (s *InoMapper) InoToCppLine(sourceLine int) int { + return s.toCpp[sourceLine] +} + +// InoToCppRange converts a source (.ino) lsp.Range into a target (.cpp) lsp.Range +func (s *InoMapper) InoToCppRange(r lsp.Range) lsp.Range { + r.Start.Line = s.InoToCppLine(r.Start.Line) + r.End.Line = s.InoToCppLine(r.End.Line) + return r +} + +// CppToInoLine converts a target (.cpp) line into a source (.ino) line +func (s *InoMapper) CppToInoLine(targetLine int) int { + return s.toIno[targetLine] +} + +// CppToInoRange converts a target (.cpp) lsp.Range into a source (.ino) lsp.Range +func (s *InoMapper) CppToInoRange(r lsp.Range) lsp.Range { + r.Start.Line = s.CppToInoLine(r.Start.Line) + r.End.Line = s.CppToInoLine(r.End.Line) + return r +} + +// CppToInoLineOk converts a target (.cpp) line into a source (.ino) line and +// returns true if the conversion is successful +func (s *InoMapper) CppToInoLineOk(targetLine int) (int, bool) { + res, ok := s.toIno[targetLine] + return res, ok +} + +// CreateInoMapper create a InoMapper from the given target file +func CreateInoMapper(targetFile io.Reader) *InoMapper { + sourceLine := -1 + targetLine := 0 + sourceLineMap := make(map[int]int) + targetLineMap := make(map[int]int) + scanner := bufio.NewScanner(targetFile) + for scanner.Scan() { + lineStr := scanner.Text() + if strings.HasPrefix(lineStr, "#line") { + nrEnd := strings.Index(lineStr[6:], " ") + var l int + var err error + if nrEnd > 0 { + l, err = strconv.Atoi(lineStr[6 : nrEnd+6]) + } else { + l, err = strconv.Atoi(lineStr[6:]) + } + if err == nil && l > 0 { + sourceLine = l - 1 + } + } else if sourceLine >= 0 { + sourceLineMap[targetLine] = sourceLine + targetLineMap[sourceLine] = targetLine + sourceLine++ + } + targetLine++ + } + sourceLineMap[targetLine] = sourceLine + targetLineMap[sourceLine] = targetLine + return &InoMapper{ + toIno: sourceLineMap, + toCpp: targetLineMap, + } +} + +// Update performs an update to the SourceMap considering the deleted lines, the +// insertion line and the inserted text +func (s *InoMapper) Update(deletedLines, insertLine int, insertText string) { + for i := 1; i <= deletedLines; i++ { + sourceLine := insertLine + 1 + targetLine := s.toCpp[sourceLine] + + // Shift up all following lines by one and put them into a new map + newMappings := make(map[int]int) + maxSourceLine, maxTargetLine := 0, 0 + for t, s := range s.toIno { + if t > targetLine && s > sourceLine { + newMappings[t-1] = s - 1 + } else if s > sourceLine { + newMappings[t] = s - 1 + } else if t > targetLine { + newMappings[t-1] = s + } + if s > maxSourceLine { + maxSourceLine = s + } + if t > maxTargetLine { + maxTargetLine = t + } + } + + // Remove mappings for the deleted line + delete(s.toIno, maxTargetLine) + delete(s.toCpp, maxSourceLine) + + // Copy the mappings from the intermediate map + copyMappings(s.toIno, s.toCpp, newMappings) + } + + addedLines := strings.Count(insertText, "\n") + if addedLines > 0 { + targetLine := s.toCpp[insertLine] + + // Shift down all following lines and put them into a new map + newMappings := make(map[int]int) + for t, s := range s.toIno { + if t > targetLine && s > insertLine { + newMappings[t+addedLines] = s + addedLines + } else if s > insertLine { + newMappings[t] = s + addedLines + } else if t > targetLine { + newMappings[t+addedLines] = s + } + } + + // Add mappings for the added lines + for i := 1; i <= addedLines; i++ { + s.toIno[targetLine+i] = insertLine + i + s.toCpp[insertLine+i] = targetLine + i + } + + // Copy the mappings from the intermediate map + copyMappings(s.toIno, s.toCpp, newMappings) + } +} + +func copyMappings(sourceLineMap, targetLineMap, newMappings map[int]int) { + for t, s := range newMappings { + sourceLineMap[t] = s + targetLineMap[s] = t + } + for t, s := range newMappings { + // In case multiple target lines are present for a source line, use the last one + if t > targetLineMap[s] { + targetLineMap[s] = t + } + } +} diff --git a/handler/sourcemapper/ino_test.go b/handler/sourcemapper/ino_test.go new file mode 100644 index 0000000..0fdede0 --- /dev/null +++ b/handler/sourcemapper/ino_test.go @@ -0,0 +1,137 @@ +package sourcemapper + +import ( + "reflect" + "strings" + "testing" +) + +func TestCreateSourceMaps(t *testing.T) { + input := `#include +#line 1 "sketch_july2a.ino" +#line 1 "sketch_july2a.ino" + +#line 2 "sketch_july2a.ino" +void setup(); +#line 7 "sketch_july2a.ino" +void loop(); +#line 2 "sketch_july2a.ino" +void setup() { + // put your setup code here, to run once: + +} + +void loop() { + // put your main code here, to run repeatedly: + +} +` + sourceMap := CreateInoMapper(strings.NewReader(input)) + if !reflect.DeepEqual(sourceMap.toIno, map[int]int{ + 3: 0, + 5: 1, + 7: 6, + 9: 1, + 10: 2, + 11: 3, + 12: 4, + 13: 5, + 14: 6, + 15: 7, + 16: 8, + 17: 9, + 18: 10, + }) { + t.Error(sourceMap.toIno) + } + if !reflect.DeepEqual(sourceMap.toCpp, map[int]int{ + 0: 3, + 1: 9, + 2: 10, + 3: 11, + 4: 12, + 5: 13, + 6: 14, + 7: 15, + 8: 16, + 9: 17, + 10: 18}, + ) { + t.Error(sourceMap.toCpp) + } +} + +func TestUpdateSourceMaps1(t *testing.T) { + sourceMap := &InoMapper{ + toCpp: map[int]int{ + 0: 1, + 1: 2, + 2: 0, + 3: 5, + 4: 3, + 5: 4, + }, + toIno: make(map[int]int), + } + for s, t := range sourceMap.toCpp { + sourceMap.toIno[t] = s + } + sourceMap.Update(0, 1, "foo\nbar\nbaz") + if !reflect.DeepEqual(sourceMap.toCpp, map[int]int{ + 0: 1, + 1: 2, + 2: 3, + 3: 4, + 4: 0, + 5: 7, + 6: 5, + 7: 6}, + ) { + t.Error(sourceMap.toCpp) + } + if !reflect.DeepEqual(sourceMap.toIno, map[int]int{ + 0: 4, + 1: 0, + 2: 1, + 3: 2, + 4: 3, + 5: 6, + 6: 7, + 7: 5}, + ) { + t.Error(sourceMap.toIno) + } +} + +func TestUpdateSourceMaps2(t *testing.T) { + sourceMap := &InoMapper{ + toCpp: map[int]int{ + 0: 1, + 1: 2, + 2: 0, + 3: 5, + 4: 3, + 5: 4}, + toIno: make(map[int]int), + } + for s, t := range sourceMap.toCpp { + sourceMap.toIno[t] = s + } + sourceMap.Update(2, 1, "foo") + if !reflect.DeepEqual(sourceMap.toCpp, map[int]int{ + 0: 0, + 1: 1, + 2: 2, + 3: 3}, + ) { + t.Error(sourceMap.toCpp) + } + if !reflect.DeepEqual(sourceMap.toIno, map[int]int{ + 0: 0, + 1: 1, + 2: 2, + 3: 3}, + ) { + t.Error(sourceMap.toIno) + } +} diff --git a/handler/textutils/textutils.go b/handler/textutils/textutils.go new file mode 100644 index 0000000..d3a24b4 --- /dev/null +++ b/handler/textutils/textutils.go @@ -0,0 +1,97 @@ +package textutils + +import ( + "fmt" + + "github.com/sourcegraph/go-lsp" +) + +// ApplyTextChange replaces startingText substring specified by replaceRange with insertText +func ApplyTextChange(startingText string, replaceRange lsp.Range, insertText string) (res string, err error) { + start, err := getOffset(startingText, replaceRange.Start) + if err != nil { + return "", err + } + end, err := getOffset(startingText, replaceRange.End) + if err != nil { + return "", err + } + + return startingText[:start] + insertText + startingText[end:], nil +} + +// getOffset computes the offset in the text expressed by the lsp.Position. +// Returns OutOfRangeError if the position is out of range. +func getOffset(text string, pos lsp.Position) (int, error) { + // Find line + lineOffset, err := getLineOffset(text, pos.Line) + if err != nil { + return -1, err + } + character := pos.Character + if character == 0 { + return lineOffset, nil + } + + // Find the character and return its offset within the text + var count = len(text[lineOffset:]) + for offset, c := range text[lineOffset:] { + if character == offset { + // We've found the character + return lineOffset + offset, nil + } + if c == '\n' { + // We've reached the end of line. LSP spec says we should default back to the line length. + // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#position + if character > offset { + return lineOffset + offset, nil + } + count = offset + break + } + } + if character > 0 { + // We've reached the end of the last line. Default to the text length (see above). + return len(text), nil + } + + // We haven't found the character in the text (character index was negative) + return -1, OutOfRangeError{"Character", count, character} +} + +// getLineOffset finds the offset/position of the beginning of a line within the text. +// For example: +// text := "foo\nfoobar\nbaz" +// getLineOffset(text, 0) == 0 +// getLineOffset(text, 1) == 4 +// getLineOffset(text, 2) == 11 +func getLineOffset(text string, line int) (int, error) { + if line == 0 { + return 0, nil + } + + // Find the line and return its offset within the text + var count int + for offset, c := range text { + if c == '\n' { + count++ + if count == line { + return offset + 1, nil + } + } + } + + // We haven't found the line in the text + return -1, OutOfRangeError{"Line", count, line} +} + +// OutOfRangeError returned if one attempts to access text out of its range +type OutOfRangeError struct { + Type string + Max int + Req int +} + +func (oor OutOfRangeError) Error() string { + return fmt.Sprintf("%s access out of range: max=%d requested=%d", oor.Type, oor.Max, oor.Req) +} diff --git a/handler/sourcemap_test.go b/handler/textutils/textutils_test.go similarity index 69% rename from handler/sourcemap_test.go rename to handler/textutils/textutils_test.go index d9fe22a..032acda 100644 --- a/handler/sourcemap_test.go +++ b/handler/textutils/textutils_test.go @@ -1,76 +1,12 @@ -package handler +package textutils import ( - "reflect" "strings" "testing" - lsp "github.com/sourcegraph/go-lsp" + "github.com/sourcegraph/go-lsp" ) -func TestCreateSourceMaps(t *testing.T) { - input := `#include -#line 1 "sketch_july2a.ino" -#line 1 "sketch_july2a.ino" - -#line 2 "sketch_july2a.ino" -void setup(); -#line 7 "sketch_july2a.ino" -void loop(); -#line 2 "sketch_july2a.ino" -void setup() { - // put your setup code here, to run once: - -} - -void loop() { - // put your main code here, to run repeatedly: - -} -` - sourceLineMap, targetLineMap := createSourceMaps(strings.NewReader(input)) - if !reflect.DeepEqual(sourceLineMap, map[int]int{ - 3: 0, 5: 1, 7: 6, 9: 1, 10: 2, 11: 3, 12: 4, 13: 5, 14: 6, 15: 7, 16: 8, 17: 9, 18: 10, - }) { - t.Error(sourceLineMap) - } - if !reflect.DeepEqual(targetLineMap, map[int]int{ - 0: 3, 1: 9, 2: 10, 3: 11, 4: 12, 5: 13, 6: 14, 7: 15, 8: 16, 9: 17, 10: 18, - }) { - t.Error(targetLineMap) - } -} - -func TestUpdateSourceMaps1(t *testing.T) { - targetLineMap := map[int]int{0: 1, 1: 2, 2: 0, 3: 5, 4: 3, 5: 4} - sourceLineMap := make(map[int]int) - for s, t := range targetLineMap { - sourceLineMap[t] = s - } - updateSourceMaps(sourceLineMap, targetLineMap, 0, 1, "foo\nbar\nbaz") - if !reflect.DeepEqual(targetLineMap, map[int]int{0: 1, 1: 2, 2: 3, 3: 4, 4: 0, 5: 7, 6: 5, 7: 6}) { - t.Error(targetLineMap) - } - if !reflect.DeepEqual(sourceLineMap, map[int]int{0: 4, 1: 0, 2: 1, 3: 2, 4: 3, 5: 6, 6: 7, 7: 5}) { - t.Error(sourceLineMap) - } -} - -func TestUpdateSourceMaps2(t *testing.T) { - targetLineMap := map[int]int{0: 1, 1: 2, 2: 0, 3: 5, 4: 3, 5: 4} - sourceLineMap := make(map[int]int) - for s, t := range targetLineMap { - sourceLineMap[t] = s - } - updateSourceMaps(sourceLineMap, targetLineMap, 2, 1, "foo") - if !reflect.DeepEqual(targetLineMap, map[int]int{0: 0, 1: 1, 2: 2, 3: 3}) { - t.Error(targetLineMap) - } - if !reflect.DeepEqual(sourceLineMap, map[int]int{0: 0, 1: 1, 2: 2, 3: 3}) { - t.Error(sourceLineMap) - } -} - func TestApplyTextChange(t *testing.T) { tests := []struct { InitialText string @@ -159,7 +95,7 @@ func TestApplyTextChange(t *testing.T) { expectation := strings.ReplaceAll(test.Expectation, "\n", "\\n") t.Logf("applyTextChange(\"%s\", %v, \"%s\") == \"%s\"", initial, test.Range, insertion, expectation) - act, err := applyTextChange(test.InitialText, test.Range, test.Insertion) + act, err := ApplyTextChange(test.InitialText, test.Range, test.Insertion) if act != test.Expectation { t.Errorf("applyTextChange(\"%s\", %v, \"%s\") != \"%s\", got \"%s\"", initial, test.Range, insertion, expectation, strings.ReplaceAll(act, "\n", "\\n")) } From a03fc2034e8ab58a44f4e44614b3ef48c7f29f6e Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 9 Nov 2020 19:55:44 +0100 Subject: [PATCH 6/6] Output stackstrace in log in case of crash --- main.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index a97dbf1..7fd478c 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime/debug" "github.com/bcmi-labs/arduino-language-server/handler" "github.com/bcmi-labs/arduino-language-server/streams" @@ -33,7 +34,13 @@ func main() { if enableLogging { logfile := openLogFile("inols-err.log") - defer logfile.Close() + defer func() { + // In case of panic output the stack trace in the log file before exiting + if r := recover(); r != nil { + log.Println(string(debug.Stack())) + } + logfile.Close() + }() log.SetOutput(logfile) } else { log.SetOutput(os.Stderr)