forked from microsoft/winget-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCompletionData.cpp
More file actions
212 lines (181 loc) · 9.15 KB
/
CompletionData.cpp
File metadata and controls
212 lines (181 loc) · 9.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
#include "CompletionData.h"
#include "Resources.h"
namespace AppInstaller::CLI
{
using namespace std::string_view_literals;
using namespace Utility::literals;
using namespace Settings;
// Completion takes in the following values:
// Word :: The token from the command line that is being targeted for completion.
// This value may have quotes surrounding it, and will need to be removed in such a case.
// CommandLine :: The full command line that contains the word to be completed.
// This value has the fully quoted strings, as well as escaped quotations if needed.
// Position :: The position of the cursor within the command line.
//
// Completions here will not attempt to take exact cursor position into account; meaning if the cursor
// is in the middle of the word, it is not different than at the beginning or end. This functionality
// could be added later.
CompletionData::CompletionData(std::string_view word, std::string_view commandLine, std::string_view position)
{
m_word = word;
AICLI_LOG(CLI, Info, << "Completing word '" << m_word << '\'');
// Determine position as an integer
size_t cursor = wil::safe_cast<size_t>(std::stoull(std::string{ position }));
AICLI_LOG(CLI, Info, << "Cursor position starts at '" << cursor << '\'');
// First, move the cursor from the UTF-8 grapheme position to the UTF-8 byte position.
// This simplifies the rest of the code.
cursor = Utility::UTF8Substring(commandLine, 0, cursor).length();
AICLI_LOG(CLI, Info, << "Cursor position moved to '" << cursor << '\'');
std::vector<std::string> argsBeforeWord;
std::vector<std::string> argsAfterWord;
// If the word is empty, we must determine where the split is. We operate as PowerShell does; the cursor
// being at the front of a token results in an empty word and an insertion rather than a replacement.
// If the user put spaces at the front of the statement, this can lead to the position being out of sorts;
// PowerShell sends the cursor position, but does not include leading spaces in the AST output. If the
// user puts too many spaces at the front we will be unable to determine the true location.
if (m_word.empty())
{
// The cursor is past the end, so everything is before the word.
if (cursor >= commandLine.length())
{
// Move the position to the end in case it was extended past it.
ParseInto(commandLine, argsBeforeWord, true);
}
// The cursor is not past the end; ensure that the preceding character is whitespace or move the
// position back until it is. This is far from foolproof, but until we have evidence otherwise,
// very few users are likely to put any spaces at the front of their statements, let alone many.
else
{
for (; cursor > 0 && !std::isspace(commandLine[cursor - 1]); --cursor);
AICLI_LOG(CLI, Info, << "Cursor position moved to '" << cursor << '\'');
// If we actually hit the front of the string, something bad probably happened.
THROW_HR_IF(APPINSTALLER_CLI_ERROR_COMPLETE_INPUT_BAD, cursor == 0);
ParseInto(commandLine.substr(0, cursor), argsBeforeWord, true);
ParseInto(commandLine.substr(cursor), argsAfterWord, false);
}
}
// If the word is not empty, the cursor is either in the middle of a token, or at the end of one.
// The value will be replaced, and we will remove it from the args here.
else
{
std::vector<std::string> allArgs;
ParseInto(commandLine, allArgs, true);
// Find the word amongst the arguments
std::vector<size_t> wordIndices;
for (size_t i = 0; i < allArgs.size(); ++i)
{
if (m_word == allArgs[i])
{
wordIndices.push_back(i);
}
}
// If we didn't find a matching string, we probably made some bad assumptions.
THROW_HR_IF(APPINSTALLER_CLI_ERROR_COMPLETE_INPUT_BAD, wordIndices.empty());
// If we find an exact match only once, we can just split on that.
size_t wordIndexForSplit = wordIndices[0];
// If we found more than one match, we have to rely on the position to
// determine which argument is the word in question.
if (wordIndices.size() > 1)
{
// Escape the word and search for it in the command line.
std::string escapedWord = m_word;
Utility::FindAndReplace(escapedWord, "\"", "\"\"");
std::vector<size_t> escapedIndices;
for (size_t offset = 0; offset < commandLine.length();)
{
size_t pos = commandLine.find(escapedWord, offset);
if (pos == std::string::npos)
{
break;
}
escapedIndices.push_back(pos);
offset = pos + escapedWord.length();
}
// If these are out of sync we don't have much hope.
THROW_HR_IF(APPINSTALLER_CLI_ERROR_COMPLETE_INPUT_BAD, wordIndices.size() != escapedIndices.size());
// Find the closest one to the position. This can be fooled as above if there is
// leading whitespace in the statement. But it is the best we can do.
size_t indexToUse = std::numeric_limits<size_t>::max();
size_t distanceToCursor = std::numeric_limits<size_t>::max();
for (size_t i = 0; i < escapedIndices.size(); ++i)
{
size_t lowerBound = escapedIndices[i];
size_t upperBound = lowerBound + escapedWord.length();
size_t distance = 0;
// The cursor is square in the middle of this location, this is the one.
if (cursor > lowerBound && cursor <= upperBound)
{
indexToUse = i;
break;
}
else if (cursor <= lowerBound)
{
distance = lowerBound - cursor;
}
else // cursor > upperBound
{
distance = cursor - upperBound;
}
if (distance < distanceToCursor)
{
indexToUse = i;
distanceToCursor = distance;
}
}
// It really would be unexpected to not find a closest one.
THROW_HR_IF(APPINSTALLER_CLI_ERROR_COMPLETE_INPUT_BAD, indexToUse == std::numeric_limits<size_t>::max());
wordIndexForSplit = wordIndices[indexToUse];
}
std::vector<std::string>* moveTarget = &argsBeforeWord;
for (size_t i = 0; i < allArgs.size(); ++i)
{
if (i == wordIndexForSplit)
{
// Intentionally leave the matched arg behind.
moveTarget = &argsAfterWord;
}
else
{
moveTarget->emplace_back(std::move(allArgs[i]));
}
}
}
// Move the arguments into an Invocation for future use.
m_argsBeforeWord = std::make_unique<CLI::Invocation>(std::move(argsBeforeWord));
m_argsAfterWord = std::make_unique<CLI::Invocation>(std::move(argsAfterWord));
AICLI_LOG(CLI, Info, << "Completion invoked for arguments:" << [&]() {
std::stringstream strstr;
for (const auto& arg : *m_argsBeforeWord)
{
strstr << " '" << arg << '\'';
}
if (m_word.empty())
{
strstr << " << [insert] >> ";
}
else
{
strstr << " << [replace] '" << m_word << "' >> ";
}
for (const auto& arg : *m_argsAfterWord)
{
strstr << " '" << arg << '\'';
}
return strstr.str();
}());
}
void CompletionData::ParseInto(std::string_view line, std::vector<std::string>& args, bool skipFirst)
{
std::wstring commandLineW = Utility::ConvertToUTF16(line);
int argc = 0;
wil::unique_hlocal_ptr<LPWSTR> argv{ CommandLineToArgvW(commandLineW.c_str(), &argc) };
THROW_LAST_ERROR_IF_NULL(argv);
for (int i = (skipFirst ? 1 : 0); i < argc; ++i)
{
args.emplace_back(Utility::ConvertToUTF8(argv.get()[i]));
}
}
}