diff --git a/lldb/tools/lldb-vscode/JSONUtils.cpp b/lldb/tools/lldb-vscode/JSONUtils.cpp --- a/lldb/tools/lldb-vscode/JSONUtils.cpp +++ b/lldb/tools/lldb-vscode/JSONUtils.cpp @@ -901,6 +901,8 @@ uint64_t bp_loc_id = thread.GetStopReasonDataAtIndex(1); snprintf(desc_str, sizeof(desc_str), "breakpoint %" PRIu64 ".%" PRIu64, bp_id, bp_loc_id); + body.try_emplace("hitBreakpointIds", + llvm::json::Array{llvm::json::Value(bp_id)}); EmplaceSafeString(body, "description", desc_str); } } break; @@ -945,9 +947,6 @@ EmplaceSafeString(body, "description", std::string(description)); } } - if (tid == g_vsc.focus_tid) { - body.try_emplace("threadCausedFocus", true); - } body.try_emplace("preserveFocusHint", tid != g_vsc.focus_tid); body.try_emplace("allThreadsStopped", true); event.try_emplace("body", std::move(body)); diff --git a/lldb/tools/lldb-vscode/Options.td b/lldb/tools/lldb-vscode/Options.td --- a/lldb/tools/lldb-vscode/Options.td +++ b/lldb/tools/lldb-vscode/Options.td @@ -17,25 +17,29 @@ Alias, HelpText<"Alias for --wait-for-debugger">; -def port: Separate<["--", "-"], "port">, +def port: S<"port">, MetaVarName<"">, HelpText<"Communicate with the lldb-vscode tool over the defined port.">; def: Separate<["-"], "p">, Alias, HelpText<"Alias for --port">; -def launch_target: Separate<["--", "-"], "launch-target">, +def launch_target: S<"launch-target">, MetaVarName<"">, HelpText<"Launch a target for the launchInTerminal request. Any argument " "provided after this one will be passed to the target. The parameter " "--comm-file must also be specified.">; -def comm_file: Separate<["--", "-"], "comm-file">, +def comm_file: S<"comm-file">, MetaVarName<"">, HelpText<"The fifo file used to communicate the with the debug adaptor " "when using --launch-target.">; -def debugger_pid: Separate<["--", "-"], "debugger-pid">, +def debugger_pid: S<"debugger-pid">, MetaVarName<"">, HelpText<"The PID of the lldb-vscode instance that sent the launchInTerminal " "request when using --launch-target.">; + +def repl_mode: S<"repl-mode">, + MetaVarName<"">, + HelpText<"The mode for handling repl evaluation requests, supported modes: variable, command, auto.">; diff --git a/lldb/tools/lldb-vscode/README.md b/lldb/tools/lldb-vscode/README.md --- a/lldb/tools/lldb-vscode/README.md +++ b/lldb/tools/lldb-vscode/README.md @@ -236,3 +236,24 @@ ] } ``` + +## repl-mode + +Inspect or adjust the behavior of lldb-vscode repl evaluation requests. The +supported modes are `variable`, `command` and `auto`. + +* `variable` - Variable mode expressions are evaluated in the context of the + current frame. Use a `\`` prefix on the command to run an lldb command. +* `command` - Command mode expressions are evaluated as lldb commands, as a + result, values printed by lldb are always stringified representations of the + expression output. +* `auto` - Auto mode will attempt to infer if the expression represents an lldb + command or a variable expression. Due to the heuristic for determining the + expression, variables that have the same name as an lldb command will only be + accessible via an explicit expression command. For example, if the current + frame contains a variable `help` and a repl evaluation expression is only the + value `"help"` then the lldb help command will be invoked. To access the + local variable use `p help`, `po help` or `var help`. + +The initial repl-mode can be configured with the cli flag `--repl-mode=` +and may also be adjusted at runtime using `lldb-vscode repl-mode `. diff --git a/lldb/tools/lldb-vscode/VSCode.h b/lldb/tools/lldb-vscode/VSCode.h --- a/lldb/tools/lldb-vscode/VSCode.h +++ b/lldb/tools/lldb-vscode/VSCode.h @@ -84,6 +84,14 @@ JSONNotObject }; +enum class ReplMode { Variable = 0, Command, Auto }; + +/// A huersitic for determining the context of an evaluation. +enum class ExpressionContext { + Variable = 0, + Command, +}; + struct Variables { /// Variable_reference start index of permanent expandable variable. static constexpr int64_t PermanentVariableStartIndex = (1ll << 32); @@ -109,7 +117,7 @@ /// \return a new variableReference. /// Specify is_permanent as true for variable that should persist entire /// debug session. - int64_t GetNewVariableRefence(bool is_permanent); + int64_t GetNewVariableReference(bool is_permanent); /// \return the expandable variable corresponding with variableReference /// value of \p value. @@ -129,6 +137,11 @@ lldb::SBCommandReturnObject &result) override; }; +struct ReplModeRequestHandler : public lldb::SBCommandPluginInterface { + bool DoExecute(lldb::SBDebugger debugger, char **command, + lldb::SBCommandReturnObject &result) override; +}; + struct VSCode { std::string debug_adaptor_path; InputStream input; @@ -173,6 +186,8 @@ std::map inflight_reverse_requests; StartDebuggingRequestHandler start_debugging_request_handler; + ReplModeRequestHandler repl_mode_request_handler; + ReplMode repl_mode; VSCode(); ~VSCode(); @@ -206,6 +221,8 @@ llvm::json::Value CreateTopLevelScopes(); + ExpressionContext DetectExpressionContext(std::string &text); + void RunLLDBCommands(llvm::StringRef prefix, const std::vector &commands); diff --git a/lldb/tools/lldb-vscode/VSCode.cpp b/lldb/tools/lldb-vscode/VSCode.cpp --- a/lldb/tools/lldb-vscode/VSCode.cpp +++ b/lldb/tools/lldb-vscode/VSCode.cpp @@ -44,7 +44,7 @@ configuration_done_sent(false), waiting_for_run_in_terminal(false), progress_event_reporter( [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }), - reverse_request_seq(0) { + reverse_request_seq(0), repl_mode(ReplMode::Auto) { const char *log_file_path = getenv("LLDBVSCODE_LOG"); #if defined(_WIN32) // Windows opens stdout and stdin in text mode which converts \n to 13,10 @@ -392,6 +392,44 @@ return llvm::json::Value(std::move(scopes)); } +ExpressionContext VSCode::DetectExpressionContext(std::string &text) { + switch (repl_mode) { + case ReplMode::Variable: + if (!text.empty() && text[0] == '`') { + text = text.substr(1); + return ExpressionContext::Command; + } + + return ExpressionContext::Variable; + case ReplMode::Command: + return ExpressionContext::Command; + case ReplMode::Auto: + // Include ` as an escape hatch. + if (!text.empty() && text[0] == '`') { + text = text.substr(1); + return ExpressionContext::Command; + } + + lldb::SBCommandReturnObject result; + g_vsc.debugger.GetCommandInterpreter().ResolveCommand(text.data(), result); + if (result.GetStatus() != lldb::eReturnStatusSuccessFinishResult) { + return ExpressionContext::Variable; + } + + llvm::StringRef output = result.GetOutput(); + if ((output.consume_front("frame variable ") || + output.consume_front("expression ") || + output.consume_front("dwim-print -- ")) && + /* Check for simple expressions, any additional flags should run as commands. */ + (!output.empty() && output[0] != '-')) { + text = output.str(); + return ExpressionContext::Variable; + } + + return ExpressionContext::Command; + } +} + void VSCode::RunLLDBCommands(llvm::StringRef prefix, const std::vector &commands) { SendOutput(OutputType::Console, @@ -636,7 +674,7 @@ expandable_variables.clear(); } -int64_t Variables::GetNewVariableRefence(bool is_permanent) { +int64_t Variables::GetNewVariableReference(bool is_permanent) { if (is_permanent) return next_permanent_var_ref++; return next_temporary_var_ref++; @@ -661,7 +699,7 @@ int64_t Variables::InsertExpandableVariable(lldb::SBValue variable, bool is_permanent) { - int64_t var_ref = GetNewVariableRefence(is_permanent); + int64_t var_ref = GetNewVariableReference(is_permanent); if (is_permanent) expandable_permanent_variables.insert(std::make_pair(var_ref, variable)); else @@ -728,4 +766,50 @@ return true; } +bool ReplModeRequestHandler::DoExecute(lldb::SBDebugger debugger, + char **command, + lldb::SBCommandReturnObject &result) { + // Command format like: `repl-mode ?` + if (!command || llvm::StringRef(command[0]).empty()) { + std::string mode = "lldb-vscode repl-mode "; + switch (g_vsc.repl_mode) { + case ReplMode::Variable: + mode += "variable"; + break; + case ReplMode::Command: + mode += "command"; + break; + case ReplMode::Auto: + mode += "auto"; + break; + } + + result.AppendMessage(mode.c_str()); + result.SetStatus(lldb::eReturnStatusSuccessFinishResult); + + return true; + } + + llvm::StringRef new_mode{command[0]}; + + if (new_mode == "variable") { + g_vsc.repl_mode = ReplMode::Variable; + result.AppendMessage("lldb-vscode repl-mode variable set."); + } else if (new_mode == "command") { + g_vsc.repl_mode = ReplMode::Command; + result.AppendMessage("lldb-vscode repl-mode command set."); + } else if (new_mode == "auto") { + g_vsc.repl_mode = ReplMode::Auto; + result.AppendMessage("lldb-vscode repl-mode auto set."); + } else { + std::string error = "Unknown repl-mode '" + new_mode.str() + "'."; + result.SetError(error.c_str()); + result.SetStatus(lldb::eReturnStatusFailed); + return false; + } + + result.SetStatus(lldb::eReturnStatusSuccessFinishNoResult); + return true; +} + } // namespace lldb_vscode diff --git a/lldb/tools/lldb-vscode/lldb-vscode.cpp b/lldb/tools/lldb-vscode/lldb-vscode.cpp --- a/lldb/tools/lldb-vscode/lldb-vscode.cpp +++ b/lldb/tools/lldb-vscode/lldb-vscode.cpp @@ -252,7 +252,7 @@ } } - // We will have cleared g_vsc.focus_tid if he focus thread doesn't have + // We will have cleared g_vsc.focus_tid if the focus thread doesn't have // a stop reason, so if it was cleared, or wasn't set, or doesn't exist, // then set the focus thread to the first thread with a stop reason. if (!focus_thread_exists || g_vsc.focus_tid == LLDB_INVALID_THREAD_ID) @@ -1065,50 +1065,62 @@ FillResponse(request, response); llvm::json::Object body; auto arguments = request.getObject("arguments"); + + // If we have a frame, try to set the context for variable completions. + lldb::SBFrame frame = g_vsc.GetLLDBFrame(*arguments); + if (frame.IsValid()) { + frame.GetThread().GetProcess().SetSelectedThread(frame.GetThread()); + frame.GetThread().SetSelectedFrame(frame.GetFrameID()); + } + std::string text = std::string(GetString(arguments, "text")); auto original_column = GetSigned(arguments, "column", text.size()); - auto actual_column = original_column - 1; + auto original_line = GetSigned(arguments, "line", 1); + auto offset = original_column - 1; + if (original_line > 1) { + llvm::StringRef text_ref{text}; + ::llvm::SmallVector<::llvm::StringRef, 2> lines; + text_ref.split(lines, '\n'); + for (int i = 0; i < original_line - 1; i++) { + offset += lines[i].size(); + } + } llvm::json::Array targets; - // NOTE: the 'line' argument is not needed, as multiline expressions - // work well already - // TODO: support frameID. Currently - // g_vsc.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions - // is frame-unaware. - - if (!text.empty() && text[0] == '`') { - text = text.substr(1); - actual_column--; - } else { - char command[] = "expression -- "; + + if (g_vsc.DetectExpressionContext(text) == ExpressionContext::Variable) { + char command[] = "frame variable "; text = command + text; - actual_column += strlen(command); + offset += strlen(command); } lldb::SBStringList matches; lldb::SBStringList descriptions; - g_vsc.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions( - text.c_str(), actual_column, 0, -1, matches, descriptions); - size_t count = std::min((uint32_t)100, matches.GetSize()); - targets.reserve(count); - for (size_t i = 0; i < count; i++) { - std::string match = matches.GetStringAtIndex(i); - std::string description = descriptions.GetStringAtIndex(i); - - llvm::json::Object item; - - llvm::StringRef match_ref = match; - for (llvm::StringRef commit_point : {".", "->"}) { - if (match_ref.contains(commit_point)) { - match_ref = match_ref.rsplit(commit_point).second; + + if (g_vsc.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions( + text.c_str(), offset, 0, 100, matches, descriptions)) { + // The first element is the common substring after the cursor position for + // all the matches. The rest of the elements are the matches. + targets.reserve(matches.GetSize() - 1); + std::string common_pattern = matches.GetStringAtIndex(0); + for (size_t i = 1; i < matches.GetSize(); i++) { + std::string match = matches.GetStringAtIndex(i); + std::string description = descriptions.GetStringAtIndex(i); + + llvm::json::Object item; + llvm::StringRef match_ref = match; + for (llvm::StringRef commit_point : {".", "->"}) { + if (match_ref.contains(commit_point)) { + match_ref = match_ref.rsplit(commit_point).second; + } } - } - EmplaceSafeString(item, "text", match_ref); + EmplaceSafeString(item, "text", match_ref); - if (description.empty()) - EmplaceSafeString(item, "label", match); - else - EmplaceSafeString(item, "label", match + " -- " + description); + if (description.empty()) + EmplaceSafeString(item, "label", match); + else + EmplaceSafeString(item, "label", match + " -- " + description); - targets.emplace_back(std::move(item)); + targets.emplace_back(std::move(item)); + } } body.try_emplace("targets", std::move(targets)); @@ -1223,12 +1235,17 @@ llvm::json::Object body; auto arguments = request.getObject("arguments"); lldb::SBFrame frame = g_vsc.GetLLDBFrame(*arguments); - const auto expression = GetString(arguments, "expression"); + std::string expression = GetString(arguments, "expression").str(); llvm::StringRef context = GetString(arguments, "context"); - if (!expression.empty() && expression[0] == '`') { - auto result = - RunLLDBCommands(llvm::StringRef(), {std::string(expression.substr(1))}); + if (context == "repl" && + g_vsc.DetectExpressionContext(expression) == ExpressionContext::Command) { + // If we're evaluating a command relative to the current frame, set the + // focus_tid to prevent steps from changing focus. + if (frame.IsValid()) { + g_vsc.focus_tid = frame.GetThread().GetThreadID(); + } + auto result = RunLLDBCommands(llvm::StringRef(), {std::string(expression)}); EmplaceSafeString(body, "result", result); body.try_emplace("variablesReference", (int64_t)0); } else { @@ -1472,11 +1489,14 @@ g_vsc.debugger = lldb::SBDebugger::Create(source_init_file, log_cb, nullptr); auto cmd = g_vsc.debugger.GetCommandInterpreter().AddMultiwordCommand( - "lldb-vscode", nullptr); + "lldb-vscode", "Commands for managing lldb-vscode."); cmd.AddCommand( "startDebugging", &g_vsc.start_debugging_request_handler, "Sends a startDebugging request from the debug adapter to the client to " "start a child debug session of the same type as the caller."); + cmd.AddCommand( + "repl-mode", &g_vsc.repl_mode_request_handler, + "Get or set the repl behavior of vscode-lldb evaluation requests."); g_vsc.progress_event_thread = std::thread(ProgressEventThreadFunction); @@ -1534,7 +1554,15 @@ // the user types very slowly... We need to address this specific issue, or // at least trigger completion only when the user explicitly wants it, which // is the behavior of LLDB CLI, that expects a TAB. - body.try_emplace("supportsCompletionsRequest", false); + body.try_emplace("supportsCompletionsRequest", true); + + llvm::json::Array completion_characters; + completion_characters.emplace_back("."); + completion_characters.emplace_back(" "); + completion_characters.emplace_back("\t"); + body.try_emplace("completionTriggerCharacters", + std::move(completion_characters)); + // The debug adapter supports the modules request. body.try_emplace("supportsModulesRequest", true); // The set of additional module information exposed by the debug adapter. @@ -3406,6 +3434,23 @@ return EXIT_SUCCESS; } + if (input_args.hasArg(OPT_repl_mode)) { + llvm::opt::Arg *repl_mode = input_args.getLastArg(OPT_repl_mode); + llvm::StringRef repl_mode_value = repl_mode->getValue(); + if (repl_mode_value == "auto") { + g_vsc.repl_mode = ReplMode::Auto; + } else if (repl_mode_value == "variable") { + g_vsc.repl_mode = ReplMode::Variable; + } else if (repl_mode_value == "command") { + g_vsc.repl_mode = ReplMode::Command; + } else { + llvm::errs() + << "'" << repl_mode_value + << "' is not a valid option, use 'variable', 'command' or 'auto'.\n"; + return EXIT_FAILURE; + } + } + if (llvm::opt::Arg *target_arg = input_args.getLastArg(OPT_launch_target)) { if (llvm::opt::Arg *comm_file = input_args.getLastArg(OPT_comm_file)) { lldb::pid_t pid = LLDB_INVALID_PROCESS_ID;