首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
136 阅读
2
nvim番外之将配置的插件管理器更新为lazy
98 阅读
3
2018总结与2019规划
69 阅读
4
从零开始配置 vim(15)——状态栏配置
58 阅读
5
PDF标准详解(五)——图形状态
46 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
elisp
文本编辑器
Java
读书笔记
反汇编
OLEDB
数据库编程
数据结构
Masimaro
累计撰写
331
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
104
篇与
的结果
2024-12-28
Emacs折腾日记(六)——elisp字符与字符串类型
本文相关的知识点主要来自 elisp 简明教程 后续内容可以直接查看这个教程上一节我们了解了elisp中基础数据类型之一的数字类型,相比于C/C++ 来说elisp的数字类型更少,学习起来可能也更加简单。那么这篇我们来学习另一个数据类型——字符串字符串的基本介绍回忆以下在C/C++中学到的关于字符的知识,字符采用char 来表示,它占一个字节,里面存储的是各个字符的编码。当然针对汉字或者其他东亚文字,一个char 可能表达不了,它会用 2个或者3个字节来表示一个汉字。后来又有unicode字符,和wchar_t 类型。而字符串则是以0结尾的字符数组。C/C++中经常会出现这样的经典考题char* pszStr = "Hello, World"; char szBuf[] = "Hello, World";它们分别占几个字节,它的考点主要有两个,第一就是指针类型存储的就是地址,它与具体的机器结构有关x86机器上占4个字节。第二个考点就是字符串里面藏了一个0作为字符串的结尾char szBuf[] = "Hello\0Word";这样的字符串虽然可以表达出来,但是我们通过 strlen 之类的函数,得到的结果却是5。因为遇到0就结束了。elisp中的字符串与C/C++中最大的不同就是elisp中字符串可以有 0。另外一个不同就是elisp中没有字符类型,字符串中每一个字符都是字符的unicode形式,用C/C++类比就是字符串通过GetAt 之类的函数返回的是字符的unicode整数值。当然严格意义上来说C/C++中的字符类型也是一个整数值。elisp中可以使用?A 这样的形式来表示一个A字符。最终得到的结果就是A的ASCII码 65,我们可以使用之前学到的数字类型检测函数来判断它得到的是不是整数类型(integerp ?A) ; ⇒ t对于一些标点符号或者有歧义的字符,可以使用 \ 进行转义,例如?\' ?\" ?\\对于一些没有歧义的标点符号加不加转义字符没有影响,但是为了美观或者同一或者说为了不增加记忆的负担,标点符号统一使用转义字符。另外,我们可以在字符串中使用10进制、八进制、16进制的形式来表示字符,例如(setq msg "\x68\x65\x6C\x6C\x6F\x2C\x20\x77\x6F\x72\x6C\x64") ; ⇒ "hello, world"使用其他进制的写法如下:十进制:使用 \d + 数字(不常用,主要用十六进制和八进制)。十六进制:使用 \x + 两位十六进制数字。八进制:使用 \0 + 三位八进制数字。字符串函数首先我们可以使用 length 来获取长度,它有点类似与 Python中的len 函数,它不光可以获取字符串类型的长度,还可以获取列表、向量等类型的长度例如(setq msg "hello, world") (length msg) ;; ⇒ 12我们在前面说过,字符串可以带上0,我们来测试一下有0的情况下,得到的长度如何(setq msg "hello\x00world") (length msg); ;; ==> 11因为elisp并不以0作为字符串的结尾,实际上elisp字符串是以向量的形式存储的,向量中每个元素都是一个整数,所以这里返回的仍然是字符向量的大小。我们可以使用 stringp 来测试一个变量是否为字符串。例如(setq msg "hello") (stringp msg) ; ⇒ t (stringp ?A) ; ⇒ nil我们也可以使用 string-or-null-p 函数来检测,顾名思义,它主要用来判断当前变量是否为字符串或者是一个nil(char-or-string-p msg) ; ⇒ t (char-or-string-p ?A) ; ⇒ t (char-or-string-p nil) ; ⇒ nil (char-or-string-p "") ; ⇒ t但是遗憾的是 elisp 中没有判断字符串是否为空的方法,我们只能自己写代码来实现(defun string-emptyp (str) (and (stringp str) (zerop (length str))))这个函数判断当前传入对象是否是字符串,并且字符串长度为0。构造函数可以使用make-string 函数来构造一个字符串,它构造一个里面都是同样字符的字符串,例如(make-string 5 ?A) ; ⇒ "AAAAA"如果想要构造一个不同字符构成的字符串,可以使用 string(string ?A ?B ?C) ; ⇒ "ABC"也可以使用 substring 和 concat 来产生一个新的字符串,前者从字符串中取子串,后者连接两个字符串。substring 接受一个字符串和两个整数,表示一个范围,是一个前开后闭的区间,也就是包含前面的范围不包含后面的范围。字符串的索引也是从0开始。(substring "Hello, World" 3 5) ; ⇒ "lo"也可以只包含一个起始位置,表示从这个位置开始往后的字符(substring "Hello, World" 3) ; ⇒ "lo, World"也可以传入负数,与Python中的索引类似,负数表示从右往左数,但是注意,最右边的字符是-1,因为0表示最左边的数(substring "Hello, World" -5 -3) ; ⇒ "Wo"concat 就相对比较容易理解一些,它就是将两个字符串合并成一个新串(concat "hello" ", world") ; ⇒ "hello, world"与C/C++类似的是,字符串定义之后无法更改,需要更改的话,它的做法是创建一个新的字符串,并且舍弃掉原来的字符串。所以这里将 substring、concat 这种取子串和连接字符串的函数也归类到构造函数中,因为它们的的确确构造了一个新的字符串。字符串比较在C/C++中,比较字符串时使用 strcmp 函数。我们根据它的返回值来决定字符串是大于小于或者等于。在比较的时候从左往右,依次比较它们的编码值,直到遇到不一样的值。它仅仅比较编码值,而不关心字符串长度,只有在前面的字符都相等的时候才会根据长度判断。相信各位在学习C/C++的时候都亲手实现过strcmp函数,这里就不展开了。elisp中字符串的比较函数就比较多了。char-equal 比较两个字符是否相等,默认情况下它会忽略大小写,例如(char-equal ?A ?a) ; ⇒ t如果要大小写敏感的话就不能用这个函数了,那么大小写敏感的时候该怎么比较呢?这个时候千万别犯迷糊,字符本身就是一个整数,完全可以使用 = 或者 eql直接判断(eql ?A 65) ; ⇒ t (= ?A 65); ⇒ t判断字符串是否相等我们可以使用 string= 或者 string-equal 。它们二者是等价的,是同一个函数的不同叫法而已。(setq foo "hello") (setq bar "hello") (string= foo bar) ; ⇒ t使用字典顺序来比较字符串大小使用的是 string< 或者 string-lessp 。与前面类似,它们也是等价的。它的判断逻辑与 strcmp 函数相同。(setq foo "Hello") (setq bar "hello world") (string-lessp foo bar) ; ⇒ t (setq foo "Hello") (setq bar "Hello") (string-lessp foo bar) ; ⇒ nil比较遗憾的是没有 string> 这样的比较。如果想要判断是否大于的话,需要判断不相等并且不小于。根据 string< 的比较逻辑来看,空串是最小的字符串,也就是任意非空字符串都比空串大,因此上面判断是否是空串的代码可以使用这一特性实现(defun string-emptyp (str) (and (stringp str) (not (string< "" str))))不知道各位是否还记得Java String类中有 Equal 函数和 == 来比较字符串。其中Equal来比较字符串内容是否相等,而 == 仅仅比较对象的地址是否相等。elisp中同样有这样的操作,我们使用 eq 来代替 == 判断对象本身是否相等,对于简单类型也就是数字类型,我们使用它来判断数字是否相等,而对于字符串、列表、向量这种复杂类型时,判断它们的地址是否相等。(setq foo ?A) (setq bar ?A) (eq foo bar) ; ⇒ t (setq foo "A") (setq bar "A") (eq foo bar) ; ⇒ nil字符串转化下面来介绍一些字符串和数字类型相互转换的函数我们可以使用 char-to-string 来将一个整数转换成字符串,或者使用 string-to-char 来将字符串转化为整数,当然这个函数只会返回第一个字符的整数值。例如(char-to-string 65) ;; ⇒ "A" (string-to-char "Hello world") ;; ⇒ 72使用string-to-char 只能获取字符串中第一个字符的值,如果我们要取字符串中任意位置的字符该怎么办呢?我们可以使用substring 来获取以对应位置开始的一个子串,然后获取这个子串的第一个字符(defun get-string-char (str index) (string-to-char (substring str index))) (get-string-char "Hello World" 5) ; ⇒ 32或者我们可以也使用 aref 函数,该函数用来取数组中任意位置的值,因为字符串也是一个数组,因此我们可以使用该函数来取字符串中任意位置的字符。例如(aref "Hello World" 5) ;; ==> 32另外我们可以将数字转化为对应的字符串或者将数字字符串转化为对应的整数。它们的功能类似于C/C++ 中的 atoi 和 itoa 函数。string-to-number 用来将字符串转化为数字,它可以支持从2到16进制的转化,例如(string-to-number "ff" 16) ;; ⇒ 255 (string-to-number "A1") ;; ⇒ 0, 默认以10进制进行转化 (string-to-number "10" 2) ;; ⇒ 2number-to-string 用于将数字转化为字符串,它只支持以10进制的形式转化(number-to-string 256) ; ⇒ "256" (number-to-string ?A) ;; ⇒ "65"如果想以任意进制来将数字转化为字符串,那么可以使用 format 函数,它类似于C/C++中sprintf。用来格式化字符串,但是它只支持8进制10进制和16进制的转换(format "%d" 256) ;; ⇒ "256" (format "%#o" 256) ;; => "0400" (format "%#x" 256) ;; ⇒ "0x100"要转化成二进制的话,没有现成的函数可以用,不过我们可以自己实现,相信学过C/C++的应该写过类似的算法,不过我记得当初我学的算法是先入栈再出栈,下面的代码也是采用类似的方式。通过取模最先算出来的在最低位,所以我们在连接字符串的时候将计算的结果放到前面,连接上之前计算的结果(defun number-to-binary (num) (if (= num 0) "0" (let ((binary "")) (while (> num 0) (setq binary (concat (number-to-string (mod num 2)) binary)) (setq num (/ num 2))) binary))) (number-to-binary 256) ;; ⇒ 100000000emacs-lisp 简明教程 中还介绍了其他类型的数据结构与字符串互相转化的函数,这里就不介绍了,等后面学到了再说。另外字符串还有一些大小写转换的函数。使用 downcase 将字符串中的字母都转换为小写字母,使用 upcase 转换为大写字母。例如(downcase "Hello World") ;; ⇒ "hello world" (upcase "Hello World") ;; ⇒ "HELLO WORLD" (downcase "你好,世界") ;; ⇒ "你好,世界" (upcase "αβγ") ;; ⇒ ""ΑΒΓ""这种字母文字它可以进行大小写转换,但是对于中文这种没有大小写字母的就不存在转化了。使用 upcase-initials 来将字符串中每个单词的第一个字符大写,其余的字符它会忽略它。(upcase-initials "hellO woRld") ;; ⇒ "HellO WoRd"函数 captialize 会将字符串每个单词首字母大写,其余的转化成小写(capitalize "hellO wORLD") ;; ⇒ "Hello World"查找与替换字符串最重要的操作还是查找和替换。elisp 中查找主要使用 string-match 。它使用正则表达式来进行查找。elisp中没有C/C++中find 那样查询子串的缩进的函数。查询子串其实也可以利用正则表达式来处理(string-match "Emacs Lisp" "This is Emacs Lisp Program") ;; ⇒ 8该函数的第一个参数是一个正则表达式,上面代码中我们直接使用字符串进行匹配,就是在精准的匹配子串。它返回子串开始的索引。它还可以接收一个数字,表示从字符串的第几个字符开始往后进行查找,这个参数的作用有点像 String.Find(int index) 这个重载函数中 index 的含义。(string-match "Emacs Lisp" "This is Emacs Lisp Program" 10) ;; ⇒ nil但是如果查找的子串中有特殊符号的话,就不能这么使用(string-match "2*" "232*3=696") ;; ⇒ 0这里因为 * 被当成正则表达式的模糊匹配符号,它表示任意一个字符。如果想要它单纯的作为普通符号,可以使用regexp-quote 来处理一下,它的作用是将字符串中的所有特殊字符转义,使其可以安全地作为正则表达式使用。这样可以确保字符串的内容被视为字面量,而不是正则表达式中的元字符。(string-match (regexp-quote "2*") "232*3=696") ; => 2不知道各位读者使用过 C/C++ 中的正则表达式没有,正则表达式匹配之后会产生一个结果对象,它包含了所有匹配上的位置。我们可以通过循环或者其他方式来得到这个位置,并且得到具体匹配上的结果。在elisp中,结果被保存在 match-data 中。它允许你获取最近一次正则表达式匹配的位置信息,包括匹配的起始和结束位置。我个人的感觉它有点像Win32 API中的GetLastError 一样,每次调用其他API结果都会被覆盖,每次调用只能得到上一次的错误码。(progn (string-match "3\\(4\\)" "01234567890123456789") (match-data)) ;; ⇒ (3 5 4 5)我们使用 3(4) 这样的正则表达式来进行匹配。这里括号表示一个捕获组,它匹配34或者单独匹配4。match-data中每对数组表示一个匹配组的起始和结束位置,它是一个前闭后开区间。也就是包括前面的,不包括后面的。上面的代码先匹配 34,发现它在第3个字符处出现,所以它返回第一组数据 (3 5),然后匹配4,它出现在第4个字符,所以产生第二组数据 (4 5)。按照这个思路,如果将正则表达式修改一下改为 3\\(4\\)\\(5\\) 将会产生3组数据,第一组匹配 345 ,第二组匹配 4 ,第三组匹配 5。最后的结果就是 (3 6 4 5 5 6)在上面说到 match-data 中每对数组表示一个匹配组的起始和结束位置。我们可以使用 match-beginning 和 match-end 来获取这两个数据,它需要一个整数作为参数,用来表示取第几组的数据,例如下面的代码(progn (string-match "3\\(4\\)" "01234567890123456789") (message "%d" (match-beginning 0)) ;; => 3 (message "%d" (match-end 0)) ;; ⇒ 5 (message "%d" (match-beginning 1)) ;; ⇒ 4 (message "%d" (match-end 1))) ;; ⇒ 5如果我们给出的参数超过了匹配组的大小,那么它们将会返回 nil ,例如如果上述匹配我们使用 (match-beginning 2) 来取第三组的结果的话,将会得到nil每一个匹配组都是一个半开半闭区间,因为 match-end 是匹配到的字符位置的下一个位置,所以使用它很容易进行循环。上述的代码在匹配的时候,只要是匹配到就停止了,我们可以写一个循环用来继续匹配后面的字符串(let ((start 0)) (while (string-match "34" "01234567890123456789" start) (message "find at %d\n" (match-beginning 0)) (setq start (match-end 0))))掌握了查找的相关操作之后,我们继续来学习有关替换的操作。我们可以使用 replace-match 来将匹配到的字符串替换成指定的字符串,它的函数原型如下(replace-match NEWTEXT &optional FIXEDCASE LITERAL STRING SUBEXP)我们主要需要关注的是 NEWTEXT 它代表的是我们希望用哪个字符串来替换匹配上的字符串,STRING 表示希望进行匹配的字符串,例如(let ((str "hello world 123")) (string-match "\\([0-9]\\)" str) (replace-match "#" nil nil str)) ;; ⇒ "hello world #23"我们也可以使用循环来将所有数字都替换成 #(let ((str "hello world 123") (start 0)) (while (string-match "\\([0-9]\\)" str start) (setq str (replace-match "#" nil nil str)) (setq start (match-end 0))) str) ;; ⇒ "hello world ###"需要注意的是,这里每次执行 replace-match 后都返回一个新的字符串,原来老的字符串保持不变,所以我们这里每次替换之后都手动的使用 setq 来对老的字符串进行赋值,然后再重新匹配。这样才能保证最后的结果是我们想要的结果。我们可以使用下面的代码来验证这一点(let* ((str "hello world 123") (start 0) (final-str str)) (while (string-match "\\([0-9]\\)" str start) (setq final-str (replace-match "#" nil nil str)) (setq start (match-end 0))) final-str) ;; ⇒ "hello world 12#"这里只替换了最后一个数字,这是因为执行 replace-match 之后的str 变量并没有被改变,每次都是之前的再进行匹配,唯一变化的只有 start 的值。最后一次我们仍然在使用原始的 str 字符串在进行匹配,它匹配到 3。所以最后一次替换只将3替换成了 #。我们也可以使用捕获组进行替换,例如我们想将 "hello world 123" 替换成 "123 hello world",那么可以使用如下代码(let ((str "hello world 123")) (string-match "\\(hello world\\) \\([0-9]+\\)" str) (replace-match "\\2 \\1" nil nil str)) ;; ⇒ "123 hello world"这里使用 \\2 \\1 来表示替换的新字符串,它表示的含义是匹配组里面的第二组结果+空格+第一组结果。它们组成了一个新串用来替换原来的字符串。replace-match 的最后一个参数表示替换哪一个捕获组,默认是0,例如上述的代码中,第0个捕获组就是整个字符串,所以它替换了整个字符串,我们可以将上述代码做一下修改(let ((str "hello world 123")) (string-match "\\(hello world\\) \\([0-9]+\\)" str) (replace-match "\\2 \\1" nil nil str 1)) ;; ⇒ "123 hello world 123"它将第一匹配组也就是 hello world 使用新字符串 123 hello world 来替换,然后加上原来剩下的字符串,最终也就得到结果 123 hello world 123到这里,已经将字符串替换的常见操作都做了一些说明。本节的内容也就到此结束了。后面将继续按照 教程来学习。敬请期待!当然了,如果各位读者觉得我的这一系列教程有抄袭的嫌疑,或者质量不如原版,又或者更新缓慢,请按照对应的链接来学习相关内容。
2024年12月28日
13 阅读
0 评论
1 点赞
2024-12-21
Emacs 折腾日记(五)——elisp 数字类型
本文是参考 emacs lisp 简明教程 写的,很多东西都是照搬里面的内容,如果各位读者觉得本文没有这篇教程优秀或者有抄袭嫌疑、又或者觉得我更新比较慢、再或者其他什么原因,请直接阅读上述链接中的教程。上一篇我们讲了elisp中的流程控制结构相关的内容,下面就该进入到对应数据结构的学习了。elisp中主要的数据结构有:整数、浮点数、列表、符号、向量、散列表等等类型。下面我们先从最简单的类型——整数和浮点数说起数字类型与C/C++对比起来,elisp数字类型少的多,C/C++ 整数类型就有好几种,包括有符号、无符号、int、short、long之类的。elisp不区分这些,它仅仅有整数和浮点数。而且elisp 中只有浮点数这一种小数类型,不像C/C++还有双精度浮点数和单精度浮点数之分。elisp的整数范围与具体的机器有关,它的范围可以通过变量 most-positive-fixnum 和 most-negative-fixnum 来得到。例如在我的机器上它们的值如下most-positive-fixnum ;; 2305843009213693951 most-negative-fixnum ;; -2305843009213693952在给变量使用数字类型赋值的时候,我们可以使用10进制或者其他任意进制的形式。例如#b101100 => 44 ; 二进制 #o54 => 44 ; 八进制 #x2c => 44 ; 十进制 #24r1k => 44 ; 二十四进制因为26个英文字母+10个数字的原因,我们最大只能使用36进制来表示一个数字,但是基本不用到这么大的。日常最多也就用用10进制、二进制、16进制、8进制都算用的少。浮点数的表达遵循 IEEE 标准,也就是可以使用带小数点的数字来表示,或者带上 e 来使用科学计数法,例如3.14 1.0e-10数字类型的测试作为动态类型的语言,在代码执行阶段,变量的类型是会发生变化的。我们无法仅通过变量名或者变量的初始化值来判断变量类型。emacs的变量在执行阶段都知道自己的类型,但是它无法主动向我们报告,我们需要使用一些函数来进行判断,关于数字类型,提供了下列的函数integerp floatp numberp从字面上能理解它们分别判断是否是整形、浮点数、以及数字类型。elisp 测试函数一般都是用 p 来结尾,p 是 predicate 的第一个字母。如果函数名是一个单词,通常只是在这个单词后加一个 p,如果是多个单词,一般是加 -p数的比较与C/C++ 类似,数字的比较一般有 >、<、>=、<= 。但是也有不同的地方,因为elisp中都是使用 setq 来进行赋值的,所以它采用 = 来表示数学意义上的相等。还有一个不同的地方因为elisp中没有 += 、-=、/= 、*= 这样的运算符,所以它使用 /= 来作为不等的判断符号与其他语言类似的,浮点数直接使用等于或者不等于来判断并不准确,需要在一定范围内忽略误差。在C/C++中,我们常见的写法是给定一个误差值,然后二者差的绝对值在这个误差值范围内则认为它们相等。我们将这个算法使用elisp改写一下就得到下面的代码(defun approx-equal (x y) (let ((fuzz-factor 1.0e-6)) (< (abs (- x y)) fuzz-factor))) (approx-equal 1.000001 1.00000000000000001) ;; => t上述的写法并不严谨,在一定误差范围内,它是对的,但是在某些情况下它就不对了,例如 1.0e-7 和 1.0e-12 。它们本身并不相等,但是它们都超过了这个误差范围,相减之后的值小于这个误差范围。但是我们看到其实它们直接的差距还是挺大的,间隔1.0e5 的数量积。我们可以将上述算法进行一些改进(defun approx-equal(x y) (let ((fuzz-factor 1.0e-6)) (or (and (= x 0) (= y 0)) (< (/ (abs (- x y)) (max (abs x) (abs y))) fuzz-factor)))) (approx-equal 1.0e-7 1.0e-12) ;; => t这段代码采用的是比较相对差距的办法。因为涉及到除法,所以先把二者等于0的情况排除了,避免发生除0的问题。上述代码改造成对应的C代码就是#define FUZZ_FACTOR 1.0e-6 // 定义误差范围 bool approx_equal(double x, double y) { // 处理特殊情况:如果两个数都是 0 if (x == 0 && y == 0) { return true; } // 计算相对差并进行比较 double relative_difference = fabs(x - y) / fmax(fabs(x), fabs(y)); return relative_difference < FUZZ_FACTOR; }另外 elisp 中有 eql 函数来判断两个数是否相等(eql 1 1.0) ;; => nil (eql 1.0e-7 1.0e-12) ;; => nileql 在判断数字时不光判断值,也判断类型。第一条语句,因为二者类型不同,第二条语句二者都是float属于同类型,但是二者的值不同,因此两个结果都是假。数字的转换elisp 中可以进行 整形和float型数字的相互转换。在C/C++ 中,整形可以通过隐式转换自动转换成float,而float转换成int时会丢失小数位,比如哪怕是 1.9 在转换为整数时也会是 1。在elisp中,可以通过float将整数转化为浮点数。例如(floatp 1) ; ⇒ nil (floatp (float 1)) ; ⇒ t (eql (float 1) 1.0) ; ⇒ t而浮点数转化成整数有下面几个函数truncate: 抹除小数位,也就是C/C++语言中float转int的操作floor: 类似于C/C++ 中的floor 函数,返回小于等于该数的最大整数ceiling: 类似于 C/C++ 中的 ceil 函数,返回大于等于该数的最小整数round: 类似于 C/C++ 中的 round 函数,返回四舍五入后的整数数的运算一般的语言,数的运算无外乎 +、-、*、/ 取整、取模。elisp 中同样有这些操作,前面的加减乘除跟其他语言一致,没什么特别的。C/C++ 以及 elisp 中的除法都不是纯粹数学意义上的除法,它会将结果抹掉小数位转换成整数。我们如果将除数或者被除数转换为float类型的话,那么就得到数学意义上的除法结果 (当然也不全是,毕竟float数据有表达数据的限制)但是python 不一样,它就是纯粹数学意义上的除法。这个设计我也不知道算是好还是不好,毕竟它与其他语言不一致增加了记忆的负担。(/ 3 2) ; ⇒ 1 (/ (float 3) 2) ; ⇒ 1.5 (/ 3.0 2) ; ⇒ 1.5C/C++ 中有 ++ 、 -- 操作,而且还分 前++ 和 后++ 。在 elisp 中没有这两个操作,也没有类似于 += 的操作。elisp的赋值一直是用的 setq。而且它提供了 1+ 1- 这两个符号来表示 ++ 和 --。至于是前 ++ 还是 后++ 呢?两个都不是,C/C++中的 ++ 本身具有改变变量值的作用,它们的区别在于是返回值之前改变还是之后改变。而elisp 主要使用 setq 来改变变量的值, 1+ 这个操作无法改变变量的,它仅仅改变这条语句返回的值。例如可以使用下面的代码来测试(defun inc (num) (1+ num)) (setq foo (inc 3)) ;; ⇒ 4这里将传入的参数加了1,但是其实函数中 num 的值并没有变化,我们可以对函数做一下修改来验证这一点(defun inc (num) (progn (1+ num) num)) (setq foo (inc 3)) ; ⇒ 3要改变变量的值需要使用 setq 来进行赋值,这个函数可以做一下修改(defun inc (num) (progn (setq num (1+ num)) num)) (setq foo (inc 3)) ; ⇒ 4取模的操作,elisp 中提供了两个方式 % 和 mod 函数,其中 % 与其他语言类似,它要求除数与被除数都是整数,而 mod 则没有这个要求。我们查看mod函数,发现它是被写在C代码里面的。它虽然也是取余,但是它与数学意义上取余的结果并不一致,例如(mod -10 3) ;; ⇒ 2 (mod 10 -3) ;; => -2 (% -10 3) ;; ⇒ -1 (% 10 -3) ;; ⇒ 1% 单纯的就是数学意义上的取模的操作,首先找到商,然后根据商来决定模而 mod 则不同,mod 中首先一个原则就是余数和除数的符号相同。所以第一个的结果应该是正数 也就是 -3 * 4 + 2 = 10,余数是2。第二个结果应该是 - 3 * (-4) - 2 = 10 mod 还有一个原则,那就是商的结果应该是整数。利用这两个原则我们就可以大概的还原一下计算的过程(mod 3.5 2) ;; ⇒ 1.5 (mod -3.5 2) ;; ⇒ 0.5 (mod 3.5 -2) ;; ⇒ -0.5根据上面两个原则,那么它们分别可以还原为1 * 2 + 1.5 = 3.5-2 * 2 + 0.5 = -3.5-2 * (-2) + 0.5 = 3.5另外还有一些其他数学上的操作,对于学习后面写配置的话,大多数应该是用不到的。后续需要使用的话再查询就好了,这里就不在多啰嗦了。到此为止我们已经介绍完了elisp中数的常见操作。后续将陆续介绍其他数据类型,敬请期待。
2024年12月21日
15 阅读
0 评论
1 点赞
2024-12-17
Emacs折腾日记(四)——elisp控制结构
目前我们接着学习elisp相关语法,这里我是按照 elisp 简明教程 来进行学习。与其说这是我自己写得教程到不如说是在这个上面做得注释。目前我不知道这样是否侵犯相关的知识产权。目前就先这样继续学习,继续写记录吧。闲话少说,进入本篇的正题,关于elisp的控制结构。一般编程语言都有三种控制结构:顺序结构、条件结构、循环结构。elisp同样有这三种控制结构。顺序结构和复合语句一般默认elisp的语句是顺序执行的,例如下面的代码(setq name "Emacs") (message "hello, %s" name)它先执行前面的 setq 语句,先给变量name 定义并赋值为 Emacs 。后面接着执行第二行代码,调用message 函数来输出一段文字。在其他语言一般都有一个复合语句。它是有多个语句共同组成的,例如 C/C++中使用{} 来将多个语句整合成一条复合语句。针对C/C++ 我们在很多地方会用到复合语句。例如如果 if , while 等语句后只需要一条语句,那么可以直接使用一条语句,例如下面的代码// 这么写代码不太正规但是符合语法规范,也能编译过 int main() { int i = 0; while(i++ < 10) printf("%d\n", i); //打印1到10,这么10个数字 return(0); }但是如果在循环或者if条件成立后,执行多条语句,就需要使用复合语句,也就是用大括号括起来。那么在elisp中也有这样的操作,在条件和循环语句中需要执行不止一条语句,也需要使用复合语句。elisp 中符合语句使用 progn 来包含一组语句组成复合语句,它的语法规则是(progn statement1 statement2 ... statement3)例如我们将上面的代码用 progn 包装一下(progn (setq name "Emacs") (message "hello, %s" name)) ;; => "hello, Emacs"使用 progn 包装的复核语句可以使用 C-x C-e 也就是 eval-last-sexp 来同时执行里面的两个子语句。如果我们将它们分开写,则使用 eval-last-sexp 做不到这点,它只能一条条的执行# 条件语句 我们使用 if 和 cond来表示条件分支,if的语法如下(if condition then else)需要注意的是 这里的 then 和 else 并不是关键字,而是对应的语句,也就说紧跟着if条件的语句表示条件成立时执行的代码,下一条则是条件不成立时执行的代码。例如我们使用下面的代码来获取两个数的最大值(defun get-max(a b) (if (> a b) a b)) (get-max 3 4) ; => 4与 C/C++ 的函数不同,elisp 函数的返回值不需要使用 return 或者其他的关键字特意指出,它是将函数最后执行的语句的返回值作为函数的返回值,这里当 a > b 时条件成立,执行 a 然后结束函数,也就是这个时候函数的最后一个语句是 a ,函数返回 a 的值。否则执行 b ,此时函数的最后执行的语句就是 b ,这个时候函数就返回 b 的值而 cond 有点像 C/C++ 中的 switch ,它的语法如下(cond (case1 do-when-case1) (case2 do-when-case2) ... (t do-when-none-meet))它的语法特点是,它与 switch 类似,由一堆 case 和 default 组成。每个case 都使用一对 () 来区分,最后可以使用 t 来表示未匹配到前面的 case 时执行的语句,类似于default语句。这里我们使用当初学习C/C++ switch 语法时的经典代码来作为示例(defun score-report (score) (cond ((>= score 90) "优秀") ((>= score 80) "良好") ((>= score 60) "及格") (t "不及格"))) (score-report 75); => 及格我们可以看到,cond 语句的使用比 switch 更为的灵活,switch case 只能进行整型变量的相等比较,而 cond 可以进行其他变量类型的不同形式的条件判断,它只是在形式上更像 switch,但是在使用的范围上更像 if-else if-else。另外 elisp 简明教程中 提供了一个使用 cond 计算 斐波那契数列的例子(defun fib(n) (cond ((= n 0) 0) ((= n 1) 1) (t (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ; => 55因为 elisp 中使用 setq 来进行赋值操作,所以它里面的= 就是数学意义上比较相等的操作符,而 其他语言中的 == 在lisp中无效。这里如果写成 == 将会报错。上面的例子也很好理解 当 n 等于 0时返回0,等于 1 时返回1,否则返回 fib(n - 1) + fib(n - 2) 使用 C/C++ 的话可能更容易理解int fib(int n) { if(i == 0) return 0; else if (i == 1) return 1; else return fib(n - 1) + fib (n - 2) }循环结构循环使用 while 关键字,它的语法结构如下(while condition body)我们可以将上述循环打印的C代码使用 elisp 实现(setq i 0) (while (< i 10) (progn (message "%d" i) (setq i (+ i 1))))我们执行完代码之后使用 switch-buffer,切换到 *message* ,可以看到它打印了从0到9的数据。上面的斐波那契数列的例子我们可以使用 while 来实现(defun fib (n) (cond ((= n 0) 0) ((= n 1) 1) (t (let ((first 1) (second 1) (third 1)) (setq n (- n 2)) (while (> n 0) (progn (setq third (+ first second)) (setq first second) (setq second third) (setq n (- n 1)))) third)))) (fib 10) ; => 55因为 elisp 中没有提供 += ++ 这样算术运算符,所以我们需要使用 setq 来赋值。下面还有一个计算阶乘的例子(defun factorial (n) (let ((res 1)) (while (> n 1) (setq res (* res n)) (setq n (- n 1))) res)) (factorial 10) ; => 3628800我们也可以提供一个递归的版本(defun factorial (n) (if (= n 1) 1 (* (factorial (- n 1)) n))) (factorial 10) ; => 3628800到此为止,本篇就结束了。本篇涉及到的elisp 代码其实也不算复杂,如果能熟练掌握一门编程语言的话,到此为止的代码应该不算太难理解。在编写这些示例代码的时候我觉得还好,主要注意括号的匹配,算法什么的就是照搬C/C++中一些经典写法就差不多了。但是即使上面的代码并不多,代码量并不大,我也能明显感觉到上述代码在阅读上不那么直观。
2024年12月17日
14 阅读
0 评论
0 点赞
2024-12-11
Emacs折腾日记(三)——简单的elisp 入门
Emacs本身的使用并不复杂,利用帮助文档,差不多半小时左右就能把一些常见的操作方式和快捷键过一遍,剩下的就是慢慢使用并且熟悉了。Emacs真正有价值的是它高度的客制化。任何人都可以利用elisp代码将Emacs改造成只属于自己的编辑器。会elisp 的不一定是高手,但是高手没有一个是不会elisp的。学习Emacs也绕不开elisp。下面我们就来简单的学点elisp一个简单的 Hello Word(message "hello world")这是一个简单的elisp版本的hello world程序。麻雀虽小但是五脏俱全。从这个简单的程序来说我们可以看出lisp的最大特点,就是以括号作为作为一个完整的表达式。曾今有一个段子说苏联的特工冒死偷到了美国阿波罗计划代码的最后一页,结果回来一看全是括号。这也鲜明的表达了一个lisp的特点,那就是大量的括号我们简单的解析一下上面的代码,上面的代码调用了一个函数,函数的参数就是一个字符串的 hello world。我们可以打开Emacs,进入 scratch buffer,输入上述代码。之后可以将光标移动到代码尾部,按下快捷键 C-x C-e 来执行代码,或者使用 M-x 输入命令 eval-buffer 来看效果。我们可以看到,mini-buffer 位置出现了 "hello world" 的字符串变量我们可以使用 setq 来定义变量,它类似于C/C++ 中的= ,用来给变量赋值或者定义并初始化一个变量。例如我们可以改一下上述的代码(setq name "Emacs") (message "hello, %s" name)将上述代码输入到scratch buffer之后就需要依次在每行的最后执行 C-x C-e 或者直接执行eval-buffer 命令,这样就可以看到效果了上面我们说,elisp的表达式是使用括号来表示的,但是如果在上面代码的基础上加上一句(name)此时就会报错,显示的错误为 void-function name 。我们的本意是想让解释器返回name的值,但是解释器将它作为了一个函数。通过这个错误我们能了解到,elisp基本的表达式中函数需要用括号括起来,但是变量自己本身被作为一个完整的表达式。除了使用setq我们还可以使用 defvar 来定义变量,defvar 的使用如下(defvar variable-name value "variable document")例如(defvar name "Emacs" "a defvar demo name") name ;; ==> "Emacs"我们将光标放到name上,按下 C-h v 可以看到关于name的说明文档。需要注意的是,defvar与setq 除了defvar可以指定变量的说明文档外,还有一个区别就是defvar在定义变量前,这个变量已经有值的话,defvar不会改变变量的值例如下面的例子(setq foo "foo") (defvar foo "this is foo" "document for variable foo") (defvar bar "this is bar" "document for variable bar") foo ;; =>"foo" bar ;; =>"this is bar"C-x C-e(eval-last-sexp) treatsdefvarexpressions specially. Normally, evaluating adefvarexpression does nothing if the variable it defines already has a value. But this command unconditionally resets the variable to the initial value specified by thedefvar; this is convenient for debugging Emacs Lisp programs.defcustomanddeffaceexpressions are treated similarly. Note the other commands documented in this section, excepteval-defun, do not have this special feature.上述英文翻译过来就是 eval-last-sexp 对 defvar 做了特殊处理,默认情况下 defvar 在变量有值的情况下不做任何操作,但是在这个命令中,defvar 会无条件的将变量值重置为它指定的值。主要是为了方便调试代码。同时 defcustom 和 deface 做了同样的操作。请注意在本节中记录的其他命令(eval-defun 除外)没有做这样的处理这就解释了为什么我们使用 C-x C-e 执行的时候 foo 的值发生了改变函数函数的定义与使用作为一门函数式编程语言,函数是elisp的一等公民。如何使用一个函数我们已经在前面的hello world程序中见识过了,那么如何定义一个函数呢?定义一个函数使用 defun 关键字。它的语法如下(defun func-name(args) "document string" body)第一行代表一个函数名和函数的参数列表,第二行表示可以使用一个双引号包含函数的说明文档,Emacs是一个自文档的系统,后续我们可以查看这里写的文档。最后一行是函数的主体内容,例如下面的例子(defun say-hello(name) "say hello to define user" (message "hello, %s" name)) (say-hello "emacs") ;; => "hello, emacs"执行将会输出对应的信息。我们将光标移动到say-hello这个函数上,执行 C-h f 默认回车将会得到我们针对函数写的文档lambda 表达式其实像C++、Java、Python 之类的语言也有lambda表达式,它就是一个匿名函数。一般我们使用函数都是先定义同时给函数取一个名字,但是有时候我们仅仅需要一个临时函数作为参数或者仅仅只会在某些地方调用一次,这个时候就可以使用匿名函数。lambda表达式的形式与defun类似,它的使用规则如下(lambda (args) "document string" body)除了关键字变了,就是不用写函数名称了。我们使用 funcall 来调用一个lambda表达式(funcall (lambda (name) (message "hello, %s" name)) "emacs")我们执行它将会在mini-buffer中看到显示的字符串信息另外我们也可以将一个lambda表达式赋值给一个变量,最后通过funcall 来调用(setq say-hello (lambda (name) (message "hello, %s" name))) (funcall say-hello "emacs") (say-hello "emacs") ;; error, void-function say-hello知乎的大牛指出,这里原本有一个错误,在这里更正:defun 会把函数值绑定到符号的 function-cell 上,setq 会绑定到符号的 value-cell 上上述的说法,我查过 funcall 、value-cell 以及 function-cell 相关的文档,里面涉及到的一些知识点比较复杂,目前我还没有完全搞明白,而且把它贴出来作为入门来讲有点过于复杂了。变量作用域elisp默认使用 setq 定义的变量不管是在函数内还是函数外全都是全局变量,它们的作用域是全局作用域,例如(defun say-hello () (setq name "Emacs") (message "hello, %s" name)) (say-hello) ;;需要执行一下函数解释器才能执行到定义`name`变量的位置 (message name)一般来说代码如果都是全局变量的话,会给代码的编写和维护带来很大的不便。elisp中同样支持定义局部变量,我们可以使用 let 和 let* 它们的用法类似(let (bindings) body)其中的 bindings 可以是单个值,也可以是括号包裹的键值对。如果是单个值,则默认赋值nil,也就是空。如果是键值对,则将值赋值给对应的键。let定义的变量只能作用在let语句块内,例如下面一个计算圆面积的函数,这里知乎的大牛告诉我,pi 是emacs中的内置变量,我采用PI来定义圆周率(defun circle-area (radix) (let ((PI 3.1415926) area) (setq area (* PI radix radix)) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3) (message "%f" area) ;; error void-variable area其中我们在let语句块中定义了两个变量,pi初始化为3.1415926,area 初始化为 nil同样的,可以使用 let* 改写上面的程序(defun circle-area (radix) (let* ((PI 3.1415926) area) (setq area (* PI radix radix)) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3)let* 与 let 的区别在于,let* 可以在binding时候使用前面已经定义过的变量。例如上面的代码可以改写成(defun circle-area (radix) (let* ((PI 3.1415926) (area (* PI radix radix))) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3)我们使用 let 来改写一下这个程序,发现它会报错(defun circle-area (radix) (let ((PI 3.1415926) (area (* PI radix radix))) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3) ;; error void-variable PI好了本篇也该结束了,本篇主要了解了基本的elisp语法,下面进行一下总结:使用 defvar 和 setq 来定义全局变量,其中defvar可以给变量设置一个说明文档,我们使用 C-h v 来查看这个文档使用 let 和 let 来定义变量,其中 let 可以在变量定义的时候使用前面定义过的变量使用 defun 来定义函数使用lambda 来定义一个lambda表达式,使用funcall 来调用lambda,lambda可以赋值给变量,后续使用funcall来调用
2024年12月11日
8 阅读
0 评论
1 点赞
2024-12-07
Emacs 折腾日记(二)——Emacs简单入门
环境 准备这里我们根据之前博客 配置的wsl2+archlinux环境,安装Emacssudo pacman -S emacs如果之前配置的关于gui的部分正确,那么在终端输入 emacs 来启动 或者在Windows的开始菜单中能找到emacs(Arch) 之类的启动项目。当然你也可以使用 emacs -nw 来开始一个终端的emacs程序Emacs 主要界面展示打开之后,映入眼帘的是一个比较丑陋原始的充满历史感的软件,看着都有点让人倒胃口我们能不能将Emacs改造的漂亮一点呢?记住在Emacs上永远不要问能不能,而应该问怎么做。Emacs上没有不可能上面的图中,我将Emacs的各个区域都给标记了一下,很好理解,一般的软件都有这些部分。中间显示的是欢迎信息,欢迎界面中这些链接都是Emacs的帮助信息,都可以点进入看。而下面的状态栏显示了一些基本的信息。最下面的部分我们称之为mini-buffer,会显示一些运行信息我们可以使用q 来退出欢迎界面,进入该界面需要注意下面几个地方软件中间位置已经出现了输入的光标,我们可以在里面输入内容下面有一个 scratch 的字样,它表示当前处于scratch buffer (草稿),顾名思义,它就是一个输入临时内容的地方。它没有绑定任何的文件后面的 Lisp Interaction 和 ElDoc表示当前的mode,会加载一些有关mode的配置,例如lispmode会加载一些有关lisp的高亮等至于状态栏每个字符都代表什么意思,这里就不解释了,后面我们希望对Emacs进行一些美化,美化完成之后就看不到这些信息了。Emacs 的一些概念介绍在学习玩Vim之后,Emacs 的一些概念就变得很好理解了,很多东西都根vim是相似的bufferVim 中的buffer与文件有关联,将来可以保存在文件中,但是Emacs的buffer除了包括vim中buffer相关的内容之外,也有一些其他类型的buffer,目前我知道的有:minibuffer: 界面最下方的一栏,主要显示当前状态和一些其他内容,后续可以对它进行一些定制,可以显示很多有用的信息scratch buffer: 顾名思义,就是草稿箱的概念,用来输入一些临时的内容Message buffer: 用来显示一些信息,例如message 函数的内容modemode,模式。Emacs中一个十分重要的概念,一般来说buffer都会有mode加载在其上,给buffer提供额外的支持。mode 分为两种, major mode 和 minor mode。一般一个buffer只能有一个major mode,但是可以有多个minor mode。每个mode都有一些额外的特征和快捷键绑定,与vim中的文件类型有点类似。不知道小伙伴还记不记得,我们可以针对vim中的文件类型,给每个类型指定额外的快捷键和语法高亮等信息。同样的Emacs的mode也具有这些功能。例如 elisp mode 里面有语法高亮、快速执行lisp代码的一些快捷键。另外也可以做到同样的功能在不同的mode中体现出不一样的效果。相比于vim的文本类型来说,mode的方式更加的灵活,我们可以将一些在不同文件类型中同样的功能抽出来,放到一个mode中,然后在需要的时候以minor mode的形式加载到buffer中。windowwindow的概念与vim中的相同,都是同时显示多个buffer。frame相比vim来说,Emacs多出来了一个frame的概念。上述emacs 的界面就是一个frame,每创建一个frame,就会多出来一个上面的带有标题栏的窗口。相信各位读者也看出来了,只有gui的Emacs可以创建多个frame。一般来说我们使用到window就已经足够了,特别是要进行全键盘操作的时候,多个frame反而是一个负担Emacs 的重要快捷键介绍快捷键之前,我们先来介绍一下Emacs相关的键位。Ctrl:ctrl键,一般简写为CMeta: 一般是Windows键盘布局中的 Alt 键盘,简写为 MSupper: 一般是WIndows键盘布局中的Win 键,简写为S因为vim利用不同的模式来映射快捷键,所以vim能用较少的按键实现很多的功能,而Emacs本身不具备mode的功能,所以它需要很多前缀来将不同的功能进行映射。对于一个vim转Emacs的用户来说,Emacs的快捷键统统不重要,第一,后续肯定会将它改造成vim的按键模式。主要是相比于vim来说,Emacs的文本操作快捷键太长了,没有vim那么简洁。第二,没有人会去特意记忆那么多快捷键,以我使用vim的经验来说,都是慢慢用,慢慢形成肌肉记忆的。作为一个初学者一上来就告诉他,需要记住这些快捷键,很容易就把人吓跑了。但是Emacs毕竟不同与vim,我们还是需要记忆一些特别重要的快捷键快捷键功能C-h f查看函数的帮助信息C-h v查看变量的帮助信息C-f k查看快捷键的帮助信息M-x执行命令前三个主要是查看各种帮助信息,Emacs有一个特点叫自文档,就是它很多东西都自带文档,一切都可以通过查询文档来了解。所以在使用和熟悉的过程中,我们离不开前三个快捷键。它可以互相查,例如忘记了某个功能的快捷键,可以使用 C-h f 输入对应的函数就可以找到它对应的快捷键。或者忘记了某个快捷键绑定到哪个命令了,可以使用 C-h k来查看关于快捷键绑定命令的信息。对于最后一个快捷键,一般来说Emacs的功能都绑定到了一个命令或者说一个函数上,即使我们忘记了快捷键,也可以通过M-x 输入命令来完成操作,有些时候配合自动补全,输入命令不比使用快捷键慢。vim与Emacs的很多地方都是相通的,学习了vim之后,Emacs的很多概念都不需要细说,很快就能上手用了,但是想要用好,用出个性来,还是要对elisp有一定的了解,后面可以会陆续介绍一些elsip的知识。
2024年12月07日
13 阅读
0 评论
1 点赞
2024-12-05
emacs 折腾日记(一)——序言
初次知道emacs这个东西是在《程序员的呐喊》这本书。书中的作者提倡学习编译原理,推崇emacs。现在距离我知道emacs已经过去了快8年,期间不断的重复学习——放弃——学习的路子。与过去学习vim类似,vim我也经历过放弃到学习,最后有项目需要使用Vim在Linux上开发,没办法慢慢学会了它的操作,以及一些简单的配置。后来我写了vim的操作和配置,我发现这样做很有用首先就是在写vim操作的时候,相关的概念和操作技巧我又重新梳理了一遍,加深了它的印象,过去一些不怎么用的操作我发现在之后我用的越来越顺手。其次就是我目前使用的配置在出错或者需要做修改的时候根据之前的文章我又梳理了一遍,很方便我回顾之前的思路能快速定位到每个模块当初的思路以及一些被遗忘的知识点。既然之前的教程对我有这么大的帮助,那么我想这次学习emacs就采用这种策略。首先日常记笔记,记录工作的日志之类的操作就从markdown转化到emacs的org-mode中以便熟悉emacs的日常操作。另外通过写博客的方式记笔记,记录自己做的一些配置以及一些操作技巧。日后用来回顾也好或者仅仅为了加深印象也好都可以。另外我想这一系列既然是折腾日记,而且我还是emacs的小白,那么这一系列的文章自然是不成体系的,基本上是想到什么就写什么。如果看这一系列的文章的读者存在问题我可能也没有能力解答。在这里给读者说一声抱歉。emacs 是什么任何技术或者工具我都喜欢从这几个方面先了解一下,再考虑是不是值得学。首先一个问题就是“是是什么”,其次是“为什么”,最后是“怎么做”。我想现在就来说说我对这几个问题的理解首先Emacs是什么呢?Emacs 其实并不特指某一款编辑器,而是一个文本编辑器大家族的统称。最初由理查德·斯托曼于1975年在MIT协同盖伊·史提尔二世共同完成。这一创意的灵感来源于TECO宏编辑器TECMAC和TMACS。这里我们提到Emacs主要特指 GNU EMacs,它是由理查德·斯托曼于1984年开始开发,至今已经有40多年的历史。它的核心理念官网上是这么介绍的An extensible, customizable, free/libre text editor — and more它是一个 可扩展、可定制、自由的文本编辑器,并且远不止于此。EMacs 是自由软件,它没有对用户做出任何限制,包括内核代码完全是开放的。你可以在此基础之上进行任何改动。同时它提供了一个完成的lisp解释器,随着这些年的发展,它的插件系统已经特别丰富了,利用插件几乎可以实现任何功能。有人戏称Emacs是一个伪装成文本编辑器的操作系统,在Emacs上几乎无所不能。emacs 是否值得学我个人认为Emacs还是很值的学习的。说说我学习Emacs的理由:它是一个与vim齐名的编辑器,因为它的插件丰富,结构设计良好,它几乎可以做到vim能做的所有事情,包括vim那一套操作逻辑它使用lisp作为扩展语言,我目前掌握了面向过程的语言C,面向对象的语言 C++/Python,但是还从来没有完整的学习过函数式编程语言,而且《黑客与画家》的作者也推荐lisp语言。平时写普通的脚本我都是用shell 或者 python 就搞定了,没什么几乎接触到lisp的实战,借这个机会学习一下lisp并且用于实战,也是一个不错的选择emacs 的 orgmode很吸引我,它的功能比markdown要丰富,借助Emacs这个平台能提供非常棒的体验,包括但不限于写博客,做计划,管理日程,甚至能在里面运行代码如何学下面是一张网上流传的图,各个编辑器的难易程度虽然有点夸张,但是Emacs的学习过程并不算简单。目前我也没有好的学习路径,而且这一系列的文章只是我的尝试。目前我也没有掌握Emacs,甚至是一个小白。我想以写文章的方式来慢慢学习关于Emac的内容网上有很多Emacs的教程,最经典应该要数这篇 一年成为Emacs高手但是我之前尝试过使用高手的配置,总是用着不是那么舒服,总想要折腾出一套属于自己的配置,后面慢慢的就放弃了。这次我想慢慢的根据其他人的教程折腾出一套属于自己的配置,然后通过使用 org-mode 来写博客、记笔记、并且进行日程管理,后续慢慢的将写脚本之类的简单开发工作转移到Emacs上,通过折腾配置并且多用Emacs来学习。祝我好运吧。
2024年12月05日
16 阅读
0 评论
0 点赞
2024-12-05
在wsl2中安装archlinux
在之前的博客中,我介绍了如何在虚拟机或者真实机上安装archlinux并且进行一定的配置,但是实际上Linux不管怎么配置在日常使用中都没有Windows简单便利,在开发有关Linux的程序时过去用虚拟机或者直接在Windows上使用ssh在远程服务器上进行开发。但是微软发布了wsl以及后续更新的wsl2,可以很方便的实现在Windows中拥有两个系统,并且两个系统可以进行互联。在不改变Windows操作习惯的基础之上操作Linux。进一步提升了Linux的便捷易用性。晚上的教程大多数都是使用wsl安装Ubuntu的,作为一个铁archlinux党,我不太用得惯Ubuntu,那么就要想办法安装自己习惯的arch,好在在网上有现成的教程。终于完成了这一工作准备工作安装wsl2,需要Windows 11或者Windows 10 的19041 及更高版本。因此如果系统版本不够需要提前进行系统的更新操作我们需要在Windows功能中开启 "适用于Linux的Windows子系统",如下图所示勾选之后可能需要重启。或者也可以在应用商店进行安装。这里就不得不吐槽一下微软的应用商店了,它居然能做到挂梯子和不挂梯子一样卡。我这里死活登录不上,就不演示这种方式了安装完成之后可以在powershell中查看它的版本wsl -v从图上可以看到,我们已经安装上了wsl2安装 archlinux安装完wsl2 之后,我们可以在GitHub官网上下载最新的ArchWSL。这里我们要下载两个东西,一个是不带online 标识的appx文件以及同名的cer文件。下载完成之后双击 .cer 文件,点击“安装证书”,选择“本地计算机”,在下一个页面中选择“将所有的证书都放入下列存储”,点击“浏览”,选择“受信任的根证书颁发机构”,执行安装。证书安装完成之后,我们双击下载的appx 文件,直接点击安装appx文件安装完成之后,可以使用wsl --list来查看当前wsl中的Linux子系统此时已经有了对应的系统了我们在命令行输入 arch 即可进入archlinux子系统,此时是以root的身份进入的配置arch配置普通用户新系统安装之后的第一件事就是创建一个普通用户,并且永远以这个普通用户进行登录,在需要的时候使用 sudo 来申请某些管理员权限进行操作我们在之前安装archlinux的教程中已经提到过对应的操作方式useradd -m -G wheel -s /bin/bash arch我们创建一个名为 arch 的用户,并指定shell为 bash接下来我们使用passwd arch passwd root来设置root和arch两个用户的密码。因为archlinux中安装的编辑器是vim,所以我们先将vim设置一个名为vi的别名ln -sf /usr/bin/vim /usr/bin/vi然后 使用visudo 将文件中 #%whell ALL=(ALL) ALL 这行的注释去掉,以便当前用户能够使用 sudo 命令接着可以使用su arch 将当前用户切换到arch。并且使用命令sudo pacman -Syyu来更新系统,同时测试一下输入用户密码之后能否执行一些root命令之后我们推出 arch 子系统,在powershell中执行Arch.exe config --default-user arch来指定默认使用arch 来登录系统设置完登录用户之后,在powershell中输入 arch 进入archlinux子系统中,此时我们发现,登录用户已经变成arch了初始化密钥环和更新源接着我们执行以下命令,来初始化密钥环sudo pacman-key --init sudo pacman-key --populate sudo pacman -Syy archlinux-keyring sudo pacman -Su鉴于目前国内的网络环境,上述几条命令有可能执行出问题或者卡着不动,我们可以进行换源的操作,这里我采用清华源在/etc/pacman.d/mirrorlist 文件的最顶端添加Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch开启32位软件库支持与ArchLinuxCN库的支持按照之前博客中的内容,我们先打开 32位软件的支持,在 /etc/pacman.conf 中去掉[multilib]一节中两行的注释,来开启 32 位库支持。然后在该文件的结尾处加入下面的文字,来开启 ArchLinuxCN 源[archlinuxcn] Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch之后通过以下命令安装 archlinuxcn-keyring 包导入 GPG key。pacman -Sy archlinuxcn-keyring具体的使用可以看镜像的官方文档如果报错可以看官网或者看我博客的办法是否能解决上述命令都成功之后使用 sudo pacman -S yay 来安装 yay成功之后我们可以试着安装一下neofetchyay -S neofetch安装成功之后,执行neofetch 就可以看到系统信息了配置终端基础内容安装完成之后,我们可以对终端进行一下美化和配置,毕竟日常使用上大部分时间都是直接在终端中使用,有一个漂亮点的终端用起来也舒服一点我们首先安装一下必要的组件sudo pacman -S net-tools man-db man-pages man-pages-zh_cn texinfo ntfs-3g tree pacman-contrib neofetch wget git usbutils pciutils acpi base-devel接着我们安装一下相应的字体sudo pacman -S adobe-source-han-serif-cn-fonts wqy-zenhei sudo pacman -S noto-fonts-cjk noto-fonts-emoji noto-fonts-extra ## 这里我把官方推荐的所有带unicode标识的全装上了,这样后续就不太会出现乱码的情况了 yay -S ttf-ubraille ttf-symbola otf-cm-unicode ttf-arphic-ukai ttf-arphic-uming ttf-dejavu gnu-free-fonts ttf-google-fonts-git nerd-fonts-complete ttf-hack ttf-joypixels默认的bash 功能比较弱,我们采用zshsudo pacman -S zsh然后将默认的shell 设置为 zshchsh -l # 列出系统中存在的所有shell chsh -s /bin/zsh #根据上一个命令得到的shell路径,设置当前shell我们推出系统再次登录的时候,shell已经切换到zsh了,我们按照提示生成一个默认的 .zshrc 文件即可为了支持终端上显示一些图形,我们需要安装 nerd-font 。可以在GitHub上找到 nerd-font可以通过git clone https://github.com/ryanoasis/nerd-fonts.git --depth=1来克隆,也可以通过在宿主机上下载zip包,然后传入archlinux 中。我们可以直接在Windows的文件资源管理器上输入 \\wsl$ 来访问archlinux的文件系统在获取项目之后,在项目的根目录执行 sh ./install.sh 来执行安装需要注意的是,如果你是通过本地的windows terminal 来ssh到远程archlinux化,本地也需要安装 nerd-font 因为这个时候显示的责任在宿主机的终端程序。需要宿主机本身也有那些字体。Windows上可以使用 .\install.ps1 来安装字体准备好后,我们可以使用 powerlevel10k 来美化终端git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k echo 'source ~/powerlevel10k/powerlevel10k.zsh-theme' >>~/.zshrc source ~/.zshrc接着按照它的提示选择样式即可,如果不满意了,可以在 .zshrc 中对应的配置,直接重新选择样式即可。完成之后,可以看到终端相对来说比较好看了接下来我们对zsh 进行一些简单的配置安装 zsh-autosuggestions ,它是一个命令提示插件,当你输入命令时,会自动推测你可能需要输入的命令,按下右键可以快速采用建议git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions在 .zshrc 中添加下列代码source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh安装完成之后效果如下我们可以按TAB自动补全[zsh-syntax-highlighting]() 是一个命令语法校验插件,在输入命令的过程中,若指令不合法,则指令显示为红色,若指令合法就会显示为绿色。git clone https://github.com/zsh-users/zsh-syntax-highlighting.git echo "source ${(q-)PWD}/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" >> ${ZDOTDIR:-$HOME}/.zshrc安装完成之后效果如下autojump 是一个懒人神奇,有了它,当我们此前跳转过某个路径之后,可以很方便的跳转到之前路径。例如我们之前跳转到/usr/bin /home/arch那么我们可以直接使用 j b 跳转到 /usr/bin 中,或者在其他目录使用 j a 跳转到家目录中,如果跳转的目录路径有相似的字符,可能需要多输入一些字符来区分我们下面来安装它git clone https://github.com/wting/autojump.git cd autojump ./install.py安装好之后,它会提示我们需要手动将下列命令加入到 /zshrc 中,我们添加就好了thefuck 这也是一个懒人神奇,当命令输入错误的时候只要fuck一下就好了在archlinux中,我们可以直接使用sudo pacman -S thefuck我们将下列代码放入到 .zshrc 中eval $(thefuck --alias)安装完成之后,效果如下使用gui程序在Windows11和Windows 10 的内部版本在19045以上的时候,wsl本身就可以直接在打开Linux的gui程序。这里我们安装一个firefox作为测试例子sudo pacman -S firefox firefox-i18n-zh-cn我们启动firefox,这个时候报错了不要慌,这里我们需要链接wslg的接口套接字到x11sudo rm -r /tmp/.X11-unix ln -s /mnt/wslg/.X11-unix /tmp/.X11-unix可以将这段代码手动添加到 .zshrc 中,以便重启之后仍然能生效if ! [ -S /tmp/.X11-unix/X0 ]; then sudo ln -sf /mnt/wslg/.X11-unix/X0 /tmp/.X11-unix/X0 fi这个时候再打开就能在Windows上看到firefox的界面了还有一个惊喜,那就是我们可以从开始菜单中找到对应的Linux gui程序,并且直接点击就能打开。使用rdp连接除了这种使用方式,我们还可以使用rdp通过Windows的远程连接来进入archlinux子系统。这里我不太推荐这种方式,因为上面那种方式可以直接在Windows上使用linux gui程序,已经特别方便了,而且这种方式需要有桌面环境,需要消耗资源,但是当你的Windows版本不够的时候,可以考虑使用这种方式来使用gui程序这里我们先安装桌面环境sudo pacman -S xfce4 xfce4-goodies这里我采用轻量的xfce4,不管是使用kde 还是gnome,感觉都比较重接着我们下载rdp相关的软件yay -S xrdp xorgxrdp-glamor pulseaudio-module-xrdp根据 arch wiki 的说法,xrdp 仅支持使用XVNC作为后端,所以这里我们需要安装一下 vncsudo pacman -S tigervnc安装完成之后,需要在home目录下,添加一个.xinitrc 文件,这个文件会在rdp新建一个虚拟桌面的时候执行,文件的内容如下unset SESSION_MANAGER unset DBUS_SESSION_BUS_ADDRESS exec dbus-launch startxfce4最后我们启动一个rdp服务sudo systemctl start xrdp.service最后我们就可以使用Windows远程桌面连接上Linux的桌面了好了,配置到这里也就基本结束了,希望各位小伙伴在其中玩的愉快。总体来说Windows的wsl用起来还是很香的,特别是使用wslg服务在Windows中使用Linux的gui程序。它用起来就跟在Windows上开启一个gui程序一样丝滑,可以很好的将Linux程序融入到Windows的工作流中。
2024年12月05日
136 阅读
10 评论
0 点赞
2023-03-04
从零开始配置vim(32)——最后再说两句
很抱歉我决定结束这个系列的内容了。原本我打算介绍markdown、orgmode相关的配置,甚至还打算介绍如何在vim 中使用 emacs 的 org-agenda 来进行日常的任务管理。但是出于一些原因我打算放弃了。首先如果将markdown 理解为另一种类似于HTML 的标记语言的话,我们在介绍LSP 的时候已经介绍过该如何新增新的编程语言的支持,再另外介绍Markdown 的配置就显得多余了。而且本系列也并不打算事无巨细的带领大家从零开始配置一套完整的配置,我仅仅希望通过这一系列的内容介绍一下vimscript 或者lua 接口以及vim 的一些特性,让大家看完之后又能力自行动手弄出一套属于自己的配置。至于orgmode 的内容,我发现目前还没有任何插件能完美的模拟emacs 的orgmode 功能。vim 上的插件也仅仅能做到渲染样式,语法高亮而已。也就没有必要单独介绍了。如果后续我能掌握 emacs 的话,再来介绍也不迟总之就是本系列到此结束了。一些建议不知道各位小伙伴在跟着我这一系列文章尝试自己配置vim 的时候有什么感觉?我当初在整理这些配置的时候发现它越来越像vs code ,甚至最近几年新推出的LSP以及 DAP 的一些插件几乎都是原生的用于 vscode 上的或者从它上面移植过来的。有些主题也是照搬 vscode 的。我们发现自己费劲心力终于将vim 变成的 vscode 。有没有觉得在做无用功?既然要将它变成 vscode 那为何不直接使用 vscode 呢?可能有人会说, vscode 对于vim的一些模式和 ex 命令的支持并不好。我想这就是我们使用 vim 的理由,也是vim 比其他编辑器强的地方。我们仅仅是在使用工具而已,哪个工具好用,哪个工具能帮助我们快速完成工作,那就用哪个。工具本身没有高低贵贱之分,只有合适与否的差异。作为程序员要拥抱新技术,千万不要抱着某个技术某个工具不放。也不要觉得用vim 的比用 vscode 或者其他编辑器的高级,就高人一等。vim自身也在吸收其他技术不断的成长,例如它从 vscode 那边学来了LSP 和 DAP 。这就有点像武侠小说中的吸功大法,集万物所长为我所用。另外一条建议就是千万不要拿我给出的配置直接来进行使用。这一套配置仅仅是为了教学使用,很多地方没有进行深度定制,并且基本采用白话的写法,完全不考虑封装性和程序设计,另外我也没有考虑通用性,很多小伙伴评论出现了各种各样的问题,最后就是它的效率也不算高。我也不希望自己的文章仅仅给各位小伙伴提供了一套配置。我更希望小伙伴们能通过这一系列文章学到一点东西,从这套配置中衍生出一套适合自己的内容。若干年以后,各位小伙伴在对vim有更深的理解回过头来看到这套配置时可能发出这样的声音:“这是什么破烂配置,连 xxx 的支持都没有;有些功能有时候会报错,我看看把它改好;启动时间咋这么慢,我能把它优化到xx毫秒;现在还在用xx技术早就落伍了,看我把它改成用xx技术”。(我自认为本系列最有价值的是开始配置之前,vim相关特性的介绍)最后的一条建议就是,如果各位小伙伴未来将长时间使用vim 进行代码的编写和日常的开发。那么我推荐使用一些社区比较活跃的第三方通用配置,例如我最近在使用的lunarVim。使用这类的配置有一些好处:不用费力折腾配置,节约时间学习高手的配置,提升自己对编辑器的审美。就像没学习vim之前我一直觉得使用编辑器用鼠标选中文本是天经地义的事,我习惯了它,甚至习惯了用鼠标翻页等操作,完全不知道这样有多么的浪费时间。通过高手配置可能能使你重新审视自己使用编辑器的习惯,从而找到一套真正适合自己的高效的文本操作术。社区活跃的话,除了问题不用自己死磕,可能有人能帮忙解决PS: 如果各位觉得我的教程不好或者有些内容没有提到,各位可以去看看lunarVim作者的另一个项目,Neovim-from-scratch 该项目也是从0开始配置vim,并且在油管上有对应的教学视频。后面的学习通过本系列的学习相信各位小伙伴已经有能力能看懂各种第三方配置的代码,能在此基础之上衍生出一套属于自己的配置。甚至能完全抛弃第三方配置独立弄出一套自己的配置。所以后面我推荐的学习路线就是:不断阅读vim官方手册熟练使用某一个第三方配置在熟练的基础之上根据自己的习惯来定制一些只属于自己的功能形成一套只属于自己的科学的、高效的文本操作习惯根据这套习惯尝试定制自己的配置在其他编辑器中通过一定的配置尝试复刻这一套科学而又高效的操作习惯目前我正在第三部分努力。希望本系列文章能带领大家真正入门vim ,不会再出现因为觉得难而中途放弃。最后祝愿各位小伙伴在vim的使用中能收获快乐,并坚持下去!
2023年03月04日
8 阅读
0 评论
1 点赞
2023-02-01
从零开始配置vim(31)——git 配置
很抱歉又拖更了这么久了,在这个新公司我想快速度过试用期,所以大部分的精力主要花在日常工作上面。但是这个系列还是得更新下去,平时只能抽有限的业余时间来准备。这就导致我写这些文章就慢了一些。废话不多说,咱们正式开始有关git相关的配置。这些配置都是根据我自身使用习惯来定义的,不一定符合各位的习惯,各位可以根据自身的习惯来调整gitsigns第一个要推荐的插件是 gitsigns。就像它的名字一样,该插件可以将最近的更改以标签的形式展现出来方便我们查看。我们可以使用这样的代码进行安装 use {'lewis6991/gitsigns.nvim' }。需要注意最新的版本需要 neovim 的版本在0.7 以上。安装完成之后我们通过配置 require('gitsigns').setup()来使用它。这样我们就可以通过 Gitsigns toggle_signs来打开或者关闭符号显示了。除了采用最基本的符号显示以外,它还可以对改变位置的行号进行标记以及高亮显示变更的行。这两个功能可以通过 Gitsigns toggle_numl和 Gitsigns toggle_linel来打开,打开之后显示如下:从图中可以看到,更改行行号被用绿色显示了出来并且更改行进行了高亮显示另外它还有其他的显示效果:它主要的一些显示功能主要有下面几个:toggle_signs: 显示变更记录toggle_numl: 显示变更行号toggle_linel:高亮变更的行toggle_delete: 显示被删除的行,以红色背景高亮显示toggle_word_diff: 在两行分别显示修改前和修改后的内容toggle_current_line_blame: 在对应行后面显示提交记录我们将所有的这些功能都打开将得到这么一个效果是不是看着有点乱?这是我修改了一处的,一旦修改多了看着会更混乱。所以我自己的经验告诉我在这个buffer里面最好是只打开 signs、numl、linel、current_line_blame的功能,其他的都关掉。也就是在当前buffer中只显示现在的代码,然后辅助以简单的符号来显示哪行是新加的,哪行被删除了,哪行被修改了,至于修改前是什么样子的,我可以通过其他方式来查阅。所有内容都在一个buffer 中显示会比较乱,不利于阅读代码。我们可以通过配置将我们要显示的内容进行定义,也可以定义使用何种图标来表示修改记录。我们采用如下的配置gitsigns.setup({ signs = { add = {hl = 'GitSignsAdd' , text = '+', numhl='GitSignsAddNr' , linehl='GitSignsAddLn'}, change = {hl = 'GitSignsChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn'}, delete = {hl = 'GitSignsDelete', text = '-', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn'}, topdelete = {hl = 'GitSignsDelete', text = '-', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn'}, untracked = {hl = 'GitSignsAdd', text = '+', numhl='GitSignsAddNr' , linehl='GitSignsAddLn'}, changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn'}, }, signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` numhl = true, -- Toggle with `:Gitsigns toggle_numhl` linehl = true, -- Toggle with `:Gitsigns toggle_linehl` word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` watch_gitdir = { interval = 1000, follow_files = true }, attach_to_untracked = true, current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` current_line_blame_opts = { virt_text = true, virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' delay = 1000, ignore_whitespace = false, }, current_line_blame_formatter = '<author>, <author_time:%Y-%m-%d> - <summary>', sign_priority = 6, update_debounce = 100, status_formatter = nil, -- Use default max_file_length = 40000, -- Disable if file is longer than this (in lines) preview_config = { -- Options passed to nvim_open_win border = 'single', style = 'minimal', relative = 'cursor', row = 0, col = 1 }, yadm = { enable = false }, })其中大部分都是官方给出的默认配置,我只是在此基础之上改了 signs在各种状态下显示的图标,以及显示哪些内容。主要是使用 + 来表示新增、| 来表示修改、- 表示删除。除了显示以外它有一个重要的功能就是在各种修改状态之间跳转,例如调用 next_hunk来跳转到下一个更改位置。并且它也集成了一些git的操作。我们对常用的操作定义一些快捷键on_attach = function() vim.api.nvim_set_keymap("n", "<leader>gj", "<cmd>Gitsigns next_hunk<CR>", {silent = true, noremap = true}) vim.api.nvim_set_keymap("n", "<leader>gk", "<Cmd>Gitsigns prev_hhunk<CR>", {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hs', ':Gitsigns stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('v', '<leader>hs', ':Gitsigns stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hr', ':Gitsigns reset_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('v', '<leader>hr', ':Gitsigns reset_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hS', '<cmd>Gitsigns stage_buffer<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hu', '<cmd>Gitsigns undo_stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hR', '<cmd>Gitsigns reset_buffer<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hp', '<cmd>Gitsigns preview_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hb', '<cmd>lua require"gitsigns".blame_line{full=true}<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>tb', '<cmd>Gitsigns toggle_current_line_blame<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hd', '<cmd>Gitsigns diffthis<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hD', '<cmd>lua require"gitsigns".diffthis("~")<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>td', '<cmd>Gitsigns toggle_deleted<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('o', 'ih', ':<C-U>Gitsigns select_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('x', 'ih', ':<C-U>Gitsigns select_hunk<CR>', {silent = true, noremap = true}) end这里是我将官方给出的配置直接粘贴了过来,这堆快捷键定义的操作中我最常用的就是 next_hunk和 prev_hunk来在各种修改之间进行跳转。虽然偶尔也用用 diffthis来显示差异,但这部分我更喜欢使用我接下来介绍的插件diffview这个插件从名字上看就知道是专门用来查看版本差异的插件。与前面介绍的 gitsigns插件相比它有下面几个优点:它是专门用来显示差异的,与gitsigns相比,显示的更加明显它可以在文件树中显示有变更的文件它可以做到任意版本之间的差异对比它可以显示单个文件的版本提交记录它还有另外的功能,可以由各位小伙伴根据官方文档自行了解。它的使用方式如下::DiffviewOpen显示当前与上一个版本之间的差异:DiffviewOpen + 版本号 可以显示当期与某一个特定版本的差异,例如 :DiffviewOpen HEAD~2或者:DiffviewOpen 906ddac317来查看版本差异● :DiffviewFileHistory + 文件名 来查看某个文件的版本差异因为它比较简单,具体的用法就不在这里演示了。关于它的配置,我自己认为平时很少使用,而且默认的配置已经够用了,也不太需要花精力为它优化快捷键了。lazygitlazygit是一个非常好用的git客户端,可以方便的进行提交、回滚、查看变更等git操作。这里我不推荐什么插件,因为它本身已经很强大了,而且脱离vim它也可以很好的工作。想来想去只有配置一个快捷键来快速打开 lazygit 的终端不知道各位小伙伴是否还记得之间介绍的 toggleterm 插件,我们将要依赖它来快速启动local lazygitterm = Terminal:new({ cmd = 'lazygit', direction = 'float' }) function lazygit_toggle() lazygitterm:toggle() end vim.api.nvim_set_keymap("n", "<leader>lg", "<Cmd>lua lazygit_toggle()<CR>", {noremap = true, silent = true})通过这样简单的配置我们已经可以使用 <leader>lg来快速启动 lazygit的客户端了。至此我们关于git的配置就完成了。一般我的使用习惯是使用 gitsigns来在更改中进行跳转,用于提交前或者合并分支前的代码审查,做到提交和合并都心中有数。在发生bug要回溯代码并且查看当前与没有问题的版本之间的差异会用到 diffview插件。在进行提交、合并、回溯等git相关操作时会使用到 lazygit。各位小伙伴也可以根据自己的使用习惯来定制这一部分的配置。
2023年02月01日
7 阅读
0 评论
0 点赞
2022-12-30
从零开始配置vim(30)——DAP的其他配置
很抱歉这么久才来更新这一系列,主要是来新公司还在试用期,我希望在试用期干出点事来,所以摸鱼的时间就少了。加上前面自己阳了休息了一段时间。在想起来更新就过去一个多月了。废话不多说了,让我们开始进入正题。在前一章,我们谈论了如何在 neovim 中使用cpptools 这个DAP 的适配器对代码进行调试,目前针对编译型和解释型语言来说我们都有了对应的方法来配置调试器对其进行调试。本节将要介绍关于dap的其他一些功能,主要包括 repl窗口和 gdb的集成repl 窗口什么是 repl 呢?它的全称是 Read Eval Print Loop 中文一般翻译为交互式解析器,可能看到这你还是一脸懵逼,你可以想想 python 或者nodejs,在控制台输入python 就可以进入到它的交互式解析器中,随着我们输入python 的语句,它会实时的给出运行的结果。交互式解析器就是这么一个东西,输入命令,它给你一个实时的结果。在调试中使用交互式解析器还是很有用的,比如我想显示当前某个变量的值,当前执行到哪个语句了等等。nvim-dap已经提供了一个内置的 repl 窗口,我们每次启动调试的时候都会看到它每次都会创建一个新的名为 dap-repl的buffer。之前我们没怎么关注它主要还是因为那个时候的重点在于如何进行调试,现在我们将要来优化这部分的显示和功能。首先我们发现每次调试结束的时候这个buffer 都会被遗留,需要我们手动的进行关闭,除了针对buffer通用的 :q 或者 :bd命令进行关闭,还可以使用 :lua require('dap').repl.close()。当然也可以配套使用 :lua require('dap').repl.open()来打开一个 repl的窗口,既然每次都会自动新建,那么这里我们就不需要进行新建,主要用于想办法关闭就可以了。还记得之前介绍 nvim-dapui 插件的时候介绍的那两个监听函数吗,同样的我们要在监听调试结束的函数中添加代码来关闭repl 窗口,函数的整个代码如下dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close({}) dap.repl.close() end dap.listeners.before.event_exited["dapui_config"] = function() dapui.close({}) dap.repl.close() end再启动调试的时候发现它已经可以在调试结束的时候自动关闭了。还有另外一个问题就是我不太喜欢现在这样在最下角显示 repl。我希望它能够在最下方以整行的形式显示。或者可以方便的不显示,只有在需要的时候显示。要达成这个目的我们需要修改 dapui 的配置。dapui 的配置主要以 element为基础,每个 element 代表一个提供对应功能的窗口。我们先来根据它默认的配置来讲解每部分的含义require("dapui").setup({ icons = { expanded = "", collapsed = "", current_frame = "" }, mappings = { -- Use a table to apply multiple mappings expand = { "<CR>", "<2-LeftMouse>" }, open = "o", remove = "d", edit = "e", repl = "r", toggle = "t", }, -- Use this to override mappings for specific elements element_mappings = { -- Example: -- stacks = { -- open = "<CR>", -- expand = "o", -- } }, -- Expand lines larger than the window -- Requires >= 0.7 expand_lines = vim.fn.has("nvim-0.7") == 1, -- Layouts define sections of the screen to place windows. -- The position can be "left", "right", "top" or "bottom". -- The size specifies the height/width depending on position. It can be an Int -- or a Float. Integer specifies height/width directly (i.e. 20 lines/columns) while -- Float value specifies percentage (i.e. 0.3 - 30% of available lines/columns) -- Elements are the elements shown in the layout (in order). -- Layouts are opened in order so that earlier layouts take priority in window sizing. layouts = { { elements = { -- Elements can be strings or table with id and size keys. { id = "scopes", size = 0.25 }, "breakpoints", "stacks", "watches", }, size = 40, -- 40 columns position = "left", }, { elements = { "repl", "console", }, size = 0.25, -- 25% of total lines position = "bottom", }, }, controls = { -- Requires Neovim nightly (or 0.8 when released) enabled = true, -- Display controls in this element element = "repl", icons = { pause = "", play = "", step_into = "", step_over = "", step_out = "", step_back = "", run_last = "", terminate = "", }, }, floating = { max_height = nil, -- These can be integers or a float between 0 and 1. max_width = nil, -- Floats will be treated as percentage of your screen. border = "single", -- Border style. Can be "single", "double" or "rounded" mappings = { close = { "q", "<Esc>" }, }, }, windows = { indent = 1 }, render = { max_type_length = nil, -- Can be integer or nil. max_value_lines = 100, -- Can be integer or nil. } })首先最上面的 icons 表示,各个部分显示的图标,这里分别定义了展开的,合并的以及当前位置的图标信息,我们可以观察一下变量栏或者调用栈显示信息的左侧就可以看到这里定义的图标。mappings 代表的是部分窗口动作定义的快捷键。例如上面定义的 expand = { "\<CR>", "" }表示可以在待展开项上按下回车或者鼠标左键双击来展开。element_mappings表示的是我们为某些窗口特意定制的一些快捷键。例如上面注释的是针对调用栈定义的快捷键。layouts代表布局,每个布局都有一个子table,而每个子table主要由 elements、size、position组成,它们分别代表采取该种布局的元素(也可以说是窗口),窗口大小以及窗口的位置。窗口一般通过id来描述,每种窗口都有固定的ID,根据官方文档的描述,它支持这么几种窗口:scopes显示全局或者当前局部变量,它支持的操作主要是 edit编辑变量的值、expand展开结构化的变量、repl将变量拷贝到repl窗口stacks显示当前正在运行的线程以及它们对应的调用栈,它主要支持的操作是 open :运行代码到当前被选中的位置, toggle:打开或者关闭该窗口watches显示我们需要追踪的变量,它支持的主要操作是 edit: 输入想要追踪的变量或者给对应的变量赋值。expand: 展开结构化的变量,remove:删除当前监视的变量,repl:将变量拷贝到repl窗口breakpoints显示当前激活的断点。它支持的主要操作有 open:执行代码到当前选中的断点处, toggle :激活或者使当前断点无效repl显示repl窗口console显示控制台窗口这些窗口的这些操作的快捷键我们已经通过上方的 mappings做了定义了,只要保持光标在对应窗口然后按下快捷键就可以执行对应的窗口命令了。如果想要单独对窗口进行快捷键定义可以在element_mappings 中被注释的代码controls 部分配置的是在repl窗口上方显示的那一堆调试按钮。由于我们定义了一些快捷键,这些按钮没太大的作用。这里我不需要它显示调试用的按键,所以我就在 controls 项中设置 enabled = false 禁用它。floating、window、render则定义的是悬浮窗口样式和普通窗口的一些样式。这里就不深究了。下面是改造之后的配置dapui.setup({ icons = { expanded = "", collapsed = "", current_frame = "" }, mappings = { -- Use a table to apply multiple mappings expand = { "<CR>", "<2-LeftMouse>" }, open = "o", remove = "d", edit = "e", repl = "r", toggle = "t", }, layouts = { { elements = { { id = 'scopes', size = 0.35 }, {id = "stacks", size = 0.35}, {id = "watches", size = 0.15}, {id = "breakpoints", size = 0.15}, }, size = 40, position = "left", }, { elements = { "repl" }, size = 5, position = "bottom", } }, controls = {enabled = false}, floating = { max_height = nil, -- These can be integers or a float between 0 and 1. max_width = nil, -- Floats will be treated as percentage of your screen. border = "single", -- Border style. Can be "single", "double" or "rounded" mappings = { close = { "q", "<Esc>" }, }, }, windows = { indent = 1 }, })repl 窗口的主要命令如下:.exit: 退出/关闭一个 repl 窗口.c/.continue: 继续执行代码.n/.next: 执行下一行代码.into: 跳转到函数中继续执行.out: 跳出函数.scopes: 打印当前栈的一些变量信息.threads: 打印线程信息.frames: 打印当前线程的调用栈.capabilities: 打印当前适配器实现的一些功能.p 暂停当前运行的程序更多的命令可以通过在 repl窗口中输入 .help查看看了这么多无聊的文字描述,不知道小伙伴们有没有觉得头晕眼花呢?我们来通过实际的例子来看看如何应用这些内容来进行调试。示例1:调试单线程死循环假设有一段程序在不知不觉中被写成死循环了,程序无法正常执行下面的操作,我们以下面的程序为例#include <stdio.h> #include <unistd.h> void loop_forever(){ for(unsigned int i = 10; i >= 0; --i){ // do something sleep(0.1); } } int main (int argc, char *argv[]) { // do something loop_forever(); printf("do other things\n"); return 0; }本来我们想的是它执行完函数 loop_forever之后会执行接下来的操作,但是我们死活看不到它执行后面的操作,这个时候我们意识到它可能在某个地方陷入死循环,无法出来了,假设前后都有大断的代码,无法快速定位到死循环的位置,该如何处理这种情况呢?我们先通过<F5>来执行操作,然后在 repl 中输入 i 进入插入模式,然后执行.p 中断当前程序执行。此时程序已经断了下来,接着我们输入.frames 查看当前调用栈信息。我们发现此时程序停留在loop_forever 函数的 sleep 中,我们在栈中找到 sleep 的位置并按下回车,这个时候我们发现程序执行到了 sleep() 函数处了。这个时候我们在这里按下<F9>下一个断点,接着使用 <F5> 继续运行到断点位置停止,这个时候我们通过实时显示的 i值已经发现问题所在了。原来i 递减到0之后,继续递减,因为它是无符号数所以永远无法达成小于0 的条件。示例2:调试多线程死锁我们以下面一个多线程程序为例#include <cstdio> #include <thread> #include <vector> using namespace std; void func(int thread_id){ int var = thread_id; while(true); } int main (int argc, char *argv[]) { vector<thread> vec; for(int i = 0; i < 5; i++){ vec.push_back(thread(func, i)); } // join for(int i = 0; i < 5; i++){ vec[i].join(); } return 0; }因为使用了c++ 相关的内容和 thread 库,因此编译的时候需要使用 g++ 并且指定链接 pthread 库,我们采用 g++ main.cpp -g -o main -lpthread 来编译它我们还是按照之前的步骤,先按下 .p来暂停程序,这个时候我们发现它会提示我们需要暂停哪个线程,遗憾的是根据线程的id还没法判断具体哪个是子线程哪个是主线程。这里我们随便选一个暂停。然后执行 .threads查看当前线程信息,在某个线程下使用回车键可以看到调用的函数栈。我们发现子线程卡在while 这句话,我们还是一样在卡主的位置按下回车跳转到对应代码位置,在此处下一个断点。然后我们在对应线程位置按下 o 命令来继续执行之前暂停的线程。这样就完美的找到了线程卡死的位置了。后面可以使用 .c 来继续执行所有被中断的线程nvim-gdb 插件该插件提供了一种方式,可以直接在neovim中进入gdb的session。例如我们可以通过命令 :GdbStart gdb -q a.out来启动一个gdb会话,并且关联了一个 a.out 的程序。后续可以直接使用gdb相关的命令来启动调试这个程序。我们可以使用如下的代码来安装它use {"sakhnik/nvim-gdb", run="./install.sh"}我们先来试试效果,直接使用快捷键 <leader>dd 来加载一个程序进行调试。进入到gdb会话之后可以使用gdb 的命令。例如我们使用 b main来在 main函数的位置打一个断点,然后通过r来启动程序运行到断点处。接着可以使用 n来执行下一步或者使用 c来直接运行到下一个断点。最后可以使用 q退出基础配置我们发现使用 nvim-gdb 插件的时候会在对应代码位置显示断点或者当前执行行。这里我们对它做一些配置,先统一使用nvim-gdb和 nvim-dap这两种情况下的显示信息。先创建一个新的配置文件为 nvimgdb.lua作为它的配置文件。因为它暂时还不支持lua的配置所以这里我们使用vim原生的写法。vim.cmd([[ let g:nvimgdb_config_override = { \ 'sign_breakpoint': [''], \ 'sign_current_line': '', \ } ]])在这里定义了断点和执行到当前行的配置,这里只能统一图标,还无法做到完全像使用 dap那样显示。另外我们可以在 nvimgdb_config_override 这个变量中定义如下快捷键以保证在调试时拥有同样的体验 \ 'key_next': '<F10>', \ 'key_step': '<F11>', \ 'key_continue': '<F5>',最后我们发现当我们输入文件名进行调试的时候,它会一直闪屏。这是因为它会不断根据我们输入的内容在文件系统中匹配合适的可执行文件,为了解决闪屏问题我们屏蔽它的这个特性,我们可以使用如下的配置来解决这个问题 let g:nvimgdb_use_find_executables = 0 let g:nvimgdb_use_cmake_to_find_executables = 0显示窗口的配置定义了显示形式,我们来定义显示的窗口,这里我让它显示常用的像调用栈,变量,以及watch窗口。我们先使用如下代码来将窗口分割为左右两个部分 let w:nvimgdb_termwin_command = "rightbelow vnew" let w:nvimgdb_codewin_command = "vnew"根据官方文档的描述,nvimgdb_termwin_command 是终端窗口,用来显示repl 相关信息并且与用户交互,后面我们可以对它进行切割用于显示其他信息。nvimgdb_codewin_command 是源代码窗口。这两句代码可以形成一个左右分屏的界面,左侧显示代码,右侧显示repl窗口。在gdb成功加载之后,我们可以使用命令 :GdbCreateWatch info locals来创建一个显示当前变量的窗口。默认是新创建一个窗口来显示,但是我们可以在命令前加上 belowright来指定在右侧继续分屏,它会在右下角新建窗口来显示变量。需要查看其它窗口可以对应传入不同的参数,例如传入 breakpoints来显示所有断点信息。传入的参数就是gdb中接收的对应参数。有了这些基础我们就可以对其进行配置了,我们要实现的目标就是当gdb成功加载的时候自动加载这些窗口。在vim中要实现自动化我们目前知道有两种方式,第一种使用自动命令,第二种使用插件配置中提供的回调函数。遗憾的是在这个插件中我没有找到回调函数,因此我们只能采用自动命令这种方法。根据官方的文档,我们主要使用这么两个事件——NvimGdbStart和 NvimGdbCleanup。它们一个是成功加载gdb的时候触发,一个是关闭gdb会话的时候触发。vim.cmd([[augroup GdbSession autocmd! autocmd User NvimGdbStart :lua StartGdbSession() autocmd User NvimGdbCleanup :lua EndGdbSession() augroup END]])在 StartGdbSession 函数中写入如下代码来完成窗口的配置StartGdbSession = function() vim.api.nvim_command(":belowright GdbCreateWatch backtrace") vim.api.nvim_command(":wincmd h") vim.api.nvim_command(":belowright GdbCreateWatch info locals") vim.api.nvim_command(":set wrap") vim.api.nvim_command(":wincmd k") end不知道各位小伙伴能不能理解这段代码是如何在分屏的。首先启动gdb的时候会将整个屏幕纵向分为两个部分,左侧为 code右侧为 repl窗口接着我们执行 :belowright GdbCreateWatch backtrace 它会在右下方创建一个窗口用来展示调用栈然后执行 :wincmd h来将光标移动到代码窗口上继续执行 :belowright GdbCreateWatch info locals它会在代码窗口的下方新增一个窗口用于显示变量的信息这样就将窗口分为4个部分了,左上部分显示代码,左下部分显示变量信息。右上部分是repl窗口,右下部分显示变量信息。最后我们通过 :set wrap设置窗口中自动换行,不然有些内容显示在一行不容易查看。通过 :wincmd k移动光标到 repl窗口。方便后续调试启动之后他的效果如下最后我们在结束gdb的时候做一些收尾工作,关闭我们创建的窗口EndGdbSession = function() vim.api.nvim_command(":bdelete! backtrace info locals") end这里我是根据buffer的名称来进行删除。最后的效果如下到此我们已经介绍了关于dap 的所有配置,至于其他语言相信各位小伙伴根据官方给出的示例可以独立完成配置,这里就不一一介绍了。
2022年12月30日
12 阅读
0 评论
0 点赞
1
...
3
4
5
...
11