llvm-project/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp
John Harrison bc6cd825ec
[lldb-dap] Creating a common configuration structure for launch and attach requests. (#133960)
This moves all the common settings of the launch and attach operations
into the `lldb_dap::protocol::Configuration`. These common settings
can be in both `launch` and `attach` requests and allows us to isolate
the DAP configuration operations into a single common location.

This is split out from #133624.
2025-04-03 09:45:00 -07:00

225 lines
8.2 KiB
C++

//===-- CompletionsHandler.cpp --------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "DAP.h"
#include "JSONUtils.h"
#include "RequestHandler.h"
#include "lldb/API/SBStringList.h"
namespace lldb_dap {
// "CompletionsRequest": {
// "allOf": [ { "$ref": "#/definitions/Request" }, {
// "type": "object",
// "description": "Returns a list of possible completions for a given caret
// position and text.\nThe CompletionsRequest may only be called if the
// 'supportsCompletionsRequest' capability exists and is true.",
// "properties": {
// "command": {
// "type": "string",
// "enum": [ "completions" ]
// },
// "arguments": {
// "$ref": "#/definitions/CompletionsArguments"
// }
// },
// "required": [ "command", "arguments" ]
// }]
// },
// "CompletionsArguments": {
// "type": "object",
// "description": "Arguments for 'completions' request.",
// "properties": {
// "frameId": {
// "type": "integer",
// "description": "Returns completions in the scope of this stack frame.
// If not specified, the completions are returned for the global scope."
// },
// "text": {
// "type": "string",
// "description": "One or more source lines. Typically this is the text a
// user has typed into the debug console before he asked for completion."
// },
// "column": {
// "type": "integer",
// "description": "The character position for which to determine the
// completion proposals."
// },
// "line": {
// "type": "integer",
// "description": "An optional line for which to determine the completion
// proposals. If missing the first line of the text is assumed."
// }
// },
// "required": [ "text", "column" ]
// },
// "CompletionsResponse": {
// "allOf": [ { "$ref": "#/definitions/Response" }, {
// "type": "object",
// "description": "Response to 'completions' request.",
// "properties": {
// "body": {
// "type": "object",
// "properties": {
// "targets": {
// "type": "array",
// "items": {
// "$ref": "#/definitions/CompletionItem"
// },
// "description": "The possible completions for ."
// }
// },
// "required": [ "targets" ]
// }
// },
// "required": [ "body" ]
// }]
// },
// "CompletionItem": {
// "type": "object",
// "description": "CompletionItems are the suggestions returned from the
// CompletionsRequest.", "properties": {
// "label": {
// "type": "string",
// "description": "The label of this completion item. By default this is
// also the text that is inserted when selecting this completion."
// },
// "text": {
// "type": "string",
// "description": "If text is not falsy then it is inserted instead of the
// label."
// },
// "sortText": {
// "type": "string",
// "description": "A string that should be used when comparing this item
// with other items. When `falsy` the label is used."
// },
// "type": {
// "$ref": "#/definitions/CompletionItemType",
// "description": "The item's type. Typically the client uses this
// information to render the item in the UI with an icon."
// },
// "start": {
// "type": "integer",
// "description": "This value determines the location (in the
// CompletionsRequest's 'text' attribute) where the completion text is
// added.\nIf missing the text is added at the location specified by the
// CompletionsRequest's 'column' attribute."
// },
// "length": {
// "type": "integer",
// "description": "This value determines how many characters are
// overwritten by the completion text.\nIf missing the value 0 is assumed
// which results in the completion text being inserted."
// }
// },
// "required": [ "label" ]
// },
// "CompletionItemType": {
// "type": "string",
// "description": "Some predefined types for the CompletionItem. Please note
// that not all clients have specific icons for all of them.", "enum": [
// "method", "function", "constructor", "field", "variable", "class",
// "interface", "module", "property", "unit", "value", "enum", "keyword",
// "snippet", "text", "color", "file", "reference", "customcolor" ]
// }
void CompletionsRequestHandler::operator()(
const llvm::json::Object &request) const {
llvm::json::Object response;
FillResponse(request, response);
llvm::json::Object body;
const auto *arguments = request.getObject("arguments");
// If we have a frame, try to set the context for variable completions.
lldb::SBFrame frame = dap.GetLLDBFrame(*arguments);
if (frame.IsValid()) {
frame.GetThread().GetProcess().SetSelectedThread(frame.GetThread());
frame.GetThread().SetSelectedFrame(frame.GetFrameID());
}
std::string text = GetString(arguments, "text").value_or("").str();
auto original_column =
GetInteger<int64_t>(arguments, "column").value_or(text.size());
auto original_line = GetInteger<int64_t>(arguments, "line").value_or(1);
auto offset = original_column - 1;
if (original_line > 1) {
llvm::SmallVector<::llvm::StringRef, 2> lines;
llvm::StringRef(text).split(lines, '\n');
for (int i = 0; i < original_line - 1; i++) {
offset += lines[i].size();
}
}
llvm::json::Array targets;
bool had_escape_prefix =
llvm::StringRef(text).starts_with(dap.configuration.commandEscapePrefix);
ReplMode completion_mode = dap.DetectReplMode(frame, text, true);
// Handle the offset change introduced by stripping out the
// `command_escape_prefix`.
if (had_escape_prefix) {
if (offset <
static_cast<int64_t>(dap.configuration.commandEscapePrefix.size())) {
body.try_emplace("targets", std::move(targets));
response.try_emplace("body", std::move(body));
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
}
offset -= dap.configuration.commandEscapePrefix.size();
}
// While the user is typing then we likely have an incomplete input and cannot
// reliably determine the precise intent (command vs variable), try completing
// the text as both a command and variable expression, if applicable.
const std::string expr_prefix = "expression -- ";
std::array<std::tuple<ReplMode, std::string, uint64_t>, 2> exprs = {
{std::make_tuple(ReplMode::Command, text, offset),
std::make_tuple(ReplMode::Variable, expr_prefix + text,
offset + expr_prefix.size())}};
for (const auto &[mode, line, cursor] : exprs) {
if (completion_mode != ReplMode::Auto && completion_mode != mode)
continue;
lldb::SBStringList matches;
lldb::SBStringList descriptions;
if (!dap.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions(
line.c_str(), cursor, 0, 100, matches, descriptions))
continue;
// The first element is the common substring after the cursor position for
// all the matches. The rest of the elements are the matches so ignore the
// first result.
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);
if (description.empty())
EmplaceSafeString(item, "label", match);
else
EmplaceSafeString(item, "label", match + " -- " + description);
targets.emplace_back(std::move(item));
}
}
body.try_emplace("targets", std::move(targets));
response.try_emplace("body", std::move(body));
dap.SendJSON(llvm::json::Value(std::move(response)));
}
} // namespace lldb_dap