Erlang LS指南(四)

启动调试会话

VS Code

选择 Run and Debug 面板,从下拉列表中选择 Existing Erlang Node 并按下播放按钮:

001

打开一个新终端并使用 curl 命令触发我们的新断点。

curl -i http://localhost:8080

HTTP/1.1 200 OK
content-length: 12
content-type: text/plain
date: Fri, 09 Jul 2021 13:35:01 GMT
server: Cowboy

Hello world!

执行将在断点处暂停。 然后,您可以使用标准的 VS Code 控件来控制执行:

002

在左侧,可以探索调用堆栈和变量绑定。 例如,我们可以逐步扩展 Cowboy 输入请求的绑定并验证 User Agent 标头的值:

003

底部的调试控制台可用作 REPL,并提供当前的变量绑定:

004

左侧的Watch列表可用于跟踪特定变量的值(例如,Opts 变量):

005

以及用于操作这些值的调试控制台:

006

VS Code 提供了广泛的调试功能。 更多信息请参考官方 VS Code 文档。

Emacs

打开 src/toppage_h.erl 并运行:

M-x dap-debug

系统将提示您输入配置模板。 选择现有的 Erlang 节点。

打开一个新终端并使用 curl 命令触发我们的新断点。

curl -i http://localhost:8080

HTTP/1.1 200 OK
content-length: 12
content-type: text/plain
date: Fri, 09 Jul 2021 13:35:01 GMT
server: Cowboy

Hello world!

执行将在断点处暂停。 然后,您可以使用标准 Emacs 控件来控制执行:

007

在右侧,可以探索调用堆栈和变量绑定。 例如,我们可以逐步扩展 Cowboy 输入请求的绑定并验证 User Agent 标头的值:

008

您还可以使用当前可用的变量绑定打开 REPL:

M-x dap-eval

dap-mode 包提供了广泛的调试功能。 更多信息请参考官方文档。

特殊断点类型

DAP 协议描述了可以在不同情况下使用的多种断点类型:

  • 条件断点
  • Logpoints
  • Hitpoints

条件断点

仅当给定条件评估为真时才会触发条件断点。 例如,我们可能希望仅当客户端传递的 Host 标头的值包含字符串 pigeon 时才中断执行:

VS Code

要设置条件断点,请右键单击行号旁边并选择添加条件断点…选项。 添加以下表达式:

maps:get(<<"host">>, maps:get(headers, Req0)) =:= <<"pigeon">>

009

Emacs

要添加条件断点,请移动到现有断点,然后运行:

M-x dap-breakpoint-condition

并添加以下表达式:

maps:get(<<"host">>, maps:get(headers, Req0)) =:= <<"pigeon">>

设置了上述条件断点后,以下请求不会导致执行中断:

curl -i http://localhost:8080

但以下将会触发断点:

curl -H "Host: pigeon" -i http://localhost:8080

Logpoints

日志断点是一种特殊类型的断点,它不会导致执行中断,但它们会导致在调试控制台中打印出一条日志消息。

VS Code

要记录每个请求的 Host 标头,请右键单击行号旁边并选择 Add logpoint… 选项。 添加以下日志消息:

maps:get(<<"host">>, maps:get(headers, Req0))

让我们用不同的(或默认的)主机头触发一些请求:

curl -i http://localhost:8080
curl -H "Host: pigeon" -i http://localhost:8080

然后我们可以在调试控制台中跟踪日志断点:

010

Emacs

要记录每个请求的 Host 标头,请移动到现有断点,然后运行:

M-x dap-breakpoint-log-message

添加以下日志消息:

maps:get(<<"host">>, maps:get(headers, Req0))

让我们用不同的(或默认的)主机头触发一些请求:

curl -i http://localhost:8080
curl -H "Host: pigeon" -i http://localhost:8080

要跟踪日志断点,请运行:

M-x dap-go-to-output-buffer

011

Hitpoints

Hitpoints是一种特殊的断点,每 N 次触发一次。

VS Code

选择一个现有断点并从下拉列表中选择 Hit Count 选项。 指定一个数字 N。相应的断点将每第 N 次触发一次。

Emacs

导航到现有断点。 运行:

M-x dap-breakpoint-hit-condition

指定一个数字 N。相应的断点将每第 N 次触发一次。

故障排除

如果某些事情没有按预期工作,请查看 Erlang LS DAP 日志。 他们很可能会指出问题的根本原因。 日志可在以下位置获得:

[USER_LOG_DIR]/[PROJECT_NAME]/dap_server.log

其中 [USER_LOG_DIR] 是以下输出:

filename:basedir(user_log, "els_dap").

例如,在 Mac OS 上,hello_world 项目的 DAP 日志将位于:

/Users/[USERNAME]/Library/Logs/els_dap/hello_world/dap_server.log

如果 DAP 日志没有帮助,请随时在 GitHub 或 Slack 上联系。

使用 Erlang LS 进行愉快的调试!

如何进行Code Lenses

在我们之前的教程中,我们学习了如何为 Erlang 语言服务器实现诊断后端。 这次我们将深入了解 Code Lenses 的世界。

目标

给定一个包含许多函数定义的 Erlang 模块,我们希望在其各自定义之上显示对每个函数的引用数。 这是code lenses在 VS Code 中的外观。

012

在本教程结束时,您将:

  • 知道什么是code lenses
  • 学习如何在 Erlang LS 中实现code lenses
  • 离成为 Erlang LS 贡献者更近一步

事不宜迟,让我们开始吧。

到底什么是 Code Lens?

Wade Anderson 将 Code Lens 定义为:

散布在您的源代码中的可操作的上下文信息

这是一种非常奇特的说法,即code lenses是出现在 IDE 中的位于您的代码旁边的任意一段文本。 文本通常会提供有关部分代码的见解,就像我们刚刚在上面看到的示例一样。

code lenses也可以是可操作的。 用户可以通过单击lenses或使用键盘快捷键来激活lenses以执行操作。 触发的动作可以是任何东西。 这是一个 Emacs code lenses,它允许用户执行给定的 Common Test 测试用例:

013

code lenses是上下文相关的,这意味着它们知道周围的上下文。 在上面的例子中,运行测试lenses知道点击时应该执行哪个特定的测试用例。

现在我们了解了什么是code lenses,让我们在 Erlang LS 中实现一个。

实现新的 Code Lens 后端

Erlang LS 提供了一个框架,使code lenses的开发尽可能简单。 要创建我们的新的code lenses,我们需要做的第一件事是为其命名并创建一个新的 Erlang 模块来实现 els_code_lens 行为。 让我们将新代码称为lens function_references。

-module(els_code_lens_function_references).
-behaviour(els_code_lens).

els_code_lens 行为需要实现三个回调函数:

-callback is_default() -> boolean().
-callback pois(els_dt_document:item()) -> [poi()].
-callback command(els_dt_document:item(), poi(), state()) -> els_command:command().

稍后我们将看到每个回调函数应该做什么。 现在,让我们将以下函数导出添加到我们的 els_code_lens_function_references 模块中:

-export([ is_default/0
        , pois/1
        , command/3
        ]).

现在我们可以专注于每个单独的回调函数。

回调函数:is_default/0

is_default/0 回调用于指定是否应默认启用当前后端。 在我们的例子中,我们希望默认启用新的后端,所以我们说:

is_default() -> true.

如果最终用户决定禁用此后端,她可以在她的 erlang_ls.config 中添加以下选项:

lenses:
  disabled:
    function_references

回调函数:pois/1

在 Erlang LS 行话中,POI 代表兴趣点。 该术语指的是作为代码库一部分的有兴趣的点位。 兴趣点由 Erlang LS 索引并存储在内存数据库中。 POI 可以指代函数定义、宏定义、记录用法,应有尽有。 Erlang LS 提供了一组实用程序,可以轻松搜索和操作兴趣点。

pois/1 函数接受一个参数,即当前文档。 它的返回值是应该为其激活lenses的 POI 列表。 在我们的例子中,我们希望我们的lenses在每个函数定义旁边都是可见的。 因此,我们写:

pois(Document) ->
  els_dt_document:pois(Document, [function]).

回调函数:command/3

我们需要实现的最后一个强制回调是 command/3 之一。 回调接受三个参数:当前文档、特定 POI 和状态。 为了本教程的目的,我们将忽略状态并仅关注前两个参数。

The Command

该函数需要返回一个命令。 命令是一个 LSP 数据结构,它包含:

  • The title – 每个选定 POI 旁边呈现的文本
  • A Command Id – 单击时执行的命令的标识符
  • Command Args – 传递给命令的参数列表

Erlang LS 提供了一个辅助函数来创建这样的数据结构: els_command:make_command/3 函数。 然后,我们的 command/3 函数将如下所示:

command(Document, POI, _State) ->
  Title = title(Document, POI),
  CommandId = command_id(),
  CommandArgs = command_args(),
  els_command:make_command(Title, CommandId, CommandArgs).

我们现在将详细描述每个参数并学习如何计算它们,从标题开始。

The title

标题是我们想要在文本编辑器中显示的文本,位于我们的兴趣点(又名 POI)旁边。 在我们的例子中,我们想要显示以下文本:

Used [N] times

为了能够计算数字 N,我们需要知道有多少对当前函数的引用分布在我们的代码库中。 因此,我们可以通过 els_dt_references:find_by_id/2 辅助函数查询 Erlang LS 数据库:

title(Document, POI) ->

%% Extract the module name from the current document
#{uri := Uri} = Document,
M = els_uri:module(Uri),

%% Extract the function name and arity from the current POI
#{id := {F, A}} = POI,

%% Query the Erlang LS DB for references to the current function
{ok, References} = els_dt_references:find_by_id(function, {M, F, A}),

%% Calculate the number of references
N = length(References),

%% Format the title for the code lens
unicode:characters_to_binary(io_lib:format("Used ~p times", [N])).

els_dt_references:find_by_id/2 函数有两个参数:我们正在寻找的引用类型(在我们的例子中是函数)和当前兴趣点的完全限定 ID。 对于函数定义,完全限定标识符是 {M, F, A} 元组,代表模块、函数名称和函数的 Arity。 如上所示,我们可以从 Document 中提取模块 M,从当前 POI 中提取 F 和 A。

CommandId and CommandArgs

CommandId 是当用户点击我们的code lenses时我们想要运行的命令的任意标识符。 在我们的例子中,这个动作将是一个空操作,但我们仍然需要为我们的命令选择一个名称。 我们称之为函数引用:

command_id() -> <<"function-references">>.

由于我们的命令是无操作的(如果用户点击lenses,我们不希望发生任何事情),我们的命令不需要任何参数:

command_args() -> [].

我们基本上完成了。 为了完整起见,这是我们完整的 els_code_lens_function_references 模块:

-module(els_code_lens_function_references).

-behaviour(els_code_lens).
-export([ is_default/0
, pois/1
, command/3
]).

is_default() ->
true.

pois(Document) ->
els_dt_document:pois(Document, [function]).

command(Document, POI, _State) ->
Title = title(Document, POI),
CommandId = command_id(),
CommandArgs = command_args(),
els_command:make_command(Title, CommandId, CommandArgs).

title(Document, POI) ->
#{uri := Uri} = Document,
M = els_uri:module(Uri),
#{id := {F, A}} = POI,
{ok, References} = els_dt_references:find_by_id(function, {M, F, A}),
N = length(References),
unicode:characters_to_binary(io_lib:format("Used ~p times", [N])).

command_id() ->
<<"function-references">>.

command_args() ->
[].

注册Code Lenses

在使用新的code lenses之前,我们还需要做一件事:我们需要告诉 Erlang LS 它的存在。 这可以通过将我们的新的code lenses添加到 els_code_lens 模块中的 available_lenses 列表中来实现:

available_lenses() ->
  [ ...
  , <<"function-references">>
  ].

就这样。

添加测试

此时我们的code lenses应该可以正常工作,但在为它编写测试之前我们无法确定! Erlang LS 提供了一个可用于此目的的测试框架。 在本节中,我们将假设您已经对 Erlang LS 测试框架有所了解。 如果您想更温和地介绍 Erlang LS 中的测试,请参阅之前的诊断教程。

创建测试模块

让我们在 code_navigation 测试应用程序中创建一个名为 code_lens_function_references 的测试模块:

$ cat apps/els_lsp/priv/code_navigation/src/code_lens_function_references.erl
-module(code_lens_function_references).

-export([ a/0 ]).

-spec a() -> ok.
a() ->
b(),
c().

-spec b() -> ok.
b() ->
c().

-spec c() -> ok.
c() ->
ok.

注册新的测试模块

只需打开 els_test_utils 模块并将新模块添加到源列表中。 这将确保新模块被正确索引并且一些辅助函数可用。

sources() ->
  [ ...
  , code_lens_function_references
  , ...
  ].

编写测试用例

然后让我们打开 els_code_lens_SUITE 模块并添加一个测试用例,我们在其中检查新的code lenses在新模块中是否按预期工作。

function_references(Config) ->
  Uri = ?config(code_lens_function_references_uri, Config),
  #{result := Result} = els_client:document_codelens(Uri),
  Expected = [ lens(5, 0) # First lens on line 5, 0 references
             , lens(10, 1) # Second lens on line 10, 1 reference
             , lens(14, 2) # Third lens on line 14, 2 references
             ],
  ?assertEqual(Expected, Result),
  ok.

在上面的测试用例中,我们通过利用 Erlang LS 测试框架来获取新添加的测试模块的 Uri。 然后,我们使用 els_client 为给定的 Uri 调用 document_codelens 方法,最终确保我们收到预期的code lenses列表。 lens/2 是一个辅助函数,它构造了 LSP 协议所期望的数据结构如下:

lens(Line, Usages) ->
  Title = unicode:characters_to_binary(
            io_lib:format("Used ~p times", [Usages])),
  #{ command =>
       #{ arguments => []
        , command => els_command:with_prefix(<<"function-references">>)
        , title => Title
        }
   , data => []
   , range =>
       #{ 'end' => #{character => 1, line => Line}
        , start => #{character => 0, line => Line}
        }
   }.

让我们运行测试并确保它通过。

$ rebar3 ct --suite apps/els_lsp/test/els_code_lens_SUITE --case function_references --group tcp
[...]
===> Running Common Test suites...
%%% els_code_lens_SUITE: .
All 1 tests passed.

看起来我们在这里完成了。

回调函数:init/1

init/1 回调允许我们对每个文件执行一次计算,并将计算值以 State 的形式传递给后续回调函数(还记得我们在 command/3 回调中忽略的 State 参数吗?)。 例如,在建议规范code lenses中使用它来为每个 Erlang 模块运行一次 TypEr,并且仍然能够为每个函数显示一个lens。

回调函数:precondition/1

precondition/1 回调允许我们只为特定类型的文档启用给定的lens。 例如,以下实现仅对 Common Test 套件启用 ct_run_test lens,由 ct.hrl 文件的 include_lib 指令标识:

precondition(Document) ->
  Includes = els_dt_document:pois(Document, [include_lib]),
  case [POI || #{id := "common_test/include/ct.hrl"} = POI <- Includes] of
    [] ->
      false;
    _ ->
      true
  end.

结论

此时,您应该可以尝试新的code lenses。 上面的code lenses已经在 Erlang LS 中可用。 您可以在以下位置查看全部贡献。我希望本教程能帮助您更好地理解code lenses以及如何在 Erlang LS 中实现code lenses。 期待您将实施的精彩lens!

Related Posts

2021 年你需要知道的关于 Erlang 的一切

今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 Erlang 不像某些现代编程语言那样流行,但它安静地运行着 WhatsApp 和微信等每天为大量用户提供服务的应用程序。 在这篇文章中,我将告诉你关于这门语言的更多事情、它的历史,以及你是否应该考虑自己学习它。 ## 什么是 Erlang,它在哪里使用? Erl

Read More

Erlang JIT中基于类型的优化

这篇文章探讨了 Erlang/OTP 25 中基于类型的新优化,其中编译器将类型信息嵌入到 BEAM 文件中,以帮助JIT(即时编译器)生成更好的代码。 ## 两全其美 OTP 22 中引入的基于SSA的编译器处理步骤进行了复杂的类型分析,允许进行更多优化和更好的生成代码。然而,Erlang 编译器可以做什么样的优化是有限制的,因为 BEAM 文件必须

Read More

Erlang JIT之路

自从Erlang 存在,就一直有让它更快的需求和野心。这篇博文是一堂历史课,概述了主要的 Erlang 实现以及如何尝试提高 Erlang 的性能。 ## Prolog 解释器 Erlang 的第一个版本是在 1986 年在 Prolog 中实现的。那个版本的 Erlang 对于创建真正的应用程序来说太慢了,但它对于找出Erlang语言的哪些功能有用,哪

Read More