548 lines
16 KiB
C++
548 lines
16 KiB
C++
//===--- Editor.cpp - Output Of Text ----------------------------*- C++ -*-===//
|
|
//
|
|
// The LLVM Compiler Infrastructure
|
|
//
|
|
// This file is distributed under the University of Illinois Open Source
|
|
// License. See LICENSE.TXT for details.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This file defines the text manipulation ("editing") interface.
|
|
//
|
|
// Axel Naumann <axel@cern.ch>, 2011-05-12
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
|
|
#include "textinput/Editor.h"
|
|
|
|
#include <cctype>
|
|
#include <vector>
|
|
#include "textinput/Callbacks.h"
|
|
#include "textinput/History.h"
|
|
#include "textinput/KeyBinding.h"
|
|
#include "textinput/StreamReaderUnix.h"
|
|
#include "textinput/TextInput.h"
|
|
#include "textinput/TextInputContext.h"
|
|
|
|
namespace textinput {
|
|
|
|
// Functions to find first/last non alphanumeric ("word-boundaries")
|
|
size_t find_first_non_alnum(const std::string &str,
|
|
std::string::size_type index = 0) {
|
|
bool atleast_one_alnum = false;
|
|
std::string::size_type len = str.length();
|
|
for(; index < len; ++index) {
|
|
const char c = str[index];
|
|
bool is_alpha = isalnum(c) || c == '_';
|
|
if (is_alpha) atleast_one_alnum = true;
|
|
else if (atleast_one_alnum) return index;
|
|
}
|
|
return std::string::npos;
|
|
}
|
|
|
|
size_t find_last_non_alnum(const std::string &str,
|
|
std::string::size_type index = std::string::npos) {
|
|
std::string::size_type len = str.length();
|
|
if (index == std::string::npos) index = len - 1;
|
|
bool atleast_one_alnum = false;
|
|
for(; index != std::string::npos; --index) {
|
|
const char c = str[index];
|
|
bool is_alpha = isalnum(c) || c == '_';
|
|
if (is_alpha) atleast_one_alnum = true;
|
|
else if (atleast_one_alnum) return index;
|
|
}
|
|
return std::string::npos;
|
|
}
|
|
|
|
Editor::EProcessResult
|
|
Editor::Process(Command cmd, EditorRange& R) {
|
|
switch (cmd.GetKind()) {
|
|
case kCKChar:
|
|
return ProcessChar(cmd.GetChar(), R);
|
|
case kCKMove:
|
|
return ProcessMove(cmd.GetMoveID(), R);
|
|
case kCKCommand:
|
|
return ProcessCommand(cmd.GetCommandID(), R);
|
|
case kCKControl:
|
|
case kCKError:
|
|
return kPRError;
|
|
}
|
|
return kPRError;
|
|
}
|
|
|
|
Range
|
|
Editor::ResetText() {
|
|
bool addToHist = !fContext->GetLine().empty()
|
|
&& !fContext->GetTextInput()->IsInputMasked()
|
|
&& fContext->GetTextInput()->IsAutoHistAddEnabled();
|
|
if (addToHist) {
|
|
fContext->GetHistory()->AddLine(fContext->GetLine().GetText());
|
|
if (fReplayHistEntry != (size_t) -1) {
|
|
// Added a line, thus renumber
|
|
++fReplayHistEntry;
|
|
}
|
|
}
|
|
Range R(0, fContext->GetLine().length());
|
|
fContext->GetLine().clear();
|
|
fContext->SetCursor(0);
|
|
ClearPasteBuf();
|
|
fSearch.clear();
|
|
CancelSpecialInputMode(R);
|
|
if (fReplayHistEntry != (size_t) -1) {
|
|
--fReplayHistEntry; // intentional overflow to -1
|
|
fContext->SetLine(fContext->GetHistory()->GetLine(fReplayHistEntry));
|
|
}
|
|
return R;
|
|
}
|
|
|
|
void
|
|
Editor::SetReverseHistSearchPrompt(Range& RDisplay) {
|
|
std::string P("[bkw'");
|
|
SetEditorPrompt(Text(P + fSearch + "'] "));
|
|
RDisplay.ExtendPromptUpdate(Range::kUpdateEditorPrompt);
|
|
}
|
|
|
|
bool
|
|
Editor::UpdateHistSearch(EditorRange& R) {
|
|
History* Hist = fContext->GetHistory();
|
|
Text& Line = fContext->GetLine();
|
|
size_t NewHistEntry = (size_t) -1;
|
|
if (fSearch.empty()) {
|
|
NewHistEntry = 0;
|
|
} else {
|
|
size_t startAt = fCurHistEntry;
|
|
if (startAt == (size_t) -1) {
|
|
startAt = 0;
|
|
}
|
|
for (size_t i = startAt, n = Hist->GetSize(); i < n; ++i) {
|
|
if (Hist->GetLine(i).find(fSearch) != std::string::npos) {
|
|
NewHistEntry = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (NewHistEntry != (size_t) -1) {
|
|
// No, even if they are unchanged: we might have
|
|
// subsequent ^R updates triggered by faking a different
|
|
// fCurHistEntry.
|
|
// if (NewHistEntry != fCurHistEntry) {
|
|
fCurHistEntry = NewHistEntry;
|
|
Line = Hist->GetLine(fCurHistEntry);
|
|
R.fEdit.Extend(Range::AllText());
|
|
R.fDisplay.Extend(Range::AllText());
|
|
// Resets mode, thus can't call:
|
|
// ProcessMove(kMoveEnd, R);
|
|
fContext->SetCursor(Line.length());
|
|
return true;
|
|
}
|
|
|
|
fCurHistEntry = (size_t) -1;
|
|
return false;
|
|
}
|
|
|
|
void
|
|
Editor::CancelSpecialInputMode(Range& DisplayR) {
|
|
// Stop incremental history search, leaving text at the
|
|
// history line currently selected.
|
|
if (fMode == kInputMode) return;
|
|
fContext->GetKeyBinding()->EnableEscCmd(false);
|
|
SetEditorPrompt(Text());
|
|
DisplayR.ExtendPromptUpdate(Range::kUpdateEditorPrompt);
|
|
fMode = kInputMode;
|
|
}
|
|
|
|
void
|
|
Editor::CancelAndRevertSpecialInputMode(EditorRange& R) {
|
|
// Stop incremental history search, reset text to what it was
|
|
// before search started.
|
|
if (fMode == kInputMode) return;
|
|
CancelSpecialInputMode(R.fDisplay);
|
|
// Original line should be top of undo buffer.
|
|
ProcessCommand(kCmdUndo, R);
|
|
}
|
|
|
|
Editor::EProcessResult
|
|
Editor::ProcessChar(char C, EditorRange& R) {
|
|
if (C < 32) return kPRError;
|
|
|
|
if (fMode == kHistSearchMode) {
|
|
fSearch += C;
|
|
SetReverseHistSearchPrompt(R.fDisplay);
|
|
if (UpdateHistSearch(R)) return kPRSuccess;
|
|
return kPRError;
|
|
}
|
|
|
|
PushUndo();
|
|
ClearPasteBuf();
|
|
|
|
Text& Line = fContext->GetLine();
|
|
size_t Cursor = fContext->GetCursor();
|
|
|
|
if (fOverwrite) {
|
|
if (Cursor < Line.length()) {
|
|
Line[Cursor] = C;
|
|
} else {
|
|
Line += C;
|
|
}
|
|
R.fEdit.Extend(Range(Cursor));
|
|
R.fDisplay.Extend(Range(Cursor));
|
|
} else {
|
|
Line.insert(Cursor, C);
|
|
R.fEdit.Extend(Range(Cursor));
|
|
R.fDisplay.Extend(Range(Cursor, Range::End()));
|
|
fContext->SetCursor(Cursor + 1);
|
|
}
|
|
return kPRSuccess;
|
|
}
|
|
|
|
Editor::EProcessResult
|
|
Editor::ProcessMove(EMoveID M, EditorRange &R) {
|
|
if (fMode == kHistSearchMode) {
|
|
if (M == kMoveRight) {
|
|
// ^G, i.e. cancel hist search and revert original line.
|
|
CancelAndRevertSpecialInputMode(R);
|
|
return kPRSuccess;
|
|
}
|
|
}
|
|
|
|
ClearPasteBuf();
|
|
CancelSpecialInputMode(R.fDisplay);
|
|
|
|
size_t Cursor = fContext->GetCursor();
|
|
size_t LineLen = fContext->GetLine().length();
|
|
|
|
switch (M) {
|
|
case kMoveEnd: fContext->SetCursor(LineLen); return kPRSuccess;
|
|
case kMoveFront: fContext->SetCursor(0); return kPRSuccess;
|
|
case kMoveRight:
|
|
if (Cursor < LineLen) {
|
|
fContext->SetCursor(Cursor + 1);
|
|
return kPRSuccess;
|
|
} else {
|
|
return kPRError;
|
|
}
|
|
case kMoveLeft:
|
|
if (Cursor > 0) {
|
|
fContext->SetCursor(Cursor - 1);
|
|
return kPRSuccess;
|
|
} else {
|
|
return kPRError;
|
|
}
|
|
case kMoveNextWord:
|
|
case kMovePrevWord:
|
|
fContext->SetCursor(FindWordBoundary(M == kMoveNextWord ? 1 : -1));
|
|
return kPRSuccess;
|
|
}
|
|
return kPRError;
|
|
}
|
|
|
|
Editor::EProcessResult
|
|
Editor::ProcessCommand(ECommandID M, EditorRange &R) {
|
|
if (M < kCmd_END_TEXT_MODIFYING_CMDS) {
|
|
PushUndo();
|
|
}
|
|
if (fMode == kHistSearchMode) {
|
|
if (M == kCmdDelLeft) {
|
|
if (fSearch.empty()) return kPRError;
|
|
fSearch.erase(fSearch.length() - 1);
|
|
SetReverseHistSearchPrompt(R.fDisplay);
|
|
if (UpdateHistSearch(R)) return kPRSuccess;
|
|
return kPRError;
|
|
} else if (M == kCmdReverseSearch) {
|
|
// Search again. Move to older hist entry:
|
|
size_t prevHistEntry = fCurHistEntry;
|
|
// intentional overflow from -1 to 0:
|
|
if (fCurHistEntry + 1 >= fContext->GetHistory()->GetSize()) {
|
|
return kPRError;
|
|
}
|
|
if (fCurHistEntry == (size_t)-1) {
|
|
fCurHistEntry = 0;
|
|
} else {
|
|
++fCurHistEntry;
|
|
}
|
|
if (UpdateHistSearch(R)) return kPRSuccess;
|
|
fCurHistEntry = prevHistEntry;
|
|
return kPRError;
|
|
} else {
|
|
CancelSpecialInputMode(R.fDisplay);
|
|
return kPRError;
|
|
}
|
|
}
|
|
|
|
Text& Line = fContext->GetLine();
|
|
size_t Cursor = fContext->GetCursor();
|
|
History* Hist = fContext->GetHistory();
|
|
|
|
switch (M) {
|
|
case kCmdIgnore:
|
|
return kPRSuccess;
|
|
case kCmdEnter:
|
|
fReplayHistEntry = (size_t) -1;
|
|
fCurHistEntry = (size_t) -1;
|
|
CancelSpecialInputMode(R.fDisplay);
|
|
return kPRSuccess;
|
|
case kCmdDelLeft:
|
|
if (Cursor == 0) return kPRError;
|
|
fContext->SetCursor(--Cursor);
|
|
// intentional fallthrough:
|
|
case kCmdDel:
|
|
if (Cursor == Line.length()) return kPRError;
|
|
AddToPasteBuf(M == kCmdDel ? 1 : -1, Line[Cursor]);
|
|
Line.erase(Cursor);
|
|
R.fEdit.Extend(Range(Cursor));
|
|
R.fDisplay.Extend(Range(Cursor, Range::End()));
|
|
return kPRSuccess;
|
|
case kCmdCutToEnd:
|
|
AddToPasteBuf(1, Line.GetText().c_str() + Cursor);
|
|
Line.erase(Cursor, Line.length() - Cursor);
|
|
R.fEdit.Extend(Range(Cursor));
|
|
R.fDisplay.Extend(Range(Cursor, Range::End()));
|
|
return kPRSuccess;
|
|
case kCmdCutNextWord:
|
|
{
|
|
size_t posWord = FindWordBoundary(1);
|
|
AddToPasteBuf(1, Line.GetText().substr(Cursor, posWord - Cursor));
|
|
R.fEdit.Extend(Range(Cursor, posWord));
|
|
R.fDisplay.Extend(Range(Cursor, Range::End()));
|
|
Line.erase(Cursor, posWord - Cursor);
|
|
return kPRSuccess;
|
|
}
|
|
case kCmdCutPrevWord:
|
|
{
|
|
size_t posWord = FindWordBoundary(-1);
|
|
AddToPasteBuf(-1, Line.GetText().substr(posWord, Cursor - posWord));
|
|
R.fEdit.Extend(Range(posWord, Cursor));
|
|
R.fDisplay.Extend(Range(posWord, Range::End()));
|
|
Line.erase(posWord, Cursor - posWord);
|
|
fContext->SetCursor(posWord);
|
|
return kPRSuccess;
|
|
}
|
|
case kCmdToggleOverwriteMode:
|
|
fOverwrite = !fOverwrite;
|
|
return kPRSuccess;
|
|
case kCmdInsertMode:
|
|
fOverwrite = false;
|
|
return kPRSuccess;
|
|
case kCmdOverwiteMode:
|
|
fOverwrite = true;
|
|
return kPRSuccess;
|
|
case kCmdCutToFront:
|
|
R.fEdit.Extend(Range(0, Cursor));
|
|
R.fDisplay.Extend(Range::AllText());
|
|
AddToPasteBuf(-1, Line.GetText().substr(0, Cursor));
|
|
Line.erase(0, Cursor);
|
|
fContext->SetCursor(0);
|
|
return kPRSuccess;
|
|
case kCmdPaste:
|
|
{
|
|
size_t PasteLen = fPasteBuf.length();
|
|
R.fEdit.Extend(Range(Cursor, PasteLen));
|
|
R.fDisplay.Extend(Range(Cursor, Range::End()));
|
|
Line.insert(Cursor, fPasteBuf);
|
|
fContext->SetCursor(Cursor + PasteLen);
|
|
ClearPasteBuf();
|
|
return kPRSuccess;
|
|
}
|
|
case kCmdSwapThisAndLeftThenMoveRight:
|
|
{
|
|
if (Cursor < 1) return kPRError;
|
|
R.fEdit.Extend(Range(Cursor - 1, Cursor));
|
|
R.fDisplay.Extend(Range(Cursor - 1, Cursor));
|
|
char tmp = Line.GetText()[Cursor];
|
|
Line[Cursor] = Line[Cursor - 1];
|
|
Line[Cursor] = tmp;
|
|
// optional:
|
|
ProcessMove(kMoveRight, R);
|
|
return kPRSuccess;
|
|
}
|
|
case kCmdToUpperMoveNextWord:
|
|
{
|
|
if (Cursor >= Line.length()) return kPRError;
|
|
Line[Cursor] = toupper(Line[Cursor]);
|
|
R.fEdit.Extend(Range(Cursor));
|
|
R.fDisplay.Extend(Range(Cursor));
|
|
ProcessMove(kMoveNextWord, R);
|
|
return kPRSuccess;
|
|
}
|
|
case kCmdWordToLower:
|
|
case kCmdWordToUpper:
|
|
{
|
|
size_t posWord = FindWordBoundary(1);
|
|
if (M == kCmdWordToUpper) {
|
|
for (size_t i = Cursor; i < posWord; ++i) {
|
|
Line[Cursor] = toupper(Line[Cursor]);
|
|
}
|
|
} else {
|
|
for (size_t i = Cursor; i < posWord; ++i) {
|
|
Line[Cursor] = tolower(Line[Cursor]);
|
|
}
|
|
}
|
|
R.fEdit.Extend(Range(Cursor, posWord - Cursor));
|
|
R.fDisplay.Extend(Range(Cursor, posWord - Cursor));
|
|
fContext->SetCursor(posWord);
|
|
return kPRSuccess;
|
|
}
|
|
case kCmdUndo:
|
|
Line = fUndoBuf.back().first;
|
|
fContext->SetCursor(fUndoBuf.back().second);
|
|
fUndoBuf.pop_back();
|
|
R.fEdit.Extend(Range::AllText());
|
|
R.fDisplay.Extend(Range::AllText());
|
|
return kPRSuccess;
|
|
case kCmdHistNewer:
|
|
// already newest?
|
|
if (fCurHistEntry == (size_t)-1) {
|
|
// not a history line ("newer" doesn't mean anything)?
|
|
return kPRError;
|
|
}
|
|
if (fCurHistEntry == 0) {
|
|
Hist->ModifyLine(fCurHistEntry, Line.GetText().c_str());
|
|
Line = fLineNotInHist;
|
|
fLineNotInHist.clear();
|
|
fCurHistEntry = (size_t)-1; // not in hist
|
|
} else {
|
|
--fCurHistEntry;
|
|
Line = Hist->GetLine(fCurHistEntry);
|
|
}
|
|
R.fEdit.Extend(Range::AllText());
|
|
R.fDisplay.Extend(Range::AllText());
|
|
ProcessMove(kMoveEnd, R);
|
|
return kPRSuccess;
|
|
case kCmdHistOlder:
|
|
// intentional overflow from -1 to 0:
|
|
if (fCurHistEntry + 1 >= Hist->GetSize()) {
|
|
return kPRError;
|
|
}
|
|
if (fCurHistEntry == (size_t)-1) {
|
|
fLineNotInHist = Line.GetText();
|
|
fCurHistEntry = 0;
|
|
} else {
|
|
Hist->ModifyLine(fCurHistEntry, Line.GetText().c_str());
|
|
++fCurHistEntry;
|
|
}
|
|
Line = Hist->GetLine(fCurHistEntry);
|
|
R.fEdit.Extend(Range::AllText());
|
|
R.fDisplay.Extend(Range::AllText());
|
|
ProcessMove(kMoveEnd, R);
|
|
return kPRSuccess;
|
|
case kCmdReverseSearch:
|
|
PushUndo();
|
|
fMode = kHistSearchMode;
|
|
fSearch.clear();
|
|
SetReverseHistSearchPrompt(R.fDisplay);
|
|
fContext->GetKeyBinding()->EnableEscCmd(true);
|
|
if (UpdateHistSearch(R)) return kPRSuccess;
|
|
return kPRError;
|
|
case kCmdHistReplay:
|
|
if (fCurHistEntry == (size_t) -1) return kPRError;
|
|
fReplayHistEntry = fCurHistEntry;
|
|
return kPRSuccess;
|
|
case kCmdClearScreen:
|
|
case kCmd_END_TEXT_MODIFYING_CMDS:
|
|
return kPRError;
|
|
case kCmdEsc:
|
|
// Already done for all commands:
|
|
//CancelSpecialInputMode(R);
|
|
return kPRSuccess;
|
|
case kCmdComplete:
|
|
{
|
|
// Completion happens below current input.
|
|
ProcessMove(kMoveEnd, R);
|
|
std::vector<std::string> completions;
|
|
TabCompletion* tc = fContext->GetCompletion();
|
|
Reader* reader = fContext->GetReaders()[0];
|
|
StreamReaderUnix* streamReader = (StreamReaderUnix*)(reader);
|
|
if (!streamReader->IsFromTTY()) return kPRSuccess;
|
|
if (tc) {
|
|
bool ret = tc->Complete(Line, Cursor, R, completions);
|
|
if (ret) {
|
|
if (!completions.empty()) {
|
|
fContext->GetTextInput()->DisplayInfo(completions);
|
|
R.fDisplay.Extend(Range::AllWithPrompt());
|
|
}
|
|
fContext->SetCursor(Cursor);
|
|
return kPRSuccess;
|
|
}
|
|
}
|
|
return kPRError;
|
|
}
|
|
case kCmdWindowResize:
|
|
fContext->GetTextInput()->HandleResize();
|
|
return kPRSuccess;
|
|
case kCmdHistComplete:
|
|
// Not handled yet, todo.
|
|
return kPRError;
|
|
}
|
|
return kPRError;
|
|
}
|
|
|
|
size_t
|
|
Editor::FindWordBoundary(int Direction) {
|
|
|
|
const Text& Line = fContext->GetLine();
|
|
size_t Cursor = fContext->GetCursor();
|
|
|
|
if (Direction < 0 && Cursor < 2) return 0;
|
|
|
|
size_t ret = Direction > 0 ?
|
|
find_first_non_alnum(Line.GetText(), Cursor + 1)
|
|
: find_last_non_alnum(Line.GetText(), Cursor - 2);
|
|
|
|
if (ret == std::string::npos) {
|
|
if (Direction > 0) return Line.length();
|
|
else return 0;
|
|
}
|
|
|
|
if (Direction < 0)
|
|
ret += 1;
|
|
|
|
if (ret == std::string::npos) {
|
|
if (Direction > 0) return Line.length();
|
|
else return 0;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
Editor::AddToPasteBuf(int Dir, std::string const &T) {
|
|
if (fCutDirection == Dir) {
|
|
if (Dir < 0) {
|
|
fPasteBuf = T + fPasteBuf;
|
|
} else {
|
|
fPasteBuf += T;
|
|
}
|
|
} else {
|
|
fCutDirection = Dir;
|
|
fPasteBuf = T;
|
|
}
|
|
}
|
|
|
|
void
|
|
Editor::AddToPasteBuf(int Dir, char T) {
|
|
if (fCutDirection == Dir) {
|
|
if (Dir < 0) {
|
|
fPasteBuf = std::string(1, T) + fPasteBuf;
|
|
} else {
|
|
fPasteBuf += T;
|
|
}
|
|
} else {
|
|
fCutDirection = Dir;
|
|
fPasteBuf = T;
|
|
}
|
|
}
|
|
|
|
void
|
|
Editor::PushUndo() {
|
|
static const size_t MaxUndoBufSize = 100;
|
|
if (fUndoBuf.size() > MaxUndoBufSize) {
|
|
fUndoBuf.pop_front();
|
|
}
|
|
fUndoBuf.push_back(std::make_pair(fContext->GetLine(),
|
|
fContext->GetCursor()));
|
|
}
|
|
|
|
// Pin vtables:
|
|
TabCompletion::~TabCompletion() {}
|
|
FunKey::~FunKey() {}
|
|
}
|