From 7cda7b31cc2ddf99601e974b37adc1cbf8e7d6d6 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Sat, 5 Jul 2025 23:01:53 +0300 Subject: [PATCH 1/3] Fixed the language server crashing upon a go to definition request on untracked file --- ls/ls.go | 34 +++++++++++++++++++++++++++------- ls/ls_clang_to_ide.go | 36 ++++++++++++++++++++++++++++++++---- ls/ls_ide_to_clang.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/ls/ls.go b/ls/ls.go index b487f087..aea9a0c 100644 --- a/ls/ls.go +++ b/ls/ls.go @@ -68,6 +68,10 @@ type INOLanguageServer struct { sketchRebuilder *sketchRebuilder } +type TranslationOpts struct { + loadRelToIde bool +} + // Config describes the language server configuration. type Config struct { Fqbn string @@ -83,6 +87,13 @@ type Config struct { Jobs int } +var extToFileType = map[string]string{ + ".ino": "arduino", + ".cpp": "cpp", + ".h": "cpp", + ".c": "c", +} + var yellow = color.New(color.FgHiYellow) func (ls *INOLanguageServer) writeLock(logger jsonrpc.FunctionLogger, requireClangd bool) { @@ -603,7 +614,8 @@ func (ls *INOLanguageServer) textDocumentDefinitionReqFromIDE(ctx context.Contex var ideLocations []lsp.Location if clangLocations != nil { - ideLocations, err = ls.clang2IdeLocationsArray(logger, clangLocations) + opts := TranslationOpts{loadRelToIde: true} + ideLocations, err = ls.clang2IdeLocationsArray2(logger, clangLocations, &opts) if err != nil { logger.Logf("Error: %v", err) ls.Close() @@ -989,15 +1001,13 @@ func (ls *INOLanguageServer) exitNotifFromIDE(logger jsonrpc.FunctionLogger) { ls.Close() } -func (ls *INOLanguageServer) textDocumentDidOpenNotifFromIDE(logger jsonrpc.FunctionLogger, ideParam *lsp.DidOpenTextDocumentParams) { - ls.writeLock(logger, true) - defer ls.writeUnlock(logger) +func (ls *INOLanguageServer) implTextDocumentDidOpenNotifFromIDE(logger jsonrpc.FunctionLogger, ideParam lsp.TextDocumentItem) *jsonrpc.ResponseError { - ideTextDocItem := ideParam.TextDocument + ideTextDocItem := ideParam clangURI, _, err := ls.ide2ClangDocumentURI(logger, ideTextDocItem.URI) if err != nil { logger.Logf("Error: %s", err) - return + return &jsonrpc.ResponseError{Code: jsonrpc.ErrorCodesInternalError, Message: err.Error()} } if ls.ideURIIsPartOfTheSketch(ideTextDocItem.URI) { @@ -1017,7 +1027,7 @@ func (ls *INOLanguageServer) textDocumentDidOpenNotifFromIDE(logger jsonrpc.Func // Notify clangd that sketchCpp has been opened only once if ls.sketchTrackedFilesCount != 1 { logger.Logf("Clang already notified, do not notify it anymore") - return + return nil } } @@ -1045,7 +1055,17 @@ func (ls *INOLanguageServer) textDocumentDidOpenNotifFromIDE(logger jsonrpc.Func logger.Logf("Error sending notification to clangd server: %v", err) logger.Logf("Please restart the language server.") ls.Close() + return &jsonrpc.ResponseError{Code: jsonrpc.ErrorCodesInternalError, Message: err.Error()} + } + return nil + +} + +func (ls *INOLanguageServer) textDocumentDidOpenNotifFromIDE(logger jsonrpc.FunctionLogger, ideParam *lsp.DidOpenTextDocumentParams) { + ls.writeLock(logger, true) + defer ls.writeUnlock(logger) + ls.implTextDocumentDidOpenNotifFromIDE(logger, ideParam.TextDocument) } func (ls *INOLanguageServer) textDocumentDidChangeNotifFromIDE(logger jsonrpc.FunctionLogger, ideParams *lsp.DidChangeTextDocumentParams) { diff --git a/ls/ls_clang_to_ide.go b/ls/ls_clang_to_ide.go index 882d1b5..c0d9d1c 100644 --- a/ls/ls_clang_to_ide.go +++ b/ls/ls_clang_to_ide.go @@ -27,12 +27,16 @@ func (ls *INOLanguageServer) clangURIRefersToIno(clangURI lsp.DocumentURI) bool return clangURI.AsPath().EquivalentTo(ls.buildSketchCpp) } +func (ls *INOLanguageServer) clang2IdeRangeAndDocumentURI(logger jsonrpc.FunctionLogger, clangURI lsp.DocumentURI, clangRange lsp.Range) (lsp.DocumentURI, lsp.Range, bool, error) { + return ls.clang2IdeRangeAndDocumentURI2(logger, clangURI, clangRange, nil) +} + // Convert Range and DocumentURI from Clang to IDE. // Returns: // - The IDE DocumentURI and Range // - a boolean that is true if the clang range is in the preprocessed area of the sketch // - an error -func (ls *INOLanguageServer) clang2IdeRangeAndDocumentURI(logger jsonrpc.FunctionLogger, clangURI lsp.DocumentURI, clangRange lsp.Range) (lsp.DocumentURI, lsp.Range, bool, error) { +func (ls *INOLanguageServer) clang2IdeRangeAndDocumentURI2(logger jsonrpc.FunctionLogger, clangURI lsp.DocumentURI, clangRange lsp.Range, opts *TranslationOpts) (lsp.DocumentURI, lsp.Range, bool, error) { // Sketchbook/Sketch/Sketch.ino <-> build-path/sketch/Sketch.ino.cpp // Sketchbook/Sketch/AnotherTab.ino <-> build-path/sketch/Sketch.ino.cpp (different section from above) if ls.clangURIRefersToIno(clangURI) { @@ -80,7 +84,25 @@ func (ls *INOLanguageServer) clang2IdeRangeAndDocumentURI(logger jsonrpc.Functio return lsp.NilURI, lsp.NilRange, false, err } idePath := ls.sketchRoot.JoinPath(rel).String() - ideURI, err := ls.idePathToIdeURI(logger, idePath) + + var ideURI lsp.DocumentURI + if opts == nil || !opts.loadRelToIde { + ideURI, err = ls.idePathToIdeURI(logger, idePath) + } else { + doc, ok := ls.trackedIdeDocs[idePath] + if !ok { + var shouldOpen bool + doc, shouldOpen, err = makeTextDocumentItem(logger, idePath) + if err != nil { + logger.Logf("ERROR: could not open '%s': %s", idePath, err) + } + if shouldOpen { + ls.implTextDocumentDidOpenNotifFromIDE(logger, doc) + } + + } + ideURI = doc.URI + } if ideRange.End.Line > 0 { ideRange.End.Line-- } @@ -296,9 +318,12 @@ func (ls *INOLanguageServer) cland2IdeTextEdits(logger jsonrpc.FunctionLogger, c } func (ls *INOLanguageServer) clang2IdeLocationsArray(logger jsonrpc.FunctionLogger, clangLocations []lsp.Location) ([]lsp.Location, error) { + return ls.clang2IdeLocationsArray2(logger, clangLocations, nil) +} +func (ls *INOLanguageServer) clang2IdeLocationsArray2(logger jsonrpc.FunctionLogger, clangLocations []lsp.Location, opts *TranslationOpts) ([]lsp.Location, error) { ideLocations := []lsp.Location{} for _, clangLocation := range clangLocations { - ideLocation, inPreprocessed, err := ls.clang2IdeLocation(logger, clangLocation) + ideLocation, inPreprocessed, err := ls.clang2IdeLocation2(logger, clangLocation, opts) if err != nil { logger.Logf("ERROR converting location %s: %s", clangLocation, err) return nil, err @@ -313,7 +338,10 @@ func (ls *INOLanguageServer) clang2IdeLocationsArray(logger jsonrpc.FunctionLogg } func (ls *INOLanguageServer) clang2IdeLocation(logger jsonrpc.FunctionLogger, clangLocation lsp.Location) (lsp.Location, bool, error) { - ideURI, ideRange, inPreprocessed, err := ls.clang2IdeRangeAndDocumentURI(logger, clangLocation.URI, clangLocation.Range) + return ls.clang2IdeLocation2(logger, clangLocation, nil) +} +func (ls *INOLanguageServer) clang2IdeLocation2(logger jsonrpc.FunctionLogger, clangLocation lsp.Location, opts *TranslationOpts) (lsp.Location, bool, error) { + ideURI, ideRange, inPreprocessed, err := ls.clang2IdeRangeAndDocumentURI2(logger, clangLocation.URI, clangLocation.Range, opts) return lsp.Location{ URI: ideURI, Range: ideRange, diff --git a/ls/ls_ide_to_clang.go b/ls/ls_ide_to_clang.go index e6ccc11..1c8c906 100644 --- a/ls/ls_ide_to_clang.go +++ b/ls/ls_ide_to_clang.go @@ -17,13 +17,43 @@ package ls import ( "fmt" + "os" + "path/filepath" "github.com/arduino/arduino-language-server/sourcemapper" "go.bug.st/lsp" "go.bug.st/lsp/jsonrpc" ) +func getFileType(filename string) string { + ext := filepath.Ext(filename) + if fileType, ok := extToFileType[ext]; ok { + return fileType + } + return "unknown" +} func (ls *INOLanguageServer) idePathToIdeURI(logger jsonrpc.FunctionLogger, inoPath string) (lsp.DocumentURI, error) { + return ls.idePathToIdeURI2(logger, inoPath, nil) +} + +func makeTextDocumentItem(logger jsonrpc.FunctionLogger, path string) (lsp.TextDocumentItem, bool, error) { + filetype := getFileType(path) + if filetype == "unknown" { + return lsp.TextDocumentItem{}, false, nil + } + uri := lsp.NewDocumentURI(path) + languageId := filetype + version := 0 + + text, err := os.ReadFile(path) + if err != nil { + logger.Logf("Could not read file: %v", err) + return lsp.TextDocumentItem{}, false, err + } + return lsp.TextDocumentItem{URI: uri, LanguageID: languageId, Version: version, Text: string(text)}, true, nil + +} +func (ls *INOLanguageServer) idePathToIdeURI2(logger jsonrpc.FunctionLogger, inoPath string, opts *TranslationOpts) (lsp.DocumentURI, error) { if inoPath == sourcemapper.NotIno.File { return sourcemapper.NotInoURI, nil } From 0ea188700b87c814ae5ee3670f5dbd3722abf481 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Tue, 8 Jul 2025 19:11:38 +0300 Subject: [PATCH 2/3] Removed unnecessary file types in go to definition and returned error on unknown file type --- ls/ls.go | 14 +++++++++++--- ls/ls_ide_to_clang.go | 8 ++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ls/ls.go b/ls/ls.go index aea9a0c..9fe6ad6 100644 --- a/ls/ls.go +++ b/ls/ls.go @@ -88,10 +88,8 @@ type Config struct { } var extToFileType = map[string]string{ - ".ino": "arduino", - ".cpp": "cpp", ".h": "cpp", - ".c": "c", + ".hpp": "cpp", } var yellow = color.New(color.FgHiYellow) @@ -1639,6 +1637,16 @@ type UnknownURIError struct { URI lsp.DocumentURI } +// UnknownFileExtensionError when a file extension isn't recognized +// and a file with it has to be opened. +type UnknownFileExtensionError struct { + extension string +} + func (e *UnknownURIError) Error() string { return "Document is not available: " + e.URI.String() } + +func (e *UnknownFileExtensionError) Error() string { + return "Unknown file extension " + e.extension +} diff --git a/ls/ls_ide_to_clang.go b/ls/ls_ide_to_clang.go index 1c8c906..bf40701 100644 --- a/ls/ls_ide_to_clang.go +++ b/ls/ls_ide_to_clang.go @@ -25,8 +25,7 @@ import ( "go.bug.st/lsp/jsonrpc" ) -func getFileType(filename string) string { - ext := filepath.Ext(filename) +func getFileType(ext string) string { if fileType, ok := extToFileType[ext]; ok { return fileType } @@ -37,11 +36,12 @@ func (ls *INOLanguageServer) idePathToIdeURI(logger jsonrpc.FunctionLogger, inoP } func makeTextDocumentItem(logger jsonrpc.FunctionLogger, path string) (lsp.TextDocumentItem, bool, error) { + ext := filepath.Ext(path) filetype := getFileType(path) + uri := lsp.NewDocumentURI(path) if filetype == "unknown" { - return lsp.TextDocumentItem{}, false, nil + return lsp.TextDocumentItem{URI: uri, LanguageID: filetype}, false, &UnknownFileExtensionError{ext} } - uri := lsp.NewDocumentURI(path) languageId := filetype version := 0 From 782e96a365ceb07bc7edc085a46e61b2046487ca Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Tue, 8 Jul 2025 19:43:59 +0300 Subject: [PATCH 3/3] Fixed linting errors --- ls/ls.go | 10 +++++----- ls/ls_clang_to_ide.go | 6 +++--- ls/ls_ide_to_clang.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ls/ls.go b/ls/ls.go index 9fe6ad6..40b497e 100644 --- a/ls/ls.go +++ b/ls/ls.go @@ -68,10 +68,6 @@ type INOLanguageServer struct { sketchRebuilder *sketchRebuilder } -type TranslationOpts struct { - loadRelToIde bool -} - // Config describes the language server configuration. type Config struct { Fqbn string @@ -92,6 +88,10 @@ var extToFileType = map[string]string{ ".hpp": "cpp", } +type translationOpts struct { + loadRelToIde bool +} + var yellow = color.New(color.FgHiYellow) func (ls *INOLanguageServer) writeLock(logger jsonrpc.FunctionLogger, requireClangd bool) { @@ -612,7 +612,7 @@ func (ls *INOLanguageServer) textDocumentDefinitionReqFromIDE(ctx context.Contex var ideLocations []lsp.Location if clangLocations != nil { - opts := TranslationOpts{loadRelToIde: true} + opts := translationOpts{loadRelToIde: true} ideLocations, err = ls.clang2IdeLocationsArray2(logger, clangLocations, &opts) if err != nil { logger.Logf("Error: %v", err) diff --git a/ls/ls_clang_to_ide.go b/ls/ls_clang_to_ide.go index c0d9d1c..d8ef4ca 100644 --- a/ls/ls_clang_to_ide.go +++ b/ls/ls_clang_to_ide.go @@ -36,7 +36,7 @@ func (ls *INOLanguageServer) clang2IdeRangeAndDocumentURI(logger jsonrpc.Functio // - The IDE DocumentURI and Range // - a boolean that is true if the clang range is in the preprocessed area of the sketch // - an error -func (ls *INOLanguageServer) clang2IdeRangeAndDocumentURI2(logger jsonrpc.FunctionLogger, clangURI lsp.DocumentURI, clangRange lsp.Range, opts *TranslationOpts) (lsp.DocumentURI, lsp.Range, bool, error) { +func (ls *INOLanguageServer) clang2IdeRangeAndDocumentURI2(logger jsonrpc.FunctionLogger, clangURI lsp.DocumentURI, clangRange lsp.Range, opts *translationOpts) (lsp.DocumentURI, lsp.Range, bool, error) { // Sketchbook/Sketch/Sketch.ino <-> build-path/sketch/Sketch.ino.cpp // Sketchbook/Sketch/AnotherTab.ino <-> build-path/sketch/Sketch.ino.cpp (different section from above) if ls.clangURIRefersToIno(clangURI) { @@ -320,7 +320,7 @@ func (ls *INOLanguageServer) cland2IdeTextEdits(logger jsonrpc.FunctionLogger, c func (ls *INOLanguageServer) clang2IdeLocationsArray(logger jsonrpc.FunctionLogger, clangLocations []lsp.Location) ([]lsp.Location, error) { return ls.clang2IdeLocationsArray2(logger, clangLocations, nil) } -func (ls *INOLanguageServer) clang2IdeLocationsArray2(logger jsonrpc.FunctionLogger, clangLocations []lsp.Location, opts *TranslationOpts) ([]lsp.Location, error) { +func (ls *INOLanguageServer) clang2IdeLocationsArray2(logger jsonrpc.FunctionLogger, clangLocations []lsp.Location, opts *translationOpts) ([]lsp.Location, error) { ideLocations := []lsp.Location{} for _, clangLocation := range clangLocations { ideLocation, inPreprocessed, err := ls.clang2IdeLocation2(logger, clangLocation, opts) @@ -340,7 +340,7 @@ func (ls *INOLanguageServer) clang2IdeLocationsArray2(logger jsonrpc.FunctionLog func (ls *INOLanguageServer) clang2IdeLocation(logger jsonrpc.FunctionLogger, clangLocation lsp.Location) (lsp.Location, bool, error) { return ls.clang2IdeLocation2(logger, clangLocation, nil) } -func (ls *INOLanguageServer) clang2IdeLocation2(logger jsonrpc.FunctionLogger, clangLocation lsp.Location, opts *TranslationOpts) (lsp.Location, bool, error) { +func (ls *INOLanguageServer) clang2IdeLocation2(logger jsonrpc.FunctionLogger, clangLocation lsp.Location, opts *translationOpts) (lsp.Location, bool, error) { ideURI, ideRange, inPreprocessed, err := ls.clang2IdeRangeAndDocumentURI2(logger, clangLocation.URI, clangLocation.Range, opts) return lsp.Location{ URI: ideURI, diff --git a/ls/ls_ide_to_clang.go b/ls/ls_ide_to_clang.go index bf40701..55a51a0 100644 --- a/ls/ls_ide_to_clang.go +++ b/ls/ls_ide_to_clang.go @@ -42,7 +42,7 @@ func makeTextDocumentItem(logger jsonrpc.FunctionLogger, path string) (lsp.TextD if filetype == "unknown" { return lsp.TextDocumentItem{URI: uri, LanguageID: filetype}, false, &UnknownFileExtensionError{ext} } - languageId := filetype + languageID := filetype version := 0 text, err := os.ReadFile(path) @@ -50,10 +50,10 @@ func makeTextDocumentItem(logger jsonrpc.FunctionLogger, path string) (lsp.TextD logger.Logf("Could not read file: %v", err) return lsp.TextDocumentItem{}, false, err } - return lsp.TextDocumentItem{URI: uri, LanguageID: languageId, Version: version, Text: string(text)}, true, nil + return lsp.TextDocumentItem{URI: uri, LanguageID: languageID, Version: version, Text: string(text)}, true, nil } -func (ls *INOLanguageServer) idePathToIdeURI2(logger jsonrpc.FunctionLogger, inoPath string, opts *TranslationOpts) (lsp.DocumentURI, error) { +func (ls *INOLanguageServer) idePathToIdeURI2(logger jsonrpc.FunctionLogger, inoPath string) (lsp.DocumentURI, error) { if inoPath == sourcemapper.NotIno.File { return sourcemapper.NotInoURI, nil }