diff --git a/ls/builder.go b/ls/builder.go index 66d5233..e56e1c7 100644 --- a/ls/builder.go +++ b/ls/builder.go @@ -25,18 +25,24 @@ import ( "google.golang.org/grpc" ) +type rebuildSketchParams struct { + trigger chan chan<- bool + fullRebuild bool +} + type SketchRebuilder struct { - ls *INOLanguageServer - trigger chan chan<- bool - cancel func() - mutex sync.Mutex + ls *INOLanguageServer + params *rebuildSketchParams + cancel func() + mutex sync.Mutex } func NewSketchBuilder(ls *INOLanguageServer) *SketchRebuilder { + p := rebuildSketchParams{trigger: make(chan chan<- bool, 1), fullRebuild: false} res := &SketchRebuilder{ - trigger: make(chan chan<- bool, 1), - cancel: func() {}, - ls: ls, + params: &p, + cancel: func() {}, + ls: ls, } go func() { defer streams.CatchAndLogPanic() @@ -45,25 +51,26 @@ func NewSketchBuilder(ls *INOLanguageServer) *SketchRebuilder { return res } -func (ls *INOLanguageServer) triggerRebuildAndWait(logger jsonrpc.FunctionLogger) { +func (ls *INOLanguageServer) triggerRebuildAndWait(logger jsonrpc.FunctionLogger, fullRebuild bool) { completed := make(chan bool) - ls.sketchRebuilder.TriggerRebuild(completed) + ls.sketchRebuilder.TriggerRebuild(completed, fullRebuild) ls.writeUnlock(logger) <-completed ls.writeLock(logger, true) } -func (ls *INOLanguageServer) triggerRebuild() { - ls.sketchRebuilder.TriggerRebuild(nil) +func (ls *INOLanguageServer) triggerRebuild(fullRebuild bool) { + ls.sketchRebuilder.TriggerRebuild(nil, fullRebuild) } -func (r *SketchRebuilder) TriggerRebuild(completed chan<- bool) { +func (r *SketchRebuilder) TriggerRebuild(completed chan<- bool, fullRebuild bool) { r.mutex.Lock() defer r.mutex.Unlock() r.cancel() // Stop possibly already running builds + r.params.fullRebuild = fullRebuild select { - case r.trigger <- completed: + case r.params.trigger <- completed: default: } } @@ -71,12 +78,12 @@ func (r *SketchRebuilder) TriggerRebuild(completed chan<- bool) { func (r *SketchRebuilder) rebuilderLoop() { logger := NewLSPFunctionLogger(color.HiMagentaString, "SKETCH REBUILD: ") for { - completed := <-r.trigger + completed := <-r.params.trigger for { // Concede a 200ms delay to accumulate bursts of changes select { - case <-r.trigger: + case <-r.params.trigger: continue case <-time.After(time.Second): } @@ -88,11 +95,12 @@ func (r *SketchRebuilder) rebuilderLoop() { ctx, cancel := context.WithCancel(context.Background()) r.mutex.Lock() - logger.Logf("Sketch rebuild started") + fullRebuild := r.params.fullRebuild + logger.Logf("Sketch rebuild started. Full-rebuild: %v", fullRebuild) r.cancel = cancel r.mutex.Unlock() - if err := r.doRebuild(ctx, logger); err != nil { + if err := r.doRebuild(ctx, fullRebuild, logger); err != nil { logger.Logf("Error: %s", err) } @@ -104,10 +112,16 @@ func (r *SketchRebuilder) rebuilderLoop() { } } -func (r *SketchRebuilder) doRebuild(ctx context.Context, logger jsonrpc.FunctionLogger) error { +func (r *SketchRebuilder) doRebuild(ctx context.Context, fullRebuild bool, logger jsonrpc.FunctionLogger) error { ls := r.ls + var buildPath *paths.Path + if fullRebuild { + buildPath = ls.fullBuildPath + } else { + buildPath = ls.buildPath + } - if success, err := ls.generateBuildEnvironment(ctx, logger); err != nil { + if success, err := ls.generateBuildEnvironment(ctx, fullRebuild, buildPath, logger); err != nil { return err } else if !success { return fmt.Errorf("build failed") @@ -123,14 +137,12 @@ func (r *SketchRebuilder) doRebuild(ctx context.Context, logger jsonrpc.Function default: } - if err := ls.buildPath.Join("compile_commands.json").CopyTo(ls.compileCommandsDir.Join("compile_commands.json")); err != nil { - logger.Logf("ERROR: updating compile_commands: %s", err) - } + ls.CopyBuildResults(logger, buildPath, fullRebuild) if cppContent, err := ls.buildSketchCpp.ReadFile(); err == nil { - oldVesrion := ls.sketchMapper.CppText.Version + oldVersion := ls.sketchMapper.CppText.Version ls.sketchMapper = sourcemapper.CreateInoMapper(cppContent) - ls.sketchMapper.CppText.Version = oldVesrion + 1 + ls.sketchMapper.CppText.Version = oldVersion + 1 ls.sketchMapper.DebugLogAll() } else { return errors.WithMessage(err, "reading generated cpp file from sketch") @@ -143,7 +155,7 @@ func (r *SketchRebuilder) doRebuild(ctx context.Context, logger jsonrpc.Function TextDocument: lsp.TextDocumentIdentifier{URI: cppURI}, } if err := ls.Clangd.conn.TextDocumentDidSave(didSaveParams); err != nil { - logger.Logf("error reinitilizing clangd:", err) + logger.Logf("error reinitializing clangd:", err) return err } @@ -159,18 +171,17 @@ func (r *SketchRebuilder) doRebuild(ctx context.Context, logger jsonrpc.Function }, } if err := ls.Clangd.conn.TextDocumentDidChange(didChangeParams); err != nil { - logger.Logf("error reinitilizing clangd:", err) + logger.Logf("error reinitializing clangd:", err) return err } return nil } -func (ls *INOLanguageServer) generateBuildEnvironment(ctx context.Context, logger jsonrpc.FunctionLogger) (bool, error) { +func (ls *INOLanguageServer) generateBuildEnvironment(ctx context.Context, fullBuild bool, buildPath *paths.Path, logger jsonrpc.FunctionLogger) (bool, error) { // Extract all build information from language server status ls.readLock(logger, false) sketchRoot := ls.sketchRoot - buildPath := ls.buildPath config := ls.config type overridesFile struct { Overrides map[string]string `json:"overrides"` @@ -204,6 +215,7 @@ func (ls *INOLanguageServer) generateBuildEnvironment(ctx context.Context, logge BuildPath: buildPath.String(), CreateCompilationDatabaseOnly: true, Verbose: true, + SkipLibrariesDiscovery: !fullBuild, } compileReqJson, _ := json.MarshalIndent(compileReq, "", " ") logger.Logf("Running build with: %s", string(compileReqJson)) @@ -264,9 +276,12 @@ func (ls *INOLanguageServer) generateBuildEnvironment(ctx context.Context, logge "--source-override", overridesJSON.String(), "--build-path", buildPath.String(), "--format", "json", - //"--clean", - sketchRoot.String(), } + if !fullBuild { + args = append(args, "--skip-libraries-discovery") + } + args = append(args, sketchRoot.String()) + cmd, err := executils.NewProcess(nil, args...) if err != nil { return false, errors.Errorf("running %s: %s", strings.Join(args, " "), err) diff --git a/ls/ls.go b/ls/ls.go index 3776d9d..c9f9ba5 100644 --- a/ls/ls.go +++ b/ls/ls.go @@ -40,6 +40,7 @@ type INOLanguageServer struct { buildPath *paths.Path buildSketchRoot *paths.Path buildSketchCpp *paths.Path + fullBuildPath *paths.Path sketchRoot *paths.Path sketchName string sketchMapper *sourcemapper.SketchMapper @@ -136,9 +137,16 @@ func NewINOLanguageServer(stdin io.Reader, stdout io.Writer, config *Config) *IN ls.buildSketchRoot = ls.buildPath.Join("sketch") } + if tmp, err := paths.MkTempDir("", "arduino-language-server"); err != nil { + log.Fatalf("Could not create temp folder: %s", err) + } else { + ls.fullBuildPath = tmp.Canonical() + } + logger.Logf("Initial board configuration: %s", ls.config.Fqbn) logger.Logf("Language server build path: %s", ls.buildPath) logger.Logf("Language server build sketch root: %s", ls.buildSketchRoot) + logger.Logf("Language server FULL build path: %s", ls.fullBuildPath) logger.Logf("Language server compile-commands: %s", ls.compileCommandsDir.Join("compile_commands.json")) ls.IDE = NewIDELSPServer(logger, stdin, stdout, ls) @@ -166,7 +174,7 @@ func (ls *INOLanguageServer) InitializeReqFromIDE(ctx context.Context, logger js ls.sketchName = ls.sketchRoot.Base() ls.buildSketchCpp = ls.buildSketchRoot.Join(ls.sketchName + ".ino.cpp") - if success, err := ls.generateBuildEnvironment(context.Background(), logger); err != nil { + if success, err := ls.generateBuildEnvironment(context.Background(), true, ls.buildPath, logger); err != nil { logger.Logf("error starting clang: %s", err) return } else if !success { @@ -209,10 +217,10 @@ func (ls *INOLanguageServer) InitializeReqFromIDE(ctx context.Context, logger js clangInitializeParams.RootPath = ls.buildSketchRoot.String() clangInitializeParams.RootURI = lsp.NewDocumentURIFromPath(ls.buildSketchRoot) if clangInitializeResult, clangErr, err := ls.Clangd.conn.Initialize(ctx, &clangInitializeParams); err != nil { - logger.Logf("error initilizing clangd: %v", err) + logger.Logf("error initializing clangd: %v", err) return } else if clangErr != nil { - logger.Logf("error initilizing clangd: %v", clangErr.AsError()) + logger.Logf("error initializing clangd: %v", clangErr.AsError()) return } else { logger.Logf("clangd successfully started: %s", string(lsp.EncodeMessage(clangInitializeResult))) @@ -347,7 +355,7 @@ func (ls *INOLanguageServer) InitializeReqFromIDE(ctx context.Context, logger js // TokenModifiers: []string{}, // }, // Range: false, - // Full: &lsp.SemantiTokenFullOptions{ + // Full: &lsp.SemanticTokenFullOptions{ // Delta: true, // }, // }, @@ -437,7 +445,7 @@ func (ls *INOLanguageServer) TextDocumentCompletionReqFromIDE(ctx context.Contex if clangItem.Command != nil { c := ls.clang2IdeCommand(logger, *clangItem.Command) if c == nil { - continue // Skit item with unsupported command convertion + continue // Skit item with unsupported command conversion } ideCommand = c } @@ -595,7 +603,7 @@ func (ls *INOLanguageServer) TextDocumentDefinitionReqFromIDE(ctx context.Contex func (ls *INOLanguageServer) TextDocumentTypeDefinitionReqFromIDE(ctx context.Context, logger jsonrpc.FunctionLogger, ideParams *lsp.TypeDefinitionParams) ([]lsp.Location, []lsp.LocationLink, *jsonrpc.ResponseError) { // XXX: This capability is not advertised in the initialization message (clangd - // does not advetise it either, so maybe we should just not implement it) + // does not advertise it either, so maybe we should just not implement it) ls.readLock(logger, true) defer ls.readUnlock(logger) @@ -974,9 +982,10 @@ func (ls *INOLanguageServer) TextDocumentDidOpenNotifFromIDE(logger jsonrpc.Func return } + // TODO: trigger `fullRebuild` if mail sketch file? if ls.ideURIIsPartOfTheSketch(ideTextDocItem.URI) { if !clangURI.AsPath().Exist() { - ls.triggerRebuildAndWait(logger) + ls.triggerRebuildAndWait(logger, true) } } @@ -1026,7 +1035,7 @@ func (ls *INOLanguageServer) TextDocumentDidChangeNotifFromIDE(logger jsonrpc.Fu ls.writeLock(logger, true) defer ls.writeUnlock(logger) - ls.triggerRebuild() + ls.triggerRebuild(false) logger.Logf("didChange(%s)", ideParams.TextDocument) for _, change := range ideParams.ContentChanges { @@ -1126,14 +1135,14 @@ func (ls *INOLanguageServer) TextDocumentDidSaveNotifFromIDE(logger jsonrpc.Func // so we will not forward notification on saves in the sketch folder. logger.Logf("notification is not forwarded to clang") - ls.triggerRebuild() + ls.triggerRebuild(false) } func (ls *INOLanguageServer) TextDocumentDidCloseNotifFromIDE(logger jsonrpc.FunctionLogger, ideParams *lsp.DidCloseTextDocumentParams) { ls.writeLock(logger, true) defer ls.writeUnlock(logger) - ls.triggerRebuild() + ls.triggerRebuild(false) inoIdentifier := ideParams.TextDocument if _, exist := ls.trackedIdeDocs[inoIdentifier.URI.AsPath().String()]; exist { @@ -1172,6 +1181,31 @@ func (ls *INOLanguageServer) TextDocumentDidCloseNotifFromIDE(logger jsonrpc.Fun } } +func (ls *INOLanguageServer) FullBuildCompletedFromIDE(logger jsonrpc.FunctionLogger, params *DidCompleteBuildParams) { + ls.writeLock(logger, true) + defer ls.writeUnlock(logger) + ls.CopyBuildResults(logger, params.BuildOutputUri.AsPath(), true) +} + +func (ls *INOLanguageServer) CopyBuildResults(logger jsonrpc.FunctionLogger, buildPath *paths.Path, fullRebuild bool) { + fromCommands := buildPath.Join("compile_commands.json") + toCommands := ls.compileCommandsDir.Join("compile_commands.json") + if err := fromCommands.CopyTo(toCommands); err != nil { + logger.Logf("ERROR: updating compile_commands: %s", err) + } else { + logger.Logf("Updated 'compile_commands'. Copied: %v to %v", fromCommands, toCommands) + } + if fullRebuild { + fromCache := buildPath.Join("libraries.cache") + toCache := ls.compileCommandsDir.Join("libraries.cache") + if err := fromCache.CopyTo(toCache); err != nil { + logger.Logf("ERROR: updating libraries.cache: %s", err) + } else { + logger.Logf("Updated 'libraries.cache'. Copied: %v to %v", fromCache, toCache) + } + } +} + func (ls *INOLanguageServer) PublishDiagnosticsNotifFromClangd(logger jsonrpc.FunctionLogger, clangParams *lsp.PublishDiagnosticsParams) { ls.readLock(logger, false) defer ls.readUnlock(logger) @@ -1196,7 +1230,7 @@ func (ls *INOLanguageServer) PublishDiagnosticsNotifFromClangd(logger jsonrpc.Fu ls.ideInoDocsWithDiagnostics[ideInoURI] = true } - // .. and cleanup all previouse diagnostics that are no longer valid... + // .. and cleanup all previous diagnostics that are no longer valid... for ideInoURI := range ls.ideInoDocsWithDiagnostics { if _, ok := allIdeParams[ideInoURI]; ok { continue @@ -1218,7 +1252,7 @@ func (ls *INOLanguageServer) PublishDiagnosticsNotifFromClangd(logger jsonrpc.Fu _ = json.Unmarshal(ideDiag.Code, &code) switch code { case "": - // Filter unkown non-string codes + // Filter unknown non-string codes case "drv_unknown_argument_with_suggestion": // Skip errors like: "Unknown argument '-mlongcalls'; did you mean '-mlong-calls'?" case "drv_unknown_argument": @@ -1296,7 +1330,7 @@ func (ls *INOLanguageServer) ideURIIsPartOfTheSketch(ideURI lsp.DocumentURI) boo func (ls *INOLanguageServer) ProgressNotifFromClangd(logger jsonrpc.FunctionLogger, progress *lsp.ProgressParams) { var token string if err := json.Unmarshal(progress.Token, &token); err != nil { - logger.Logf("error decoding progess token: %s", err) + logger.Logf("error decoding progress token: %s", err) return } switch value := progress.TryToDecodeWellKnownValues().(type) { @@ -1470,7 +1504,7 @@ func (ls *INOLanguageServer) clang2IdeCommand(logger jsonrpc.FunctionLogger, cla converted, err := json.Marshal(v) if err != nil { - panic("Internal Error: json conversion of codeAcion command arguments") + panic("Internal Error: json conversion of codeAction command arguments") } ideCommand.Arguments[i] = converted } @@ -1496,7 +1530,7 @@ func (ls *INOLanguageServer) cpp2inoWorkspaceEdit(logger jsonrpc.FunctionLogger, continue } - // ...otherwise convert edits to the sketch.ino.cpp into multilpe .ino edits + // ...otherwise convert edits to the sketch.ino.cpp into multiple .ino edits for _, edit := range edits { inoURI, inoRange, inPreprocessed, err := ls.clang2IdeRangeAndDocumentURI(logger, editURI, edit.Range) if err != nil { diff --git a/ls/lsp_server_ide.go b/ls/lsp_server_ide.go index 2f295d5..aeaa496 100644 --- a/ls/lsp_server_ide.go +++ b/ls/lsp_server_ide.go @@ -20,6 +20,7 @@ func NewIDELSPServer(logger jsonrpc.FunctionLogger, in io.Reader, out io.Writer, ls: ls, } server.conn = lsp.NewServer(in, out, server) + server.conn.RegisterCustomNotification("ino/didCompleteBuild", server.ArduinoBuildCompleted) server.conn.SetLogger(&LSPLogger{ IncomingPrefix: "IDE --> LS", OutgoingPrefix: "IDE <-- LS", @@ -267,3 +268,17 @@ func (server *IDELSPServer) TextDocumentDidSave(logger jsonrpc.FunctionLogger, p func (server *IDELSPServer) TextDocumentDidClose(logger jsonrpc.FunctionLogger, params *lsp.DidCloseTextDocumentParams) { server.ls.TextDocumentDidCloseNotifFromIDE(logger, params) } + +// DidCompleteBuildParams is a custom notification from the Arduino IDE, sent +type DidCompleteBuildParams struct { + BuildOutputUri *lsp.DocumentURI `json:"buildOutputUri"` +} + +func (server *IDELSPServer) ArduinoBuildCompleted(logger jsonrpc.FunctionLogger, raw json.RawMessage) { + var params DidCompleteBuildParams + if err := json.Unmarshal(raw, ¶ms); err != nil { + logger.Logf("ERROR decoding FullBuildResult: %s", err) + } else { + server.ls.FullBuildCompletedFromIDE(logger, ¶ms) + } +}