Newer
Older
Import / research / mini-shell / completions.cpp
//
//  completions.cpp
//  DescriptionHere
//
//  Created by John Ryland (jryland@xiaofrog.com) on 19/11/2017.
//  Copyright 2017 InvertedLogic. All rights reserved.
//
#include "utils.h"
#include "completions.h"
#include <string.h>


ReadLineWithHistory::ReadLineWithHistory(EditTextInterface& text)
  : Parent(text)
{
}


void ReadLineWithHistory::prepare()
{
  Parent::prepare();
  historyI = lineHistoryBuffer.size();
  lineHistoryBuffer.push_back("");
}


bool ReadLineWithHistory::processKeyPress(unsigned int ch)
{
  switch (ch) {
    case KeyProviderInterface::UP:
      if (historyI >= 0)
        lineHistoryBuffer[historyI] = text.getString();
      if (historyI > 0) {
        historyI--;
        text.fromString(lineHistoryBuffer[historyI]);
      } else {
        historyI = -1;
        text.fromString("");
      }
      break;
    case KeyProviderInterface::DOWN:
      if (historyI >= 0)
        lineHistoryBuffer[historyI] = text.getString();
      if (historyI == -1 || historyI < lineHistoryBuffer.size()-1) {
        historyI++;
        text.fromString(lineHistoryBuffer[historyI]);
      }
      break;
    default:
      if (Parent::processKeyPress(ch)) {
        lineHistoryBuffer.back() = text.getString();
        return true;
      }
      break;
  }
  return false;
}


FileTabCompletions::FileTabCompletions(FileSystem& fileSys)
  : fs(fileSys)
{
}


std::vector<CompletionsProviderInterface::Completion>
  FileTabCompletions::getCompletions(const std::string& contextBeforeText, const std::string& partialText)
{
  // TODO: search PATH for exe names to complete on
  //       perhaps some context sensitive smarts on completion context
  //bool incPathSearch = contextBeforeText.empty();
  //bool incPathSearch = contextBeforeText.size() == partialText.size();
  std::vector<Completion> matches;
  DirectoryEntrySet entries;
  std::string path = fs.dirname(partialText) + "/";
  fs.getDirectoryListing(entries, path);
  if (path == "./" && (partialText.size() < 1 || partialText[0] != '.'))
    path = "";
  for (int i = 0; i < entries.entries.size(); i++)
  {
    std::string basename = entries.entries[i].basename;
    bool isDirectory = entries.entries[i].type == DirectoryEntrySet::DirectoryEntryMember::Directory;
    if (strncmp(partialText.c_str(), (path + basename).c_str(), partialText.size()) == 0)
    {
      Completion completion;
      completion.shortDisplayText = basename + (isDirectory ? "/" : " ");
      completion.completionText = path + basename + (isDirectory ? "/" : " ");
      matches.push_back(completion);
    }
  }
  return matches;
}


void FileTabCompletions::showPossibleCompletions(const std::vector<Completion>& completions)
{
  printf("\n");
  for (int i = 0; i < completions.size(); i++)
    printf("%s ", completions[i].shortDisplayText.c_str());
  printf("\n");
}


ReadLineWithCompletions::ReadLineWithCompletions(EditTextInterface& text, CompletionsProviderInterface& cp)
  : Parent(text)
  , completions(cp)
{
}


void ReadLineWithCompletions::prepare()
{
  Parent::prepare();
  lastCh = 0;
}


bool ReadLineWithCompletions::processKeyPress(unsigned int ch)
{
  if (ch == KeyProviderInterface::TAB) {
    std::string curText = text.getStringUpToCursor();
    std::vector<std::string> args = strSplit(curText.c_str(), ' ');
    std::string lstArg = "";
    if (args.size() && curText[curText.size()-1] != ' ')
      lstArg = args.back();

    //printf("lst arg: -%s-\n", lstArg.c_str());
    std::vector<CompletionsProviderInterface::Completion> matches = completions.getCompletions(curText, lstArg);

    int commonLength = 0;
    if (matches.size() >= 1) {
      bool done = false;
      // Find the common sub-expression in the matches
      while (!done) {
        int offset = lstArg.size() + commonLength;
        if (matches[0].completionText.size() <= offset) {
          commonLength++;
          break;
        }
        char c = matches[0].completionText.c_str()[offset];
        for (int i = 1; i < matches.size(); i++) {
          if (matches[i].completionText.size() <= offset) {
            done = true;
            break;
          }
          if (c != matches[i].completionText.c_str()[offset]) {
            done = true;
            break;
          }
        }
        commonLength++;
      }
    }

    if (commonLength)
      commonLength--;

    // if there is a common-sub-expression to expand with the tab completion, output it
    if (commonLength) {
      // insert the TAB completion in to the input buffer
      const char* s = matches[0].completionText.c_str() + lstArg.size();
      for (int i = 0; i < commonLength; i++) {
        text.insertChar(*s);
        s++;
      }
      lastCh = ' ';
      return false; // prevent if TAB pressed again that it doesn't do double-TAB action
    }

    // If press TAB twice, list the options
    if (matches.size() > 1 && lastCh == KeyProviderInterface::TAB) {
      completions.showPossibleCompletions(matches);
    }
    lastCh = ch;
    return false;
  }
  lastCh = ch;
  return Parent::processKeyPress(ch);
}