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