winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }