1 | //===-- CompletionsHandler.cpp --------------------------------------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "DAP.h" |
10 | #include "JSONUtils.h" |
11 | #include "RequestHandler.h" |
12 | #include "lldb/API/SBStringList.h" |
13 | |
14 | namespace lldb_dap { |
15 | |
16 | // "CompletionsRequest": { |
17 | // "allOf": [ { "$ref": "#/definitions/Request" }, { |
18 | // "type": "object", |
19 | // "description": "Returns a list of possible completions for a given caret |
20 | // position and text.\nThe CompletionsRequest may only be called if the |
21 | // 'supportsCompletionsRequest' capability exists and is true.", |
22 | // "properties": { |
23 | // "command": { |
24 | // "type": "string", |
25 | // "enum": [ "completions" ] |
26 | // }, |
27 | // "arguments": { |
28 | // "$ref": "#/definitions/CompletionsArguments" |
29 | // } |
30 | // }, |
31 | // "required": [ "command", "arguments" ] |
32 | // }] |
33 | // }, |
34 | // "CompletionsArguments": { |
35 | // "type": "object", |
36 | // "description": "Arguments for 'completions' request.", |
37 | // "properties": { |
38 | // "frameId": { |
39 | // "type": "integer", |
40 | // "description": "Returns completions in the scope of this stack frame. |
41 | // If not specified, the completions are returned for the global scope." |
42 | // }, |
43 | // "text": { |
44 | // "type": "string", |
45 | // "description": "One or more source lines. Typically this is the text a |
46 | // user has typed into the debug console before he asked for completion." |
47 | // }, |
48 | // "column": { |
49 | // "type": "integer", |
50 | // "description": "The character position for which to determine the |
51 | // completion proposals." |
52 | // }, |
53 | // "line": { |
54 | // "type": "integer", |
55 | // "description": "An optional line for which to determine the completion |
56 | // proposals. If missing the first line of the text is assumed." |
57 | // } |
58 | // }, |
59 | // "required": [ "text", "column" ] |
60 | // }, |
61 | // "CompletionsResponse": { |
62 | // "allOf": [ { "$ref": "#/definitions/Response" }, { |
63 | // "type": "object", |
64 | // "description": "Response to 'completions' request.", |
65 | // "properties": { |
66 | // "body": { |
67 | // "type": "object", |
68 | // "properties": { |
69 | // "targets": { |
70 | // "type": "array", |
71 | // "items": { |
72 | // "$ref": "#/definitions/CompletionItem" |
73 | // }, |
74 | // "description": "The possible completions for ." |
75 | // } |
76 | // }, |
77 | // "required": [ "targets" ] |
78 | // } |
79 | // }, |
80 | // "required": [ "body" ] |
81 | // }] |
82 | // }, |
83 | // "CompletionItem": { |
84 | // "type": "object", |
85 | // "description": "CompletionItems are the suggestions returned from the |
86 | // CompletionsRequest.", "properties": { |
87 | // "label": { |
88 | // "type": "string", |
89 | // "description": "The label of this completion item. By default this is |
90 | // also the text that is inserted when selecting this completion." |
91 | // }, |
92 | // "text": { |
93 | // "type": "string", |
94 | // "description": "If text is not falsy then it is inserted instead of the |
95 | // label." |
96 | // }, |
97 | // "sortText": { |
98 | // "type": "string", |
99 | // "description": "A string that should be used when comparing this item |
100 | // with other items. When `falsy` the label is used." |
101 | // }, |
102 | // "type": { |
103 | // "$ref": "#/definitions/CompletionItemType", |
104 | // "description": "The item's type. Typically the client uses this |
105 | // information to render the item in the UI with an icon." |
106 | // }, |
107 | // "start": { |
108 | // "type": "integer", |
109 | // "description": "This value determines the location (in the |
110 | // CompletionsRequest's 'text' attribute) where the completion text is |
111 | // added.\nIf missing the text is added at the location specified by the |
112 | // CompletionsRequest's 'column' attribute." |
113 | // }, |
114 | // "length": { |
115 | // "type": "integer", |
116 | // "description": "This value determines how many characters are |
117 | // overwritten by the completion text.\nIf missing the value 0 is assumed |
118 | // which results in the completion text being inserted." |
119 | // } |
120 | // }, |
121 | // "required": [ "label" ] |
122 | // }, |
123 | // "CompletionItemType": { |
124 | // "type": "string", |
125 | // "description": "Some predefined types for the CompletionItem. Please note |
126 | // that not all clients have specific icons for all of them.", "enum": [ |
127 | // "method", "function", "constructor", "field", "variable", "class", |
128 | // "interface", "module", "property", "unit", "value", "enum", "keyword", |
129 | // "snippet", "text", "color", "file", "reference", "customcolor" ] |
130 | // } |
131 | void CompletionsRequestHandler::operator()( |
132 | const llvm::json::Object &request) const { |
133 | llvm::json::Object response; |
134 | FillResponse(request, response); |
135 | llvm::json::Object body; |
136 | const auto *arguments = request.getObject(K: "arguments" ); |
137 | |
138 | // If we have a frame, try to set the context for variable completions. |
139 | lldb::SBFrame frame = dap.GetLLDBFrame(arguments: *arguments); |
140 | if (frame.IsValid()) { |
141 | frame.GetThread().GetProcess().SetSelectedThread(frame.GetThread()); |
142 | frame.GetThread().SetSelectedFrame(frame.GetFrameID()); |
143 | } |
144 | |
145 | std::string text = GetString(obj: arguments, key: "text" ).value_or(u: "" ).str(); |
146 | auto original_column = |
147 | GetInteger<int64_t>(obj: arguments, key: "column" ).value_or(u: text.size()); |
148 | auto original_line = GetInteger<int64_t>(obj: arguments, key: "line" ).value_or(u: 1); |
149 | auto offset = original_column - 1; |
150 | if (original_line > 1) { |
151 | llvm::SmallVector<::llvm::StringRef, 2> lines; |
152 | llvm::StringRef(text).split(A&: lines, Separator: '\n'); |
153 | for (int i = 0; i < original_line - 1; i++) { |
154 | offset += lines[i].size(); |
155 | } |
156 | } |
157 | llvm::json::Array targets; |
158 | |
159 | bool had_escape_prefix = |
160 | llvm::StringRef(text).starts_with(Prefix: dap.configuration.commandEscapePrefix); |
161 | ReplMode completion_mode = dap.DetectReplMode(frame, expression&: text, partial_expression: true); |
162 | |
163 | // Handle the offset change introduced by stripping out the |
164 | // `command_escape_prefix`. |
165 | if (had_escape_prefix) { |
166 | if (offset < |
167 | static_cast<int64_t>(dap.configuration.commandEscapePrefix.size())) { |
168 | body.try_emplace(K: "targets" , Args: std::move(targets)); |
169 | response.try_emplace(K: "body" , Args: std::move(body)); |
170 | dap.SendJSON(json: llvm::json::Value(std::move(response))); |
171 | return; |
172 | } |
173 | offset -= dap.configuration.commandEscapePrefix.size(); |
174 | } |
175 | |
176 | // While the user is typing then we likely have an incomplete input and cannot |
177 | // reliably determine the precise intent (command vs variable), try completing |
178 | // the text as both a command and variable expression, if applicable. |
179 | const std::string expr_prefix = "expression -- " ; |
180 | std::array<std::tuple<ReplMode, std::string, uint64_t>, 2> exprs = { |
181 | ._M_elems: {std::make_tuple(args: ReplMode::Command, args&: text, args&: offset), |
182 | std::make_tuple(args: ReplMode::Variable, args: expr_prefix + text, |
183 | args: offset + expr_prefix.size())}}; |
184 | for (const auto &[mode, line, cursor] : exprs) { |
185 | if (completion_mode != ReplMode::Auto && completion_mode != mode) |
186 | continue; |
187 | |
188 | lldb::SBStringList matches; |
189 | lldb::SBStringList descriptions; |
190 | if (!dap.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions( |
191 | current_line: line.c_str(), cursor_pos: cursor, match_start_point: 0, max_return_elements: 100, matches, descriptions)) |
192 | continue; |
193 | |
194 | // The first element is the common substring after the cursor position for |
195 | // all the matches. The rest of the elements are the matches so ignore the |
196 | // first result. |
197 | for (size_t i = 1; i < matches.GetSize(); i++) { |
198 | std::string match = matches.GetStringAtIndex(idx: i); |
199 | std::string description = descriptions.GetStringAtIndex(idx: i); |
200 | |
201 | llvm::json::Object item; |
202 | llvm::StringRef match_ref = match; |
203 | for (llvm::StringRef commit_point : {"." , "->" }) { |
204 | if (match_ref.contains(Other: commit_point)) { |
205 | match_ref = match_ref.rsplit(Separator: commit_point).second; |
206 | } |
207 | } |
208 | EmplaceSafeString(obj&: item, key: "text" , str: match_ref); |
209 | |
210 | if (description.empty()) |
211 | EmplaceSafeString(obj&: item, key: "label" , str: match); |
212 | else |
213 | EmplaceSafeString(obj&: item, key: "label" , str: match + " -- " + description); |
214 | |
215 | targets.emplace_back(A: std::move(item)); |
216 | } |
217 | } |
218 | |
219 | body.try_emplace(K: "targets" , Args: std::move(targets)); |
220 | response.try_emplace(K: "body" , Args: std::move(body)); |
221 | dap.SendJSON(json: llvm::json::Value(std::move(response))); |
222 | } |
223 | |
224 | } // namespace lldb_dap |
225 | |