实现一个简单的行编辑器

Posted PyLearn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现一个简单的行编辑器相关的知识,希望对你有一定的参考价值。

0.目录

1.要求

2.开启、关闭回显和缓冲

3.显示大小写字母和数字

4.实现退格键

5.实现光标左右移动

6.实现Del键删除整行

7.总代码

1.要求

设计完成一个行编辑器:能够接受用户输入,能倒退删除,插入,移动光标等。

2.开启、关闭回显和缓冲

想要实现行编辑器的功能,就得解决两个问题:
一是在Linux命令行的默认模式下,输入一个字符就会回显在屏幕上,但是行编辑器不能让每个字符都输出在屏幕上,有些键是要作为功能键来使用的,所以必须关闭回显设置,让我们自己来设计有选择的输出字符。
二是在Linux命令行的默认模式下,输入字符后必须按回车才能把缓冲区字符送到程序那里去执行,所以必须开启立即响应模式,只要用户按下了键就立刻响应一个功能。
以下是具体代码:
我把开启、关闭回显和缓冲封装成为了一个函数void fun_set(struct termios *info, char set);
set设置为0就是关闭,设置为1就是打开。
在执行循环代码之前打开,循环代码之后关闭,这样就不会影响terminal的正常使用了。

/* 设计完成一个行编辑器
 * 开启、关闭回显和缓冲
 */
#include   <stdio.h>
#include   <termios.h>

#define oops(s, x) { perror(s); exit(x); }

void fun_set(struct termios *info, char set);//设置回显位,设置缓冲

int main()
{
    int c;

    struct termios info;
    fun_set( &info, 0 );//关掉回显位,关掉缓冲

    while( ( c=getchar() ) != EOF )
    {
        break;
    }

    fun_set( &info, 1 );//打开回显位,打开缓冲
}

void fun_set(struct termios *info, char set)
{
    if ( tcgetattr(0, info) == -1 )          /* get attribs   */
        oops("tcgettattr", 1);
    /*set为1,打开回显位,打开缓冲;set为0,关掉回显位,关掉缓冲*/
    if( set )
    {
        (*info).c_lflag |= ECHO;    /* turn on bit   */
        (*info).c_lflag &= ICANON;  /* turn on bit   */
    }
    else
    {
        (*info).c_lflag &= ~ECHO;   /* turn off bit   */
        (*info).c_lflag &= ~ICANON; /* turn off bit   */
    }
    if ( tcsetattr(0, TCSANOW, info) == -1 ) /* set attribs    */
        oops("tcsetattr",2);
}

3.显示大小写字母和数字

想要显示大小写字母和数字,只需要在while函数中判断一下是不是大小写字母和数字,是的话就输出在屏幕上,不是的话就跳过。

while( ( c=getchar() ) != EOF )
{
    if( isalnum(c) )
    {
        //isalnum()函数:如果c是一个数字或字母返回非0值,否则为0
        //user input a letter or a number
        putchar(c);
    }
}

isalnum()函数判断是不是一个数字或字母,然后通过putchar打印出字符即可。
在这里可以通过一个数组来保存这些输出的字符,这样当你在之后想保存到文件中去的时候也会更方便。

char str[30];//保存输出的字符
int p = 0;//当前位置

while( ( c=getchar() ) != EOF )
{
    if( isalnum(c) )
    {
        //isalnum()函数:如果c是一个数字或字母返回非0值,否则为0
        //user input a letter or a number
        putchar(c);
        str[p++] = c;
    }
}

4.实现退格键

退格键的实现其实是非常简单的。
当你按下退格键的时候,程序实际上执行了三条指令:
1.putchar(\'\\b\');//将光标左移
2.putchar(\' \');//输出一个空字符
3.putchar(\'\\b\');//将光标左移

什么意思呢?其实就是先将光标移动到左边,然后输出一个空字符将原来的字符覆盖掉,这样就相当于把左边的字符删掉了,但是此时光标还停留在空字符的右边。
比如原来是输入了HelloWorld:

然后执行putchar(\'\\b\');putchar(\' \');就变成了:

这时候再执行一个putchar(\'\\b\');

将退格键封装成一个函数void fun_backspace():

void fun_backspace()
{
    //实现退格功能
    putchar(\'\\b\');
    putchar(\' \');
    putchar(\'\\b\');
}

因为退格键的ASCII码为十六进制的0x7f,所以while循环里代码为:

while( ( c=getchar() ) != EOF )
{
    if( isalnum(c) )
    {
        //isalnum()函数:如果c是一个数字或字母返回非0值,否则为0
        //user input a letter or a number
        putchar(c);
        str[p++] = c;
    }
    else if( c == 0x7f )
    {
        //退格键(user input a backspace)
        fun_backspace();
        str[p--] = \'\\0\';
    }
}

这样就实现了退格键的功能,但是程序还有一个小bug:就是当所有字符都删完了之后,你还是可以按退格键。虽然这时候退格键已经没有删任何字符了,但是在之后还是会带来一些小的隐患。
所以为了解决这个问题,我们引入字符数组的长度len,让len记录字符数组的长度,当len大于0的时候,可以执行退格键

char str[30];//保存输出的字符
int p = 0;//当前位置
int len = 0;//总长度

while( ( c=getchar() ) != EOF )
{
    if( isalnum(c) )
    {
        //isalnum()函数:如果c是一个数字或字母返回非0值,否则为0
        //user input a letter or a number
        putchar(c);
        str[p++] = c;
        len++;
    }
    else if( c == 0x7f )
    {
        //退格键(user input a backspace)
        if( len > 0 )
        {
            fun_backspace();
            str[p--] = \' \';
            len--;
        }
    }
}

可以简化为:

else if( c == 0x7f && len )
{
    //退格键(user input a backspace)
    fun_backspace();
    str[p--] = \' \';
    len--;
}

5.实现光标左右移动

因为方向键上下左右在实际测试中发现,其实是由三个字符组成的,实现起来需要一些特殊的方法。所以在这里先用\'[{\'键来实现\'←\'(也就是左键),用\'→\'键来实现\'->\'(也就是右键)。
之前实现退格键的时候,发现可以使用putchar(\'\\b\');来实现光标左移,这样也就直接实现了左键的功能。
那么如何实现光标右移呢?我想到的办法是将当前所在位置的值重新输出一遍。比如说:Hello,此时光标在最左边的H上,那么我要右移,那就在这个位置上将H再输出一遍,这样也就间接实现了光标的右移。

上代码:

else if ( c == \'[\' && p )
{
    //使用\'[\'进行左移光标
    putchar(\'\\b\');
    p--;
}
else if ( c == \']\' && p < len )
{
    //使用\']\'进行右移光标
    //将当前位置的值再输出一遍
    putchar(str[p++]);
}

这里用p来代表当前光标所在的位置。
同样,当光标已经移到最左边的时候,就不执行左移键了;当光标已经移到最右边的时候,也不执行右移键了。
到这里,也就实现了左移右移光标的功能了。
但是!如果就到这里结束了的话,那么程序会有很大的bug!比如说当你移动光标到HelloWorld的W上的时候:

再执行退格键,那么就会发现是这样的:

你会发现删除了一个字符后整个显示的字符数组都会乱掉。
如果在这个时候还输入一些大小写字母或者数字,你就会发现情况会变得更乱。

那么怎么解决这个问题呢?
那就是分别在显示字符的地方和实现退格的地方加入一些代码来实现这些功能。
首先在while循环外面定义int i, j;来辅助我们工作。然后是处理输出大小写字母和数字的函数:

if( isalnum(c) )
{
    //isalnum()函数:如果c是一个数字或字母返回非0值,否则为0
    //user input a letter or a number
    //1.将当前位置之后的值依次后移
    j = ++len;
    while( j-- > p )
        str[j] = str[j-1];
    str[p] = c;
    j = len - p - 1;//光标要移动的距离
    //2.从当前位置开始重新输出数组
    while( p < len )
        putchar(str[p++]);
    //3.将光标移动到之前的位置
    while( j-- > 0 && p-- )
        putchar(\'\\b\');
}

然后是解决中间退格的问题:

else if( c == 0x7f && p )
{
    //退格键(user input a backspace)
    j = len - p;//光标要移动的距离
    //1.将当前位置之后的值依次前移
    putchar(\'\\b\');
    while( p < len )
    {
        str[p-1] = str[p];
        putchar(str[p]);
        p++;
    }
    //2.将最后一个元素删除
    putchar(\' \');
    putchar(\'\\b\');
    len--;
    p--;
    //3.将光标移动到之前的位置
    while( j-- > 0 && p-- )
        putchar(\'\\b\');
}

6.实现Del键删除整行

实现Del键删除整行的思路其实很简单:
首先将光标移动到尾部,然后只要len大于0,就循环执行退格键。这样就能够将整行删除了。
PS:Del键的ASCII码为十六进制的0x7e,所以while循环里代码为:

else if ( c == 0x7e && len )
{
    //删除键(Del):删除整行(user input delete)
    //1.从光标处移动到结尾
    while( ++p <= len )
        putchar(\' \');
    //2.从结尾往前依次退格
    while( --p )
        fun_backspace();
    //3.len置0
    len = 0;
}

7.总代码

/* 设计完成一个行编辑器
 * 能够接受用户输入,能倒退删除,插入,移动光标等
 */
#include   <stdio.h>
#include   <termios.h>

#define oops(s, x) { perror(s); exit(x); }

void fun_set(struct termios *info, char set);//设置回显位,设置缓冲
void fun_backspace();//实现退格功能

int main()
{
    int c;
    int i, j;

    struct termios info;
    fun_set( &info, 0 );//关掉回显位,关掉缓冲
    
    char str[30];//保存输出的字符
    int p = 0;//当前位置
    int len = 0;//总长度

    while( ( c=getchar() ) != EOF )
    {
        if( isalnum(c) )
        {
            //isalnum()函数:如果c是一个数字或字母返回非0值,否则为0
            //user input a letter or a number
            //1.将当前位置之后的值依次后移
            j = ++len;
            while( j-- > p )
                str[j] = str[j-1];
            str[p] = c;
            j = len - p - 1;//光标要移动的距离
            //2.从当前位置开始重新输出数组
            while( p < len )
                putchar(str[p++]);
            //3.将光标移动到之前的位置
            while( j-- > 0 && p-- )
                putchar(\'\\b\');
        }
        else if( c == 0x7f && p )
        {
            //退格键(user input a backspace)
            j = len - p;//光标要移动的距离
            //1.将当前位置之后的值依次前移
            putchar(\'\\b\');
            while( p < len )
            {
                str[p-1] = str[p];
                putchar(str[p]);
                p++;
            }
            //2.将最后一个元素删除
            putchar(\' \');
            putchar(\'\\b\');
            len--;
            p--;
            //3.将光标移动到之前的位置
            while( j-- > 0 && p-- )
                putchar(\'\\b\');
        }
        else if ( c == \'[\' && p )
        {
            //使用\'[\'进行左移光标
            putchar(\'\\b\');
            p--;
        }
        else if ( c == \']\' && p < len )
        {
            //使用\']\'进行右移光标
            //将当前位置的值再输出一遍
            putchar(str[p++]);
        }
        else if ( c == 0x7e && len )
        {
            //删除键(Del):删除整行(user input delete)
            //1.从光标处移动到结尾
            while( ++p <= len )
                putchar(\' \');
            //2.从结尾往前依次退格
            while( --p )
                fun_backspace();
            //3.len置0
            len = 0;
        }
    }

    fun_set( &info, 1 );//打开回显位,打开缓冲
}

void fun_set(struct termios *info, char set)
{
    if ( tcgetattr(0, info) == -1 )          /* get attribs   */
        oops("tcgettattr", 1);
    /*set为1,打开回显位,打开缓冲;set为0,关掉回显位,关掉缓冲*/
    if( set )
    {
        (*info).c_lflag |= ECHO;    /* turn on bit   */
        (*info).c_lflag &= ICANON;  /* turn on bit   */
    }
    else
    {
        (*info).c_lflag &= ~ECHO;   /* turn off bit   */
        (*info).c_lflag &= ~ICANON; /* turn off bit   */
    }
    if ( tcsetattr(0, TCSANOW, info) == -1 ) /* set attribs    */
        oops("tcsetattr",2);
}

void fun_backspace()
{
    putchar(\'\\b\');
    putchar(\' \');
    putchar(\'\\b\');
}

以上是关于实现一个简单的行编辑器的主要内容,如果未能解决你的问题,请参考以下文章

简单的方法来分享/讨论/协作的代码片段?

Notepad++编辑器——Verilog代码片段直接编译

angularJS使用ocLazyLoad实现js延迟加载

代码片段 - Golang 实现简单的 Web 服务器

文本编辑器 - 偏移数据结构的行

Azure 机器人微软Azure Bot 编辑器系列 : 机器人/用户提问回答模式,机器人从API获取响应并组织答案 (The Bot Framework Composer tutorial(代码片段