如何在Emacs中打开文件:有关Lisp,技术和人类进步的故事

2021-01-04 09:04:37

我最近加入一家公司,出于安全原因,该公司不允许将其源代码存储在笔记本电脑上。开发仅在专用网络内部的工作站上进行,其中一些使用其文本编辑器的支持进行远程文件编辑,而另一些则通过SSH在这些计算机上运行编辑器。

适应这种情况间接使我陷入困境,迫使我承认自己的核心价值观,更好地理解进步与人类繁荣之间的关系,并思考以下问题:为什么选择技术?

Windows记事本,单内核操作系统和国际空间站之间的一种混合。

我使用Emacs已有一段时间了,有机会使用它来处理远程机器上的项目。有一些怪癖,但在各处更改设置后,流浪汉几乎可以使用。使用Tramp,可以透明地将远程目录,文件,命令等视为本地目录。通过M-x查找文件/ ssh:user @ remote-host:/ some / project打开远程缓冲区,开箱即用Magit和Eshellwork之类的工具感觉很神奇。

Emacs可作为独立的图形应用程序(GUI)或在终端仿真器(TUI)中运行。在功能方面,GUI Emacs可以看作是TUI Emacs的超集。除其他外,它还可以显示:

TUI Emacs具有终端仿真器(通常也为tmux)限制并与剪贴板,键绑定,颜色等冲突。这些限制在GUI Emacs中均不存在。

由于这些及其他原因,我使用GUI Emacs和Tramp而不是通过SSH的TUI Emacs来处理远程项目。有些人则相反,这也很好!即使存在上述限制,TUI Emacs还是非常强大的,并且它在SSH会话中的性能仍然优于Tramp。

即使这样,人们可能会认为在Emacs中显示图像并不是什么大问题。只需打开image.png并获得一个实际的图像查看器应用程序来渲染它,对吗?在远程Tramp缓冲区中,事情变得很有趣:M-x find-file image.png将在本地Emacsinstance中在那里显示远程图像。或者,如果您在远程Dired缓冲区中,则在图像文件上按RET也可以这样做。这是一种无中断的工作流程,可让您始终保持Emacs的舒适感。

如果您不是Emacs用户,则前一句话可能不太重要。留在Emacs中有什么好处?

可以安全地假设您的大部分开发或更广义的计算环境如下所示:

上面的每个项目都是一个离散的计算机程序。它们是可扩展的,互不相关的方式,并且程度不同。他们是岛屿。大多数时候,将它们集成在一起是一场艰苦的战斗,并且通常只是不切实际的。使用围绕TUI Vimand tmux的计算环境已经有十年了,我知道尝试使shell,终端和运行于其中的每个命令行应用程序具有与Vim相同的外观,感觉和行为的痛苦。是不可能的。

Web浏览器作为软件平台的出现部分是这种断开连接的答案。

相比之下,Emacs可以按照人们的意愿统一和均衡计算体验,因此它可以在单个凝聚的环境中发生并扩展。

人们通常使用Emacs来处理其电子邮件收件箱,交流信息,浏览网络,编写代码和散文。这些活动在本质上是相同的:文本编辑。能够使用相同的键绑定和功能来执行它们,例如移动,搜索,文本操作,完成,撤消重做,复制粘贴,这已经很重要了,但这还不是全部:将它们集成到一个环境中将释放令人愉悦且高效的工作流程在其他地方不方便。

为您的伴侣刚刚问您的事情创建待办事项(当您专心于一项任务时),该事项会自动显示在明天的议程上吗?只需几次击键,您就可以回到任务。您可以通过LaTeX将一些文本注释快速转换为漂亮的PDF,然后将其上传到S3,然后在经过改写的现有gitcommit消息中引用该URL?如果您是经验丰富的Emacs用户,那么您可能会不费吹灰之力就可以直观地看到这些代码,而不必费心Emacs。

这种互连性是Emacs提供的计算环境价值的一部分。整体变得大于各部分之和。

可编程性赋予了它更大的价值范围:可以按照您认为合适的方式,以任意的并且可能是无法预料的方式组合零件。

现在,回到显示远程主机用例的图像。为此,我可以想到一些替代方案:

从能够渲染图像并使用像icat这样的程序的现代终端仿真器ssh到远程主机

在工作中,我每天为7万个文件,8位代码行和数百个PR合并而成的中等大小的monorepo做贡献。一天,我在该存储库中打开了一个远程缓冲区,并运行了M-x查找文件。

find-file是一种交互式功能,可显示当前目录中缩小的文件列表,提示用户筛选和滚动浏览候选对象,并打开文件。

Emacs冻结了5秒钟,然后才向我显示查找文件提示。这不好,因为在编写软件时,打开文件实际上是人们一直需要做的事情。

幸运的是,Emacs是“可扩展,可自定义,自记录实时显示编辑器”,并具有概要分析功能:Mx profiler-启动启动配置文件,Mx profiler-report显示调用树,显示每个函数调用花费了多少CPU周期。在启动配置文件后,运行配置文件并运行Mx查找文件表明,所有时间都花在了名为ffap-guess-file-name-at-point的函数中,而该函数又被file-name-point-point-functions所调用,调用find-file时发生异常的钩子运行。

如果您熟悉Vim,则可以将Emacs钩子视为Vim自动命令,但只有更好的人体工程学。

我用Mx describe-function ffap-guess-file-name-point-point检查了ffap-guess-file-point-point-point的文档,这似乎不是必需的,所以我通过运行Mx删除了钩eval-expression,在下面编写表格,然后按RET。

每次我运行查找文件时,这解决了Emacs阻塞5秒钟的直接问题,没有明显的缺点。

在撰写本文时,我尝试通过将ffap-guess-file-point-point-point-point-point-point-name重新添加到point-name-point-point-functions中来重现该问题。最初的问题可能是通过临时代码评估(从配置中定义的状态漂移)手动更改了Emacs环境引起的

或以上的某种组合。我不知道到底是什么,这是说:维护Emacs配置很复杂。

我现在可以浏览并打开文件。我在此远程git存储库中尝试的下一件事是搜索项目文件。很棒的弹丸包为此提供了弹丸查找文件功能,但是我之前已经放弃了让弹丸在远程缓冲区中表现良好的打算。考虑到当前如何实施,这似乎是不切实际的。因此,我安装了“在项目中查找文件”软件包,专门用于远程项目:M-x软件包安装“在项目中查找文件”。

大多数Emacs命令可通过组合键进行访问,其默认值可以自定义为所需的任何内容。我会坚持自己引用命令名称,而不是默认键绑定。

这导致projectile-project-root函数不在远程缓冲区上运行其通常的实现,而是无条件返回nil。 projectile-project-root用于获取给定缓冲区的项目根目录(远程或非远程),或用作布尔谓词以测试缓冲区是否在项目中(例如git存储库目录)。在远程缓冲区上使它返回nil可以有效地禁用远程缓冲区上的射弹。

Emacs建议是一种无需重新定义现有功能即可修改其行为的方式。它们的作用类似于钩子,但更加灵活。

然后,我编写了一个函数,该函数在禁用弹丸时将回退到ffip并将其绑定到我对弹丸查找文件的键绑定中,这样我想搜索项目文件时就可以按相同的键绑定,而不必考虑是否在远程缓冲区上还是不在:

(defun may-projectile-find-file()"如果在项目缓冲区中则运行`projectile-find-file'否则为`ffip'。"(交互式)(if(projectile- project-p)(投射物查找文件)(ffip)))

Emacs冻结了30秒。之后,它会显示提示,并带有缩小的项目列表。 30秒!在整个过程中都在做什么?让我们再次尝试分析器。

这告诉我们98%的CPU时间都花在了...上。在一行上按TAB将通过显示其子函数调用来扩展它。

功能CPU样本%-... 21027 98%+常春藤-插入迷你缓冲区13689 64%+#<已编译0x131f715d2b6fa0a8> 3819 17%自动GC 2017 9%+ shell命令1424 6%+ ffip-get-project-root目录77 0%+运行模式钩1 0%+命令执行361 1%

扩展...表明Emacs在常春藤(插入迷你缓冲区)中花费了64%的CPU时间,在9%的时间(大约3整秒!)内收集了垃圾。我将垃圾收集消息设置为t,所以我已经可以说Emacs进行了很多GC操作。启用此设置后,每当Emacs垃圾回收时,都会在回显区域显示消息。我还可以看到Emacs进程在冻结且对输入无响应的同时消耗了一个CPU内核的100%。

探查器软件包实现了采样探查器。 elppackage可用于获取实际的挂钟时间。

深入研究#<已编译的0x131f715d2b6fa0a8>表示那里的周期(CPU时间的17%)花费在Emacs上等待用户输入,因此我们现在可以忽略它。

随着我深入研究常春藤-插入-迷你缓冲区,“功能”列中的名称开始被截断,因为该列太窄。快速的Google搜索(通过Mx google-此emacs分析器报告宽度)向我展示了扩大范围:

从探查器报告缓冲区中,我运行M-x eval-expression,将上面的表格与C-y粘贴在一起,然后按RET。我还将这个表单保留为我的配置。在探查器报告缓冲区(绑定到profiler-report-render-calltree)中按c可以重新绘制它,现在具有较宽的列,使我能够看到函数名称。

功能CPU采样%-ffip 13586 63%-ffip查找文件13586 63%-let * 13586 63%-setq 13585 63%-ffip-project-search 13585 63%-let * 13585 63%-mapcar 13531 63%- #< lambda 0xb210342292> 13528 63%-缺点13521 63%-扩展文件名12936 60%-流浪者文件名处理程序12918 60%-适用9217 43%-流浪者文件名处理程序9158 42%-适用9124 42 %-tramp-sh-handle扩展文件名8952 41%-目录中的文件名5812 27%-tramp-文件名处理程序5793 27%+ tramp-find-外国文件名处理程序3166 14%+应用1237 5%+流浪者解剖文件名527 2%+#< compiled -0x1589d0aab96d9542> 337 1%流浪者文件名等于p 312 1%流浪者流浪文件-p 33 0%+流浪者替换环境变量6 0%#<已编译0x1e202496df87> 1 0%+流浪者可连接的p 1006 4%+流浪者解剖文件名628 2%+评估517 2%+流浪者运行真实处理程序339 1%+流浪者下落体积字母60 0%流浪者制作流浪者文件名30 0%+流浪者文件操作名40 0%+流浪者查找外国文件名处理程序2981 13%+流浪者解剖文件名518 2 %tramp-tramp-file-p 34 0%#<已编译0x1e202496df87> 1 0%+替换环境变量1 0%+字符串正则表达式替换153 0%+分割字符串15 0%+ ffip-create-shell-command 4 0%cond 1 0%

这里有几件要解压的东西。从第8-11行可以推断出ffip映射了一个lambda,该lambda调用了所有completioncandidate上的expand-file-name,在这种情况下,大约有7万个文件名。运行M-x查找功能ffip-project-search并缩小到该功能的相关区域,即可确切地表明:

在探查器报告的第11行上,我们可以看到30秒(18秒)中的60%花费在了扩展文件名调用上。通过将18秒除以70000,我们得出扩展文件名调用平均需要250µs。 250µs是现代计算机从RAM顺序读取1MB所需的时间!为什么我的计算机只需要显示700多次的工作量就能显示文件列表?

expand-file-name是在C源代码中定义的函数.Signature(expand-file-name NAME& DEFAULT-DIRECTORY)Documentation将文件名NAME转换为绝对名称并对其进行规范化。第二个arg DEFAULT-DIRECTORY是以以下内容开头的目录NAME是相对的(不以斜杠或波浪号开头);目录名和目录文件名均被接受。如果DEFAULT-DIRECTORY为nil ormissing,则使用默认目录的当前缓冲区值。NAME应该是一个字符串,该字符串是基础文件系统的有效文件名。

好的,这听起来像Expand-file-name本质上是根据当前缓冲区的目录或可选地作为附加参数传入的目录将文件路径转换为绝对路径。让我们尝试使用局部缓冲区和远程缓冲区上的M-x eval-expression评估某些形式,以了解其作用。

ffip-project-search中的expand-file-name调用未指定DEFAULT-DIRECTORY(expand-file-name的可选第二个参数),因此如上例所示,它默认为当前缓冲区的目录,该目录位于剖析的案例是一个远程路径,如上面的第二个示例中所示。

在更好地了解expand-file-name的作用之后,现在让我们尝试了解其性能。我们可以使用本地和远程缓冲区中的基准测试来对其进行基准测试,并比较它们的运行时间。

基准运行是在Benchmark.el.gz.Signature(基准运行&可选REPETITIONS& rest FORMS)中定义的自动加载的宏。结果循环的开销。否则,运行一次一次。返回执行的总时间,运行的垃圾收集数量以及垃圾收集所花费的时间的列表。

显示了在本地缓冲区上运行70,000个时间的扩展文件名需要0.3秒,而在远程缓冲区上运行则需要30秒:慢两个数量级。 30秒比分析器报告中观察到的时间(18秒)还要多,我将这种差异归因于未知; ffip执行可能利用了字节编译代码评估的优势,或者与基准测试运行相关的一些开销,或者其他全部开销。但是,该实验明显证实了分析器报告的结果。

所以!回到ffip。再次查看上一个屏幕截图,似乎显示的文件列表甚至没有显示绝对文件路径。为什么根本要调用expand-file-name?也许称它不太重要&mldr

更快仅此一项更改就使ffip显示候选清单的时间从30秒减少到8秒,没有明显的缺点。虽然更好,但仍然不能令人满意。

对更改后的功能进行性能分析表明,现在大部分时间都花在了使用常春藤预排序功能和垃圾回收对候选人进行分类的过程上。我已经安装并配置了出色的常春藤和常春藤预包装,基于选择新近度对候选人进行自动排序。使用M-x禁用常春藤常春藤模式并重新运行我的功能可将时间从8秒减少到4秒。

我注意到的另一件事是ffip允许fd用作后端,而不是GNU find。 fd声称具有更好的性能,因此我将其安装在远程主机上并配置ffip以使用它。我像以前那样评估下面的表格,但我也可以使用非常方便的Mx顾问集变量,它显示了Emacs中所有变量的狭窄候选列表(在我的设置中大约为2万个)以及其文档字符串的摘要,选择时允许设置变量值。方便!

通过以下方式,我的函数的运行时间缩短了2秒多一点,总体性能提高了15倍:

最后一点不是真正的问题,但是整个情况是理想的。即使抛开上述所有要点,我也不希望每次在该项目中搜索文件时都等待2秒以上。

到目前为止,我们主要是在配置和内省Emacs。现在让我们通过满足我们需求的新功能对其进行扩展。

将fd的输出显示为候选文件的缩小列表,可以过滤,滚动和从列表中选择候选

让我们看看项目中的查找文件中是否有任何可重用的内容。我知道ffip正在找出项目根目录并以某种方式运行shell命令。通过使用Mx find-library find-file-in-project(它会使用已安​​装的find-file-in-project.el包文件打开缓冲区)检出其库文件,我可以看到shell-command-to-string函数(包含在Emacs中)用于运行Shell命令,并且有一个名为ffip-project-root的函数听起来很像我们需要的功能。

我有一个按键绑定,显示了光标下的东西的文档。我用它来检查两个功能:

shell-command-to-string是一个在insimple.el.gz.Signature(shell-command-to-string COMMAND)中定义的编译函数文档执行shell命令COMMAND并以字符串形式返回其输出。

我也知道,由常春藤提供的常春藤读取功能应注意显示缩小的文件列表。看起来我们不需要编写很多代码。

为了验证我们的代码可以在远程缓冲区上运行,我们需要在一个上下文中评估表单。 with-current-buffer宏可以用于此目的。

with-current-buffer是在subr.el.gz.Signature中定义的宏(with-current-buffer BUFFER-OR-NAME& rest BODY)文档在BODY中暂时使用BUFFER-OR-NAME的名称执行表单.BUFFER-OR -NAME必须是缓冲区或现有缓冲区的名称。返回的值是BODY中最后一个形式的值。另请参见with-temp-buffer。

为了编写函数,我们不用打开M-x eval-expression临时评估表单,而是打开暂存缓冲区并从那里直接编写和评估表单,这应该更加方便。

我在远程主机上具有Linux git存储库的克隆。让我们为Linux内核中的最有趣的文件jiffies.c分配一个远程缓冲区。

请注意,缓冲区只是一个值,可以传递给函数。我们将在以后使用它,通过with-current-buffer宏来模拟评估表单,就像我们已经打开了该缓冲区一样。

让我们开始进行探索,首先写入* scratch *缓冲区,然后继续使用eval-defun逐一评估表单。

现在,让我们在远程缓冲区的上下文中评估某些表格。注意,在shell中运行主机名会返回不同的结果。

(with-current-buffer远程文件缓冲区(shell-command-to-string" hostname"));; => "远程主机(带有当前缓冲区的远程文件缓冲区默认目录); => " / ssh:mpereira @ remote-host:/ home / mpereira / linux / kernel / time /"(with-current-buffer remote-file-buffer(ffip-project-root));; => " / ssh:mpereira @ remote-host:/ home / mpereira / linux /"(with-current-buffer remote-file-buffer(shell-command-to-string" fd- 版本"));; => " fd 8.1.1"(with-current-buffer远程文件缓冲区(executable-find" fd" t));; => " / usr / bin / fd" 可执行文件查找需要秒 ......