-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEditor.cpp
More file actions
973 lines (903 loc) · 30.8 KB
/
Editor.cpp
File metadata and controls
973 lines (903 loc) · 30.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
/*
* Editor.cpp
* CS 15 Project 2: typewriter — Phase 2
* Author: Adetoye Adebayo
* Date: 15th October, 2025
* This file is the heart of the editor. It loads and saves a text file,
* keeps the text as lines with a cursor, handles typing and keys
* (letters, Enter, Backspace, arrows), supports undo/redo, and
* asks TextUI to draw the screen, with optional logging.
*
*/
#include "Editor.h"
#include <fstream>
/*
name: Editor::Editor(std::string filename)
purpose: construct an editor for the given text file
and prepare the UI and buffer to begin editing
arguments: filename — path to the text file to open
returns: none
effects: initializes internal state (buffer, cursor, stacks),
loads the file into the buffer, may create an empty buffer if the
file is missin and triggers an initial screen render
note: none
*/
Editor::Editor(std::string filename) {
this->filename = filename; // store filename
this->quitRequested = false; // loop guard for run()
this->cursorLine = 0;
this->cursorColumn = 0;
this->targetColumn = 0; // target column for wrap-aware vertical moves
loadFile(filename);
if (lines.empty()) {
lines.push_back(std::string());
}
redraw();
}
/*
name: Editor::Editor(std::string filename, std::string logfile)
purpose: construct an editor for the given text file and enable
logging of program state to the given log file
arguments: filename — path to the text file to open;
logfile — path to the log file used by TextUI
returns: none
effects: initializes internal state, loads the file, enables UI log mode
and triggers an initial screen render
note: none
*/
Editor::Editor(std::string filename, std::string logfile) {
this->filename = filename;
this->quitRequested = false;
this->cursorLine = 0;
this->cursorColumn = 0;
this->targetColumn = 0;
loadFile(filename);
if (lines.empty()) {
lines.push_back(std::string()); // ensure at least one line
}
ui.startLogMode(logfile); // enable log mode
redraw();
}
/*
name: Editor::~Editor()
purpose: shut down the editor cleanly
arguments: none
returns: none
effects: closes the TextUI and any UI-related resources
note: none
*/
Editor::~Editor() {
ui.close(); // close the UI
}
/*
name: Editor::loadFile(std::string filePath)
purpose: populate the editor’s buffer from a file path
arguments: filePath — path to the text file to read from disk
returns: none
effects: replaces current buffer contents with the file’s lines.
If the file cannot be opened, sets the buffer to a single empty line
note: none
*/
void Editor::loadFile(std::string filePath) {
std::ifstream infile(filePath);
lines.clear();
if (not infile.is_open()) {
lines.push_back(std::string()); // file missing, start with empty line
return;
}
std::string lineFromFile;
while (std::getline(infile, lineFromFile)) {
lines.push_back(lineFromFile); // read each line into buffer
}
if (lines.empty()) {
lines.push_back(std::string()); // ensure at least one line
}
infile.close();
}
/*
name: Editor::saveToStream(std::ostream &out)
purpose: write the current buffer to an output stream
arguments: out — an output stream that receives the document text
returns: none
effects: outputs the document’s lines to the provided stream
note: none
*/
void Editor::saveToStream(std::ostream &out) {
for (size_t i = 0; i < lines.size(); ++i) {
out << lines[i] << '\n'; // write each line followed by newline
}
}
/*
name: Editor::saveFile(std::string filePath)
purpose: save the current buffer contents to a file path
arguments: filePath — path to the destination file
returns: none
effects: writes the document’s lines to the specified file
note: none
*/
void Editor::saveFile(std::string filePath) {
std::ofstream outfile(filePath);
if (not outfile.is_open()) {
return; // unable to open file for writing
}
saveToStream(outfile);
}
/*
name: Editor::redraw()
purpose: refresh the on-screen view to match the
current buffer and cursor state
arguments: none
returns: none
effects: calls TextUI::render which updates the terminal display
note: none
*/
void Editor::redraw() {
// Ask TextUI to render the current buffer and cursor position
ui.render(lines, cursorColumn, cursorLine);
}
/*
name: Editor::lineLength(int lineIndex)
purpose: report the length of a specific logical line in the buffer
arguments: lineIndex — zero-based index of the line in the buffer
returns: the number of characters in the requested line, or 0 if out of range
effects: none
note: none
*/
int Editor::lineLength(int lineIndex) {
if (lineIndex < 0 or lineIndex >= (int)lines.size()) {
return 0; // out of range
}
return (int)lines[lineIndex].size(); // should return length of the line
}
/*
name: Editor::clampColumnToLine(int lineIndex, int desiredColumn)
purpose: adjust a desired column so it fits within the valid range
of a given line
arguments: lineIndex — line to consult;
desiredColumn — column the caller wants to clamp
returns: a valid column in [0, lineLength(lineIndex)]
effects: none
note: none
*/
int Editor::clampColumnToLine(int lineIndex, int desiredColumn) {
int length = lineLength(lineIndex); // get line length
if (desiredColumn < 0) {
return 0;
} else if (desiredColumn > length) { // too far right
return length;
} else {
return desiredColumn;
}
}
/*
name: Editor::clampCursorInsideBuffer()
purpose: keep the cursor’s line and column within the valid bounds
of the current buffer
arguments: none
returns: none
effects: may modify cursorLine and/or cursorColumn to valid positions
note: none
*/
void Editor::clampCursorInsideBuffer() {
if (cursorLine < 0) {
cursorLine = 0; // clamp to first line
} else if ((size_t)cursorLine >= lines.size()) {
cursorLine = (int)lines.size() - 1; // clamp to last line
}
int length = lineLength(cursorLine);
if (cursorColumn < 0) {
cursorColumn = 0; // clamp to start of line
} else if (cursorColumn > length) {
cursorColumn = length; // clamp to end of line
}
}
/*
name: Editor::getCurrentTerminalWidth()
purpose: obtain the terminal width from TextUI for wrapping and
vertical movement logic
arguments: none
returns: the current terminal width in columns
effects: none
note: none
*/
int Editor::getCurrentTerminalWidth() {
int terminalWidth = ui.getTerminalWidth();
if (terminalWidth <= 0) {
terminalWidth = 1; // Just to be safe
}
return terminalWidth; // return the terminal width
}
/*
name: Editor::countVisualWrapsForLine(int lineIndex)
purpose: compute how many visual rows a logical line
occupies given the terminal width
arguments: lineIndex — the line to measure
returns: the number of wrapped screen rows (at least 1) used by that line
effects: none
note: none
*/
int Editor::countVisualWrapsForLine(int lineIndex) {
if (lineIndex < 0 or lineIndex >= (int)lines.size()) {
return 0; // out of range
}
int terminalWidth = getCurrentTerminalWidth();
int length = lineLength(lineIndex);
int wraps = (length + terminalWidth - 1) / terminalWidth;
if (wraps < 1) wraps = 1; // even empty lines should have at least
// one wrap
return wraps;
}
/*
name: Editor::computeWrapIndexForColumn(int column)
purpose: to determine when the given column should wrap based
on terminal width
arguments: column — the absolute column within the logical line
returns: the zero-based wrap index containing that column
effects: none
note: none
*/
int Editor::computeWrapIndexForColumn(int column) {
int terminalWidth = getCurrentTerminalWidth();
if (terminalWidth <= 0) {
return 0; // Just to be safe
}
if (column < 0) {
return 0; // if negative, return first wrap
}
return column / terminalWidth; // integer division for wrap index
}
/*
name: Editor::computeColumnInsideWrap(int column)
purpose: compute the column position within its wrap segment based on
terminal width
arguments: column — the absolute column within the logical line
returns: the column within the wrap in [0, terminal width)
effects: none
note: none
*/
int Editor::computeColumnInsideWrap(int column) {
int terminalWidth = getCurrentTerminalWidth();
if (terminalWidth <= 0) {
return 0; // Just to be safe
}
if (column < 0) {
return 0; // if negative, return start of wrap
}
return column % terminalWidth; // modulus for position inside wrap
}
/*
name: Editor::moveUpOneVisualRow
purpose: move the cursor up by one visual row, respecting line wrapping
and preserving target horizontal column
arguments: none
returns: none
effects: updates cursorLine and cursorColumn appropriately
note: none
*/
void Editor::moveUpOneVisualRow() {
int terminalWidth = getCurrentTerminalWidth();
int currentWrapIndex = computeWrapIndexForColumn(cursorColumn);
int insideWrapColumn = computeColumnInsideWrap(cursorColumn);
if (currentWrapIndex > 0) {
// can move up within the same line
int newColumn = (currentWrapIndex - 1) * terminalWidth +
insideWrapColumn;
cursorColumn = clampColumnToLine(cursorLine, newColumn);
return;
}
if (cursorLine > 0) {
// move to the last wrap of the previous line
int previousLine = cursorLine - 1;
int previousWraps = countVisualWrapsForLine(previousLine);
int lastWrapStartColumn = (previousWraps - 1) * terminalWidth;
int newColumn = lastWrapStartColumn + insideWrapColumn;
cursorLine = previousLine;
// clamp to the previous line length if it is shorter
cursorColumn = clampColumnToLine(cursorLine, newColumn);
return;
}
}
/*
name: Editor::moveDownOneVisualRow
purpose: move the cursor down by one visual row, respecting line wrapping and
preserving target horizontal column
arguments: none
returns: none
effects: updates cursorLine and cursorColumn appropriately
note: none
*/
void Editor::moveDownOneVisualRow() {
int terminalWidth = getCurrentTerminalWidth();
int currentWrapIndex = computeWrapIndexForColumn(cursorColumn);
int insideWrapColumn = computeColumnInsideWrap(cursorColumn);
int totalWrapsOnLine = countVisualWrapsForLine(cursorLine);
if (currentWrapIndex + 1 < totalWrapsOnLine) {
// can move down within the same line
int newColumn = (currentWrapIndex + 1) * terminalWidth +
insideWrapColumn;
cursorColumn = clampColumnToLine(cursorLine, newColumn);
return;
}
if ((size_t)(cursorLine + 1) < lines.size()) {
// move to the first wrap of the next line
int nextLine = cursorLine + 1;
int newColumn = insideWrapColumn;
cursorLine = nextLine;
// clamp to the next line length if it is shorter
cursorColumn = clampColumnToLine(cursorLine, newColumn);
return;
}
}
/*
name: Editor::insertCharAt
purpose: insert a character at a given line and column inside the buffer
arguments: ch — the character to insert; lineIndex — target line;
columnIndex — target column
returns: none
effects: modifies the buffer by inserting into a line or splitting on newline
note: none
*/
void Editor::insertCharAt(char ch, int lineIndex, int columnIndex) {
if (ch == '\n') {
// Insert newline by splitting the line
insertNewlineAt(lineIndex, columnIndex);
return;
}
std::string ¤tLine = lines[lineIndex];
if (columnIndex < 0) {
columnIndex = 0; // clamp to start of line
} else if (columnIndex > (int)currentLine.size()) {
columnIndex = (int)currentLine.size(); // clamp to end of line
}
// Insert character at the specified position
currentLine.insert(currentLine.begin() + columnIndex, ch);
}
/*
name: Editor::deleteCharAt
purpose: delete a character at a given line and column, or remove the newline
between two lines when at end-of-line
arguments: lineIndex — line of deletion; columnIndex — column of deletion;
removedCharacter — set to the character removed; removedWasNewline — set true
if a newline boundary was removed
returns: none
effects: modifies the buffer (either erases a char or joins two lines) and
updates out-parameters with what was removed
note: none
*/
void Editor::deleteCharAt(int lineIndex, int columnIndex,
char &removedCharacter, bool &removedWasNewline) {
removedWasNewline = false; // initialize out-parameter
std::string ¤tLine = lines[lineIndex];
// Deleting at the newline boundary should remove the newline
if (columnIndex == (int)currentLine.size()) {
if (lineIndex + 1 < (int)lines.size()) {
removedCharacter = '\n';
removedWasNewline = true;
currentLine += lines[lineIndex + 1];
lines.erase(lines.begin() + lineIndex + 1);
}
return;
}
// Deleting a normal character inside the line
if (columnIndex >= 0 and columnIndex < (int)currentLine.size()) {
removedCharacter = currentLine[columnIndex]; // store removed char
// for undo
currentLine.erase(currentLine.begin() + columnIndex);
}
}
/*
name: Editor::insertNewlineAt
purpose: split a line at the given column, creating a new line after it
arguments: lineIndex — line to split; columnIndex — column where the
split occurs
returns: none
effects: modifies the buffer by splitting one line into two and
increases line count
note: none
*/
void Editor::insertNewlineAt(int lineIndex, int columnIndex) {
std::string ¤tLine = lines[lineIndex];
if (columnIndex < 0) {
columnIndex = 0; // clamp to start of line
} else if (columnIndex > (int)currentLine.size()) {
columnIndex = (int)currentLine.size(); // clamp to end of line
}
std::string rightHalf;
// Extract right half of the line
for (int i = columnIndex; i < (int)currentLine.size(); i++) {
rightHalf.push_back(currentLine[i]);
}
currentLine.erase(columnIndex);
// Insert rightHalf as a new line
lines.insert(lines.begin() + lineIndex + 1, rightHalf);
}
/*
name: Editor::joinAtNewlineBoundary
purpose: join a line with the line that follows it, removing the
newline boundary
arguments: lineIndex — index of the first of the two lines to join
returns: none
effects: merges two adjacent lines into one and decreases line count
note: none
*/
void Editor::joinAtNewlineBoundary(int lineIndex) {
if ((size_t)(lineIndex + 1) < lines.size()) {
lines[lineIndex] += lines[lineIndex + 1]; // append next line
lines.erase(lines.begin() + (lineIndex + 1)); // remove next line
}
}
/*
name: Editor::pushInsertAction
purpose: record an insert operation on the undo stack
arguments: ch — inserted character; lineIndex — line where it occurred;
columnIndex — column where it occurred
returns: none
effects: pushes an Action onto undoStack and leaves redoStack unchanged
note: none
*/
void Editor::pushInsertAction(char ch, int lineIndex, int columnIndex) {
ActionStack::Action action;
action.character = ch;
action.deleted = false; // was_delete
action.line = (std::size_t)lineIndex;
action.column = (std::size_t)columnIndex;
undoStack.push(action); // push the action onto the undo stack
}
/*
name: Editor::pushDeleteAction
purpose: record a delete operation on the undo stack
arguments: c — deleted character (or '\n' if the newline boundary was
removed); lineIndex — line of deletion; columnIndex — column of deletion
returns: none
effects: pushes an Action onto undoStack and leaves redoStack unchanged
note: none
*/
void Editor::pushDeleteAction(char ch, int lineIndex, int columnIndex) {
ActionStack::Action action;
action.character = ch;
action.deleted = true; // was_delete
action.line = (std::size_t)lineIndex;
action.column = (std::size_t)columnIndex;
undoStack.push(action); // push the action onto the undo stack
}
/*
name: Editor::clearRedoOnNewEdit
purpose: clear redo history after a new user edit
arguments: none
returns: none
effects: clears redoStack so redo reflects only actions after the most
recent edit
note: none
*/
void Editor::clearRedoOnNewEdit() {
redoStack.clear(); // clear redo stack
}
/*
name: Editor::handleCharacter
purpose: handle a printable key by inserting it at the cursor and updating
state
arguments: keyCode — ASCII code of the pressed key
returns: none
effects: modifies the buffer, updates cursor, records undo, clears redo and
triggers redraw
note: none
*/
void Editor::handleCharacter(int keyCode) {
char ch = (char)keyCode;
insertCharAt(ch, cursorLine, cursorColumn);
pushInsertAction(ch, cursorLine, cursorColumn); // record undo
clearRedoOnNewEdit(); // clear redo stack on new edit
cursorColumn += 1;
targetColumn = computeColumnInsideWrap(cursorColumn); // track
// desired x for vertical moves
redraw(); // update the display
}
/*
name: Editor::handleBackspace
purpose: handle a backspace key by deleting the character
before the cursor or joining lines
arguments: none
returns: none
effects: modifies the buffer, updates cursor, records undo,
clears redo and triggers redraw
note: none
*/
void Editor::handleBackspace() {
if (cursorColumn == 0 and cursorLine == 0) {
return; // Nothing to delete
}
char removedCharacter;
bool removedWasNewline;
int deleteLine = cursorLine;
int deleteColumn = cursorColumn;
if (cursorColumn > 0) {
deleteColumn -= 1; // delete character before cursor
} else {
deleteLine -= 1;
deleteColumn = lineLength(deleteLine); // delete at end of previous
// line
}
deleteCharAt(deleteLine, deleteColumn, removedCharacter,
removedWasNewline); // handles both char and newline deletions
pushDeleteAction(removedCharacter, deleteLine, deleteColumn);
clearRedoOnNewEdit();
cursorLine = deleteLine; // update cursor position to deletion point
cursorColumn = deleteColumn;
targetColumn = computeColumnInsideWrap(cursorColumn);
redraw();
}
/*
name: Editor::handleEnter
purpose: handle Enter by inserting a newline at the cursor
arguments: none
returns: none
effects: splits the current line, moves cursor to the new line start,
records undo, clears redo and triggers redraw
note: none
*/
void Editor::handleEnter() {
pushInsertAction('\n', cursorLine, cursorColumn); // record undo
clearRedoOnNewEdit();
insertNewlineAt(cursorLine, cursorColumn); // split the line
cursorLine += 1;
cursorColumn = 0;
targetColumn = 0;
redraw();
}
/*
name: Editor::handleLeft
purpose: move the cursor one character left, wrapping to the previous
line if needed
arguments: none
returns: none
effects: updates cursor position and triggers redraw
note: none
*/
void Editor::handleLeft() {
if (cursorColumn > 0) {
cursorColumn -= 1; // move left within the line
} else if (cursorLine > 0) {
cursorLine -= 1;
cursorColumn = lineLength(cursorLine); // move to end of
// previous line
}
targetColumn = computeColumnInsideWrap(cursorColumn);
redraw();
}
/*
name: Editor::handleRight
purpose: move the cursor one character right, advancing to the next line if
at end
arguments: none
returns: none
effects: updates cursor position and triggers redraw
note: none
*/
void Editor::handleRight() {
int lineLen = lineLength(cursorLine);
if (cursorColumn < lineLen) {
cursorColumn += 1; // move right within the line
} else if (cursorLine + 1 < (int)lines.size()) {
cursorLine += 1; // move to start of next line
cursorColumn = 0; // start of next line
}
targetColumn = computeColumnInsideWrap(cursorColumn);
redraw();
}
/*
name: Editor::handleUp
purpose: move the cursor up by one visual row considering line wraps
arguments: none
returns: none
effects: updates cursor position and triggers redraw
note: none
*/
void Editor::handleUp() {
moveUpOneVisualRow(); // wrap aware move up
targetColumn = computeColumnInsideWrap(cursorColumn);
redraw();
}
/*
name: Editor::handleDown
purpose: move the cursor down by one visual row considering line wraps
arguments: none
returns: none
effects: updates cursor position and triggers redraw
note: none
*/
void Editor::handleDown() {
moveDownOneVisualRow(); // wrap aware move down
targetColumn = computeColumnInsideWrap(cursorColumn);
redraw();
}
/*
name: Editor::handleCommand
purpose: process a command after the user presses ESC (save, quit, undo, redo)
arguments: none
returns: none
effects: may save to disk, may update buffer and cursor via undo/redo,
may request program exit and triggers redraw
note: none
*/
void Editor::handleCommand() {
int cmd = ui.getChar(); // get the command character
if (cmd == 's') { // save command
saveFile(filename);
ui.displaySaveMessage();
redraw();
} else if (cmd == 'x') { // quit command
bool wantsSave = ui.savePrompt();
if (wantsSave) {
saveFile(filename);
}
ui.close();
quitRequested = true;
} else if (cmd == 'u') { // undo command
doUndoChunk();
redraw();
} else if (cmd == 'r') { // redo command
doRedoChunk();
redraw();
} else { // unrecognized command, ignore
redraw();
}
}
/*
name: Editor::applyInverseAction
purpose: apply the inverse effect of a recorded Action (used during undo)
arguments: action — the Action to invert and apply
returns: none
effects: modifies the buffer, moves the action to the redo stack and
updates cursor position
note: none
*/
void Editor::applyInverseAction(const ActionStack::Action &action) {
bool cursorDone = false;
if (not action.deleted) {
// inverse of INSERT so DELETE
if (action.character == '\n') {
if ((std::size_t)action.line < lines.size()) {
joinAtNewlineBoundary((int)action.line); // merge lines
cursorLine = (int)action.line; // update cursor to be at
// join point
cursorColumn = clampColumnToLine(cursorLine,
(int)action.column);
cursorDone = true;
}
} else {
if ((std::size_t)action.line < lines.size()) {
std::string ¤tLine = lines[(int)action.line];
if ((std::size_t)action.column < currentLine.size()) {
currentLine.erase(currentLine.begin() +
(int)action.column); // delete the previously inserted
// character
}
}
cursorLine = (int)action.line;
cursorColumn = clampColumnToLine(cursorLine, (int)action.column);
cursorDone = true;
}
redoStack.push(action);
} else {
// inverse of DELETE so INSERT
if (action.character == '\n') {
if ((std::size_t)action.line < lines.size()) {
// split the line to re-insert the newline
insertNewlineAt((int)action.line, (int)action.column);
cursorLine = (int)action.line + 1; // move cursor to new line
cursorColumn = 0;
cursorDone = true;
}
} else {
if ((std::size_t)action.line < lines.size()) {
insertCharAt(action.character, (int)action.line,
(int)action.column);
cursorLine = (int)action.line;
cursorColumn = clampColumnToLine(cursorLine,
(int)action.column + 1);
cursorDone = true;
}
}
redoStack.push(action); // push to redo stack
}
if (not cursorDone) {
// if cursor was not updated yet, set it now
cursorLine = (int)action.line;
cursorColumn = clampColumnToLine(cursorLine, (int)action.column);
}
targetColumn = computeColumnInsideWrap(cursorColumn);
}
/*
name: Editor::applyOriginalAction
purpose: reapply an Action as originally recorded (used during redo)
arguments: action — the Action to apply in its original sense
returns: none
effects: modifies the buffer, moves the action to the undo stack and
updates cursor position
note: none
*/
void Editor::applyOriginalAction(const ActionStack::Action &action) {
bool cursorDone = false;
if (not action.deleted) {
// re-apply INSERT
if (action.character == '\n') {
if ((std::size_t)action.line < lines.size()) {
insertNewlineAt((int)action.line, (int)action.column);
cursorLine = (int)action.line + 1; // move cursor to new line
cursorColumn = 0;
cursorDone = true;
}
} else {
if ((std::size_t)action.line < lines.size()) {
insertCharAt(action.character, (int)action.line,
(int)action.column);
cursorLine = (int)action.line;
cursorColumn = clampColumnToLine(cursorLine,
(int)action.column + 1); // after inserted char
cursorDone = true;
}
}
undoStack.push(action); // push to undo stack
} else {
// re-apply DELETE
if (action.character == '\n') {
if ((std::size_t)action.line < lines.size()) {
joinAtNewlineBoundary((int)action.line); // merge lines
cursorLine = (int)action.line;
cursorColumn = clampColumnToLine(cursorLine,
(int)action.column); // at join point
cursorDone = true;
}
} else {
if ((std::size_t)action.line < lines.size()) {
std::string ¤tLine = lines[(int)action.line];
if ((std::size_t)action.column < currentLine.size()) {
currentLine.erase(currentLine.begin() +
(int)action.column); // delete the character again
}
}
cursorLine = (int)action.line; // update cursor to deletion point
cursorColumn = clampColumnToLine(cursorLine, (int)action.column);
cursorDone = true;
}
undoStack.push(action);
}
if (not cursorDone) {
cursorLine = (int)action.line; // update cursor if not done yet
cursorColumn = clampColumnToLine(cursorLine, (int)action.column);
}
// track desired x for vertical moves
targetColumn = computeColumnInsideWrap(cursorColumn);
}
/*
name: Editor::undoRunContinues
purpose: decide whether the next Action should be grouped into the current
undo chunk
arguments: modeDelete — true if the current chunk is deleting,
false if inserting; next — the candidate next Action
returns: true if the next Action belongs in the same undo chunk under the
grouping rules and false otherwise
effects: none
note: none
*/
bool Editor::undoRunContinues(bool modeDelete, ActionStack::Action &next) {
if (next.deleted != modeDelete) {
return false; // stop when action type changes
}
if (not modeDelete and next.character == '\n') {
return false; // stop inserting chunk at newlines
}
return true; // keep grouping in this run
}
/*
name: Editor::redoRunContinues
purpose: decide whether the next Action should be grouped into the
current redo chunk
arguments: modeDelete — true if the current chunk is deleting,
false if inserting; next — the candidate next Action
returns: true if the next Action belongs in the same redo chunk
under the grouping rules and false otherwise
effects: none
note: none
*/
bool Editor::redoRunContinues(bool modeDelete, ActionStack::Action &next) {
if (next.deleted != modeDelete) {
return false; // stop when action type changes
}
if (not modeDelete and next.character == '\n') {
return false; // stop inserting chunk at newlines
}
return true; // keep grouping in this run
}
/*
name: Editor::doUndoChunk
purpose: perform a single undo operation that may include multiple Actions
under the grouping rules
arguments: none
returns: none
effects: updates the buffer and cursor, moves Actions from undoStack
to redoStack and triggers redraw by caller
note: none
*/
void Editor::doUndoChunk() {
if (undoStack.isEmpty()) {
return; // nothing to undo
}
ActionStack::Action first = undoStack.top();
undoStack.pop();
bool runIsDelete = first.deleted; // group by the kind of the first action
applyInverseAction(first); // moved to redoStack inside
bool keepGoing = true;
while (keepGoing and not undoStack.isEmpty()) {
ActionStack::Action next = undoStack.top();
if (not undoRunContinues(runIsDelete, next)) {
keepGoing = false; // stop grouping when rules say so
} else {
undoStack.pop();
applyInverseAction(next); // keep moving actions into redo stack
}
}
}
/*
name: Editor::doRedoChunk
purpose: perform a single redo operation that may include multiple Actions
under the grouping rules
arguments: none
returns: none
effects: updates the buffer and cursor, moves Actions from redoStack back
to undoStack and triggers redraw by caller
note: none
*/
void Editor::doRedoChunk() {
if (redoStack.isEmpty()) {
return; // nothing to redo
}
ActionStack::Action first = redoStack.top();
redoStack.pop();
bool runIsDelete = first.deleted; // group by the kind of the first action
applyOriginalAction(first); // moved to undoStack inside
bool keepGoing = true;
while (keepGoing and not redoStack.isEmpty()) {
ActionStack::Action next = redoStack.top();
if (not redoRunContinues(runIsDelete, next)) {
keepGoing = false; // stop grouping when rules say so
} else {
redoStack.pop();
applyOriginalAction(next); // keep moving actions into undo stack
}
}
}
/*
name: Editor::run
purpose: drive the editor’s main loop, reading keys and dispatching to
the appropriate handlers until exit
arguments: none
returns: none
effects: repeated UI input and rendering, edits the buffer and may
write files on save/quit
note: returns when the user requests to quit
*/
void Editor::run() {
while (not quitRequested) {
int key = ui.getChar();
if (key >= 32 and key <= 126) {
handleCharacter(key); // printable characters in ASCII range
} else if (key == KEY_LEFT) {
handleLeft();
} else if (key == KEY_RIGHT) {
handleRight();
} else if (key == KEY_UP) {
handleUp();
} else if (key == KEY_DOWN) {
handleDown();
} else if (key == KEY_BACKSPACE) {
handleBackspace();
} else if (key == '\n') { // Enter key
handleEnter();
} else if (key == 27) {
handleCommand(); // ESC key for commands
} else {
redraw();
}
}
}