动手写编辑器(四) ~ 文本编辑器
Table of Contens
文本编辑器
在之前的三篇博客中,编辑器实现了文件的查看功能。这篇博客的工作主要是在前面的基础上修改 和添加一些内容,使得程序可以真正的实现编辑的功能。
插入字符
首先,要实现编辑功能,从小功能出发。我们使用编辑器的时候都是进行交互的操作,将键 盘的字符一个个的输入到编辑器中。因此需要实现从键盘读入字符然后正确的显示到编辑区 的对应位置。这意味着我们必须处理每一个可打印字符。在之前实现的功能中,我们使用 h, j, k, l 作为方向键来定位光标的位置,但是它们是可打印字符。所以在这里必须重新 映射方向键的返回值。
重新映射方向键
键盘上的每一个键都有一个 ASCII 码,不能将方向键映射到该范围。我们知道 ASCII 的范
围是 0~127。因此只需要将方向键映射到 128, 127, 129, 130 即可。 之前定义保存
ASCII 码的 key_
为 char
类型,最大值为 127。为保险起见,修改为 usigned int
。修改代码如下:
enum key { ARROW_UP=128, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT };
struct textor{
...
/* 按键 */
unsigned char key_;
...
}
按键处理
现在已经移出了方向键的影响,那么默认我们按下任何可打印字符都应该显示在编辑器中。
因此需要处理每一个按键,在 process_key__
中添加以下按键处理:
void process_key__(){
switch(key_){
...
default:
insert_char__();
break;
}
}
插入字符
现在就只需要实现上面的 insert_char__
函数即可,过程也比较简单,直接追加到
text_
变量的对应位置并移动光标到下一个位置即可。
void editor::insert_char__() {
if (cx_ == numrows_) {
text_.push_back("");
numrows_++;
}
if (cy_ < 0 || cy_ > text_[cx_].size()) cy_ = text_[cx_].size();
text_[cx_].insert(cy_, 1, key_);
cy_++;
}
删除字符
除了添加内容,也需要实现修改或者删除文件内容。因此需要实现删除字符的功能,并将次 功能绑定到退格键上。
void process_key__(){
switch(key_){
...
case BACKSPACE:
case CTRL('h'):
delete_char__();
...
}
}
删除字符
该功能是将光标的前面的字符删除。在这部分需要注意的是边界条件的检查。
void editor::delete_char__() {
if (cy_ < 0 || cy_ > text_[cx_].size()) return;
if (cy_ == 0) { // 行首
if (cx_ == 0) return;
while (text_.empty() && cx_) { // 删除空行
cx_--;
numrows_--;
}
cx_--;
cy_ = text_[cx_].size();
text_[cx_] += text_[cx_ + 1]; // 合并两行
text_.erase(text_.begin() + cx_ + 1);
} else // 不在行首
text_[cx_].erase(--cy_, 1);
}
添加行
有时候我们有将长的一行拆分成两行的需求,因此需要实现拆分行的功能。这个功能由
Enter
键触发。
void editor::process_key__() {
read_key__();
switch (key_) {
case '\r':
insert_row__();
break;
...
}
}
换行
将光标的后一部分保存起来,然后将其删除。即可得到两个字符串。将保存的字符串插入到 光标后一行。
void editor::insert_row__() {
if (cy_ < 0 || cy_ >= text_[cx_].size()) return;
std::string tail(text_[cx_].begin() + cy_, text_[cx_].end());
text_[cx_].erase(text_[cx_].begin() + cy_ + 1, text_[cx_].end());
text_[cx_].resize(cy_);
cx_++;
text_.insert(text_.begin() + cx_, tail);
numrows_++;
cy_ = 0;
}
修改提示
如果修改了文件,那么我们实现一个小小的功能来提示文件已经修改过。只需要一个
bool
值即可,如果要获取修改的字节数,可以使用 int
类型。
struct textor{
/* 是否修改 */
bool dirty_;
}
然后我们需要在修改文件的每个的最后添加修改检查。
if (!dirty_) dirty_ = true;
保存文件
编辑器的编辑功能到这里已经完成了,但是目前的修改只是修改了内存缓冲区。要实现长久 的修改,需要将其写入到硬盘中。
void editor::save__() {
if (filename_.empty()) return;
std::ofstream ofs;
ofs.open(filename_.c_str(), std::ifstream::out);
if (ofs.fail()) SPDLOG_ERROR("open file failed");
for (auto row : text_) ofs << row + "\n";
ofs.close();
status_msg__("Save to %s successful!", filename_.c_str());
dirty_ = false;
quit_times_ = 2;
}
退出提示
如果修改了文件,我们却还没有保存,此时如果直接退出,那么修改的内容将不会写入磁盘。 因此在文件修改了的情况下,退出时给出善意的提示是必要的。定义一个退出次数,初始化 为两次,即连续两次退出即可不保存保存文件。
struct textor{
int quit_times_;
}
void process_key__(){
...
case CTRL_KEY('q'):
if (dirty_ && quit_times_) {
status_msg__(
"WARNING!!! Files had modifed. Please press Ctrl+S to save or "
"Ctrl+Q to quit without save!");
quit_times_--;
return;
}
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);
exit(0);
break;
...
}
另存为
当我们打开空白文件时,保存文件必须交互的给出文件的名字。也可以实现另存为的功能。
因此先定义一个在消息框中获得文件名的函数。并将交互过程中的文件名存储在
filename_
中。
void editor::prompt__(const std::string &prompt) {
while (1) {
status_msg__(prompt.c_str(), filename_.c_str());
flush__();
read_key__();
if (key_ == BACKSPACE && !filename_.empty()) {
filename_.pop_back();
} else if (key_ == '\x1b') { // Esc 退出
filename_.clear();
return;
} else if (key_ == '\r') {
if (!filename_.empty()) return; // 文件为空持续等待,除非使用 Esc 退出
} else if (!iscntrl(key_) && key_ < 128) { // 可打印字符
filename_ += key_;
}
}
}
同在保存的时候,如果 filename_
为空,则需要给出文件名进行保存。
void editor::save__() {
if (filename_.empty()) prompt__("Save as : %s (Esc to cancel)");
if (filename_.empty()) {
status_msg__("Save aborted");
return;
}
...
}
效果
修改并保存现有文件:
保存新建文件:
总结
目前已经实现了简单查看和编辑功能,麻雀虽小,五脏俱全。由于马上要找实习,该项目在
过年期间利用空闲时间完成,可能还有很多 bug 以及没有考虑周全的地方。也算告一段落了吧。诸如高亮、丰富的快捷键这些功能以后有
机会在实现吧。代码不多,先上传到 github
吧。
https://github.com/Jerling/Textor
参考
https://viewsourcecode.org/snaptoken/kilo/05.aTextEditor.html