vnote/src/utils/vvim.h
Le Tan e305024a58 vim-mode: support simple marks(a-z)
Different behaviors from Vim: after deleting the line with a mark set,
VNote could not detect if this mark is set or not. VNote just simply
jumps to the same line.
2017-06-20 23:50:27 +08:00

686 lines
18 KiB
C++

#ifndef VVIM_H
#define VVIM_H
#include <QObject>
#include <QString>
#include <QTextCursor>
#include <QMap>
#include <QDebug>
#include "vutils.h"
class VEdit;
class QKeyEvent;
class VEditConfig;
class QKeyEvent;
enum class VimMode {
Normal = 0,
Insert,
Visual,
VisualLine,
Replace,
Invalid
};
class VVim : public QObject
{
Q_OBJECT
public:
explicit VVim(VEdit *p_editor);
// Struct for a location.
struct Location
{
Location() : m_blockNumber(-1), m_positionInBlock(0)
{
}
Location(int p_blockNumber, int p_positionInBlock)
: m_blockNumber(p_blockNumber), m_positionInBlock(p_positionInBlock)
{
}
bool isValid() const
{
return m_blockNumber > -1;
}
// Block number of the location, based on 0.
int m_blockNumber;
// Position in block, based on 0.
int m_positionInBlock;
};
struct Register
{
Register(QChar p_name, const QString &p_value)
: m_name(p_name), m_value(p_value), m_append(false)
{
}
Register(QChar p_name)
: m_name(p_name), m_append(false)
{
}
Register()
: m_append(false)
{
}
// Register a-z.
bool isNamedRegister() const
{
char ch = m_name.toLatin1();
return ch >= 'a' && ch <= 'z';
}
bool isUnnamedRegister() const
{
return m_name == c_unnamedRegister;
}
bool isBlackHoleRegister() const
{
return m_name == c_blackHoleRegister;
}
bool isSelectionRegister() const
{
return m_name == c_selectionRegister;
}
bool isBlock() const
{
return m_value.endsWith('\n');
}
// @p_value is the content to update.
// If @p_value ends with \n, then it is a block.
// When @p_value is a block, we need to add \n at the end if necessary.
// If @m_append is true and @p_value is a block, we need to add \n between
// them if necessary.
void update(const QString &p_value);
// Read the value of this register.
const QString &read();
QChar m_name;
QString m_value;
// This is not info of Register itself, but a hint to the handling logics
// whether we need to append the content to this register.
// Only valid for a-z registers.
bool m_append;
};
struct Mark
{
QChar m_name;
Location m_location;
QString m_text;
};
// We only support simple local marks a-z.
class Marks
{
public:
Marks();
// Set mark @p_name to point to @p_cursor.
void setMark(QChar p_name, const QTextCursor &p_cursor);
void clearMark(QChar p_name);
// Return the location of mark @p_name.
Location getMarkLocation(QChar p_name);
const QMap<QChar, VVim::Mark> &getMarks() const;
QChar getLastUsedMark() const;
private:
QMap<QChar, Mark> m_marks;
QChar m_lastUsedMark;
};
// Handle key press event.
// Returns true if the event is consumed and need no more handling.
bool handleKeyPressEvent(QKeyEvent *p_event);
// Return current mode.
VimMode getMode() const;
// Set current mode.
void setMode(VimMode p_mode, bool p_clearSelection = true);
// Set current register.
void setRegister(QChar p_reg);
// Get m_registers.
const QMap<QChar, Register> &getRegisters() const;
QChar getCurrentRegisterName() const;
// Get pending keys.
// Turn m_pendingKeys to a string.
QString getPendingKeys() const;
// Get m_marks.
const VVim::Marks &getMarks() const;
signals:
// Emit when current mode has been changed.
void modeChanged(VimMode p_mode);
// Emit when VVim want to display some messages.
void vimMessage(const QString &p_msg);
// Emit when current status updated.
void vimStatusUpdated(const VVim *p_vim);
private slots:
// When user use mouse to select texts in Normal mode, we should change to
// Visual mode.
void selectionToVisualMode(bool p_hasText);
private:
// Struct for a key press.
struct Key
{
Key(int p_key, int p_modifiers = Qt::NoModifier)
: m_key(p_key), m_modifiers(p_modifiers)
{
}
Key() : m_key(-1), m_modifiers(Qt::NoModifier) {}
int m_key;
int m_modifiers;
bool isDigit() const
{
return m_key >= Qt::Key_0
&& m_key <= Qt::Key_9
&& (m_modifiers == Qt::NoModifier || m_modifiers == Qt::KeypadModifier);
}
int toDigit() const
{
V_ASSERT(isDigit());
return m_key - Qt::Key_0;
}
bool isAlphabet() const
{
return m_key >= Qt::Key_A
&& m_key <= Qt::Key_Z
&& (m_modifiers == Qt::NoModifier || m_modifiers == Qt::ShiftModifier);
}
QChar toAlphabet() const
{
V_ASSERT(isAlphabet());
if (m_modifiers == Qt::NoModifier) {
return QChar('a' + (m_key - Qt::Key_A));
} else {
return QChar('A' + (m_key - Qt::Key_A));
}
}
bool isValid() const
{
return m_key > -1 && m_modifiers > -1;
}
bool operator==(const Key &p_key) const
{
return p_key.m_key == m_key && p_key.m_modifiers == m_modifiers;
}
};
// Supported actions.
enum class Action
{
Move = 0,
Delete,
Copy,
Paste,
PasteBefore,
Change,
Indent,
UnIndent,
ToUpper,
ToLower,
Undo,
Redo,
RedrawAtTop,
RedrawAtCenter,
RedrawAtBottom,
JumpPreviousLocation,
JumpNextLocation,
Invalid
};
// Supported movements.
enum class Movement
{
Left = 0,
Right,
Up,
Down,
VisualUp,
VisualDown,
PageUp,
PageDown,
HalfPageUp,
HalfPageDown,
StartOfLine,
EndOfLine,
FirstCharacter,
LineJump,
StartOfDocument,
EndOfDocument,
WordForward,
WORDForward,
ForwardEndOfWord,
ForwardEndOfWORD,
WordBackward,
WORDBackward,
BackwardEndOfWord,
BackwardEndOfWORD,
FindForward,
FindBackward,
TillForward,
TillBackward,
MarkJump,
MarkJumpLine,
Invalid
};
// Supported ranges.
enum class Range
{
Line = 0,
WordInner,
WordAround,
WORDInner,
WORDAround,
QuoteInner,
QuoteAround,
DoubleQuoteInner,
DoubleQuoteAround,
ParenthesisInner,
ParenthesisAround,
BracketInner,
BracketAround,
AngleBracketInner,
AngleBracketAround,
BraceInner,
BraceAround,
Invalid
};
enum class TokenType { Action = 0, Repeat, Movement, Range, Invalid };
struct Token
{
Token(Action p_action)
: m_type(TokenType::Action), m_action(p_action) {}
Token(int p_repeat)
: m_type(TokenType::Repeat), m_repeat(p_repeat) {}
Token(Movement p_movement)
: m_type(TokenType::Movement), m_movement(p_movement) {}
Token(Movement p_movement, Key p_key)
: m_type(TokenType::Movement), m_movement(p_movement), m_key(p_key) {}
Token(Range p_range)
: m_type(TokenType::Range), m_range(p_range) {}
Token() : m_type(TokenType::Invalid) {}
bool isRepeat() const
{
return m_type == TokenType::Repeat;
}
bool isAction() const
{
return m_type == TokenType::Action;
}
bool isMovement() const
{
return m_type == TokenType::Movement;
}
bool isRange() const
{
return m_type == TokenType::Range;
}
bool isValid() const
{
return m_type != TokenType::Invalid;
}
QString toString() const
{
QString str;
switch (m_type) {
case TokenType::Action:
str = QString("action %1").arg((int)m_action);
break;
case TokenType::Repeat:
str = QString("repeat %1").arg(m_repeat);
break;
case TokenType::Movement:
str = QString("movement %1").arg((int)m_movement);
break;
case TokenType::Range:
str = QString("range %1").arg((int)m_range);
break;
default:
str = "invalid";
}
return str;
}
TokenType m_type;
union
{
Action m_action;
int m_repeat;
Range m_range;
Movement m_movement;
};
// Used in some Movement.
Key m_key;
};
// Stack for all the jump locations.
// When we execute a jump action, we push current location to the stack and
// remove older location with the same block number.
// Ctrl+O is also a jum action. If m_pointer points to the top of the stack,
// Ctrl+O will insert a location to the stack.
class LocationStack
{
public:
LocationStack(int p_maximum = 100);
// Add @p_cursor's location to stack.
// Need to delete all older locations with the same block number.
void addLocation(const QTextCursor &p_cursor);
// Go up through the stack. Need to add current location if we are at
// the top of the stack currently.
const Location &previousLocation(const QTextCursor &p_cursor);
// Go down through the stack.
const Location &nextLocation();
bool hasPrevious() const;
bool hasNext() const;
private:
// A stack containing locations.
QList<Location> m_locations;
// Pointer to current element in the stack.
// If we are not in the history of the locations, it points to the next
// element to the top element.
int m_pointer;
// Maximum number of locations in stack.
const int c_maximumLocations;
};
// Returns true if the event is consumed and need no more handling.
bool handleKeyPressEvent(int key, int modifiers);
// Reset all key state info.
void resetState();
// Now m_tokens constitute a command. Execute it.
// Will clear @p_tokens.
void processCommand(QList<Token> &p_tokens);
// Return the number represented by @p_keys.
// Return -1 if @p_keys is not a valid digit sequence.
int numberFromKeySequence(const QList<Key> &p_keys);
// Try to generate a Repeat token from @p_keys and insert it to @p_tokens.
// If succeed, clear @p_keys and return true.
bool tryGetRepeatToken(QList<Key> &p_keys, QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::Move action.
void processMoveAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::Delete action.
void processDeleteAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::Copy action.
void processCopyAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::Paste and Action::PasteBefore action.
void processPasteAction(QList<Token> &p_tokens, bool p_pasteBefore);
// @p_tokens is the arguments of the Action::Change action.
void processChangeAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::Indent and Action::UnIndent action.
void processIndentAction(QList<Token> &p_tokens, bool p_isIndent);
// @p_tokens is the arguments of the Action::ToLower and Action::ToUpper action.
void processToLowerAction(QList<Token> &p_tokens, bool p_toLower);
// @p_tokens is the arguments of the Action::Undo action.
void processUndoAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::Redo action.
void processRedoAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::RedrawAtBottom/RedrawAtCenter/RedrawAtTop action.
// @p_dest: 0 for top, 1 for center, 2 for bottom.
void processRedrawLineAction(QList<Token> &p_tokens, int p_dest);
// Action::JumpPreviousLocation and Action::JumpNextLocation action.
void processJumpLocationAction(QList<Token> &p_tokens, bool p_next);
// Clear selection if there is any.
// Returns true if there is selection.
bool clearSelection();
// Get the block count of one page step in vertical scroll bar.
int blockCountOfPageStep() const;
// Expand selection to whole lines which will change the position
// of @p_cursor.
void expandSelectionToWholeLines(QTextCursor &p_cursor);
// Init m_registers.
// Currently supported registers:
// a-z, A-Z (append to a-z), ", +, _
void initRegisters();
// Check m_keys to see if we are expecting a register name.
bool expectingRegisterName() const;
// Check m_keys to see if we are expecting a target for f/t/F/T command.
bool expectingCharacterTarget() const;
// Check if we are in command line mode.
bool expectingCommandLineInput() const;
// Check if we are in a leader sequence.
bool expectingLeaderSequence() const;
// Check m_keys to see if we are expecting a mark name to create a mark.
bool expectingMarkName() const;
// Check m_keys to see if we are expecting a mark name as the target.
bool expectingMarkTarget() const;
// Return the corresponding register name of @p_key.
// If @p_key is not a valid register name, return a NULL QChar.
QChar keyToRegisterName(const Key &p_key) const;
// Check if @m_tokens contains an action token.
bool hasActionToken() const;
// Check if @m_tokens contains a repeat token.
bool hasRepeatToken() const;
// Try to add an Action::Move action at the front if there is no any action
// token.
void tryAddMoveAction();
// Add an Action token in front of m_tokens.
void addActionToken(Action p_action);
// Get the action token from m_tokens.
const Token *getActionToken() const;
// Get the repeat token from m_tokens.
Token *getRepeatToken();
// Add an Range token at the end of m_tokens.
void addRangeToken(Range p_range);
// Add an Movement token at the end of m_tokens.
void addMovementToken(Movement p_movement);
// Add an Movement token at the end of m_tokens.
void addMovementToken(Movement p_movement, Key p_key);
// Delete selected text if there is any.
// @p_clearEmptyBlock: whether to remove the empty block after deletion.
void deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock);
// Copy selected text if there is any.
// Will clear selection.
// @p_addNewLine: whether to add a new line \n to the selection.
void copySelectedText(bool p_addNewLine);
void copySelectedText(QTextCursor &p_cursor, bool p_addNewLine);
// Convert the case of selected text if there is any.
// Will clear selection.
// @p_toLower: to lower or upper.
void convertCaseOfSelectedText(QTextCursor &p_cursor, bool p_toLower);
// Save @p_text to the Register pointed by m_regName.
// Remove QChar::ObjectReplacementCharacter before saving.
void saveToRegister(const QString &p_text);
// Move @p_cursor according to @p_moveMode and @p_token.
// Return true if it has moved @p_cursor.
bool processMovement(QTextCursor &p_cursor, const QTextDocument *p_doc,
QTextCursor::MoveMode p_moveMode,
const Token &p_token, int p_repeat);
// Move @p_cursor according to @p_moveMode and @p_range.
// Return true if it has moved @p_cursor.
bool selectRange(QTextCursor &p_cursor, const QTextDocument *p_doc,
Range p_range, int p_repeat);
// Check if there is an Action token with Delete/Copy/Change action.
bool hasActionTokenValidForTextObject() const;
// Check if m_keys only contains @p_key.
bool checkPendingKey(const Key &p_key) const;
// Check if m_tokens only contains action token @p_action.
bool checkActionToken(Action p_action) const;
// Repeat m_lastFindToken.
void repeatLastFindMovement(bool p_reverse);
void message(const QString &p_str);
// Check if m_mode equals to p_mode.
bool checkMode(VimMode p_mode);
// In command line mode, read input @p_key and process it.
// Returns true if a command has been completed, otherwise returns false.
bool processCommandLine(const Key &p_key);
// Execute command specified by @p_keys.
// @p_keys does not contain the leading colon.
// Following commands are supported:
// :w, :wq, :q, :q!, :x
void executeCommand(const QList<Key> &p_keys);
// Check if m_keys has non-digit key.
bool hasNonDigitPendingKeys();
// Reading a leader sequence, read input @p_key and process it.
// Returns true if a sequence has been replayed or it is being read,
// otherwise returns false.
// Following sequences are supported:
// y: "+y
// d: "+d
// p: "+p
// P: "+P
bool processLeaderSequence(const Key &p_key);
VEdit *m_editor;
const VEditConfig *m_editConfig;
VimMode m_mode;
// A valid command token should follow the rule:
// Action, Repeat, Movement.
// Action, Repeat, Range.
// Action, Repeat.
QList<Key> m_keys;
QList<Token> m_tokens;
// Keys for status indication.
QList<Key> m_pendingKeys;
// Whether reset the position in block when moving cursor.
bool m_resetPositionInBlock;
QMap<QChar, Register> m_registers;
// Currently used register.
QChar m_regName;
// Last f/F/t/T Token.
Token m_lastFindToken;
// Whether in command line mode.
bool m_cmdMode;
// The leader key, which is Key_Space by default.
Key m_leaderKey;
// Whether we are parsing a leader sequence.
// We will map a leader sequence to another actual sequence. When replaying
// this actual sequence, m_leaderSequence will be true.
bool m_replayLeaderSequence;
LocationStack m_locations;
Marks m_marks;
static const QChar c_unnamedRegister;
static const QChar c_blackHoleRegister;
static const QChar c_selectionRegister;
};
#endif // VVIM_H