Add support for Undo, Redo
This commit is contained in:
@@ -41,6 +41,8 @@ Ctrl-D : Flip line downwards
|
||||
Ctrl-C : Copy line
|
||||
Ctrl-X : Cut line
|
||||
Ctrl-V : Paste line
|
||||
Ctrl-Z : Undo
|
||||
Ctrl-Y : Redo
|
||||
Ctrl-P : Pause tte (type "fg" to resume)
|
||||
```
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
@@ -58,6 +59,11 @@
|
||||
// Highlight flags
|
||||
#define HL_HIGHLIGHT_NUMBERS (1 << 0)
|
||||
#define HL_HIGHLIGHT_STRINGS (1 << 1)
|
||||
// Status print indicators
|
||||
#define NO_STATUS false
|
||||
#define STATUS_YES true
|
||||
|
||||
typedef struct ActionList ActionList;
|
||||
|
||||
/*** Data section ***/
|
||||
|
||||
@@ -112,6 +118,7 @@ struct editor_config {
|
||||
char* copied_char_buffer;
|
||||
struct editor_syntax* syntax;
|
||||
struct termios orig_termios;
|
||||
ActionList* actions;
|
||||
} ec;
|
||||
|
||||
// Having a dynamic buffer will allow us to write only one
|
||||
@@ -146,6 +153,18 @@ enum editor_highlight {
|
||||
HL_MATCH
|
||||
};
|
||||
|
||||
/*** Edit actions ***/
|
||||
enum ActionType {
|
||||
CutLine,
|
||||
PasteLine,
|
||||
FlipUp,
|
||||
FlipDown,
|
||||
NewLine,
|
||||
InsertChar,
|
||||
DelChar,
|
||||
};
|
||||
typedef enum ActionType ActionType;
|
||||
|
||||
/*** Filetypes ***/
|
||||
|
||||
char* C_HL_extensions[] = {".c", ".h", ".cpp", ".hpp", ".cc", NULL}; // Array must be terminated with NULL.
|
||||
@@ -932,20 +951,20 @@ void editorFlipRow(int dir) {
|
||||
ec.dirty++;
|
||||
}
|
||||
|
||||
void editorCopy(int cut) {
|
||||
void editorCopy(bool printStatus) {
|
||||
ec.copied_char_buffer = realloc(ec.copied_char_buffer, strlen(ec.row[ec.cursor_y].chars) + 1);
|
||||
strcpy(ec.copied_char_buffer, ec.row[ec.cursor_y].chars);
|
||||
editorSetStatusMessage(cut ? "Content cut" : "Content copied");
|
||||
if(printStatus) editorSetStatusMessage("Content copied");
|
||||
}
|
||||
|
||||
void editorCut() {
|
||||
editorCopy(-1);
|
||||
editorDelRow(ec.cursor_y);
|
||||
if (ec.num_rows - ec.cursor_y > 0)
|
||||
editorUpdateSyntax(&ec.row[ec.cursor_y]);
|
||||
editorUpdateSyntax(&ec.row[ec.cursor_y]);
|
||||
if (ec.num_rows - ec.cursor_y > 1)
|
||||
editorUpdateSyntax(&ec.row[ec.cursor_y + 1]);
|
||||
editorUpdateSyntax(&ec.row[ec.cursor_y + 1]);
|
||||
ec.cursor_x = ec.cursor_y == ec.num_rows ? 0 : ec.row[ec.cursor_y].size;
|
||||
editorSetStatusMessage("Content cut");
|
||||
}
|
||||
|
||||
void editorPaste() {
|
||||
@@ -1012,6 +1031,31 @@ void editorRowDelChar(editor_row* row, int at) {
|
||||
ec.dirty++;
|
||||
}
|
||||
|
||||
void editorRowDelString(editor_row* row, int at, int len) {
|
||||
if (at < 0 || (at + len - 1) >= row -> size)
|
||||
return;
|
||||
// Overwriting the deleted string with the characters that come
|
||||
// after it.
|
||||
memmove(&row -> chars[at], &row -> chars[at + len], row -> size - (at + len) + 1);
|
||||
row -> size -= len;
|
||||
editorUpdateRow(row);
|
||||
ec.dirty += len;
|
||||
}
|
||||
|
||||
void editorRowInsertString(editor_row* row, int at, char* str) {
|
||||
int len = strlen(str);
|
||||
if (at < 0 || at > row -> size)
|
||||
return;
|
||||
row->chars = realloc(row->chars, row->size + strlen(str) + 2);
|
||||
// Move 'after-at' part of string content to the end.
|
||||
memmove(&row -> chars[at + len], &row -> chars[at], row -> size - at);
|
||||
// Copy contents of str into the created space.
|
||||
memcpy(&row -> chars[at], str, strlen(str));
|
||||
row -> size += len;
|
||||
editorUpdateRow(row);
|
||||
ec.dirty += len;
|
||||
}
|
||||
|
||||
/*** Editor operations ***/
|
||||
|
||||
void editorInsertChar(int c) {
|
||||
@@ -1233,6 +1277,290 @@ void editorSearch() {
|
||||
}
|
||||
}
|
||||
|
||||
/*** Undo Redo section ***/
|
||||
|
||||
typedef struct Action Action;
|
||||
struct Action {
|
||||
ActionType t;
|
||||
int cpos_x;
|
||||
int cpos_y;
|
||||
char* string;
|
||||
};
|
||||
|
||||
Action* createAction(char* str, ActionType t) {
|
||||
Action* newAction = malloc(sizeof(Action));
|
||||
newAction->t = t;
|
||||
newAction->cpos_x = ec.cursor_x;
|
||||
newAction->cpos_y = ec.cursor_y;
|
||||
newAction->string = str;
|
||||
return newAction;
|
||||
}
|
||||
|
||||
void freeAction(Action *action) {
|
||||
if(action) {
|
||||
if(action->string) free(action->string);
|
||||
free(action);
|
||||
}
|
||||
}
|
||||
|
||||
void execute(Action* action) {
|
||||
if(!action) return;
|
||||
switch(action->t) {
|
||||
case InsertChar:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
if(ec.cursor_y < ec.num_rows) {
|
||||
editorRowInsertString(&ec.row[ec.cursor_y], ec.cursor_x, action->string);
|
||||
ec.cursor_x += strlen(action->string);
|
||||
} else {
|
||||
editorInsertChar((int)(*action->string));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DelChar:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editorDelChar();
|
||||
}
|
||||
break;
|
||||
case PasteLine:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
// store current copied char buffer
|
||||
char* curr_copy_buffer = ec.copied_char_buffer;
|
||||
// set editor copy buffer to action string
|
||||
ec.copied_char_buffer = action->string;
|
||||
editorPaste();
|
||||
// reset editor copy buffer
|
||||
ec.copied_char_buffer = curr_copy_buffer;
|
||||
}
|
||||
break;
|
||||
case CutLine:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editorCut();
|
||||
}
|
||||
break;
|
||||
case FlipDown:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editorFlipRow(-1);
|
||||
}
|
||||
break;
|
||||
case FlipUp:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editorFlipRow(1);
|
||||
}
|
||||
break;
|
||||
case NewLine:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editorInsertNewline();
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void revert(Action *action) {
|
||||
if(!action) return;
|
||||
switch(action->t) {
|
||||
case InsertChar:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editorRowDelString(&ec.row[ec.cursor_y], ec.cursor_x, strlen(action->string));
|
||||
}
|
||||
break;
|
||||
case DelChar:
|
||||
{
|
||||
if(action->string) {
|
||||
ec.cursor_x = action->cpos_x - 1;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
int c = *(action->string);
|
||||
editorInsertChar(c);
|
||||
} else {
|
||||
editorInsertNewline();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PasteLine:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editor_row* row = &ec.row[ec.cursor_y];
|
||||
if(action->string)
|
||||
editorRowDelString(row, ec.cursor_x, strlen(action->string));
|
||||
}
|
||||
break;
|
||||
case CutLine:
|
||||
{
|
||||
ec.cursor_x = 0;
|
||||
ec.cursor_y = action->cpos_y;
|
||||
editorInsertRow(ec.cursor_y, "", 0);
|
||||
// store current copied char buffer
|
||||
char* curr_copy_buffer = ec.copied_char_buffer;
|
||||
// set editor copy buffer to action string
|
||||
ec.copied_char_buffer = action->string;
|
||||
editorPaste();
|
||||
// reset editor copy buffer
|
||||
ec.copied_char_buffer = curr_copy_buffer;
|
||||
}
|
||||
break;
|
||||
case FlipDown:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y + 1;
|
||||
editorFlipRow(1);
|
||||
}
|
||||
break;
|
||||
case FlipUp:
|
||||
{
|
||||
ec.cursor_x = action->cpos_x;
|
||||
ec.cursor_y = action->cpos_y - 1;
|
||||
editorFlipRow(-1);
|
||||
}
|
||||
break;
|
||||
case NewLine:
|
||||
{
|
||||
ec.cursor_x = 0;
|
||||
ec.cursor_y = action->cpos_y + 1;
|
||||
editorDelChar();
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct AListNode AListNode;
|
||||
struct AListNode {
|
||||
Action* action;
|
||||
AListNode* next;
|
||||
AListNode* prev;
|
||||
};
|
||||
|
||||
struct ActionList {
|
||||
AListNode* head;
|
||||
AListNode* tail;
|
||||
AListNode* current;
|
||||
};
|
||||
|
||||
ActionList* actionListInit() {
|
||||
ActionList* list = malloc(sizeof(ActionList));
|
||||
list->head = NULL;
|
||||
list->tail = NULL;
|
||||
list->current = NULL;
|
||||
return list;
|
||||
}
|
||||
|
||||
void clearAlistFrom(AListNode* begin) {
|
||||
if(begin && begin->prev )
|
||||
begin->prev->next = NULL;
|
||||
AListNode* curr_ptr = begin;
|
||||
while( curr_ptr ) {
|
||||
AListNode* temp = curr_ptr;
|
||||
curr_ptr = curr_ptr->next;
|
||||
freeAction(temp->action);
|
||||
free(temp);
|
||||
}
|
||||
}
|
||||
|
||||
void freeAlist() {
|
||||
ActionList* list = ec.actions;
|
||||
if(list){
|
||||
clearAlistFrom(list->head);
|
||||
free(list);
|
||||
}
|
||||
}
|
||||
|
||||
void addAction(Action* action) {
|
||||
ActionList* list = ec.actions;
|
||||
AListNode* node = malloc(sizeof(AListNode));
|
||||
node->action = action;
|
||||
node->prev = NULL;
|
||||
node->next = NULL;
|
||||
|
||||
if(list->head == NULL) {
|
||||
list->head = node;
|
||||
list->tail = node;
|
||||
list->current = node;
|
||||
} else if (list->tail == list->current) {
|
||||
list->tail->next = node;
|
||||
node->prev = list->tail;
|
||||
list->tail = node;
|
||||
list->current = node;
|
||||
} else {
|
||||
AListNode* clear_from = list->current == NULL ? list->head : list->current->next;
|
||||
clearAlistFrom(clear_from);
|
||||
if(list->current) {
|
||||
list->current->next = node;
|
||||
node->prev = list->current;
|
||||
list->tail = node;
|
||||
list->current = node;
|
||||
} else {
|
||||
list->current = list->head = list->tail = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool concatWithLastAction(ActionType t, char* str) {
|
||||
if(t == InsertChar &&
|
||||
ec.actions->current &&
|
||||
ec.actions->current == ec.actions->tail &&
|
||||
ec.actions->current->action->t == t &&
|
||||
ec.actions->current->action->cpos_y == ec.cursor_y &&
|
||||
(int)(ec.actions->current->action->cpos_x + strlen(ec.actions->current->action->string)) == ec.cursor_x
|
||||
) {
|
||||
int c = *(str);
|
||||
editorInsertChar(c);
|
||||
char* string = ec.actions->current->action->string;
|
||||
strcat(string, str);
|
||||
ec.actions->current->action->string = string;
|
||||
free(str);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Creates Action, adds it to ActionList and executes it.
|
||||
void makeAction(ActionType t, char* str) {
|
||||
if(!concatWithLastAction(t, str)) {
|
||||
Action* newAction = createAction(str, t);
|
||||
addAction(newAction);
|
||||
execute(newAction);
|
||||
}
|
||||
}
|
||||
|
||||
void undo() {
|
||||
ActionList* list = ec.actions;
|
||||
if(list && list->current) {
|
||||
revert(list->current->action);
|
||||
// may set current to NULL
|
||||
list->current = list->current->prev;
|
||||
}
|
||||
}
|
||||
|
||||
void redo() {
|
||||
ActionList* list = ec.actions;
|
||||
if(list && list->current && list->current->next) {
|
||||
execute(list->current->next->action);
|
||||
list->current = list->current->next;
|
||||
}
|
||||
// when current points to NULL but head is not NULL, do head
|
||||
if(list && list->head && !list->current) {
|
||||
list->current = list->head;
|
||||
execute(list->current->action);
|
||||
}
|
||||
}
|
||||
|
||||
/*** Append buffer section **/
|
||||
|
||||
void abufAppend(struct a_buf* ab, const char* s, int len) {
|
||||
@@ -1562,7 +1890,7 @@ void editorProcessKeypress() {
|
||||
|
||||
switch (c) {
|
||||
case '\r': // Enter key
|
||||
editorInsertNewline();
|
||||
makeAction(NewLine, NULL);
|
||||
break;
|
||||
case CTRL_KEY('q'):
|
||||
if (ec.dirty && quit_times > 0) {
|
||||
@@ -1571,6 +1899,7 @@ void editorProcessKeypress() {
|
||||
return;
|
||||
}
|
||||
editorClearScreen();
|
||||
freeAlist();
|
||||
consoleBufferClose();
|
||||
exit(0);
|
||||
break;
|
||||
@@ -1579,22 +1908,34 @@ void editorProcessKeypress() {
|
||||
break;
|
||||
case CTRL_KEY('e'):
|
||||
if (ec.cursor_y > 0 && ec.cursor_y <= ec.num_rows - 1)
|
||||
editorFlipRow(1);
|
||||
makeAction(FlipUp, NULL);
|
||||
break;
|
||||
case CTRL_KEY('d'):
|
||||
if (ec.cursor_y < ec.num_rows - 1)
|
||||
editorFlipRow(-1);
|
||||
makeAction(FlipDown, NULL);
|
||||
break;
|
||||
case CTRL_KEY('x'):
|
||||
if (ec.cursor_y < ec.num_rows)
|
||||
editorCut();
|
||||
{
|
||||
if (ec.cursor_y < ec.num_rows) {
|
||||
editorCopy(NO_STATUS);
|
||||
char* string = NULL;
|
||||
if(ec.copied_char_buffer)
|
||||
string = strndup(ec.copied_char_buffer, strlen(ec.copied_char_buffer));
|
||||
makeAction(CutLine, string);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CTRL_KEY('c'):
|
||||
if (ec.cursor_y < ec.num_rows)
|
||||
editorCopy(0);
|
||||
editorCopy(STATUS_YES);
|
||||
break;
|
||||
case CTRL_KEY('v'):
|
||||
editorPaste();
|
||||
{
|
||||
char* string = NULL;
|
||||
if(ec.copied_char_buffer)
|
||||
string = strndup(ec.copied_char_buffer, strlen(ec.copied_char_buffer));
|
||||
makeAction(PasteLine, string);
|
||||
}
|
||||
break;
|
||||
case CTRL_KEY('p'):
|
||||
consoleBufferClose();
|
||||
@@ -1632,15 +1973,26 @@ void editorProcessKeypress() {
|
||||
case BACKSPACE:
|
||||
case CTRL_KEY('h'):
|
||||
case DEL_KEY:
|
||||
if (c == DEL_KEY)
|
||||
editorMoveCursor(ARROW_RIGHT);
|
||||
editorDelChar();
|
||||
{
|
||||
if(ec.cursor_x == 0 && ec.cursor_y == 0) break;
|
||||
if (c == DEL_KEY)
|
||||
editorMoveCursor(ARROW_RIGHT);
|
||||
editor_row* row = &ec.row[ec.cursor_y];
|
||||
char* string = ec.cursor_x > 0 ? strndup(&row->chars[ec.cursor_x-1], 1) : NULL;
|
||||
makeAction(DelChar, string);
|
||||
}
|
||||
break;
|
||||
case CTRL_KEY('l'):
|
||||
case '\x1b': // Escape key
|
||||
break;
|
||||
case CTRL_KEY('z'):
|
||||
undo();
|
||||
break;
|
||||
case CTRL_KEY('y'):
|
||||
redo();
|
||||
break;
|
||||
default:
|
||||
editorInsertChar(c);
|
||||
makeAction(InsertChar, strndup((char*) &c, 1));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1664,6 +2016,7 @@ void initEditor() {
|
||||
ec.status_msg_time = 0;
|
||||
ec.copied_char_buffer = NULL;
|
||||
ec.syntax = NULL;
|
||||
ec.actions = actionListInit();
|
||||
|
||||
editorUpdateWindowSize();
|
||||
// The SIGWINCH signal is sent to a process when its controlling
|
||||
@@ -1687,6 +2040,8 @@ void printHelp() {
|
||||
printf("Ctrl-C \t\tCopy line\n");
|
||||
printf("Ctrl-X \t\tCut line\n");
|
||||
printf("Ctrl-V \t\tPaste line\n");
|
||||
printf("Ctrl-Z \t\tUndo\n");
|
||||
printf("Ctrl-Y \t\tRedo\n");
|
||||
printf("Ctrl-P \t\tPause tte (type \"fg\" to resume)\n");
|
||||
|
||||
printf("\n\nOPTIONS\n-------\n\n");
|
||||
|
||||
Reference in New Issue
Block a user