首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
21
篇与
的结果
2025-05-14
Emacs 折腾日记(二十三)——进一步提升编辑效率
在前面的几篇,我们完成了Emacs的vim模拟器、中文输入、多行编辑以及基本的补全功能的添加。这一篇没有具体的提升哪一方面的能力,这一篇我想整合我在其他教程中看到的我认为对我比较有用的用法和插件,算是对前期功能的一个总结。让Emacs记住一些信息一般的编辑器都会在下次打开时记住上次的一些信息,例如记住之前打开过的文件,执行过的命令,或者记住上次的窗口布局。记住上次执行的命令我们每次使用 M-x 执行命令时,minibuffer中显示的提示都是一样的,那些常用命令要么不在上面要么太靠下了,我们希望能记住某些命令,以便能快速找到它。记住上次执行的命令可以使用 savehist 插件。它是一个Emacs自带的插件,默认是关闭的状态,我们可以通过将 use-package 来加载它,但是因为是自带的,不需要从镜像中下载所以它的 :ensure 项应该设置为 nil(use-package savehist :ensure nil :hook (after-init . savehist-mode) :custom (savehist-file (locate-user-emacs-file "custom/savehist")) ;; 设置保存文件的位置 (savehist-additional-variables '(kill-rings shell-command-history)) ;; 额外保存剪切板和shell命令行历史 (savehist-ignored-variables '(message-history)) ;; 不保存消息历史 (history-delete-duplicates t) ;; 自动去重 (history-length 1000) ;; 保存历史数据条目 )在执行一些操作关掉Emacs之后,我们会发现它在 ~/.emacs.d/custom 生成了一个名为 savehist 的文件,它记录了之前在minibuffer中执行的命令。为了保持git工程的干净,我打算将这种历史文件排除在git管理之外,所以单独将它放到custom目录,并忽略它其实该插件不光可以保留执行的命令,minibuffer中的许多信息它都可以保存和持久化。minibuffer-history (所有 minibuffer 输入历史)command-history (执行过的命令)search-ring (搜索历史)regexp-search-ring (正则搜索历史)extended-command-history (M-x 命令历史)file-name-history (文件路径历史)记住上次打开的文件一般的编辑器都可以记录上次打开的文件,并列出来。Emacs也有一个类似的内置插件—— recentf(use-package recentf :ensure nil :hook (after-init . recentf-mode) :custom (recentf-max-menu-item 10) ;; 最多只记录10条历史记录 (recentf-save-file (locate-user-emacs-file "custom/.recentf")) ;; 定义保存历史记录的临时文件路径 )搜索功能的增强实现全局搜索我们可以依赖Linux上的命令行工具 grep 和最近(也不算近了)的 ripgrep。之前在介绍vim的时候,vim内部集成了 grep。但是我们使用更为强大的 ripgrep。在Emacs中可以配合插件 consult 和 ripgrep,调用 consult-ripgrep 来进行全局搜索。它会自动搜索当前项目下的所有目录。我们对之前 consult 插件的配置代码进行一些修改,并添加 ripgrep 的配置(use-package consult :ensure t) (use-package ripgrep :ensure t :after consult :bind (("C-s" . consult-ripgrep)) )这里我们将 C-s 绑定的快捷键修改为 consult-ripgrep。神奇的是,配合之前安装的orderless,我们只需要按照一个模糊的记忆来匹配想要的内容。同时它也能支持输入中文批量替换批量替换这个功能,根据我找到的教程,它需要依赖 embark、consult、和 wgrep 这么三个插件。其中 consult 用来进行搜索,而 embark 可以为不同场景下的文本/候选项(如搜索结果、补全列表、文件路径等)提供动态的快捷操作菜单。简化了minibuffer上的一些操作。而 wgrep 则是其中的核心插件,用来批量修改内容并保存(use-package embark :ensure t :after consult :bind (("C-e" . embark-export))) (use-package embark-consult :ensure t :after embark) (use-package wgrep :ensure t :custom (wgrep-auto-save-buffer t) ;; 自动保存修改 )这里我们使用 :after 来保证插件的加载顺序依次为 consult、embark、embark-consult,特别是 embark-consult,它依赖 consult 和 embark,一定要将它放到后面加载。下面来演示如何进行批量替换,这里我们将配置中所有 use-package 修改为 package-install,修改之前记得使用git等版本管理工具进行备份首先,使用 C-s 搜索 use-package 关键字接着使用 C-e, 也就是上面绑定的快捷键来将结果从 minibuffer 导出到 buffer然后使用 C-c C-p 调用 wgrep-change-to-wgrep-mode 将 buffer 的mode由 grep-mode 修改为 wgrep-mode接着使用 M-% 调用 query-replace 进行替换,这个时候它需要输入被替换的字符和替换后的字符确定后,对于每个待替换的位置使用 y 或者 n 来表示替换或者不替换。也可以使用 ! 替换所有最后使用 C-c C-c 调用 wgrep-finish-edit 来结束编辑,配置之前设置的自动保存,此时修改内容已经被保存了修改之后如何不满意,可以使用 C-c C-k 撤销修改小节这应该是最后一篇关于Emacs自身编辑功能的增强了,在这一块我没有使用太多的Emacs经验。倒是在vim上有点经验,所以很多东西我不自觉地就往vim上面靠,总想着vim在编辑上有些功能Emacs上还没有,该如何进行添加,这几篇就显得比较分散,总是想到什么功能就往上面堆。为此造成各位读者阅读体验不佳,我表示道歉。谢谢各位读者的支持和鼓励!
2025年05月14日
1 阅读
0 评论
0 点赞
2025-05-07
Emacs 折腾日记(二十二)——补全强化
在之前的一系列文章中,我们对Emacs做了一些小范围的定制,目前它已经可以很好的模拟vim的一些基础功能。我们也在模拟vim基础功能之上做了一些能力的提升。本篇我们将对Emacs自带的补全系统做一个升级,并且给出一些搜索和替换的方案,进一步提升Emacs的效率Emacs上有很多很好用的补全插件,著名的有前期的 ivy 体系和当前社区比较火的vertico 体系。为了与时俱进,而且Emacs-China中的很多帖子也推荐使用vertico,所以这里我也介绍这个体系中的插件。vertico 体系中包括下面几个插件:verticoconsultcorfumarginaliaorderlessconsultconsult 插件提供了一系列的查找和补全命令(use-package consult :ensure t :bind (("C-s" . consult-line)))这样我们可以通过使用 C-s 来进行搜索vertico默认情况下,我们使用M-x 输入命令时没有补全提示,但是可以使用TAB 键补全。我们可以通过命令 icomplete-mode 来启用这个mode,以便在输入命令时能拥有一个补全。但是这个补全采用的是横向排版的方式,显示上也不太直观。这里我们可以通过vertico 插件对补全进行增强。vertico 提供了一个垂直样式的补全系统。我们可以通过下列代码来安装并启用它(use-package vertico :ensure t :hook (after-init . vertico-mode))重启emacs之后,再执行 M-x 之后发现它已经可以竖直的显示命令,并且会列出可能的命令了。可以使用 C-n、C-p 来选择下一个或者上一个命令orderless顾名思义,orderless 提供一种无序补全。它可以将一个搜索的范式变成数个以空格分隔的部分,各部分之间没有顺序,你要做的就是根据记忆输入关键词、空格、关键词。它改变了我们使用和思考的习惯,我们不再需要关心信息的顺序,我们只需要在脑海中搜索关键信息片段,然后把这些片段组合起来即可,剩下的都交给Emacs。例如我们要输入 package-refresh-contents 来刷新包管理里面的源。常规的做法我们需要先输入 pack 等等字符,然后由补全信息给我们提示,加入 orderless 之后,可以凭借模糊的记忆输入类似 refre pack 这样的片段来进行匹配(use-package orderless :ensure t :init (setq completion-styles '(orderless)))orderless 是针对整个minibuffer进行增强的,只要是使用minibuffer的地方都可以使用。例如我们上面使用了 consult 插件并且绑定了 C-s 来进行搜索,这里我们就可以使用orderless 来配合完成搜索功能marginaliamarginalia 可以给minibuffer中候选条目显示一段注释或者其他信息。其实不光是执行命令的时候marginalia是启用的,现在只要是minibuffer中的选项,marginalia都是可以使用的,例如使用 switch-buffer 和 find-file 或者使用帮助信息的时候也可以展示相关信息corfucorfu 可以让我们通过弹窗进行补全。(use-package corfu :ensure t :hook (after-init . global-corfu-mode) :custom (corfu-auto t) (corfu-auto-deply 0) (corfu-min-width 1) :init (corfu-history-mode) (corfu-popupinfo-mode))在安装完成之后,我们在编写相关配置的时候可以配合orderless,只输入函数的部分,仅仅凭借模糊的记忆让Emacs自己来匹配我们想要的内容,极大的提高了输入的效率capecorfu 插件仅仅是一个补全的前端,它需要补全后端提供数据。好在Emacs 自己提供了有关elisp 的补全后端,所以上面在测试corfu补全的时候可以出现。但是在其他文本类型不会产生补全选项。而cape则是集成了多种补全后端,它与corfu联合起来可以起到很好的补全效果(use-package cape :ensure t :init (add-to-list 'completion-at-point-functions #'cape-dabbrev) (add-to-list 'completion-at-point-functions #'cape-file) (add-to-list 'completion-at-point-functions #'cape-keyword) (add-to-list 'completion-at-point-functions #'cape-ispell) (add-to-list 'completion-at-point-functions #'cape-dict) (add-to-list 'completion-at-point-functions #'cape-symbol) (add-to-list 'completion-at-point-functions #'cape-line))上述代码中 completion-at-point-functions 保存的是Emacs在补全时调用的相关函数来获取补全项,我们将cape 的相关函数添加到这个列表中,供Emacs在触发补全时调用。到此为止,我们对Emacs自身的补全进行了加强。进一步提升了编辑的效率
2025年05月07日
1 阅读
0 评论
0 点赞
2025-04-22
Emacs 折腾日记(二十一)——编辑能力提升
上一篇文章,我们补充了一些基本的配置,并且关闭了一些默认的行为。这里我们继续对它进行配置。本篇将要使用一些插件来修改默认的编辑行为进一步提高编辑文本的效率。avy 插件基础用法在vim中有 easymotion 可以使用,在Emacs中可以使用 avy 插件。它的功能于前面介绍的easymotion 类似。通过下面的代码来安装(use-package avy :ensure t :after general ;; 确保 general 插件已经安装 :config (setq avy-timeout-seconds 0.5) (my-leader-def "f" 'avy-goto-char-timer))我们在安装配置的时候使用之前定义的leader键来定义它的一些行为。首先定义 SPC-f 来进行快速跳转。avy-goto-char-timer 的功能与 easymotion 类似,将要查找的字符使用不同的字母进行标识,然后根据下一步的输入来确定光标的位置,例如下面的例子我们可以连续输入一段内容减少待筛选项。当然这个速度要快,否则在一定时间内没有输入文本之后,avy会认为已经结束输入了。这个等待的时间我们在上面通过 avy-timeout-seconds 定义的是 0.5。当然也可以扩大这个时间范围进阶用法这个简单的功能只是 avy 功能的冰山一角。它还有许多有用的功能,如果能熟练使用将会极大的提高编辑文本的效率。实际上avy 在筛选、跳转之前可以执行用户指定的动作,它支持哪些动作呢?我们可以在输入筛选的部分文本后输入?,查看支持的动作。我们来举一个例子:下面有一段文本,我希望将text1复制到最后一行,那么可以这么操作使用<leader>f 激活 avy,然后输入筛选文本输入 Y 表示复制整行输入对应字符表示选中text1 所在行此时我们会发现,text1 已经被复制到光标所在行了多行操作和块操作在vim中我们介绍了多行操作,主要是使用C-v 来选中某些行,然后通过使用A 或者 I 来选中行尾或者行首,并进入插入模式。这种方式可以同时在对应位置插入多个相同的字符。emacs 中的 evil 插件也可以进行相同的操作。但是对于行间或者每行在不同位置插入的情况就不适用了。我们可以使用插件 evil-multiedit 来达到这一效果。evil-multiedit 深度绑定了vim的快捷键,在选中区域之后可以直接使用vim中的 I/A 来编辑选中区域的首部或者尾部。也可以使用 ciw 之类的同时修改多个选中区域。在选区时既可以使用vim 中的 * 来查找并选中,也可使用 / 来搜索并且同时选中多个。这个插件相当于增强了vim的多行编辑功能我们使用下列的代码来安装和简单的配置(use-package evil-multiedit :ensure t :after evil :config (evil-multiedit-default-keybinds) (my-leader-def "m m" #'evil-multiedit-match-and-next ; 标记当前符号并跳转下一个 "m M" #'evil-multiedit-match-and-prev ; 标记当前符号并跳转上一个 "m a" #'evil-multiedit-match-all ; 标记所有相同符号 "m r" #'evil-multiedit-restore ; 恢复单光标模式 "m c" #'evil-multiedit-toggle-or-restrict ; 切换选区/限制编辑区域 ))我们可以使用 <leader> mm 来选中符合条件的项也可以使用 <leader> ma 来选择所有符合条件的项好了,本节到此也就结束了,本节依靠两个插件,进一步模拟vim相关的功能,并且对vim原有的功能进行了一定程度的补强。熟练使用这两个插件将会对编程的效率有一个进一步的提升。作为一个普通的文本编辑器也足够了。
2025年04月22日
4 阅读
0 评论
0 点赞
2025-04-12
使用CMake跨平台的一些经验
使用CMake构建工程的一个原因是不希望Windows上有一个工程文件,Linux又单独配置一套工程文件。我们希望对于整个工程只配置一便,就可以直接编译运行。CMake本身也具备这样的特性,脚本只用写一次,在其他平台能自动构建工程。这里谈的跨平台主要是如何组织工程和编写CMake来针对Windows和Linux进行一些特殊的处理,在这里说说我常用的几种办法介绍这些方法的前提是有一些代码只能在Windows上跑,例如调用Win32 API或者使用GDI/GDI+,而在Linux上也有一些Windows不支持的代码。我们就需要一些方法来隔离开这两套代码。假设现在有一个项目,它有一套共同的接口对外提供功能,而在Windows上和Linux上各有一份代码来实现这些接口。可以假设有一套图形相关的功能,对外采用统一的接口,具体实现时Windows上使用GDI+,而Linux上使用其他方案来实现。现在整个项目的目录结构如下. ├── include └── platform ├── linux └── windowsinclude 目录用来对外提供接口,是导出的头文件。platform隔离了Windows和Linux上的实现代码。使用宏来控制我们知道Windows和Linux平台有特定编译器定义的宏,根据这些宏是否定义我们可以知道当前是在Linux还是Windows上编译,是需要编译成32位或者64位程序,又或者编译成debug版本或者release版本。例如下面是我用的简单的判断版本的方式#define MY_PLATFORM_WINDOWS 0 #define MY_PLATFORM_LINUX 1 #define MY_PLATFORM_APPLE 2 #define MY_PLATFORM_ANDROID 3 #define MY_PLATFORM_UNIX 4 #define MY_ARCH32 1 #define MY_ARCH64 2 #if defined(_WIN32) || defined(_WIN64) #define MY_PLATFORM MY_PLATFORM_WINDOWS #ifdef _WIN64 #define PLATFORM_NAME "Windows 64-bit" #define MY_ARCH MY_ARCH64 #else #define PLATFORM_NAME "Windows 32-bit" #define MY_ARCH MY_ARCH32 #endif #elif defined(__APPLE__) #include "TargetConditionals.h" #define MY_PLATFORM MY_PLATFORM_APPLE #ifdef ARCHX64 #define MY_ARCH MO_ARCH64 #else #define MY_ARCH MO_ARCH32 #endif #if TARGET_IPHONE_SIMULATOR #define PLATFORM_NAME "iOS Simulator" #elif TARGET_OS_IPHONE #define PLATFORM_NAME "iOS" #elif TARGET_OS_MAC #define PLATFORM_NAME "macOS" #endif #elif defined(__linux__) #define MY_PLATFORM MY_PLATFORM_LINUX #if defined (ARCHX64) || defined (__x86_64__) #define MY_ARCH MY_ARCH64 #else #define MY_ARCH MY_ARCH32 #endif #define PLATFORM_NAME "Linux" #elif defined(__unix__) #ifdef ARCHX64 #define MY_ARCH MY_ARCH64 #else #define MY_ARCH MY_ARCH32 #endif #define PLATFORM_NAME "Unix" #define MY_PLATFORM MY_PLATFORM_UNIX #else #error "Unknown platform" #endif上面代码根据一些常见的编译器宏来决定是什么版本,并且根据不同的版本将MY_PLATFORM 进行赋值。后面只需要使用 MY_PLATFORM 进行版本判断即可。同样的关于架构使用 MY_ARCH 来判断。例如根据架构来定义不同的数据长度#if (MY_PLATFORM == MY_PLATFORM_WINDOWS) typedef __int64 MY_INT64; typedef unsigned __int64 MY_UINT64; #else typedef long long MY_INT64; typedef unsigned long long MY_UINT64; #endif #if (MY_ARCH == MY_ARCH64) typedef MY_UINT64 MY_ULONG_PTR; typedef MY_INT64 MY_INT_PTR; #else typedef MY_UINT MY_ULONG_PTR; typedef MY_INT MY_INT_PTR; #endif定义的常见的数据结构之后,对于一些接口的视线就可以利用宏来隔开// platform/windows/image.c #if (MY_PLATFORM == MY_PLATFORM_WINDOWS) // todo something #endif// platform/linux/image.c #if (MY_PLATFORM == MY_PLATFORM_LINUX) // todo something #endif这样我们可以利用上一节介绍过的 cmake 的 file 或者 aux_source_directory 将整个platform目录都包含到工程里面。使用cmake来判断版本除了在C/C++ 源码中利用编译器特定的宏来判断版本,其实CMake自身也有一些方式来判断编译的版本。CMake 检测操作系统使用 CMAKE_SYSTEM_NAME 来判断。这里要提一句,CMake中的变量本质上都是一个字符串值,没有严格的区分类型,所以 set(variable 1) 和 set(variable "1") 在底层存储都是字符串 1。所以cmake在定义变量的时候可以不使用双引号,但是对于特殊的字符串,例如带有空格的字符串,为了避免语法上的歧义,可以带上双引号。虽然说底层存储的都是字符串,但是在上层判断变量是否相等的时候却又区分数字和字符串。判断变量是否等于某个值可以使用 EQUAL 或者 STREQUAL。EQUAL 是用来判断数字类型是否相等,一般判断版本号或者数字参数。而 STREQUAL 来判断字符串是否相等,一般用来判断配置选项、路径、平台标识符。例如这里的 CMAKE_SYSTEM_NAME 就需要采用 STREQUAL 来判断# 检测平台 set(PLATFORM_WINDOWS 1) set(PLATFORM_LINUX 2) set(PLATFORM_MACOS 3) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(PLATFORM ${PLATFORM_WINDOWS}) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(PLATFORM ${PLATFORM_LINUX}) elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(PLATFORM ${PLATFORM_MACOS}) endif()判断架构可以使用 CMAKE_SIZEOF_VOID_P。顾名思义,它表示一个void* 指针变量的大小,8位就是64位,4位则是32位架构。if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(PLATFORM_ARCH "x64") elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) set(PLATFORM_ARCH "x86") else() message(FATAL_ERROR "Unsupported architecture pointer size: ${CMAKE_SIZEOF_VOID_P}") endif()至于判断当前编译的版本是debug 还是 release 可以使用 CMAKE_BUILD_TYPE 来判断,它的值主要有4个,分别是 Debug、RelWithDebInfo、MinSizeRel、Release。它们四个各有特色。其中 RelWithDebInfo 是带有符号表的发布版,便于调试,它的优化级别最低。MinSizeRel和Release在优化上各有千秋,前者追求最小体积,后者追求最快的速度,所以后者有时候会为了运行速度添加一些额外的内容导致体积变大。我们可以在cmake文件中判断对应的值以便做出一些额外的设置。例如if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_definitions(-D_DEBUG) else() add_compile_definitions(-DNDEBUG) endif()有了这些基础,我们可以在不同的条件下,定义不同的编译宏,然后根据编译宏的不同在C/C++ 源码中判断这些宏从而隔离不同平台的实现代码通过cmake list 来隔离不同平台的代码使用 file 或者 aux_source_directory 的到的是一个源代码文件的列表。我们可以操作这个列表来达到控制编译文件的需求。cmake 中针对列表的操作符是 list,它的定义如下:Reading list(LENGTH <list> <out-var>) list(GET <list> <element index> [<index> ...] <out-var>) list(JOIN <list> <glue> <out-var>) list(SUBLIST <list> <begin> <length> <out-var>) Search list(FIND <list> <value> <out-var>) Modification list(APPEND <list> [<element>...]) list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>) list(INSERT <list> <index> [<element>...]) list(POP_BACK <list> [<out-var>...]) list(POP_FRONT <list> [<out-var>...]) list(PREPEND <list> [<element>...]) list(REMOVE_ITEM <list> <value>...) list(REMOVE_AT <list> <index>...) list(REMOVE_DUPLICATES <list>) list(TRANSFORM <list> <ACTION> [...]) Ordering list(REVERSE <list>) list(SORT <list> [...])官方提供了这么一些操作list的操作符,但是在这个需求中我们只需要两个操作符APPEND 和 REMOVE_ITEM 即可。后面的参数分别是源列表,以及需要增加或者删除的项,它们都可以传入多项。但是删除时它是根据传入字符串,从列表中进行字符串比较,如果相等则进行删除。所以在传入路径的时候需要特别注意,不能一个传入全路径一个传入相对路径或者一个传入 / 开头的路径,另一个传入 ~ 开头的路径。上述两个操作都可以传入多个单个的字符串也可以传入一个列表。如果我们有下列目录结构src/platform/windows src/platform/linux src/*.cpp src/other/*.cpp也就说我们将不同平台的代码放入到src目录,并且src目录也有其他代码,我们如果使用 file 操作符来查找src目录中的源码文件必定会包含两个平台的实现代码。这个时候就可以考虑使用REMOVE_ITEM 根据平台来舍弃一些代码,例如file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.c ) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") file(GLOB_RECURSE NOT_INCLUDE ${PROJECT_SOURCE_DIR}/src/platform/linux/*.cpp ) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") file(GLOB_RECURSE NOT_INCLUDE ${PROJECT_SOURCE_DIR}/src/platform/windows/*.cpp ) endif() list( REMOVE_ITEM SOURCES SOURCE ${NOT_INCLUDE} )又或者我们采用最上面的给出的目录结构,也就是说platform 目录位于src目录之外,相对于src来说是额外添加的代码文件,那么就可以使用 APPEND 来进行添加if(CMAKE_SYSTEM_NAME STREQUAL "Windows") aux_source_directory(${PROJECT_SOURCE_DIR}/platform/windows PLATFORM_SOURCE) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") aux_source_directory(${PROJECT_SOURCE_DIR}/platform/linux PLATFORM_SOURCE) endif() list(APPEND SOURCES ${PLATFORM_SOURCE})通过 toolchain 文件来组织平台特殊配置cmake 允许我们在生成Makefile的时候指定toolchain 文件来实现一些自定义的配置。例如可以根据平台的不同将生成路径指定在对应的toolchain中。toolchain 的语法与cmake语法相同,例如针对Windows和Linux可以创建 win32_toolchain.cmake win64_toolchain.cmake linux_86_toolchain.cmake 和 linux_x64_toolchain.cmake 文件来区别我还是以上一篇文章中多工程嵌套的例子作为示例来演示如何使用,它的目录结构如下. ├── calc │ ├── add.cpp │ ├── CMakeLists.txt │ ├── div.cpp │ ├── mult.cpp │ └── sub.cpp ├── CMakeLists.txt ├── include │ ├── calc.h │ └── sort.h ├── sort │ ├── CMakeLists.txt │ ├── insert_sort.cpp │ └── select_sort.cpp ├── test_calc │ ├── CMakeLists.txt │ └── main.cpp └── test_sort ├── CMakeLists.txt └── main.cpp这个例子我们只需要改动根目录下的生成库和可执行程序的路径。cmake_minimum_required(VERSION 3.15) project(test) add_subdirectory(sort) add_subdirectory(calc) add_subdirectory(test_calc) add_subdirectory(test_sort)这个文件只需要保留最基础的配置即可,而生成程序的路劲都在 toolchain 中。下面是 linux_x86_toolchain.cmake 文件的内容set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/linux/x86) set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/linux/x86) set(HEADPATH ${PROJECT_SOURCE_DIR}/include) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CALCLIB calc_d) set(SORTLIB sort_d) set(CALCAPP test_calc_d) set(SORTAPP test_sort_d) else() set(CALCLIB calc) set(SORTLIB sort) set(CALCAPP test_calc) set(SORTAPP test_sort) endif() set(CMAKE_SYSTEM_PROCESSOR i686) set(CMAKE_C_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE) set(CMAKE_EXE_LINKER_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE)这个文件我们定义了debug和release的库名称和生成的路径,并且指定相关参数用于生成32位程序。CMAKE 中定义了一堆 CMAKE_LANGUAGE_FLAGS 这些都是相关工具的参数,这里的FLAGS 分别是 gcc 和 ld 编译链接的参数。在使用时直接用命令 cmake .. -DCMAKE_TOOLCHAIN_FILE=../linux_x32_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug 生成Makefile。Windows平台上上面的参数稍微有些差距。例如下面是 windows_x32_toolchain.cmake 的定义# 静态库生成路径 set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/windows/x86) # 可执行程序的存储目录 set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/windows/x86) # 头文件路径 set(HEADPATH ${PROJECT_SOURCE_DIR}/include) if(CMAKE_BUILD_TYPE STREQUAL "Debug") # calc库名称 set(CALCLIB calc_d) # sort 库名称 set(SORTLIB sort_d) # 测试程序的名字 set(CALCAPP test_calc_d) set(SORTAPP test_sort_d) else() # calc库名称 set(CALCLIB calc) # sort 库名称 set(SORTLIB sort) # 测试程序的名字 set(CALCAPP test_calc) set(SORTAPP test_sort) endif() set(CMAKE_GENERATOR_PLATFORM "Win32" CACHE STRING "Target Platform") # 指定32位编译器路径 set(CMAKE_C_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe") set(CMAKE_CXX_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe") set(CMAKE_SYSTEM_PROCESSOR x86)需要注意的是 CMAKE_GENERATOR_PLATFORM 对应的是VS 中的解决方案平台,也就是 Win32 和 x64 这两个选项。 而 CMAKE_SYSTEM_PROCESSOR 对应的是VS中的目标计算机选项,一般是X86、X64 或者 ARM64 和 AMD64。我们可以使用命令 cmake -G "Visual Studio 15 2017" -A "win32" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="..\windows_x86_toolchain.cmake" .. 这里指定使用 VS 2017 进行构建,构建架构是32位,版本是Debug。命令成功执行之后会生成一个.sln 文件,我们可以用VS打开然后在VS中编译,也可以执行使用命令 cmake --build . --config Debug 来编译。一般来说我习惯使用后者,过去使用vs 打开.sln 可以在vs中进行开发,如今vs 已经可以打开并编译cmake工程,所以现在我基本不使用 .sln 文件了,除非公司项目要求使用 .sln。好了,目前我掌握的关于cmake的内容就是这些了,我利用这些知识已经完成了公司项目的跨平台开发和部署。后续如果有新的需求说不定我会学点新的内容,到时候再来更新这一系列文章吧!!!
2025年04月12日
4 阅读
0 评论
0 点赞
2025-03-17
Emacs 折腾日记(十八)——改变Emacs的样貌
截止到上一篇文章为止,之前教程 的内容都看完了,虽然它的后记部分提供了一些后续进阶的内容需要我们自己读手册。但是我不太想继续在elisp上死磕了。看着自己学了那么久的elisp,但是自己的emacs仍然没有半点改变,这个时候各位读者的兴趣一定会大打折扣,是时候试试配置一下自己的emacs了。教程后记中提到的内容等配置的时候涉及到了再来了解吧所谓人靠衣装马靠鞍,一个编辑器好不好用首先要看的就是它好不好看,对于难看的编辑器可能一眼就要给它发卡了——“你是一个很优秀的编辑器,但是我们不合适”。所有配置时第一件事就是将emacs变帅变好看。emacs的基础配置Emacs 在加载的时候会首先读取 ~/.emcas.d/init.el 中的代码。整个配置程序的入口就在这里。但是如果我们一股脑将所有代码都写在这个文件中日后想要维护肯定是不方便的,所以在写配置之前需要了解一些它的模块化提供一个模块,我们只需要在代码文件最后的位置写上 (provide 'package-name) 这样的代码即可。这里的 provide。可以理解为导出,后面是导出模块的名称。在需要引入模块时,只需要添加一行 (require 'basic)。但是与其他语言类似,有时候会出现找不到对应的模块,这里涉及到一个查找路径的问题。Emacs 中加载路径被保存在变量 load-path 中。该变量是一个list,我们可以将指定路径放入到这个变量中来添加用户定义代码的路径。load-path 中的目录顺序决定了 Emacs 搜索文件的优先级。如果多个目录中存在同名的 Lisp 文件,Emacs 会优先加载 load-path 中靠前的目录中的文件。因此,你可以通过调整 load-path 的顺序来控制加载的优先级。为了添加路径到 load-path 中,我们要了解一个新的函数, add-to-list。为什么这里我们不使用 push 或者其他之前学过的操作list的函数呢?最关键的一点是 add-to-list 具有去重的功能,能避免多次重复加入同一个路径。如果我们将用户代码放入到 ~/.emacs.d/lisp 这个目录中,我们可以使用下面的代码;; init.el (add-to-list 'load-path "~/.emacs.d/lisp")前面的知识介绍完了,现在我们新建 ~/.emacs.d/lisp/basic.el 文件,进行基础的配置。目前添加的代码主要是取消Emacs上的滚动条、菜单栏、工具栏、以及每次打开的开始界面。;; basic.el ;; 禁止菜单栏、工具栏、滚动条模式,禁止启动屏幕和文件对话框 (menu-bar-mode -1) (tool-bar-mode -1) (scroll-bar-mode -1) (setq inhibit-splash-screen t) ;; 禁止启动画面 ;; 显示行号 (setq display-line-numbers-type 'relative) ;;显示相对行号 (global-display-line-numbers-mode 1) (provide 'basic)然后在启动的时候使用它;; init.el (add-to-list 'load-path "~/.emacs.d/lisp") (require 'basic)重启Emacs就能看到具体的效果了使用包管理器要想它变的好看,最好的办法是加载开源大佬提供的主题。作为一个小菜鸡不太可能自己开发重型的功能,我们要做的这是将大佬提供的包整合到自己的配置中。所以我们先来介绍包和包管理器。这里的包我们可以理解为提供了某种功能的模块,有点类似与C/C++ 的静态库或者Java的类库。Emacs中的包管理主要通过 package.el 模块提供。它包含了模块的查找,下载,更新以及删除等操作。它的一些常用命令如下:M-x list-packages:列出所有可用的包,并进入包管理界面。M-x package-install:安装指定的包。M-x package-refresh-contents:刷新包列表,获取最新的包信息。M-x package-upgrade:更新所有已安装的包。M-x package-delete:删除指定的包。Emacs中默认的仓库是 Emacs 默认使用 MELPA(Milkypostman’s Emacs Lisp Package Archive)作为主要的包仓库。MELPA 提供了大量高质量的第三方包。除此之外,还有其他仓库,如:GNU ELPA:官方仓库,包含 Emacs 自带的包。MELPA Stable:提供稳定版本的包。NonGNU ELPA:包含一些非 GNU 的包。因为国内的网络环境,我们常常需要使用国内的源。这里我们创建一个新的文件 package 用来管理包。;; package-conf.el (require 'package) (setq package-enable-at-startup nil) (setq package-archives '(("gnu" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/") ("nongnu" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu/") ("melpa" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/"))) (package-initialize) ;; You might already have this line (provide 'package-conf)这里我使用清华源,各位读者可以选择自己喜欢的源。上面的代码我们使用 package-enable-at-startup 来控制Emacs是否自动初始化package包管理器。这里为了更灵活我们禁止它自动初始化,改由手动初始化。我们可以在代码中使用类似于 (package-install 'package-name) 的方式来自动安装包,但是这里介绍更加高级的包管理器——use-package。如果使用Emacs原生的包管理器,那么就是先安装,然后想办法组织包配置的代码,这样将安装与配置分散起来了,不利于管理。使用use-package 可以方便的将它们组织起来。本质上use-package 提供了一系列的宏将包的安装和包的配置组合到一起,方便维护。而且它还提供了一些高级的特性方便我们灵活的控制各种配置生效的时间。我们可以使用如下语句进行安装;; package-conf.el (unless (package-installed-p 'use-package) (package-refresh-contents) (package-install 'use-package))它的基本语法如下:(use-package package-name :keyword1 value1 :keyword2 value2 ...)它的常用关键字如下::ensure: 确保包已安装。如果包未安装,use-package 会自动安装它,一般使用Emacs自带的包这里设置成nil,安装第三方的包,这里设置成t:init:在包加载之前执行的代码:config: 在包加载之后执行的代码:bind: 为包中的函数绑定快捷键:mode: 为特定文件类型启用包:hook: 在特定模式下启用包:defer: 延迟加载包,直到首次使用包:custom: 设置包的变量除了这些我们可以使用 :if 或 :when 关键字实现更复杂的条件加载。或者通过 :requires 关键字来指定包的依赖项配置主题说了这么多,我们使用 use-package 来安装一个主题来提高一下Emacs的颜值。这里我选用 doom-themes 包中的 doom-dracula 主题。关于ui部分的配置,我们都放在 ~/.emacs.d/lisp/init-ui.el 中;; init-ui.el (use-package doom-themes :ensure t :config (load-theme 'doom-dracula t)) (provide 'init-ui)我们在init.el 中加载init-ui之后,再次打开效果如下:设置字体我们在介绍文本属性的时候使用过face这个属性,Emacs中跟文字显示相关的属性都是face,它包括:字体、字号、颜色、背景。我们之前介绍了一系列的函数来处理字体字号,但是之前介绍的只能绑定到具体的文字上,默认的字体字号使用那些函数是无法设置的。我们可以使用 set-face-attribute 来设置字体属性。该函数的定义如下(set-face-attribute FACE FRAME &rest ARGS)参数face 表示设置的是哪个部分的字体属性,例如 default(默认字体) 、mode-line (状态栏字体)、region (选中区域字体)等。我们可以使用 M-x list-faces-display 来查看支持的face参数 frame 表示需要设置哪个窗口框架(通常用 nil 表示当前窗口或所有窗口)的字体属性。参数 ARGS 来设置具体的字体属性。以下属性可用于控制字体和样式:属性名功能描述示例值:family字体名称(需系统已安装)"Fira Code", "Consolas":height字号(以百分比或绝对点数表示,默认 100 = 10pt)120(12pt), 14(14pt):weight字重(如正常、加粗)'normal, 'bold:slant字体倾斜'normal, 'italic:width字体宽度(如压缩或扩展)'normal, 'condensed:underline下划线样式nil(无), t(实线):foreground前景色(文本颜色)"#FFFFFF", "red":background背景色"#333333":inherit继承其他 FACE 的属性'fixed-pitch这里我打算使用 Source Code Pro 字体,可以在init-ui.el 中这么设置(set-face-attribute 'default nil :family "Source Code Pro" :height 120)到此为止我们已经给Emacs做了基本的美化,日常使用也不那么碍眼了。
2025年03月17日
5 阅读
0 评论
0 点赞
2025-03-16
Emacs 折腾日记(十七)——文本属性
我们在上一篇中介绍了如何对文件中的文本进行操作,本篇主要来介绍关于文本的属性。是的,文本也有属性。这里的文本属性有点类似于Word中的文字属性,文本中对应的字符只是文本属性的一种,它还包括文本大小、字体、颜色等等内容。Emacs中的文本也是类似的。于符号的属性类似,文本的属性也是由键值对构成。名 字和值都可以是一个 lisp 对象,但是通常名字都是一个符号,这样可以用这个 符号来查找相应的属性值。复制文本通常都会复制相应的字符的文本属性,但是 也可以用相应的函数只复制文本字符串,比如 substring-no-properties、 insert-buffer-substring-no-properties、buffer-substring-no-properties。产生一个带属性的字符串可以用 propertize 函数。(propertize "abc" 'face 'bold) ;; ⇒ #("abc" 0 3 (face bold))这里我们使用 face 来设置它的字体为粗体。或者我们可以使用C-x C-f 任意的打开或者创建一个文本文件,在文件中输入(insert (propertize "abc" 'face 'bold))我们可以看到它会在当前光标后面插入一个粗体的 abc 字符串。需要注意的是,我们在*scratch* buffer 中是无法看到这个效果的。因为*scratch* buffer 中开启了font-lock-mode。正如它的名字表示的那样,它锁定了字体,它里面的字体属性都是实时计算出来的。在插入文本之后它的属性很快就被修改了。因为*scratch* buffer 本质上还是一个elisp的编程环境,它里面有关于elisp的语法高亮、自动对齐等特性。它会自动的修改输入的文本属性。我们可以使用 (font-lock-mode - 1) 来关闭这个mode,然后执行上述代码就可以看到具体的效果了。虽然文本属性的名字可以是任意的,但是一些名字是有特殊含义的。属性名含义category值必须是一个符号,这个符号的属性将作为这个字符的属性face控制文本的字体和颜色font-lock-face和 face 相似,可以作为 font-lock-mode 中静态文本的 facemouse-face当鼠标停在文本上时的文本 facefontified记录是否使用 font lock 标记了 facedisplay改变文本的显示方式,比如高、低、长短、宽窄,或者用图片代替help-echo鼠标停在文本上时显示的文字keymap光标或者鼠标在文本上时使用的按键映射local-map和 keymap 类似,通常只使用 keymapsyntax-table字符的语法表read-only不能修改文本,通过 stickness 来选择可插入的位置invisible不显示在屏幕上intangible把文本作为一个整体,光标不能进入field一个特殊标记,有相应的函数可以操作带这个标记的文本cursor(不知道具体用途)pointer修改鼠标停在文本上时的图像line-spacing新的一行的距离line-height本行的高度modification-hooks修改这个字符时调用的函数insert-in-front-hooks与 modification-hooks 相似,在字符前插入调用的函数insert-behind-hooks与 modification-hooks 相似,在字符后插入调用的函数point-entered当光标进入时调用的函数point-left当光标离开时调用的函数composition将多个字符显示为一个字形这些东西我觉得也不需要记住,在需要的时候查查文档就好了。但是我参考的教程把它列出来了,那么我也在这里列出来把。由于字符串和缓冲区都可以有文本属性(如果没有特别指定文本对象的属性,那么默认使用缓冲区定义的文本属性),所以下面的函数通常不提供特定参数就是检 查当前缓冲区的文本属性,如果提供文本对象,则是操作对应的文本属性。查看文本属性查看文本对象在某处的文本属性可以用 get-text-property 函数。(setq foo (propertize "abc" 'face 'bold)) ;; ⇒ #("abc" 0 3 (face bold)) (get-text-property 0 'face foo) ;; ⇒ bold这里有两个问题需要注意一下,首先我们使用 propertize 为abc设置了文本属性face的值为bold,也就是将字符串设置为粗体。但是其中的0 和 3 代表什么呢?这里的0和3代表的是采用这个属性的字符在字符串中的范围,上面的代码中,整个abc字符串都采用整个属性,所以它的范围是[0, 3) 这个区间。要验证这一点我们可以使用下列代码(setq foo (concat "abc" (propertize "cde" 'face 'bold))) ;; ⇒ #("abccde" 3 6 (face bold)) (insert foo) 我们插入foo发现,它会插入 "abccde" 这么几个字符串,但是只有 "cde" 三个是加粗的根据这个提示,很明显的,get-text-property 中输入的0代表的就是“abc”字符串第0个,也就是字符a的属性。get-char-property 和 get-text-property 相似,但是它是先查找 overlay 的 文本属性。overlay 是缓冲区文字在屏幕上的显示方式,它属于某个缓冲区,具 有起点和终点,也具有文本属性,可以修改缓冲区对应区域上文本的显示方式。get-text-property 是查找某个属性的值,用 text-properties-at 可以得到某 个位置上文本的所有属性。修改文本属性put-text-property 可以给文本对象添加一个属性。它也是需要传入一个范围值,例如我们在前面foo的基础上使用以下代码(put-text-property 0 3 'face 'italic foo)我们再针对 foo 执行插入操作,此时会发现 abc 这个子串变成斜体了。和 put-text-property 类似,add-text-properties 可以给文本对象添加一系列的属性。和 add-text-properties 不同,可以用 set-text-properties 直接设置文本属性列表。你可以用 (set-text-properties start end nil) 来除去 某个区间上的文本属性。也可以用 remove-text-properties 和 remove-list-of-text-properties 来除去某个区域的指定文本属性。这两个函数的属性列表参数只有名字起作用,值是被忽略的。以下的例子还是建立在之前的 foo 变量之上,此时它的值为 #("baccde" 0 3 (face italic) 3 6 (face bold))。也就是前三个字符是斜体,后三个是加粗(set-text-properties 0 1 nil foo) ;; 取消了 a 字符的文本属性 foo ;; ⇒ #("abccde" 1 3 (face italic) 3 6 (face bold)) (remove-text-properties 2 4 '(face nil) foo) foo ;; ⇒ #("abccde" 1 2 (face italic) 4 6 (face bold)) (remove-list-of-text-properties 4 6 '(face nil) foo) foo ;; ⇒ #("abccde" 1 2 (face italic))查找文本属性文本属性通常都是连成一个区域的,所以查找文本属性的函数是查找属性变化的 位置。这些函数一般都不作移动,只是返回查找到的位置。使用这些函数时最好 使用 LIMIT 参数,这样可以提高效率,因为有时一个属性直到缓冲区末尾也没 有变化,在这些文本中可能就是多余的。next-property-change 查找从当前位置起任意一个文本属性发生改变的位置。 next-single-property-change 查找指定的一个文本属性改变的位置。 next-char-property-change 把 overlay 的文本属性考虑在内查找属性发生改 变的位置。next-single-property-change 类似的查找指定的一个考虑 overlay 后文本属性改变的位置。这四个函数都对应有 previous- 开头的函数,用于查找当前位置之前文本属性改变的位置(setq foo (concat "abc" (propertize "edf" 'face 'bold) (propertize "hij" 'pointer 'hand))) ;; ⇒ #("abcdefhji" 3 6 (face italic) 6 9 (face bold)) (next-property-change 1 foo) ;; ⇒ 3 (next-single-property-change 1 'pointer foo) ;; ⇒ 6text-property-any 查找区域内第一个指定属性值为给定值的字符位置。 text-property-not-all 和它相反,查找区域内第一个指定属性值不是给定值的 字符位置。(text-property-any 0 9 'face 'bold foo) ;; ⇒ 3 (text-property-not-all 3 9 'face 'bold foo) ;; ⇒ 6
2025年03月16日
4 阅读
0 评论
0 点赞
2025-03-07
Emacs 折腾日记(十五)——窗口
在上一节提到,当前buffer不一定是当前显示在屏幕上的那个buffer,想要修改显示的buffer,可以使用窗口相关的api。这节来介绍一些窗口的操作。窗口是屏幕上用于显示一个缓冲区 的部分。和它要区分开来的一个概念是 frame。frame 是 Emacs 能够使用屏幕的 部分。可以用窗口的观点来看 frame 和窗口,一个 frame 里可以容纳多个(至 少一个)窗口,而 Emacs 可以有多个 frame。不知道各位读者是否学习过MFC或者QT,这里的窗口就是MFC中的View,而frame则是整个界面框架,包括菜单栏工具栏、标题栏、状态栏等等部分。而窗口仅仅是最中间显示buffer的那一部分。分割窗口刚启动时,emacs 都是只有一个 frame 一个窗口。多个窗口都是用分割窗口的函 数生成的。分割窗口的内建函数是split-window。这个函数的参数如下:(split-window &optional window size horizontal)这个函数的功能是把当前或者指定窗口进行分割,默认分割方式是水平分割,可 以将参数中的 horizontal 设置为 non-nil 的值,变成垂直分割。如果不指定 大小,则分割后两个窗口的大小是一样的。分割后的两个窗口里的缓冲区是同 一个缓冲区。使用这个函数后,光标仍然在原窗口,而返回的新窗口对象:(split-window) ;; ==> #<window 7 on *scratch*>根据前面对于 optional 后参数的介绍,要填入 horizontal 的值实现竖直切分,需要填充前面的几个参数,如果不给则默认是nil。实际上上面的代码传入的可选参数都是nil,那么我们可以进行如下调用实现竖直分割窗口:(split-window nil nil 1) ;; ==> #<window 10 on *scratch*>我们也可以使用 selected-window 来获取当前选中的窗口,当前选中的窗口就是光标所在的窗口(split-window (selected-window) nil 1) ;; ==> #<window 11 on *scratch*>在进行实验的时候发现,分割的时候是在当前窗口的基础之上分割的,它是类似于这样的一个过程,它只在Win1所在的窗口区域进行划分,除非改变当前窗口。 +---------------+ +---------------+ | | | | | | win1 | | win1 | win2 | | | --> | | | | | | | | | | | | | +---------------+ +---------------+ | v +---------------+ +---------------+ | 4 | 5 | | | | | | | | win2 | | win1 | win2 | |--------| | <-- |-------| | | win3 | | | win3 | | | | | | | | +---------------+ +---------------+可以看成是这样一种结构(win1) -> (win1 win2) -> ((win1 win3) win2) -> (((win4 win5) win3) win2)删除窗口如果要让一个窗口不显示在屏幕上,要使用 delete-window 函数。如果没有指定 参数,删除的窗口是当前选中的窗口,如果指定了参数,删除的是这个参数对应 的窗口。删除的窗口多出来的空间会自动加到它的邻接的窗口中。如果要删除除 了当前窗口之外的窗口,可以用 delete-other-windows 函数。当一个窗口不可见之后,这个窗口对象也就消失了(setq foo (selected-window)) (delete-window foo) (windowp foo) ;; ==> t (window-live-p foo) ;; ==> nil (delete-other-windows foo) ;; ==> error, 因为先删除foo所对应的窗口,现在已经无法找到这个窗口了,所以这里删除它以外的会报错窗口配置窗口配置(window configuration) 包含了 frame 中所有窗口的位置信息:窗口 大小,显示的缓冲区,缓冲区中光标的位置和 mark,还有 fringe,滚动条等等。 用 current-window-configuration 得到当前窗口配置,用 set-window-configuration 来还原。(setq foo (selected-window)) (split-window foo nil t) (split-window) (setq wc (current-window-configuration)) (delete-other-windows foo) (set-window-configuration wc)我们一行一行的执行上述代码,会发现调用 delete-other-windows 删除之前的窗口之后再次调用 set-window-configuration 会恢复上次保存的结果。看到这里各位读者是否有这么一个想法:利用这两个函数实现一个自动保存和恢复窗口结构的功能呢?但是经过测试,current-window-configuration 得到的对象并不能持久化的保存的到文件中,即使写到文件中,读取的时候也会报错。下面是我的测试代码(setq workspace-file-path "~/.session") ;; 保存窗口的配置 (defun my/save-current-workspace () (with-temp-file workspace-file-path (print (current-window-configuration) (current-buffer)))) ;; 加载窗口的配置 (defun my/load-current-workspace () (when (file-exists-p workspace-file-path) (with-temp-buffer (insert-file-contents workspace-file-path) (set-window-configuration (read (current-buffer))))))在执行保存之后,我们查看文件得到的是一个类似于 #<window-configuration> 的字符串,并没有别的内容,在调用 set-window-configuration的时候会报错。选择窗口前面提到过可以使用 selected-window 来获取当前光标所在的窗口。我们可以使用 select-window 来选择某个窗口作为当前窗口。使用 other-window 来选择另外的窗口。该函数是一个在不同窗口之间快速跳转的一个函数,它按照窗口创建的时间的逆序进行排序,根据传入的整数参数来决定跳转到第几个窗口。(progn (setq foo (selected-window)) (message "Original window: %S" foo) (other-window 1) (message "Current window: %S" (selected-window)) (select-window foo) (message "Back to original window: %S" foo))这里有两个特殊的宏 save-selected-window 和 with-selected-window。它的作用是在执行语句之后,选择的窗口回到之前选择的窗口。with-selected-window 和 save-selected-window 几乎相同, 只不过 save-selected-window 选择了其它窗口。这两个宏不会保存窗口的位置 信息,如果执行语句结束后,保存的窗口已经消失,则会选择最后一个选择的窗口(save-selected-window (select-window (next-window)) (goto-char (point-min)))上述代码会选择另一个窗口并将光标移动到缓冲的开始位置。当前 frame 里所有的窗口可以用 window-list 函数得到。可以用 next-window 来得到在 window-list 里排在某个 window 之后的窗口。对应的用 previous-window 得到排在某个 window 之前的窗口walk-windows 可以遍历窗口,相当于 (mapc proc (window-list))。 get-window-with-predicate 用于查找符合某个条件的窗口窗口大小信息窗口是一个长方形区域,所以窗口的大小信息包括它的高度和宽度。用来度量窗 口大小的单位都是以字符数来表示,所以窗口高度为 45 指的是这个窗口可以容 纳 45 行字符,宽度为 140 是指窗口一行可以显示 140 个字符mode line 和 header line 都包含在窗口的高度里,所以有 window-height 和 window-body-height 两个函数,后者返回把 mode-line 和 header line 排除后 的高度(window-body-height) ;; ==> 53 (window-height) ;; ==> 54滚动条和 fringe 不包括在窗口的亮度里,window-width 返回窗口的宽度。所以 window-body-width 和 window-width 返回的结果一样(window-body-width) ;; ==> 234 (window-width) ;; ==> 234也可以用 window-edges 返回各个顶点的坐标信息。window-edges 返回的区域包含了 滚动条、fringe、mode line、header line 在内,如果单纯的想要返回文本所在区域可以使用 window-inside-edges(window-edges);; ==> (0 0 238 54) (window-inside-edges) ;; ==> (1 0 236 54) 如果需要的话也可以得到用像素表示的窗口位置信息,这里用到的函数是 window-pixel-edges 和 window-inside-pixel-edges(window-pixel-edges) ;; ==> (0 0 1908 922) (window-inside-pixel-edges) ;; ==> (8 0 1884 905)到目前为止,我们有了手段可以遍历窗口以及获取窗口的坐标,那么利用这些数据就可以做到记录和恢复之前的窗口布局了。我最开始的思路是采用 walk-windows 来遍历窗口,并且使用 window-pixel-edges 来记录每个窗口的区域。但是这么做有一些问题无法解决:首先还原的时候创建窗口只能采用 split-window,而 split-window 是基于之前的窗口来创建的,walk-windows 无法反映出这种层级关系。另外就是emacs 中没有函数来设置窗口左上角的坐标,我们只能通过函数来改变窗口的宽和高,窗口的位置在使用 split-window 创建的时候已经决定了。所以我们需要一种能表示层级关系的结构来存储窗口的信息。这个时候就要引入 window-tree 函数了。这个函数可以返回当前 frame 窗口布局的树状结构。为了说明它的返回值,我们先来举一个例子。首先打开emacs,此时看到只有一个窗口,暂时叫它窗口A在窗口上垂直分割一个窗口,新生成的窗口叫做窗口B,此时左侧的窗口是A,右侧的是B在B窗口上水平分割一个窗口,生成一个新的C窗口此时应该有3个窗口,它们的布局如下:+---------------+ | | | | A | B | | |-------| | | C | | | | +---------------+如果用树来表示这个布局,可以组成这么一颗树 frame / \ left right (win A) / \ / \ top bottom win B win C对于叶子节点来说,window-tree 返回的数据形式是 (DIR EDGES CHILD1 CHILD2 ...) 各部分代表的含义如下:DIR,表示分割类型,t表示竖直分割,nil表示水平分割EDGES, 表示窗口区域的坐标,格式为 (LEFT TOP RIGHT BOTTOM),以字符为单位CHILDREN, 子节点列表,可以是分支节点或叶子节点而叶子节点是一个窗口对象。上面的窗口布局,使用 window-tree 得到的结果如下( (nil (0 0 84 35) #<window 3 on *scratch*> (t (42 0 84 35) #<window 7 on *scratch*> #<window 9 on *scratch*>)) #<window 4 on *Minibuf-0*>)去除掉minibuffer部分,着重分析一下文本区域的分割(nil (0 0 84 35) #<window 3 on *scratch*> (t (42 0 84 35) #<window 7 on *scratch*> #<window 9 on *scratch*>))首先水平分割,占区域大小为 (0 0 84 35)。此时上面一个部分是 win3。下半部分右进行了分割。下半部分采用竖直方式进行分割,占区域为 (42 0 84 35)。这个部分有两个子窗口win7 和 win9。感觉分割的顺序与我们的直觉相悖。但是仔细想想好像又能产生之前那种结果 (42 0) +---------------+ | | | | win3 | win7 | | |-------| | | win9 | | | | +---------------+ (84 35)我们可以写下如下代码来进行这个结构的解析(defun my-current-window-configuration () ;; pai chu minibuffer de shu ju (my-window-tree-to-list (car (window-tree)))) (defun my-window-tree-to-list (tree) (if (windowp tree) 'win (let ((dir (car tree)) (children (cddr tree))) (list (if dir 'vertical 'horizontal) (if dir (my-window-height (car children)) (my-window-width (car children))) (my-window-tree-to-list (car children)) (if (> (length children) 2) (my-window-tree-to-list (cons dir (cons nil (cdr children)))) (my-window-tree-to-list (cadr children))))))) (defun my-window-height (win) (if (windowp win) (window-height win) (let ((edge (cadr win))) (- (nth 3 edge) (nth 1 edge))))) (defun my-window-width (win) (if (windowp win) (window-width win) (let (edge (cadr win)) (- (nth 2 edge) (car edge)))))根据这个结构编写一个还原的功能(defun my-list-to-window-tree (conf) (when (listp conf) (let (newwin) (setq newwin (split-window nil (cadr conf) (eq (car conf) 'horizontal))) (my-list-to-window-tree (nth 2 conf)) (select-window newwin) (my-list-to-window-tree (nth 3 conf))))) (defun my-set-window-configuration (winconf) (delete-other-windows) (my-list-to-window-tree winconf))可以使用如下代码进行调用(setq foo (my-current-window-configuration)) ;; do something (my-set-window-configuration foo)窗口对应的缓冲区窗口对应的缓冲区可以用 window-buffer 函数得到:(window-buffer) ;; ==> #<buffer *scratch*>缓冲区对应的窗口也可以用 get-buffer-window 得到。如果有多个窗口显示同一 个缓冲区,那这个函数只能返回其中的一个,由window-list 决定。如果要得到 所有的窗口,可以用 get-buffer-window-list(get-buffer-window (get-buffer "*scratch*")) (get-buffer-window-list (get-buffer "*scratch*"))让某个窗口显示某个缓冲区可以用 set-window-buffer 函数。 让一个缓冲区可见可以用 display-buffer。默认的行为是当缓冲区已经显示在某个窗口中时,如果不是当前选中窗口,则返回那个窗口,如果是当前选中窗口, 且如果传递的 not-this-window 参数为 non-nil 时,会新建一个窗口,显示缓 冲区。如果没有任何窗口显示这个缓冲区,则新建一个窗口显示缓冲区,并返回 这个窗口。display-buffer 是一个比较高级的命令,用户可以通过一些变量来改 变这个命令的行为。比如控制显示的 pop-up-windows, display-buffer-reuse-frames,pop-up-frames,控制新建窗口高度的 split-height-threshold,even-window-heights,控制显示的 frame 的 special-display-buffer-names,special-display-regexps, special-display-function,控制是否应该显示在当前选中窗口 same-window-buffer-names,same-window-regexps 等等。这里的函数实在是太多了,我想暂时不用都记住,现在又有各种大模型,到时候有需求直接使用问就行。或者记住这一个函数,后面要扩展自己去查文档
2025年03月07日
1 阅读
0 评论
0 点赞
2025-02-21
Emacs折腾日记(十四)——buffer操作
教程 中的下一节应该是正则表达式。但是我觉得目前来说正则表达式对我来说不是重点,而且正则表达式我还是比较了解,没必要专门去学习,在使用的时候看看相应的章节就好。况且现在有AI这个利器,在处理正则表达式应该问题不大。所以这里就略过这节,直接进入后面的学习截止到前面的一些文章,我觉得应该已经涉及到了emacs lisp中的语法要点,现在去看一些emacs配置中的代码不太会一头雾水了。离攒自己的配置又进了一步。期间我想过是不是可以跳过教程后面的内容直接进入配置的过程呢?仔细考虑了一下,我觉得还是有必要跟着教程深入了解一下操作emacs对象的一些API。就像我之前学习C/C++编程一样,如果只学语法部分,最多也就能写写基于链表等数据结构的黑框框的信息管理系统。如果想要写点带界面的或者带网络功能的或者稍微复杂点的程序就离不开操作系统,网络编程,数据库等等知识。emacs的学习可能也是这样,现在也只能写点算术运算或者say-hello 这样的玩具。想要跟emacs结合起来,真正流畅的操作emacs,还需要学一些emacs自身的知识。缓冲区名称在学习vim的时候已经很详细的了解过什么是缓冲区,以及缓冲区与文件有什么区别。在这里我想就没有必要再谈论了,如果有读者不太清楚这方面的内容,欢迎阅读我博客中关于vim缓冲区的部分。emacs中缓冲区的概念与vim基本没什么区别。唯一的区别可能是emacs中一些内置的缓冲区与vim的不太一样。emacs 里的所有缓冲区都有一个不重复的名字。所以和缓冲区相关的函数通常都是可以接受一个缓冲区对象或一个字符串作为缓冲区名查找对应的缓冲区。有一个习惯是名字以空格开头的缓冲区是临时的,用户不需要关心的缓冲区。所以现在一般显示缓冲区列表的命令都不会显示这样的变量,除非这个缓冲区关联一个文件。要得到缓冲区的名字,可以用 buffer-name 函数,它的参数是可选的,如果不指定参数,则返回当前缓冲区的名字,否则返回指定缓冲区的名字。更改一个缓冲区的名字用 rename-buffer,这是一个命令,所以你可以用 M-x 调用来修改当前缓冲区的名字。如果你指定的名字与现有的缓冲区冲突,则会产生一个错误,除非你使用第二个可选参数以产生一个不相同的名字,通常是在名字后加上 <序号> 的方式使名字变得不同。你也可以用 generate-new-buffer-name 来产生一个唯一的缓冲区名。它需要传入一个参数,表示buffer的名称,如果当前buffer有名称与指定名称冲突,它会在你提供的名称后面加一些后缀,否则就采用传入的名称;; scratch buffer 中 (buffer-name) ;; ==> *scratch* ;; 此时 *scratch* 已经被重命名成了 scratch (rename-buffer (generate-new-buffer-name "scratch")) ;; ==> scratch当前缓冲区可以使用 current-buffer 来获取当前缓冲区,需要注意的是当前缓冲区不一定是显示在当前屏幕上的那个缓冲区。这个跟工作目录有点像,当前目录并不一定就是程序所在的目录或者当前打开的文件所在的目录。我们可以使用 set-buffer 来设置当前缓冲区,但是前面我们说过当前缓冲区并不一定是显示在屏幕上的那个缓冲区,即使修改当前缓冲区,也不会改变当前窗口上显示的缓冲区(set-buffer "*Messages*") ;; ==> #<buffer *Messages*>,但是屏幕上显示的缓冲区没有变化如果要切换当前屏幕显示的缓冲区需要配置窗口相关的函数,例如我们可以使用 switch-to-buffer(switch-to-buffer "*Messages*")前面提到 set-buffer 可以改变当前缓冲区,但是我们调用 buffer-name 获取当前缓冲区的名称时得到还是 scratch buffer(set-buffer "*Messages*") ;; ==> #<buffer *Messages*> (buffer-name) ;; ==> #<buffer *scratch*> (buffer-name) ;; ==> "*scratch*"这是因为我们如果采用 C-x C-e 来分别执行,这就相当于在命令行执行命令一样,每次语句结束之后,emacs会重新刷新上下文环境。而 buffer-name获取的是当前上下文环境中的当前缓冲区名称。上下文环境随着上一条语句的结束而更新,这就导致了当前缓冲区变化。这个过程可以描述为如下的过程[主进程环境] │ ├── [逐行执行L1] → 创建临时子环境 → 执行set-buffer → 销毁子环境 └── [逐行执行L2] → 创建新子环境 → 读取buffer-name → 返回主环境值 所以如果要得到正确的结果,就是一次性执行完这两条语句,按照我当前的知识储备有三种办法:第一个办法就是使用 eval-buffer,从messages buffer 中获取输出信息第二个办法,使用 progn 将两条语句包含起来第三个办法就是将它包装成一个函数或者宏来执行(progn (set-buffer "*Messages*") (buffer-name)) ;; ==> "*Messages*"但是我们不能仅仅依靠这种包裹代码的方式来实现切换buffer的效果。因为这个命令很可以会被另一个程序员来调用。你也不能直接用 set-buffer 设置成原来的缓冲区,因为set-buffer不能处理错误或退出情况。正确的作法是使用 save-current-buffer、with-current-buffer 和 save-excursion 等方法save-current-buffer 能保存当前缓冲区,执行其中的表达式,最后恢复为原来的缓冲区。如果原来的缓冲区被关闭了,则使用最后使用的那个当前缓冲区作为语句返回后的当前缓冲区。lisp 中很多以 with 开头的宏,这些宏通常是在不改变当前状态下,临时用另一个变量代替现有变量执行语句。比如 with-current-buffer 使用另一个缓冲区作为当前缓冲区,语句执行结束后恢复成执行之前的那个缓冲区save-excursion 与 save-current-buffer 不同之处在于,它不仅保存当前缓冲区,还保存了当前的位置和 mark。在 scratch 缓冲区中运行下面两个语句就能看出它们的差别了(save-current-buffer (set-buffer "*scratch*") (goto-char (point-min)) (save-excursion (set-buffer "*scratch*") (goto-char (point-min))上面两段代码,都是先保存当前缓冲区,然后使用 set-buffer 保证当前缓冲区是 scratch buffer,接着调用goto-char移动鼠标光标到buffer最开始的位置。随着代码块的结束,会自动切换回对应的buffer。但是因为 save-excursion 会额外保存当前位置和 mark,所以我们发现第一段代码光标位置跑到缓冲区最开始的位置,而第二段代码光标位置不变在对比一下它们与 with-current-buffer 的区别,with-current-buffer 调用时已经帮我们使用 set-buffer设置好了当前缓冲区,而且也会保存当前缓冲区,在结束之后也会还原当前缓冲区。但是它使用的是 save-current-buffer。我们可以使用 C-h C-f 来查看并找到它的源代码(defmacro with-current-buffer (buffer-or-name &rest body) (declare (indent 1) (debug t)) `(save-current-buffer (set-buffer ,buffer-or-name) ,@body))我们发现它其实就是用 save-current-buffer 做了一次封装。如果我们对上面的测试代码稍加修改使用 with-current-buffer 实现,例如(with-current-buffer "*Messages*" (goto-char (point-min)))执行之后我们发现,它的光标位置也改变了。创建和关闭缓冲区产生一个缓冲区必须用给这个缓冲区一个名字,所以两个能产生新缓冲区的函数都是以一个字符串为参数:get-buffer-create 和 generate-new-buffer。这两个函数的差别在于前者如果给定名字的缓冲区已经存在,则返回这个缓冲区对象,否则新建一个缓冲区,名字为参数字符串,而后者在给定名字的缓冲区存在时,会使用加上后缀 (N 是一个整数,从2开始) 的名字创建新的缓冲区。(get-buffer-create "temp") (with-current-buffer "temp" (insert "this is temp buffer")) (switch-to-buffer "temp")上面的代码,我们先创建一个新的temp buffer,并且切换到这个buffer,然后在这个buffer中调用insert函数,插入一段话。最后可以让窗口显示这个buffer来验证结果关闭一个缓冲区可以用 kill-buffer。当关闭缓冲区时,如果要用户确认是否要关闭缓冲区,可以加到 kill-buffer-query-functions 里。如果要做一些善后处理,可以用 kill-buffer-hook。通常一个接受缓冲区作为参数的函数都需要参数所指定的缓冲区是存在的。如果要确认一个缓冲区是否依然还存在可以使用 buffer-live-p。要对所有缓冲区进行某个操作,可以用 buffer-list获得所有缓冲区的列表。如果你只是想使用一个临时的缓冲区,而不想先建一个缓冲区,使用结束后又需要关闭这个缓冲区,可以用 with-temp-buffer 这个宏。从这个宏的名字可以看出,它所做的事情是先新建一个临时缓冲区,并把这个缓冲区作为当前缓冲区,使用结束后,关闭这个缓冲区,并恢复之前的缓冲区为当前缓冲区。在缓冲区内移动在学会移动函数之前,先要理解两个概念:位置(position)和标记(mark)。位置是指某个字符在缓冲区内的下标,它从1开始。更准确的说位置是在两个字符之间,所以有在位置之前的字符和在位置之后的字符之说。但是通常我们说在某个位置的字符都是指在这个位置之后的字符。这点很符合我们的直觉,一般在编写代码或者文档的时候当前的光标就是在文本之间移动。标记和位置的区别在于位置会随文本插入和删除而改变位置。一个标记包含了缓冲区和位置两个信息。在插入和删除缓冲区里的文本时,所有的标记都会检查一遍,并重新设置位置。这对于含有大量标记的缓冲区处理是很花时间的,所以当你确认某个标记不用的话应该释放这个标记。创建一个标记使用函数 make-marker。这样产生的标记不会指向任何地方。你需要用 set-marker 命令来设置标记的位置和缓冲区。(setq foo (make-marker)) ; ==> #<marker in no buffer> (set-marker foo (point)) ; ==> #<marker at 195 in *scratch*>point 函数其实返回一个整数,表示当前光标在哪个位置,既然这里只用传入位置就可以正确的将foo这个标签绑定到对应的缓冲区,这里set-marker 应该是以当前缓冲区作为标签的缓冲区,我们可以使用下面的代码来验证(with-current-buffer "*Messages*" (set-marker foo (point))) ;; ==> #<marker at 477 in *Messages*>也可以用 point-marker 直接得到 point 处的标记。或者用 copy-marker 复制一个标记或者直接用位置生成一个标记(point-marker) ;; ==> #<marker at 211 in *scratch*> (copy-marker 20) ;; ==> #<marker at 20 in *scratch*> (copy-marker foo) ;; ==> #<marker at 195 in *scratch*>如果要得一个标记的内容,可以用 marker-position,marker-buffer(marker-position foo) ;; ==> 195 (marker-buffer foo) ;; ==> #<buffer *scratch*>位置就是一个整数,而标记在一般情况下都是以整数的形式使用,所以很多接受整数运算的函数也可以接受标记为参数。比如加减乘。(goto-char (+ (marker-position foo) 10)例如上面的代码我们移动光标到第205个字符的位置。和缓冲区相关的变量,有的可以用变量得到,比如缓冲区关联的文件名,有的只能用函数来得到,比如 point。point 是一个特殊的缓冲区位置,许多命令在这个位置进行文本插入。每个缓冲区都有一个 point 值,它总是比函数 point-min 大,比另一个函数 point-max 返回值小。注意,point-min 的返回值不一定是 1,point-max 的返回值也不定是比缓冲区大小函数 buffer-size 的返回值大 1 的数,因为 emacs 可以把一个缓冲区缩小(narrow)到一个区域,这时 point-min 和 point-max 返回值就是这个区域的起点和终点位置。所以要得到 point 的范围,只能用这两个函数,而不能用 1 和 buffer-size 函数。按单个字符位置来移动的函数主要使用 goto-char 和 forward-char、backward-char。前者是按缓冲区的绝对位置移动,而后者是按 point 的偏移位置移动比如(goto-char (point-min)) ; 跳到缓冲区开始位置 (forward-char 10) ; 向前移动 10 个字符 (forward-char -10) ; 向后移动 10 个字符 (backward-char 10) ; 向后移动 10 个字符 (backward-char -10) ; 向前移动 10 个字符按词移动使用 forward-word 和 backward-word。至于什么是词,这就要看语法表格的定义了。按行移动使用 forward-line。没有 backward-line。forward-line 每次移动都是移动到行首的。所以,如果要移动到当前行的行首,使用 (forward-line 0)。如果不想移动就得到行首和行尾的位置,可以用 line-beginning-position 和 line-end-position。得到当前行的行号可以用 line-number-at-pos。需要注意的是这个行号是从当前状态下的行号,如果使用 narrow-to-region 或者用 widen 之后都有可能改变行号。由于 point 只能在 point-min 和 point-max 之间,所以 point 位置测试有时是很重要的,特别是在循环条件测试里。常用的测试函数是 bobp(beginning of buffer predicate)和 eobp(end of buffer predicate)。对于行位置测试使用 bolp(beginning of line predicate)和 eolp(end of line predicate)缓冲区的内容要得到整个缓冲区的文本,可以用 buffer-string 函数。如果只要一个区间的文本,使用 buffer-substring 函数。point 附近的字符可以用 char-after 和 char-before 得到。point 处的词可以用 current-word 得到,其它类型的文本,比如符号,数字,S 表达式等等,可以用 thing-at-point 函数得到。ting-at-point 可以获取光标处的很多类型的内容,它需要传入一个符号作为类型,例如 'word、'symbol、'url。不同的类型包含有不同的文本属性,第二个参数表示是否去除文本属性。如果当前位置有内容,则返回内容,否则返回nil(defun show-current-word () (interactive) (let ((word (thing-at-point 'word t))) ;; 'word 类型,t 表示去除文本属性 (if word (message "当前单词: %s" word) (message "光标位置没有单词"))))修改缓冲区的内容要修改缓冲区的内容,最常见的就是插入、删除、查找、替换了。下面就分别介绍这几种操作。插入文本最常用的命令是 insert。它可以插入一个或者多个字符串到当前缓冲区的 point 后。也可以用 insert-char 插入单个字符。插入另一个缓冲区的一个区域使用 insert-buffer-substring。删除一个或多个字符使用 delete-char 或 delete-backward-char。删除一个区间使用 delete-region。如果既要删除一个区间又要得到这部分的内容使用 delete-and-extract-region,它返回包含被删除部分的字符串。最常用的查找函数是 re-search-forward 和 re-search-backward。这两个函数参数如下(re-search-forward REGEXP &optional BOUND NOERROR COUNT) (re-search-backward REGEXP &optional BOUND NOERROR COUNT)其中 BOUND 指定查找的范围,默认是 point-max(对于 re-search-forward)或 point-min(对于 re-search-backward),NOERROR 是当查找失败后是否要产生一个错误,一般来说在 elisp 里都是自己进行错误处理,所以这个一般设置为 t,这样在查找成功后返回区配的位置,失败后会返回 nil。COUNT 是指定查找匹配的次数。替换一般都是在查找之后进行,也是使用 replace-match 函数。和字符串的替换不同的是不需要指定替换的对象了。
2025年02月21日
8 阅读
0 评论
0 点赞
2025-02-11
Emacs 折腾日记(十二)——变量
本文是依据 emacs lisp 简明教程 而来在此之前我们已经了解了elisp中的全局变量和函数中的局部变量,也了解了elisp中各种数据类型。这一篇主要谈谈elisp中各种变量的生命周期和作用域let 绑定的变量使用let绑定的变量只在let范围内有效,如果是多层嵌套的let,只有最里层的那个变量是有效的,用 setq 改变的也只是最里层的变量,而不影响外层的变量。比如(progn (setq foo "I'm global variable!") (let ((foo 5)) (message "foo value is: %S" foo) ;; ==> "foo value is: 5" (let (foo) (setq foo "I'm local variable!") (message foo)) ;; ==> "i’m local variable!" (message "foo value is still: %S" foo)) ;; ==> "foo value is still: 5" (message foo)) ;; ==> "i’m global variable!"这个有点像C/C++中的{} 定义的语句块中的变量只在当前 {} 内有效,当内部变量与外部变量重名的时候只影响{}内,而不影响外部。我们可以给出这样的c++代码int n = 0; printf("n = %d\n", n); { int n = 10; printf("n = %d\n", n); } printf("n = %d\n", n);elisp中的let两边的括号就有点像这里的 {},出了这个范围定义的变量就无效了。但是定义变量使用的是栈空间,而程序的栈空间是有大小限制的,一旦超过这个范围就会发生栈溢出。在C/C++程序中一般发生在递归层数过大。我们可以在编译时修改这个栈空间的大小。在elisp中也有变量控制递归层数,它就是 max-specpdl-size 但是在最新的29中已经将它弃用,新版的emacs可以使用 max-lisp-eval-depth 来限制specpdl堆栈的大小,该堆栈主要用于 存储动态变量绑定和 unwind-protect 激活等。buffer-local 变量顾名思义,它的值只在当前buffer中生效,在其他buffer中可能是另外的值。它有点像之前介绍的vim中的setlocal变量,仅在当前缓冲区内生效。该特性常用于实现缓冲区特定的配置和行为。声明一个 buffer-local 的变量可以用 make-variable-buffer-local 或用 make-local-variable。这两个函数的区别在于前者是在所有缓冲区都创建一个 buffer-local 的变量。而后者只在声明时所在的缓冲区内产生一个局部变量,而其它缓冲区仍然使用的是全局变量。一般来说推荐使用 make-local-variable。下面来举例说明它们的区别,在举例之前介绍例子中用到的函数或者宏with-current-buffer, 它的使用方式是(with-current-buffer buffer body)其中 buffer 可以是一个缓冲区对象,也可以是缓冲区的名字。它的作用是使其中的 body 表达式在指定的缓冲区里执行。default-value 可以访问符号所对应的全局变量下面是使用 make-local-variable 创建buffer-local变量的例子(setq foo "i'm a global variable!") (make-local-variable 'foo) foo ;; ==> "i'm a global variable!" (setq foo "i'm buffer-local variable in scratch buffer") foo ;; ==> "i'm buffer-local variable in scratch buffer" (with-current-buffer "*Messages*" (progn (message "%s" foo) ;; ==> "i'm a global variable!" (setq foo "i'm buffer-local variable in message buffer") (message "%s" foo))) ;; ==> "i'm buffer-local variable in message buffer" (default-value 'foo) ;; ==> i'm buffer-local variable in message buffer上述代码因为message buffer 中未定义foo的buffer-local 变量,所以它修改的是全局变量的值,我们使用 default-value 发现全局变量的值被修改了。(setq foo "i'm a global variable!") (make-variable-buffer-local 'foo) foo (setq foo "i'm buffer-local variable in scratch buffer") foo (with-current-buffer "*Messages*" (progn (message "%s" foo) (setq foo "i'm buffer-local variable in message buffer") (message "%s" foo))) (default-value 'foo) ;; ==> "i'm a global variable!"前面的结果都一样,但是我们关注一下最后输出的全局的 foo 变量,它的值没有被修改,而使用 make-local-variable 的时候它被修改了。这是因为 make-variable-buffer-local 会在每一个缓冲区内都创建一个 buffer-local 的拷贝,所以后面在 message 缓冲区中修改的是缓冲区内自己的 buffer-local 变量而不影响全局变量的值,而 make-local-variable则不同,它会在 message 缓冲区中为foo也创建一个buffer-local变量。message 缓冲区中修改的是 buffer-local 变量,不影响全局的foo变量。一般来说根据实际情况选择使用哪种,我并不是资深的elisp开发者,以我浅薄的认知,一般使用 make-local-variable情况较多,它影响面较小,仅仅影响当前缓冲区。而make-variable-buffer-local 影响面较大,一旦使用它设置了local-buffer 变量,那么在后面其他缓冲区中想要使用 setq 设置全局的值就没那么简单了。这种一般是需要隐藏起来的核心变量,例如某些功能依靠这个变量来驱动,一旦修改了可能导致后面的代码运行行为不准确。可能会使用 make-variable-buffer-local 定义,不让用户自己随便修改全局的值。我们可以使用 setq-default 来设置全局变量的值。这里的例子就不给出了,各位读者可以根据上面的例子稍加修改就可以了。测试一个变量是不是 buffer-local 可以用 local-variable-p。这里我们再介绍一个新的函数 get-buffer。它需要一个buffer的名称作为参数,返回这个buffer的对象,如果未找到对应的buffer,则返回nil。(setq foo 5) (make-local-variable 'foo) (local-variable-p 'foo) ;; ==> t (local-variable-p 'foo (get-buffer "*Messages*")) ;; ==> nil如果要在当前缓冲区里得到其它缓冲区的 buffer-local 变量的值可以用 buffer-local-value(setq foo "i'm a global variable!") (make-local-variable 'foo) (setq foo "i'm buffer-local variable in scratch buffer") (with-current-buffer "*Messages*" (buffer-local-value 'foo (get-buffer "*scratch*"))) ;; ==> "i'm buffer-local variable in scratch buffer"变量的作用域在之前我们已经介绍过几种变量,分别是使用 setq 或者 defvar 定义的变量,它们是全局变量,即使在一些语法块或者函数中定义的,在外围也可以正常访问使用 let 或者 let* 定义的变量,只在 let 语法块中有效使用 make-local-variable 或者 make-variable-buffer-local 定义的buffer-local 变量,在当前buffer中有效但是函数参数列表的变量生命周期与我们平常在C/C++、Java、Python等语言中有些不同。作用域(scope)是指变量在代码中能够访问的位置。emacs lisp 这种绑定称为 indefinite scope。indefinite scope 也就是说可以在任何位置都可能访问一个变量名。而 lexical scope(词法作用域)指局部变量只能作用在函数中和一个块里(block)。比如 let 绑定和函数参数列表的变量在整个表达式内都是可见的,这有别于其它语言词法作用域的变量。先看下面这个例子(defun foo(x) (getx)) (defun getx() x) (message "%s" (foo "hello,x")) ;; ==> "hello,x"我们可以看到最终成功输出了结果,而根据之前学习C/C++的经验,在C、C++等语言中,这样的代码是无法执行成功的,因为在getx中并未定义x的值。但是在elisp 中,foo函数执行期间,x变量的都是有效的且可以正常访问到的。当然,在elisp中,let也具有这一效果,在let的语法块中,定义的变量总是有效的。例如(let ((x "hello, x")) (message "%s" (getx))) (defun getx() x)在let语句块中 x 是一直有效的,如果脱离let,在最外层调用 getx 将会得到一个x未定义的错误需要注意的是,上面的例子无法再使用 C-x C-e 来一条条的执行了,这个时候需要使用 eval-buffer 来执行整个缓冲区的代码。emacs 从 24.1 版本开始,引入了 lexical binding 这一特性,在代码中如果启用这个属性,那么它将采用C/C++ 等普通编程语言的那种 lexical scope 的作用形式。官方文档中提到使用 lexical binding 特性可以提高代码的运行效率,并且鼓励使用。可以在代码文件最开始的位置添加这么一个注释 -*- lexical-binding: t -*- 来开启这一特性。上面的代码只要加上这一特性就能得到不一样的结果;; -*- lexical-binding: t -*- (let ((x "hello, x")) (message "%s" (getx))) (defun getx() x)这个时候执行将会得到一个错误信息 "Symbol’s value as variable is void: x",x这个符号是一个未定义的变量。生存期是指程序运行过程中,变量什么时候是有效的。全局变量和 buffer-local 变量都是始终存在的,前者只能当关闭emacs 或者用 unintern 从 obarray 里除去时才能消除。而 buffer-local 的变量也只能关闭缓冲区或者用 kill-local-variable 才会消失。而对于局部变量,elisp 使用的方式称为动态生存期:只有当绑定了这个变量的表达式运行时才是有效的。在 emacs lisp 简明教程 中举了一个闭包的例子,在elisp中并不支持闭包,它采用的是与普通编程语言一样的变量生存周期,这里我就不列出来了。JavaScript等语言中是支持闭包的,有兴趣的读者可以去看看JavaScript中的闭包。其他函数一个符号如果值为空,直接使用可能会产生一个错误。可以用 boundp 来测试一个变量是否有定义。这通常用于 elisp 扩展的移植(用于不同版本或 XEmacs)。对于一个 buffer-local 变量,它的缺省值可能是没有定义的,这时用 default-value 函数可能会出错。这时就先用 default-boundp 先进行测试。使一个变量的值重新为空,可以用 makunbound。要消除一个 buffer-local 变量用函数 kill-local-variable。可以用 kill-all-local-variables 消除所有的 buffer-local 变量。但是有属性 permanent-local 的不会消除,带有这些标记的变量一般都是和缓冲区模式无关的,比如输入法。(setq foo "I'm local variable!") foo ; ==> "I'm local variable!" (boundp 'foo) ; ==> t (default-boundp 'foo) ; ==> t (with-current-buffer "*Messages*" (boundp 'foo)) ; ==> t (makunbound 'foo) ; ==> foo foo ; This will signal an error (boundp 'foo) ; ==> t (default-boundp 'foo) ; ==> t (kill-local-variable 'foo) ; ==> foo (with-current-buffer "*Messages*" (boundp 'foo)) ; ==> t上面的例子需要注意以下几点这里只是使变量的值为空,并没有消除这个变量的符号。所以在执行makunbound 之后,关于foo 是否绑定的测试都是tkill-local-variable 只是消除了foo作为buffer-local变量,并没有影响到全局变量,所以在messages-buffer中测试它仍然是有效的变量变量命名习惯对于变量的命名,有一些习惯,这样可以从变量名就能看出变量的用途:hook 一个在特定情况下调用的函数列表,比如关闭缓冲区时,进入某个模式时。function 值为一个函数functions 值为一个函数列表flag 值为 nil 或 non-nilpredicate 值是一个作判断的函数,返回 nil 或 non-nilprogram 或 -command 一个程序或 shell 命令名form 一个表达式forms 一个表达式列表。map 一个按键映射(keymap)
2025年02月11日
14 阅读
0 评论
0 点赞
2025-01-21
Emacs折腾日记(十一)——求值规则
截至到现在,我觉得我自己的elisp水平有了一定的提高,希望各位读者借助之前的文章也能有一些收获。现在已经可以尝试写一点elisp的程序了,但是如果想深入了解一下 lisp 是如何工作的,不妨先花些时间看看 lisp 的求值过程。对于我这样一个日常使用C/C++的程序员来说,习惯了C/C++的语法和写法,初次见到lisp这样使用括号并且主要是S-表达式的语言,开始总会有点不习惯,但是在尝试自己写了这么些文章之后,对lisp有那么一点感觉。这篇我想就着 求值规则 这篇文章以及自己的一些理解来尝试梳理一下自己是如何理解elisp表达式的。S表达式要理解S表达式,我们先从如何解析四则运算开始。在之前我鸽了一个系列就是使用C来实现C语言解析器的系列。在那个系列中提到,一个普通的4则运算最终会生成一个抽象语法树,例如 a * b - (c + d) 最终可以生成如下的抽象语法树 - / \ * + / \ / \ a b c d二叉树的每个节点,或者是叶节点,或者有2个子节点,叶节点可以用来存储数据。而每颗子树的根节点存储操作符,或者说表示要对数据进行的操作,而如果操作符需要一个或者多个操作数,那么可以对抽象语法树进行调整。可以用上面的图来表示树的话有些麻烦了,后来发明了点对表示法, 如果只关心叶子节点,每颗子树的根节点采用.来表示,那么这颗二叉树可以表示为 ((a . b) . (c . d)) 。看到这里各位读者想到了什么呢?cons cell。S表达式是点对表示法的形式定义:原子 -> 数字 | 符号 S表达式 -> 原子 | (S表达式 . S表达式)所以,S表达式或者是原子,或者是递归的由其他S表达式构成的点对。虽然抽象语法树可以使用这种点对来描述,但是语法树大了,点的数量大了,其实也挺麻烦的,所以lisp中有一些简单的写法。回顾一下之前学习列表和cons cell的知识,简化也就得到了列表,例如'((a . b) . (c . d)) ⇒ ((a . b) c . d) '((a . b) . (c . (d . nil))) ⇒ ((a . b) c d)如果我们考虑这颗树的根节点,并且采用先序遍历的方式访问,结果仍然采用点对来表示,那么将得到这样的结果 (- (* a b) (+ c d))。 这样我们得到了计算这个四则运算的lisp代码,这个它可以作为列表,也可以让lisp解释器来执行。到此为止,各位读者应该理解了S表达式。它其实就是对应了一颗语法树。现在看到S表达式也不那么恐惧了,解释器如何执行它似乎也慢慢的清晰起来了呢S表达式的求值理解了S表达式,再回过头来看看它的求值过程。所有的表达式可以分为三种:符号、列表和其它类型。我们来分别说明最简单的就是自求值表达式,前面说过数字、字符串、向量都是自求值表达式。还有两个特殊的符号 t 和 nil 也可以看成是自求值表达式。第二种表达式是符号。符号的求值结果就是符号的值。如果它没有值,就会出现 void-variable 的错误。第三种表达式是列表表达式。而列表表达式又可以根据第一个元素分为函数调用、宏调用和特殊表达式(special form)三种。根据上面对S表达式的理解,这里的第一个元素也就是放在语法树的每颗子树的根节点上,表示对它的子节点进行的操作,例如上面的加减乘除,或者使用car之类的函数。而它的子节点可以是一颗语法树,也可以是简单的值,对应在elisp中的话,就是这个操作可以针对上面两种自求值表达式或者符号值,也可以是另一个S表达式。整个求值过程就是不断的求子树然后使用根节点来对子树进行操作,例如针对上面的二叉树可以写下这么一段伪代码来实现求值float calc-ast(ast* pRoot) { switch(pRoot->eOprType) { case function: //函数调用 return function(calc-ast(pRoot->left), calc-ast(pRoot->right)); case math: // 数学计算 return calc-ast(pRoot->left) + calc-ast(pRoot->right); //这里以加法为例 .... default: calc-ast(pRoot->left); calc-ast(pRoot->right); break } }第一个元素如果是一个特殊表达式时,它的参数可能并不会全求值。这些特殊表达式通常是用于控制结构或者变量绑定。每个特殊表达式都有对应的求值规则。这个就根据具体的语法来定,例如 and 和 or 这些操作符具有短路的特性。本文内容到此就结束了,本文比较简单,算是对之前的一个总结,对lisp有一个大概的了解。最后,本文可能是年前最后一篇文章了,在这里提前祝各位读者新年快乐!
2025年01月21日
8 阅读
0 评论
0 点赞
1
2
3