diff --git a/.editorconfig b/.editorconfig
index 7c31c4841..c277dff9a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,7 +8,7 @@ end_of_line = LF
[*]
indent_style = space
-indent_size = 2
+indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 80
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 000000000..36694b0a5
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,2 @@
+# Apply erlfmt to the entire codebase (#1297)
+c19fb7239ef6766aa7e583d1c010ac38588a3de3
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 57386bfcd..be21cf38f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
- otp-version: [21, 22, 23, 24]
+ otp-version: [24, 25, 26, 27]
runs-on: ${{ matrix.platform }}
container:
image: erlang:${{ matrix.otp-version }}
@@ -37,31 +37,28 @@ jobs:
run: rebar3 compile
- name: Escriptize LSP Server
run: rebar3 escriptize
- - name: Store LSP Server Escript
- uses: actions/upload-artifact@v2
- with:
- name: erlang_ls
- path: _build/default/bin/erlang_ls
- - name: Escriptize DAP Server
- run: rebar3 as dap escriptize
- - name: Store DAP Server Escript
- uses: actions/upload-artifact@v2
- with:
- name: els_dap
- path: _build/dap/bin/els_dap
+ # - name: Store LSP Server Escript
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: erlang_ls
+ # path: _build/default/bin/erlang_ls
+ # overwrite: true
+ - name: Check formatting
+ run: rebar3 fmt -c
- name: Lint
run: rebar3 lint
- name: Generate Dialyzer PLT for usage in CT Tests
- run: dialyzer --build_plt --apps erts kernel stdlib
+ run: dialyzer --build_plt --apps erts kernel stdlib compiler crypto parsetools
- name: Start epmd as daemon
run: epmd -daemon
- name: Run CT Tests
run: rebar3 ct
- - name: Store CT Logs
- uses: actions/upload-artifact@v2
- with:
- name: ct-logs
- path: _build/test/logs
+ # - name: Store CT Logs
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: ct-logs
+ # path: _build/test/logs
+ # overwrite: true
- name: Run PropEr Tests
run: rebar3 proper --cover --constraint_tries 100
- name: Run Checks
@@ -70,43 +67,71 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: rebar3 do cover, coveralls send
- - name: Produce Documentation
- run: rebar3 edoc
- - name: Publish Documentation
- uses: actions/upload-artifact@v2
- with:
- name: edoc
- path: |
- apps/els_core/doc
- apps/els_lsp/doc
- apps/els_dap/doc
+ if: matrix.otp-version == '27'
+ # - name: Produce Documentation
+ # run: rebar3 edoc
+ # if: ${{ matrix.otp-version == '24' }}
+ # - name: Publish Documentation
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: edoc
+ # path: |
+ # apps/els_core/doc
+ # apps/els_lsp/doc
+ # overwrite: true
windows:
- runs-on: windows-latest
+ strategy:
+ matrix:
+ platform: [windows-latest]
+ otp-version: [26.2.5.3]
+ runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Erlang
- run: choco install -y erlang --version 22.3
+ run: choco install -y erlang --version ${{ matrix.otp-version }}
- name: Install rebar3
- run: choco install -y rebar3 --version 3.13.1
+ run: choco install -y rebar3 --version 3.23.0
- name: Compile
run: rebar3 compile
- name: Lint
run: rebar3 lint
- name: Generate Dialyzer PLT for usage in CT Tests
- run: dialyzer --build_plt --apps erts kernel stdlib
+ run: dialyzer --build_plt --apps erts kernel stdlib crypto compiler parsetools
- name: Start epmd as daemon
run: erl -sname a -noinput -eval "halt(0)."
- name: Run CT Tests
run: rebar3 ct
- - name: Store CT Logs
- uses: actions/upload-artifact@v2
- with:
- name: ct-logs
- path: _build/test/logs
+ # - name: Store CT Logs
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: ct-logs
+ # path: _build/test/logs
+ # overwrite: true
- name: Run PropEr Tests
run: rebar3 proper --cover --constraint_tries 100
- name: Run Checks
run: rebar3 do dialyzer, xref
- name: Produce Documentation
run: rebar3 edoc
+ macos:
+ # Smaller job for MacOS to avoid excessive billing
+ strategy:
+ matrix:
+ platform: [macos-latest]
+ otp-version: [27]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Erlang
+ run: brew install erlang@${{ matrix.otp-version }}
+ - name: Install Rebar3
+ run: brew install rebar3
+ - name: Compile
+ run: rebar3 compile
+ - name: Generate Dialyzer PLT for usage in CT Tests
+ run: dialyzer --build_plt --apps erts kernel stdlib compiler crypto parsetools
+ - name: Start epmd as daemon
+ run: epmd -daemon
+ - name: Run CT Tests
+ run: rebar3 ct
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..8c1931521
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,197 @@
+name: Release
+on:
+ release:
+ types:
+ - created
+
+# Test trigger. Uncomment to test basic flow.
+# NOTE: it will fail on trying to get the release url
+# on:
+# push:
+# branches:
+# - '*'
+
+jobs:
+ linux:
+ strategy:
+ matrix:
+ platform: [ubuntu-latest]
+ otp-version: [24, 25, 26, 27]
+ runs-on: ${{ matrix.platform }}
+ container:
+ image: erlang:${{ matrix.otp-version }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Cache Hex packages
+ uses: actions/cache@v1
+ with:
+ path: ~/.cache/rebar3/hex/hexpm/packages
+ key: ${{ runner.os }}-hex-${{ hashFiles(format('{0}{1}', github.workspace, '/rebar.lock')) }}
+ restore-keys: |
+ ${{ runner.os }}-hex-
+ - name: Cache Dialyzer PLTs
+ uses: actions/cache@v1
+ with:
+ path: ~/.cache/rebar3/rebar3_*_plt
+ key: ${{ runner.os }}-dialyzer-${{ hashFiles(format('{0}{1}', github.workspace, '/rebar.config')) }}
+ restore-keys: |
+ ${{ runner.os }}-dialyzer-
+ - name: Compile
+ run: rebar3 compile
+ - name: Escriptize LSP Server
+ run: rebar3 escriptize
+ # - name: Store LSP Server Escript
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: erlang_ls
+ # path: _build/default/bin/erlang_ls
+ # overwrite: true
+ - name: Check formatting
+ run: rebar3 fmt -c
+ - name: Lint
+ run: rebar3 lint
+ - name: Generate Dialyzer PLT for usage in CT Tests
+ run: dialyzer --build_plt --apps erts kernel stdlib crypto compiler parsetools
+ - name: Start epmd as daemon
+ run: epmd -daemon
+ - name: Run CT Tests
+ run: rebar3 ct
+ # - name: Store CT Logs
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: ct-logs
+ # path: _build/test/logs
+ # overwrite: true
+ - name: Run PropEr Tests
+ run: rebar3 proper --cover --constraint_tries 100
+ - name: Run Checks
+ run: rebar3 do dialyzer, xref
+ - name: Create Cover Reports
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: rebar3 do cover, coveralls send
+ - name: Produce Documentation
+ run: rebar3 edoc
+ if: ${{ matrix.otp-version == '24' }}
+ # - name: Publish Documentation
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: edoc
+ # path: |
+ # apps/els_core/doc
+ # apps/els_lsp/doc
+ # overwrite: true
+
+ # Make release artifacts : erlang_ls
+ - name: Make erlang_ls-linux-${{ matrix.otp-version }}.tar.gz
+ run: 'tar -zcvf erlang_ls-linux-${{ matrix.otp-version }}.tar.gz -C _build/default/bin/ erlang_ls'
+ - env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ id: get_release_url
+ name: Get release url
+ uses: "bruceadams/get-release@v1.3.2"
+ - env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ name: Upload release erlang_ls-linux-${{ matrix.otp-version }}.tar.gz
+ uses: "actions/upload-release-asset@v1.0.2"
+ with:
+ asset_content_type: application/octet-stream
+ asset_name: "erlang_ls-linux-${{ matrix.otp-version }}.tar.gz"
+ asset_path: "erlang_ls-linux-${{ matrix.otp-version }}.tar.gz"
+ upload_url: "${{ steps.get_release_url.outputs.upload_url }}"
+ windows:
+ strategy:
+ matrix:
+ platform: [windows-latest]
+ otp-version: [26.2.5.3]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Install Erlang
+ run: choco install -y erlang --version ${{ matrix.otp-version }}
+ - name: Install rebar3
+ run: choco install -y rebar3 --version 3.23.0
+ - name: Compile
+ run: rebar3 compile
+ - name: Escriptize LSP Server
+ run: rebar3 escriptize
+ # - name: Store LSP Server Escript
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: erlang_ls
+ # path: _build/default/bin/erlang_ls
+ # overwrite: true
+ - name: Lint
+ run: rebar3 lint
+ - name: Generate Dialyzer PLT for usage in CT Tests
+ run: dialyzer --build_plt --apps erts kernel stdlib crypto compiler parsetools
+ - name: Start epmd as daemon
+ run: erl -sname a -noinput -eval "halt(0)."
+ - name: Run CT Tests
+ run: rebar3 ct
+ # - name: Store CT Logs
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: ct-logs
+ # path: _build/test/logs
+ # overwrite: true
+ - name: Run PropEr Tests
+ run: rebar3 proper --cover --constraint_tries 100
+ - name: Run Checks
+ run: rebar3 do dialyzer, xref
+ - name: Produce Documentation
+ run: rebar3 edoc
+
+ # Make release artifacts : erlang_ls
+ - name: Make erlang_ls-windows-${{ matrix.otp-version }}.tar.gz
+ run: 'tar -zcvf erlang_ls-windows-${{ matrix.otp-version }}.tar.gz -C _build/default/bin/ erlang_ls'
+ - env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ id: get_release_url
+ name: Get release url
+ uses: "bruceadams/get-release@v1.3.2"
+ - env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ name: Upload release erlang_ls-windows-${{ matrix.otp-version }}.tar.gz
+ uses: "actions/upload-release-asset@v1.0.2"
+ with:
+ asset_content_type: application/octet-stream
+ asset_name: erlang_ls-windows-${{ matrix.otp-version }}.tar.gz
+ asset_path: erlang_ls-windows-${{ matrix.otp-version }}.tar.gz
+ upload_url: "${{ steps.get_release_url.outputs.upload_url }}"
+ macos:
+ # Smaller job for MacOS to avoid excessive billing
+ strategy:
+ matrix:
+ platform: [macos-latest]
+ otp-version: [24, 25, 26, 27]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Erlang
+ run: brew install erlang@${{ matrix.otp-version }}
+ - name: Install Rebar3
+ run: brew install rebar3
+ - name: Compile
+ run: rebar3 compile
+ - name: Escriptize LSP Server
+ run: rebar3 escriptize
+ # Make release artifacts : erlang_ls
+ - name: Make erlang_ls-macos-${{ matrix.otp-version }}.tar.gz
+ run: 'tar -zcvf erlang_ls-macos-${{ matrix.otp-version }}.tar.gz -C _build/default/bin/ erlang_ls'
+ - env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ id: get_release_url
+ name: Get release url
+ uses: "bruceadams/get-release@v1.3.2"
+ - env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ name: Upload release erlang_ls-macos-${{ matrix.otp-version }}.tar.gz
+ uses: "actions/upload-release-asset@v1.0.2"
+ with:
+ asset_content_type: application/octet-stream
+ asset_name: erlang_ls-macos-${{ matrix.otp-version }}.tar.gz
+ asset_path: erlang_ls-macos-${{ matrix.otp-version }}.tar.gz
+ upload_url: "${{ steps.get_release_url.outputs.upload_url }}"
diff --git a/DEVELOPERS-README.md b/DEVELOPERS-README.md
new file mode 100644
index 000000000..db8a83635
--- /dev/null
+++ b/DEVELOPERS-README.md
@@ -0,0 +1,14 @@
+# Developers README
+
+## Release Process
+
+To create a new release:
+
+* Access the [releases](https://github.com/erlang-ls/erlang_ls/releases) page
+* Click on "Draft a new release"
+* Click on the "Choose a tag" dropdown and enter a new tag (use semantic versioning)
+* Click on "Generate release notes"
+* Optionally amend the generated notes with highlights or other important information
+* Click on "Publish Release"
+
+To publish a new version of the VS Code extension, please refer to the [DEVELOPERS-README.md file in the erlang-ls/vscode repository](https://github.com/erlang-ls/vscode/blob/main/DEVELOPERS-README.md).
diff --git a/Makefile b/Makefile
index 349d3ce59..effef5d98 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,16 @@
.PHONY: all
+PREFIX ?= '/usr/local'
+
all:
@ echo "Building escript..."
@ rebar3 escriptize
- @ rebar3 as dap escriptize
+.PHONY: install
install: all
@ echo "Installing escript..."
- @ cp _build/default/bin/erlang_ls /usr/local/bin
- @ cp _build/dap/bin/els_dap /usr/local/bin
+ @ mkdir -p '${PREFIX}/bin'
+ @ cp _build/default/bin/erlang_ls ${PREFIX}/bin
.PHONY: clean
clean:
diff --git a/README.md b/README.md
index 7c314c25b..82194edcc 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,12 @@
+
+> [!WARNING]
+> The Erlang LS project is currently unmaintained.
+>
+> We recommend switching to the [Erlang Language Platform (ELP)](https://github.com/whatsapp/erlang-language-platform) language server.
+>
+> * [VS Code extension](https://marketplace.visualstudio.com/items?itemName=erlang-language-platform.erlang-language-platform)
+> * [Documentation](https://whatsapp.github.io/erlang-language-platform/docs/get-started/)
+
# erlang_ls

@@ -5,13 +14,19 @@

[](https://coveralls.io/github/erlang-ls/erlang_ls?branch=main)
-An Erlang server implementing Microsoft's Language Server Protocol 3.15.
+An Erlang server implementing Microsoft's Language Server Protocol 3.17.
+
+[Documentation](https://erlang-ls.github.io/)
## Minimum Requirements
-* [Erlang OTP 21+](https://github.com/erlang/otp)
+* [Erlang OTP 24+](https://github.com/erlang/otp)
* [rebar3 3.9.1+](https://github.com/erlang/rebar3)
+## Supported OTP versions
+
+* 24, 25, 26, 27
+
## Quickstart
Compile the project:
@@ -22,6 +37,10 @@ To install the produced `erlang_ls` escript in `/usr/local/bin`:
make install
+To install to a different directory set the `PREFIX` environment variable:
+
+ PREFIX=/path/to/directory make install
+
## Command-line Arguments
These are the command-line arguments that can be provided to the
diff --git a/SPAWNFEST.md b/SPAWNFEST.md
index 7c61fbdf4..f843d14c9 100644
--- a/SPAWNFEST.md
+++ b/SPAWNFEST.md
@@ -20,14 +20,14 @@ The [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/
## Rationale
-One of the strengths of the Erlang programming language is the ability to seemlessly _debug_ and _trace_ Erlang code. Many tools and libraries exist, but they are sometimes under-utilized by the Community, either because their API is not intuitive (think to the [dbg](https://erlang.org/doc/man/dbg.html) Erlang module), or because they offer a limited, obsolete, UI (think the [debugger](http://erlang.org/doc/apps/debugger/debugger_chapter.html) application).
+One of the strengths of the Erlang programming language is the ability to seamlessly _debug_ and _trace_ Erlang code. Many tools and libraries exist, but they are sometimes under-utilized by the Community, either because their API is not intuitive (think to the [dbg](https://erlang.org/doc/man/dbg.html) Erlang module), or because they offer a limited, obsolete, UI (think the [debugger](http://erlang.org/doc/apps/debugger/debugger_chapter.html) application).
We want to solve this problem by leveraging some of the existing debugging and tracing facilities provided by Erlang/OTP and bringing the debugging experience directly in the text-editor, next to the code, improving the user experience when using such
tools.
[This video](https://www.youtube.com/watch?v=ydcrdwQKqI8&t=3s) shows what debugging Erlang code is like via the _debugger_ application. [This other video](https://www.youtube.com/watch?v=ydcrdwQKqI8) shows what the same experience looks like from Emacs.
-Due to the editor-agnostic nature of the _DAP_ protocol, a very similar experience is delivered to users of a different developement tool, be it _Vim_, _VS Code_ or _Sublime Text 3_.
+Due to the editor-agnostic nature of the _DAP_ protocol, a very similar experience is delivered to users of a different development tool, be it _Vim_, _VS Code_ or _Sublime Text 3_.
## Project Goal
diff --git a/apps/els_core/include/els_core.hrl b/apps/els_core/include/els_core.hrl
index 2ec182a12..a9cb862d0 100644
--- a/apps/els_core/include/els_core.hrl
+++ b/apps/els_core/include/els_core.hrl
@@ -17,58 +17,60 @@
%%------------------------------------------------------------------------------
%% Abstract Message
%%------------------------------------------------------------------------------
--type message() :: #{ jsonrpc := jsonrpc_vsn()
- }.
+-type message() :: #{jsonrpc := jsonrpc_vsn()}.
%%------------------------------------------------------------------------------
%% Request Message
%%------------------------------------------------------------------------------
--type request() :: #{ jsonrpc := jsonrpc_vsn()
- , id := number() | binary()
- , method := binary()
- , params => [any()] | map()
- }.
+-type request() :: #{
+ jsonrpc := jsonrpc_vsn(),
+ id := number() | binary(),
+ method := binary(),
+ params => [any()] | map()
+}.
%%------------------------------------------------------------------------------
%% Response Message
%%------------------------------------------------------------------------------
--type response() :: #{ jsonrpc := jsonrpc_vsn()
- , id := number() | binary() | null
- , result => any()
- , error => error(any())
- }.
+-type response() :: #{
+ jsonrpc := jsonrpc_vsn(),
+ id := number() | binary() | null,
+ result => any(),
+ error => error(any())
+}.
--type error(Type) :: #{ code := number()
- , message := binary()
- , data => Type
- }.
+-type error(Type) :: #{
+ code := number(),
+ message := binary(),
+ data => Type
+}.
%% Defined by JSON RPC
--define(ERR_PARSE_ERROR , -32700).
--define(ERR_INVALID_REQUEST , -32600).
--define(ERR_METHOD_NOT_FOUND , -32601).
--define(ERR_INVALID_PARAMS , -32602).
--define(ERR_INTERNAL_ERROR , -32603).
--define(ERR_SERVER_ERROR_START , -32099).
--define(ERR_SERVER_ERROR_END , -32000).
--define(ERR_SERVER_NOT_INITIALIZED , -32002).
--define(ERR_UNKNOWN_ERROR_CODE , -32001).
+-define(ERR_PARSE_ERROR, -32700).
+-define(ERR_INVALID_REQUEST, -32600).
+-define(ERR_METHOD_NOT_FOUND, -32601).
+-define(ERR_INVALID_PARAMS, -32602).
+-define(ERR_INTERNAL_ERROR, -32603).
+-define(ERR_SERVER_ERROR_START, -32099).
+-define(ERR_SERVER_ERROR_END, -32000).
+-define(ERR_SERVER_NOT_INITIALIZED, -32002).
+-define(ERR_UNKNOWN_ERROR_CODE, -32001).
%% Defined by the protocol
--define(ERR_REQUEST_CANCELLED , -32800).
+-define(ERR_REQUEST_CANCELLED, -32800).
%%------------------------------------------------------------------------------
%% Notification Message
%%------------------------------------------------------------------------------
--type notification(Method, Params) :: #{ jsonrpc := jsonrpc_vsn()
- , method := Method
- , params => Params
- }.
+-type notification(Method, Params) :: #{
+ jsonrpc := jsonrpc_vsn(),
+ method := Method,
+ params => Params
+}.
%%------------------------------------------------------------------------------
%% Cancellation Support
%%------------------------------------------------------------------------------
--type cancel_params() :: #{ id := number() | binary()
- }.
+-type cancel_params() :: #{id := number() | binary()}.
%%==============================================================================
%% Language Server Protocol
@@ -77,11 +79,12 @@
%%------------------------------------------------------------------------------
%% Position
%%------------------------------------------------------------------------------
--type line() :: number().
--type column() :: number().
--type position() :: #{ line := line()
- , character := column()
- }.
+-type line() :: number().
+-type column() :: number().
+-type position() :: #{
+ line := line(),
+ character := column()
+}.
%% This is used for defining folding ranges. It is not possible to just use
%% positions as this one `{Line + 1, 0}' in a folding range, because of
@@ -94,58 +97,64 @@
%%------------------------------------------------------------------------------
%% Range
%%------------------------------------------------------------------------------
--type range() :: #{ start := position()
- , 'end' := position()
- }.
+-type range() :: #{
+ start := position(),
+ 'end' := position()
+}.
%%------------------------------------------------------------------------------
%% Location
%%------------------------------------------------------------------------------
--type location() :: #{ uri := uri()
- , range := range()
- }.
+-type location() :: #{
+ uri := uri(),
+ range := range()
+}.
%%------------------------------------------------------------------------------
%% Folding Range
%%------------------------------------------------------------------------------
--type folding_range() :: #{ startLine := pos_integer()
- , startCharacter := pos_integer()
- , endLine := pos_integer()
- , endCharacter := pos_integer()
- }.
+-type folding_range() :: #{
+ startLine := pos_integer(),
+ startCharacter := pos_integer(),
+ endLine := pos_integer(),
+ endCharacter := pos_integer()
+}.
%%------------------------------------------------------------------------------
%% Diagnostics
%%------------------------------------------------------------------------------
--define(DIAGNOSTIC_ERROR , 1).
--define(DIAGNOSTIC_WARNING , 2).
--define(DIAGNOSTIC_INFO , 3).
--define(DIAGNOSTIC_HINT , 4).
+-define(DIAGNOSTIC_ERROR, 1).
+-define(DIAGNOSTIC_WARNING, 2).
+-define(DIAGNOSTIC_INFO, 3).
+-define(DIAGNOSTIC_HINT, 4).
%%------------------------------------------------------------------------------
%% Insert Text Format
%%------------------------------------------------------------------------------
-define(INSERT_TEXT_FORMAT_PLAIN_TEXT, 1).
--define(INSERT_TEXT_FORMAT_SNIPPET, 2).
--type insert_text_format() :: ?INSERT_TEXT_FORMAT_PLAIN_TEXT
- | ?INSERT_TEXT_FORMAT_SNIPPET.
+-define(INSERT_TEXT_FORMAT_SNIPPET, 2).
+-type insert_text_format() ::
+ ?INSERT_TEXT_FORMAT_PLAIN_TEXT
+ | ?INSERT_TEXT_FORMAT_SNIPPET.
%%------------------------------------------------------------------------------
%% Text Edit
%%------------------------------------------------------------------------------
--type text_edit() :: #{ range := range()
- , newText := binary()
- }.
+-type text_edit() :: #{
+ range := range(),
+ newText := binary()
+}.
%%------------------------------------------------------------------------------
%% Text Document Edit
%%------------------------------------------------------------------------------
--type text_document_edit() :: #{ textDocument := versioned_text_document_id()
- , edits := [text_edit()]
- }.
+-type text_document_edit() :: #{
+ textDocument := versioned_text_document_id(),
+ edits := [text_edit()]
+}.
%%------------------------------------------------------------------------------
%% Workspace Edit
@@ -153,25 +162,25 @@
-type document_change() :: text_document_edit().
--type workspace_edit() :: #{ changes => #{ uri() := [text_edit()]
- }
- , documentChanges => [document_change()]
- }.
-
+-type workspace_edit() :: #{
+ changes => #{uri() := [text_edit()]},
+ documentChanges => [document_change()]
+}.
%%------------------------------------------------------------------------------
%% Text Document Identifier
%%------------------------------------------------------------------------------
--type text_document_id() :: #{ uri := uri() }.
+-type text_document_id() :: #{uri := uri()}.
%%------------------------------------------------------------------------------
%% Text Document Item
%%------------------------------------------------------------------------------
--type text_document_item() :: #{ uri := uri()
- , languageId := binary()
- , version := number()
- , text := binary()
- }.
+-type text_document_item() :: #{
+ uri := uri(),
+ languageId := binary(),
+ version := number(),
+ text := binary()
+}.
%%------------------------------------------------------------------------------
%% Text Document Sync Kind
@@ -181,65 +190,75 @@
-define(TEXT_DOCUMENT_SYNC_KIND_FULL, 1).
-define(TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL, 2).
--type text_document_sync_kind() :: ?TEXT_DOCUMENT_SYNC_KIND_NONE
- | ?TEXT_DOCUMENT_SYNC_KIND_FULL
- | ?TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL.
+-type text_document_sync_kind() ::
+ ?TEXT_DOCUMENT_SYNC_KIND_NONE
+ | ?TEXT_DOCUMENT_SYNC_KIND_FULL
+ | ?TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL.
%%------------------------------------------------------------------------------
%% Text Document Sync Kind
%%------------------------------------------------------------------------------
--define(COMPLETION_TRIGGER_KIND_INVOKED, 1).
--define(COMPLETION_TRIGGER_KIND_CHARACTER, 2).
+-define(COMPLETION_TRIGGER_KIND_INVOKED, 1).
+-define(COMPLETION_TRIGGER_KIND_CHARACTER, 2).
-define(COMPLETION_TRIGGER_KIND_FOR_INCOMPLETE_COMPLETIONS, 3).
+-type completion_trigger_kind() ::
+ ?COMPLETION_TRIGGER_KIND_INVOKED
+ | ?COMPLETION_TRIGGER_KIND_CHARACTER
+ | ?COMPLETION_TRIGGER_KIND_FOR_INCOMPLETE_COMPLETIONS.
+
%%------------------------------------------------------------------------------
%% Versioned Text Document Identifier
%%------------------------------------------------------------------------------
--type versioned_text_document_id() :: #{ version := number() | null
- }.
+-type versioned_text_document_id() :: #{version := number() | null}.
%%------------------------------------------------------------------------------
%% Text Document Position Params
%%------------------------------------------------------------------------------
--type text_document_position_params() :: #{ textDocument := text_document_id()
- , position := position()
- }.
+-type text_document_position_params() :: #{
+ textDocument := text_document_id(),
+ position := position()
+}.
%%------------------------------------------------------------------------------
-%% Document Fiter
+%% Document Filter
%%------------------------------------------------------------------------------
--type document_filter() :: #{ language => binary()
- , scheme => binary()
- , pattern => binary()
- }.
+-type document_filter() :: #{
+ language => binary(),
+ scheme => binary(),
+ pattern => binary()
+}.
-type document_selector() :: [document_filter()].
%%------------------------------------------------------------------------------
%% Markup Content
%%------------------------------------------------------------------------------
--define(PLAINTEXT , plaintext).
--define(MARKDOWN , markdown).
+-define(PLAINTEXT, plaintext).
+-define(MARKDOWN, markdown).
--type markup_kind() :: ?PLAINTEXT
- | ?MARKDOWN.
+-type markup_kind() ::
+ ?PLAINTEXT
+ | ?MARKDOWN.
--type markup_content() :: #{ kind := markup_kind()
- , value := binary()
- }.
+-type markup_content() :: #{
+ kind := markup_kind(),
+ value := binary()
+}.
%%------------------------------------------------------------------------------
%% Document Highlight Kind
%%------------------------------------------------------------------------------
--define(DOCUMENT_HIGHLIGHT_KIND_TEXT, 1).
--define(DOCUMENT_HIGHLIGHT_KIND_READ, 2).
+-define(DOCUMENT_HIGHLIGHT_KIND_TEXT, 1).
+-define(DOCUMENT_HIGHLIGHT_KIND_READ, 2).
-define(DOCUMENT_HIGHLIGHT_KIND_WRITE, 3).
--type document_highlight_kind() :: ?DOCUMENT_HIGHLIGHT_KIND_TEXT
- | ?DOCUMENT_HIGHLIGHT_KIND_READ
- | ?DOCUMENT_HIGHLIGHT_KIND_WRITE.
+-type document_highlight_kind() ::
+ ?DOCUMENT_HIGHLIGHT_KIND_TEXT
+ | ?DOCUMENT_HIGHLIGHT_KIND_READ
+ | ?DOCUMENT_HIGHLIGHT_KIND_WRITE.
%%==============================================================================
%% Actual Protocol
@@ -248,275 +267,274 @@
%%------------------------------------------------------------------------------
%% Initialize Request
%%------------------------------------------------------------------------------
--type workspace_folder() :: #{ uri => uri()
- , name => binary()
- }.
+-type workspace_folder() :: #{
+ uri => uri(),
+ name => binary()
+}.
--define(COMPLETION_ITEM_KIND_TEXT, 1).
--define(COMPLETION_ITEM_KIND_METHOD, 2).
--define(COMPLETION_ITEM_KIND_FUNCTION, 3).
+-define(COMPLETION_ITEM_KIND_TEXT, 1).
+-define(COMPLETION_ITEM_KIND_METHOD, 2).
+-define(COMPLETION_ITEM_KIND_FUNCTION, 3).
-define(COMPLETION_ITEM_KIND_CONSTRUCTOR, 4).
--define(COMPLETION_ITEM_KIND_FIELD, 5).
--define(COMPLETION_ITEM_KIND_VARIABLE, 6).
--define(COMPLETION_ITEM_KIND_CLASS, 7).
--define(COMPLETION_ITEM_KIND_INTERFACE, 8).
--define(COMPLETION_ITEM_KIND_MODULE, 9).
--define(COMPLETION_ITEM_KIND_PROPERTY, 10).
--define(COMPLETION_ITEM_KIND_UNIT, 11).
--define(COMPLETION_ITEM_KIND_VALUE, 12).
--define(COMPLETION_ITEM_KIND_ENUM, 13).
--define(COMPLETION_ITEM_KIND_KEYWORD, 14).
--define(COMPLETION_ITEM_KIND_SNIPPET, 15).
--define(COMPLETION_ITEM_KIND_COLOR, 16).
--define(COMPLETION_ITEM_KIND_FILE, 17).
--define(COMPLETION_ITEM_KIND_REFERENCE, 18).
--define(COMPLETION_ITEM_KIND_FOLDER, 19).
+-define(COMPLETION_ITEM_KIND_FIELD, 5).
+-define(COMPLETION_ITEM_KIND_VARIABLE, 6).
+-define(COMPLETION_ITEM_KIND_CLASS, 7).
+-define(COMPLETION_ITEM_KIND_INTERFACE, 8).
+-define(COMPLETION_ITEM_KIND_MODULE, 9).
+-define(COMPLETION_ITEM_KIND_PROPERTY, 10).
+-define(COMPLETION_ITEM_KIND_UNIT, 11).
+-define(COMPLETION_ITEM_KIND_VALUE, 12).
+-define(COMPLETION_ITEM_KIND_ENUM, 13).
+-define(COMPLETION_ITEM_KIND_KEYWORD, 14).
+-define(COMPLETION_ITEM_KIND_SNIPPET, 15).
+-define(COMPLETION_ITEM_KIND_COLOR, 16).
+-define(COMPLETION_ITEM_KIND_FILE, 17).
+-define(COMPLETION_ITEM_KIND_REFERENCE, 18).
+-define(COMPLETION_ITEM_KIND_FOLDER, 19).
-define(COMPLETION_ITEM_KIND_ENUM_MEMBER, 20).
--define(COMPLETION_ITEM_KIND_CONSTANT, 21).
--define(COMPLETION_ITEM_KIND_STRUCT, 22).
--define(COMPLETION_ITEM_KIND_EVENT, 23).
--define(COMPLETION_ITEM_KIND_OPERATOR, 24).
--define(COMPLETION_ITEM_KIND_TYPE_PARAM, 25).
-
--type completion_item_kind() :: ?COMPLETION_ITEM_KIND_TEXT
- | ?COMPLETION_ITEM_KIND_METHOD
- | ?COMPLETION_ITEM_KIND_FUNCTION
- | ?COMPLETION_ITEM_KIND_CONSTRUCTOR
- | ?COMPLETION_ITEM_KIND_FIELD
- | ?COMPLETION_ITEM_KIND_VARIABLE
- | ?COMPLETION_ITEM_KIND_CLASS
- | ?COMPLETION_ITEM_KIND_INTERFACE
- | ?COMPLETION_ITEM_KIND_MODULE
- | ?COMPLETION_ITEM_KIND_PROPERTY
- | ?COMPLETION_ITEM_KIND_UNIT
- | ?COMPLETION_ITEM_KIND_VALUE
- | ?COMPLETION_ITEM_KIND_ENUM
- | ?COMPLETION_ITEM_KIND_KEYWORD
- | ?COMPLETION_ITEM_KIND_SNIPPET
- | ?COMPLETION_ITEM_KIND_COLOR
- | ?COMPLETION_ITEM_KIND_FILE
- | ?COMPLETION_ITEM_KIND_REFERENCE
- | ?COMPLETION_ITEM_KIND_FOLDER
- | ?COMPLETION_ITEM_KIND_ENUM_MEMBER
- | ?COMPLETION_ITEM_KIND_CONSTANT
- | ?COMPLETION_ITEM_KIND_STRUCT
- | ?COMPLETION_ITEM_KIND_EVENT
- | ?COMPLETION_ITEM_KIND_OPERATOR
- | ?COMPLETION_ITEM_KIND_TYPE_PARAM.
-
--type completion_item() :: #{ label := binary()
- , kind => completion_item_kind()
- , insertText => binary()
- , insertTextFormat => insert_text_format()
- , data => map()
- }.
+-define(COMPLETION_ITEM_KIND_CONSTANT, 21).
+-define(COMPLETION_ITEM_KIND_STRUCT, 22).
+-define(COMPLETION_ITEM_KIND_EVENT, 23).
+-define(COMPLETION_ITEM_KIND_OPERATOR, 24).
+-define(COMPLETION_ITEM_KIND_TYPE_PARAM, 25).
+
+-type completion_item_kind() ::
+ ?COMPLETION_ITEM_KIND_TEXT
+ | ?COMPLETION_ITEM_KIND_METHOD
+ | ?COMPLETION_ITEM_KIND_FUNCTION
+ | ?COMPLETION_ITEM_KIND_CONSTRUCTOR
+ | ?COMPLETION_ITEM_KIND_FIELD
+ | ?COMPLETION_ITEM_KIND_VARIABLE
+ | ?COMPLETION_ITEM_KIND_CLASS
+ | ?COMPLETION_ITEM_KIND_INTERFACE
+ | ?COMPLETION_ITEM_KIND_MODULE
+ | ?COMPLETION_ITEM_KIND_PROPERTY
+ | ?COMPLETION_ITEM_KIND_UNIT
+ | ?COMPLETION_ITEM_KIND_VALUE
+ | ?COMPLETION_ITEM_KIND_ENUM
+ | ?COMPLETION_ITEM_KIND_KEYWORD
+ | ?COMPLETION_ITEM_KIND_SNIPPET
+ | ?COMPLETION_ITEM_KIND_COLOR
+ | ?COMPLETION_ITEM_KIND_FILE
+ | ?COMPLETION_ITEM_KIND_REFERENCE
+ | ?COMPLETION_ITEM_KIND_FOLDER
+ | ?COMPLETION_ITEM_KIND_ENUM_MEMBER
+ | ?COMPLETION_ITEM_KIND_CONSTANT
+ | ?COMPLETION_ITEM_KIND_STRUCT
+ | ?COMPLETION_ITEM_KIND_EVENT
+ | ?COMPLETION_ITEM_KIND_OPERATOR
+ | ?COMPLETION_ITEM_KIND_TYPE_PARAM.
+
+-type completion_item() :: #{
+ label := binary(),
+ kind => completion_item_kind(),
+ insertText => binary(),
+ insertTextFormat => insert_text_format(),
+ data => map()
+}.
-type client_capabilities() ::
- #{ workspace => workspace_client_capabilities()
- , textDocument => text_document_client_capabilities()
- , experimental => any()
- }.
+ #{
+ workspace => workspace_client_capabilities(),
+ textDocument => text_document_client_capabilities(),
+ experimental => any()
+ }.
-type workspace_client_capabilities() ::
- #{ applyEdit => boolean()
- , workspaceEdit =>
- #{ documentChanges => boolean()
- }
- , didChangeConfiguration =>
- #{ dynamicRegistration => boolean()
- }
- , didChangeWatchedFiles =>
- #{ dynamicRegistration => boolean()
- }
- , symbol =>
- #{ dynamicRegistration => boolean()
- , symbolKind =>
- #{ valueSet => [symbol_kind()]
- }
- }
- , executeCommand =>
- #{ dynamicRegistration => boolean()
- }
- , workspaceFolders => boolean()
- , configuration => boolean()
- }.
+ #{
+ applyEdit => boolean(),
+ workspaceEdit =>
+ #{documentChanges => boolean()},
+ didChangeConfiguration =>
+ #{dynamicRegistration => boolean()},
+ didChangeWatchedFiles =>
+ #{dynamicRegistration => boolean()},
+ symbol =>
+ #{
+ dynamicRegistration => boolean(),
+ symbolKind =>
+ #{valueSet => [symbol_kind()]}
+ },
+ executeCommand =>
+ #{dynamicRegistration => boolean()},
+ workspaceFolders => boolean(),
+ configuration => boolean()
+ }.
-type text_document_client_capabilities() ::
- #{ synchronization =>
- #{ dynamicRegistration => boolean()
- , willSave => boolean()
- , willSaveWaitUntil => boolean()
- , didSave => boolean()
- }
- , completion =>
- #{ dynamicRegistration => boolean()
- , completionItem =>
- #{ snippetSupport => boolean()
- , commitCharactersSupport => boolean()
- , documentationFormat => markup_kind()
- , deprecatedSupport => boolean()
- }
- , completionItemKind =>
- #{ valueSet => [completion_item_kind()]
- }
- , contextSupport => boolean()
- }
- , hover =>
- #{ dynamicRegistration => boolean()
- , contentFormat => [markup_kind()]
- }
- , signatureHelp =>
- #{ dynamicRegistration => boolean()
- , signatureInformation =>
- #{ documentationFormat => [markup_kind()]
- }
- }
- , references =>
- #{ dynamicRegistration => boolean()
- }
- , documentHighlight =>
- #{ dynamicRegistration => boolean()
- }
- , documentSymbol =>
- #{ dynamicRegistration => boolean()
- , symbolKind =>
- #{ valueSet => [symbol_kind()]
- }
- }
- , formatting =>
- #{ dynamicRegistration => boolean()
- }
- , rangeFormatting =>
- #{ dynamicRegistration => boolean()
- }
- , onTypeFormatting =>
- #{ dynamicRegistration => boolean()
- }
- , definition =>
- #{ dynamicRegistration => boolean()
- }
- , typeDefinition =>
- #{ dynamicRegistration => boolean()
- }
- , implementation =>
- #{ dynamicRegistration => boolean()
- }
- , codeAction =>
- #{ dynamicRegistration => boolean()
- , codeActionLiteralSupport =>
- #{ codeActionKind :=
- #{ valueSet := [code_action_kind()]
- }
- }
- }
- , codeLens =>
- #{ dynamicRegistration => boolean()
- }
- , documentLink =>
- #{ dynamicRegistration => boolean()
- }
- , colorProvider =>
- #{ dynamicRegistration => boolean()
- }
- , rename =>
- #{ dynamicRegistration => boolean()
- }
- , publishDiagnostics =>
- #{ relatedInformation => boolean()
- }
- , foldingRange =>
- #{ dynamicRegistration => boolean()
- , rangeLimit => number()
- , lineFoldingOnly => boolean()
- }
- }.
+ #{
+ synchronization =>
+ #{
+ dynamicRegistration => boolean(),
+ willSave => boolean(),
+ willSaveWaitUntil => boolean(),
+ didSave => boolean()
+ },
+ completion =>
+ #{
+ dynamicRegistration => boolean(),
+ completionItem =>
+ #{
+ snippetSupport => boolean(),
+ commitCharactersSupport => boolean(),
+ documentationFormat => markup_kind(),
+ deprecatedSupport => boolean()
+ },
+ completionItemKind =>
+ #{valueSet => [completion_item_kind()]},
+ contextSupport => boolean()
+ },
+ hover =>
+ #{
+ dynamicRegistration => boolean(),
+ contentFormat => [markup_kind()]
+ },
+ signatureHelp =>
+ #{
+ dynamicRegistration => boolean(),
+ signatureInformation =>
+ #{documentationFormat => [markup_kind()]}
+ },
+ references =>
+ #{dynamicRegistration => boolean()},
+ documentHighlight =>
+ #{dynamicRegistration => boolean()},
+ documentSymbol =>
+ #{
+ dynamicRegistration => boolean(),
+ symbolKind =>
+ #{valueSet => [symbol_kind()]}
+ },
+ formatting =>
+ #{dynamicRegistration => boolean()},
+ rangeFormatting =>
+ #{dynamicRegistration => boolean()},
+ onTypeFormatting =>
+ #{dynamicRegistration => boolean()},
+ definition =>
+ #{dynamicRegistration => boolean()},
+ typeDefinition =>
+ #{dynamicRegistration => boolean()},
+ implementation =>
+ #{dynamicRegistration => boolean()},
+ codeAction =>
+ #{
+ dynamicRegistration => boolean(),
+ codeActionLiteralSupport =>
+ #{
+ codeActionKind :=
+ #{valueSet := [code_action_kind()]}
+ }
+ },
+ codeLens =>
+ #{dynamicRegistration => boolean()},
+ documentLink =>
+ #{dynamicRegistration => boolean()},
+ colorProvider =>
+ #{dynamicRegistration => boolean()},
+ rename =>
+ #{dynamicRegistration => boolean()},
+ publishDiagnostics =>
+ #{relatedInformation => boolean()},
+ foldingRange =>
+ #{
+ dynamicRegistration => boolean(),
+ rangeLimit => number(),
+ lineFoldingOnly => boolean()
+ }
+ }.
%%------------------------------------------------------------------------------
%% ShowMessage Notification
%%-----------------------------------------------------------------------------
--type show_message_notification() :: notification( show_message_method()
- , show_message_params()
- ).
+-type show_message_notification() :: notification(
+ show_message_method(),
+ show_message_params()
+).
-type show_message_method() :: 'window/showMessage'.
--type show_message_params() :: #{ type := show_message_type()
- , message := binary()
- }.
+-type show_message_params() :: #{
+ type := show_message_type(),
+ message := binary()
+}.
--define(MESSAGE_TYPE_ERROR , 1).
--define(MESSAGE_TYPE_WARNING , 2).
--define(MESSAGE_TYPE_INFO , 3).
--define(MESSAGE_TYPE_LOG , 4).
+-define(MESSAGE_TYPE_ERROR, 1).
+-define(MESSAGE_TYPE_WARNING, 2).
+-define(MESSAGE_TYPE_INFO, 3).
+-define(MESSAGE_TYPE_LOG, 4).
--type show_message_type() :: ?MESSAGE_TYPE_ERROR
- | ?MESSAGE_TYPE_WARNING
- | ?MESSAGE_TYPE_INFO
- | ?MESSAGE_TYPE_LOG.
+-type show_message_type() ::
+ ?MESSAGE_TYPE_ERROR
+ | ?MESSAGE_TYPE_WARNING
+ | ?MESSAGE_TYPE_INFO
+ | ?MESSAGE_TYPE_LOG.
%%------------------------------------------------------------------------------
%% Symbol Kinds
%%------------------------------------------------------------------------------
--define(SYMBOLKIND_FILE , 1).
--define(SYMBOLKIND_MODULE , 2).
--define(SYMBOLKIND_NAMESPACE , 3).
--define(SYMBOLKIND_PACKAGE , 4).
--define(SYMBOLKIND_CLASS , 5).
--define(SYMBOLKIND_METHOD , 6).
--define(SYMBOLKIND_PROPERTY , 7).
--define(SYMBOLKIND_FIELD , 8).
--define(SYMBOLKIND_CONSTRUCTOR , 9).
--define(SYMBOLKIND_ENUM , 10).
--define(SYMBOLKIND_INTERFACE , 11).
--define(SYMBOLKIND_FUNCTION , 12).
--define(SYMBOLKIND_VARIABLE , 13).
--define(SYMBOLKIND_CONSTANT , 14).
--define(SYMBOLKIND_STRING , 15).
--define(SYMBOLKIND_NUMBER , 16).
--define(SYMBOLKIND_BOOLEAN , 17).
--define(SYMBOLKIND_ARRAY , 18).
--define(SYMBOLKIND_OBJECT , 19).
--define(SYMBOLKIND_KEY , 20).
--define(SYMBOLKIND_NULL , 21).
--define(SYMBOLKIND_ENUM_MEMBER , 22).
--define(SYMBOLKIND_STRUCT , 23).
--define(SYMBOLKIND_EVENT , 24).
--define(SYMBOLKIND_OPERATOR , 25).
--define(SYMBOLKIND_TYPE_PARAMETER , 26).
-
--type symbol_kind() :: ?SYMBOLKIND_FILE
- | ?SYMBOLKIND_MODULE
- | ?SYMBOLKIND_NAMESPACE
- | ?SYMBOLKIND_PACKAGE
- | ?SYMBOLKIND_CLASS
- | ?SYMBOLKIND_METHOD
- | ?SYMBOLKIND_PROPERTY
- | ?SYMBOLKIND_FIELD
- | ?SYMBOLKIND_CONSTRUCTOR
- | ?SYMBOLKIND_ENUM
- | ?SYMBOLKIND_INTERFACE
- | ?SYMBOLKIND_FUNCTION
- | ?SYMBOLKIND_VARIABLE
- | ?SYMBOLKIND_CONSTANT
- | ?SYMBOLKIND_STRING
- | ?SYMBOLKIND_NUMBER
- | ?SYMBOLKIND_BOOLEAN
- | ?SYMBOLKIND_ARRAY
- | ?SYMBOLKIND_OBJECT
- | ?SYMBOLKIND_KEY
- | ?SYMBOLKIND_NULL
- | ?SYMBOLKIND_ENUM_MEMBER
- | ?SYMBOLKIND_STRUCT
- | ?SYMBOLKIND_EVENT
- | ?SYMBOLKIND_OPERATOR
- | ?SYMBOLKIND_TYPE_PARAMETER.
-
--type symbol_information() :: #{ name := binary()
- , kind := symbol_kind()
- , deprecated => boolean()
- , location := location()
- , containerName => binary()
- }.
+-define(SYMBOLKIND_FILE, 1).
+-define(SYMBOLKIND_MODULE, 2).
+-define(SYMBOLKIND_NAMESPACE, 3).
+-define(SYMBOLKIND_PACKAGE, 4).
+-define(SYMBOLKIND_CLASS, 5).
+-define(SYMBOLKIND_METHOD, 6).
+-define(SYMBOLKIND_PROPERTY, 7).
+-define(SYMBOLKIND_FIELD, 8).
+-define(SYMBOLKIND_CONSTRUCTOR, 9).
+-define(SYMBOLKIND_ENUM, 10).
+-define(SYMBOLKIND_INTERFACE, 11).
+-define(SYMBOLKIND_FUNCTION, 12).
+-define(SYMBOLKIND_VARIABLE, 13).
+-define(SYMBOLKIND_CONSTANT, 14).
+-define(SYMBOLKIND_STRING, 15).
+-define(SYMBOLKIND_NUMBER, 16).
+-define(SYMBOLKIND_BOOLEAN, 17).
+-define(SYMBOLKIND_ARRAY, 18).
+-define(SYMBOLKIND_OBJECT, 19).
+-define(SYMBOLKIND_KEY, 20).
+-define(SYMBOLKIND_NULL, 21).
+-define(SYMBOLKIND_ENUM_MEMBER, 22).
+-define(SYMBOLKIND_STRUCT, 23).
+-define(SYMBOLKIND_EVENT, 24).
+-define(SYMBOLKIND_OPERATOR, 25).
+-define(SYMBOLKIND_TYPE_PARAMETER, 26).
+
+-type symbol_kind() ::
+ ?SYMBOLKIND_FILE
+ | ?SYMBOLKIND_MODULE
+ | ?SYMBOLKIND_NAMESPACE
+ | ?SYMBOLKIND_PACKAGE
+ | ?SYMBOLKIND_CLASS
+ | ?SYMBOLKIND_METHOD
+ | ?SYMBOLKIND_PROPERTY
+ | ?SYMBOLKIND_FIELD
+ | ?SYMBOLKIND_CONSTRUCTOR
+ | ?SYMBOLKIND_ENUM
+ | ?SYMBOLKIND_INTERFACE
+ | ?SYMBOLKIND_FUNCTION
+ | ?SYMBOLKIND_VARIABLE
+ | ?SYMBOLKIND_CONSTANT
+ | ?SYMBOLKIND_STRING
+ | ?SYMBOLKIND_NUMBER
+ | ?SYMBOLKIND_BOOLEAN
+ | ?SYMBOLKIND_ARRAY
+ | ?SYMBOLKIND_OBJECT
+ | ?SYMBOLKIND_KEY
+ | ?SYMBOLKIND_NULL
+ | ?SYMBOLKIND_ENUM_MEMBER
+ | ?SYMBOLKIND_STRUCT
+ | ?SYMBOLKIND_EVENT
+ | ?SYMBOLKIND_OPERATOR
+ | ?SYMBOLKIND_TYPE_PARAMETER.
+
+-type symbol_information() :: #{
+ name := binary(),
+ kind := symbol_kind(),
+ deprecated => boolean(),
+ location := location(),
+ containerName => binary()
+}.
-define(SYMBOLTAG_DEPRECATED, 1).
@@ -525,99 +543,114 @@
%%------------------------------------------------------------------------------
%% Signatures
%%------------------------------------------------------------------------------
--type parameter_information() :: #{ label := binary()
- , documentation => binary()
- }.
--type signature_information() :: #{ label := binary()
- , documentation => binary()
- , parameters => [parameter_information()]
- }.
--type signature_help() :: #{ signatures := [signature_information()]
- , active_signature => number()
- , active_parameters => number()
- }.
+-type parameter_information() :: #{
+ label := binary(),
+ documentation => markup_content()
+}.
+-type signature_information() :: #{
+ label := binary(),
+ documentation => markup_content(),
+ parameters => [parameter_information()]
+}.
+-type signature_help() :: #{
+ signatures := [signature_information()],
+ activeSignature => non_neg_integer(),
+ activeParameter => non_neg_integer()
+}.
%%------------------------------------------------------------------------------
%% Formatting
%%------------------------------------------------------------------------------
--type formatting_options() :: #{ tabSize := integer()
- , insertSpaces := boolean()
- %% Spec says further properties must
- %% meet the following signature
- %% [key: string]: boolean | number | string;
- }.
+-type formatting_options() :: #{
+ tabSize := integer(),
+ insertSpaces := boolean()
+ %% Spec says further properties must
+ %% meet the following signature
+ %% [key: string]: boolean | number | string;
+}.
--type document_ontypeformatting_options() :: false |
- #{ first_trigger_character := string()
- , more_trigger_character => string()
- }.
+-type document_ontypeformatting_options() ::
+ false
+ | #{
+ first_trigger_character := string(),
+ more_trigger_character => string()
+ }.
%%------------------------------------------------------------------------------
%% Code Actions
%%------------------------------------------------------------------------------
-define(CODE_ACTION_KIND_QUICKFIX, <<"quickfix">>).
+-define(CODE_ACTION_KIND_BROWSE, <<"browse">>).
+
-type code_action_kind() :: binary().
--type code_action_context() :: #{ diagnostics := [els_diagnostics:diagnostic()]
- , only => [code_action_kind()]
- }.
+-type code_action_context() :: #{
+ diagnostics := [els_diagnostics:diagnostic()],
+ only => [code_action_kind()]
+}.
+
+-type code_action_params() :: #{
+ textDocument := text_document_id(),
+ range := range(),
+ context := code_action_context()
+}.
+
+-type code_action() :: #{
+ title := binary(),
+ kind => code_action_kind(),
+ diagnostics => [els_diagnostics:diagnostic()],
+ edit => workspace_edit(),
+ command => els_command:command()
+}.
+
+%%------------------------------------------------------------------------------
+%% Inlay hint
+%%------------------------------------------------------------------------------
+-define(INLAY_HINT_KIND_TYPE, 1).
+-define(INLAY_HINT_KIND_PARAMETER, 2).
+
+-type inlay_hint_kind() ::
+ ?INLAY_HINT_KIND_TYPE
+ | ?INLAY_HINT_KIND_PARAMETER.
+
+-type inlay_hint_label_part() ::
+ #{
+ value := binary(),
+ tooltip => binary() | markup_content(),
+ location => location(),
+ command => els_command:command()
+ }.
+
+-type inlay_hint() :: #{
+ position := position(),
+ label := binary() | [inlay_hint_label_part()],
+ kind => inlay_hint_kind(),
+ textEdits => [text_edit()],
+ tooltip => binary() | markup_content(),
+ paddingLeft => boolean(),
+ paddingRight => boolean(),
+ data => map()
+}.
+
+%%------------------------------------------------------------------------------
+%% Workspace
+%%------------------------------------------------------------------------------
--type code_action_params() :: #{ textDocument := text_document_id()
- , range := range()
- , context := code_action_context()
- }.
+-define(FILE_CHANGE_TYPE_CREATED, 1).
+-define(FILE_CHANGE_TYPE_CHANGED, 2).
+-define(FILE_CHANGE_TYPE_DELETED, 3).
--type code_action() :: #{ title := string()
- , kind => code_action_kind()
- , diagnostics => [els_diagnostics:diagnostic()]
- , edit => workspace_edit()
- , command => els_command:command()
- }.
+-type file_change_type() ::
+ ?FILE_CHANGE_TYPE_CREATED
+ | ?FILE_CHANGE_TYPE_CHANGED
+ | ?FILE_CHANGE_TYPE_DELETED.
%%------------------------------------------------------------------------------
%% Internals
%%------------------------------------------------------------------------------
--type pos() :: {integer(), integer()}.
--type uri() :: binary().
--type poi_kind() :: application
- | atom
- | behaviour
- | callback
- | define
- | export
- | export_entry
- | export_type
- | export_type_entry
- | folding_range
- | function
- | function_clause
- | implicit_fun
- | import_entry
- | include
- | include_lib
- | macro
- | module
- | parse_transform
- | record
- | record_def_field
- | record_expr
- | record_field
- | spec
- | type_application
- | type_definition
- | variable.
--type poi_range() :: #{ from := pos(), to := pos() }.
--type poi_id() :: atom()
- | {atom(), atom()} %% record_def_field, record_field
- | string() %% include, include_lib
- | {atom(), arity()}
- | {module(), atom(), arity()}.
--type poi() :: #{ kind := poi_kind()
- , id := poi_id()
- , data := any()
- , range := poi_range()
- }.
--type tree() :: erl_syntax:syntaxTree().
+-type pos() :: {integer(), integer()}.
+-type uri() :: binary().
+-type tree() :: erl_syntax:syntaxTree().
-endif.
diff --git a/apps/els_core/src/els_client.erl b/apps/els_core/src/els_client.erl
index 345322a14..0b49bc1da 100644
--- a/apps/els_core/src/els_client.erl
+++ b/apps/els_core/src/els_client.erl
@@ -8,54 +8,60 @@
%%==============================================================================
-behaviour(gen_server).
%% gen_server callbacks
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , code_change/3
- ]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ code_change/3
+]).
%%==============================================================================
%% Exports
%%==============================================================================
%% API
--export([ '$_cancelrequest'/0
- , '$_cancelrequest'/1
- , '$_settracenotification'/0
- , '$_unexpectedrequest'/0
- , completion/5
- , completionitem_resolve/1
- , definition/3
- , did_open/4
- , did_save/1
- , did_close/1
- , document_symbol/1
- , exit/0
- , hover/3
- , implementation/3
- , initialize/1
- , initialize/2
- , initialized/0
- , references/3
- , document_highlight/3
- , document_codeaction/3
- , document_codelens/1
- , document_formatting/3
- , document_rangeformatting/3
- , document_ontypeformatting/4
- , document_rename/4
- , folding_range/1
- , shutdown/0
- , start_link/1
- , stop/0
- , workspace_symbol/1
- , workspace_executecommand/2
- , preparecallhierarchy/3
- , callhierarchy_incomingcalls/1
- , callhierarchy_outgoingcalls/1
- , get_notifications/0
- ]).
-
--export([ handle_responses/1 ]).
+-export([
+ '$_cancelrequest'/0,
+ '$_cancelrequest'/1,
+ '$_settracenotification'/0,
+ '$_unexpectedrequest'/0,
+ completion/5,
+ completionitem_resolve/1,
+ signature_help/3,
+ definition/3,
+ did_open/4,
+ did_save/1,
+ did_change_watched_files/1,
+ did_close/1,
+ document_symbol/1,
+ exit/0,
+ hover/3,
+ implementation/3,
+ initialize/1,
+ initialize/2,
+ initialized/0,
+ references/3,
+ document_highlight/3,
+ document_codeaction/3,
+ document_codelens/1,
+ document_formatting/3,
+ document_rangeformatting/3,
+ document_ontypeformatting/4,
+ rename/4,
+ prepare_rename/3,
+ folding_range/1,
+ shutdown/0,
+ start_link/1,
+ stop/0,
+ workspace_symbol/1,
+ workspace_executecommand/2,
+ preparecallhierarchy/3,
+ callhierarchy_incomingcalls/1,
+ callhierarchy_outgoingcalls/1,
+ get_notifications/0,
+ inlay_hint/2
+]).
+
+-export([handle_responses/1]).
%%==============================================================================
%% Includes
@@ -72,287 +78,318 @@
%%==============================================================================
%% Record Definitions
%%==============================================================================
--record(state, { io_device :: atom() | pid()
- , request_id = 1 :: request_id()
- , pending = []
- , notifications = []
- , requests = []
- }).
+-record(state, {
+ io_device :: atom() | pid(),
+ request_id = 1 :: request_id(),
+ pending = [],
+ notifications = [],
+ requests = []
+}).
%%==============================================================================
%% Type Definitions
%%==============================================================================
--type state() :: #state{}.
+-type state() :: #state{}.
-type init_options() :: #{}.
--type request_id() :: pos_integer().
+-type request_id() :: pos_integer().
%%==============================================================================
%% API
%%==============================================================================
-spec '$_cancelrequest'() -> ok.
'$_cancelrequest'() ->
- gen_server:call(?SERVER, {'$_cancelrequest'}).
+ gen_server:call(?SERVER, {'$_cancelrequest'}).
-spec '$_cancelrequest'(request_id()) -> ok.
'$_cancelrequest'(Id) ->
- gen_server:call(?SERVER, {'$_cancelrequest', Id}).
+ gen_server:call(?SERVER, {'$_cancelrequest', Id}).
-spec '$_settracenotification'() -> ok.
'$_settracenotification'() ->
- gen_server:call(?SERVER, {'$_settracenotification'}).
+ gen_server:call(?SERVER, {'$_settracenotification'}).
-spec '$_unexpectedrequest'() -> ok.
'$_unexpectedrequest'() ->
- gen_server:call(?SERVER, {'$_unexpectedrequest'}).
+ gen_server:call(?SERVER, {'$_unexpectedrequest'}).
%% TODO: More accurate and consistent parameters list
--spec completion( uri()
- , non_neg_integer()
- , non_neg_integer()
- , integer()
- , binary()
- ) ->
- ok.
+-spec completion(
+ uri(),
+ non_neg_integer(),
+ non_neg_integer(),
+ integer(),
+ binary()
+) ->
+ ok.
completion(Uri, Line, Char, TriggerKind, TriggerCharacter) ->
- Opts = {Uri, Line, Char, TriggerKind, TriggerCharacter},
- gen_server:call(?SERVER, {completion, Opts}).
+ Opts = {Uri, Line, Char, TriggerKind, TriggerCharacter},
+ gen_server:call(?SERVER, {completion, Opts}).
-spec completionitem_resolve(completion_item()) -> ok.
completionitem_resolve(CompletionItem) ->
- gen_server:call(?SERVER, {completionitem_resolve, CompletionItem}).
+ gen_server:call(?SERVER, {completionitem_resolve, CompletionItem}).
+
+-spec signature_help(uri(), non_neg_integer(), non_neg_integer()) -> ok.
+signature_help(Uri, Line, Char) ->
+ gen_server:call(?SERVER, {signature_help, {Uri, Line, Char}}).
-spec definition(uri(), non_neg_integer(), non_neg_integer()) -> ok.
definition(Uri, Line, Char) ->
- gen_server:call(?SERVER, {definition, {Uri, Line, Char}}).
+ gen_server:call(?SERVER, {definition, {Uri, Line, Char}}).
-spec hover(uri(), non_neg_integer(), non_neg_integer()) -> ok.
hover(Uri, Line, Char) ->
- gen_server:call(?SERVER, {hover, {Uri, Line, Char}}).
+ gen_server:call(?SERVER, {hover, {Uri, Line, Char}}).
-spec implementation(uri(), non_neg_integer(), non_neg_integer()) -> ok.
implementation(Uri, Line, Char) ->
- gen_server:call(?SERVER, {implementation, {Uri, Line, Char}}).
+ gen_server:call(?SERVER, {implementation, {Uri, Line, Char}}).
-spec references(uri(), non_neg_integer(), non_neg_integer()) -> ok.
references(Uri, Line, Char) ->
- gen_server:call(?SERVER, {references, {Uri, Line, Char}}).
+ gen_server:call(?SERVER, {references, {Uri, Line, Char}}).
-spec document_highlight(uri(), non_neg_integer(), non_neg_integer()) -> ok.
document_highlight(Uri, Line, Char) ->
- gen_server:call(?SERVER, {document_highlight, {Uri, Line, Char}}).
+ gen_server:call(?SERVER, {document_highlight, {Uri, Line, Char}}).
-spec document_codeaction(uri(), range(), [els_diagnostics:diagnostic()]) -> ok.
document_codeaction(Uri, Range, Diagnostics) ->
- gen_server:call(?SERVER, {document_codeaction, {Uri, Range, Diagnostics}}).
+ gen_server:call(?SERVER, {document_codeaction, {Uri, Range, Diagnostics}}).
-spec document_codelens(uri()) -> ok.
document_codelens(Uri) ->
- gen_server:call(?SERVER, {document_codelens, {Uri}}).
+ gen_server:call(?SERVER, {document_codelens, {Uri}}).
-spec document_formatting(uri(), non_neg_integer(), boolean()) ->
- ok.
+ ok.
document_formatting(Uri, TabSize, InsertSpaces) ->
- gen_server:call(?SERVER, {document_formatting, {Uri, TabSize, InsertSpaces}}).
+ gen_server:call(?SERVER, {document_formatting, {Uri, TabSize, InsertSpaces}}).
-spec document_rangeformatting(uri(), range(), formatting_options()) ->
- ok.
+ ok.
document_rangeformatting(Uri, Range, FormattingOptions) ->
- gen_server:call(?SERVER, {document_rangeformatting,
- {Uri, Range, FormattingOptions}}).
-
--spec document_ontypeformatting(uri(), position(), string()
- , formatting_options()) ->
- ok.
+ gen_server:call(?SERVER, {document_rangeformatting, {Uri, Range, FormattingOptions}}).
+
+-spec document_ontypeformatting(
+ uri(),
+ position(),
+ string(),
+ formatting_options()
+) ->
+ ok.
document_ontypeformatting(Uri, Position, Char, FormattingOptions) ->
- gen_server:call(?SERVER, {document_ontypeformatting,
- {Uri, Position, Char, FormattingOptions}}).
+ gen_server:call(?SERVER, {document_ontypeformatting, {Uri, Position, Char, FormattingOptions}}).
+
+-spec rename(uri(), non_neg_integer(), non_neg_integer(), binary()) ->
+ ok.
+rename(Uri, Line, Character, NewName) ->
+ gen_server:call(?SERVER, {rename, {Uri, Line, Character, NewName}}).
--spec document_rename(uri(), non_neg_integer(), non_neg_integer(), binary()) ->
- ok.
-document_rename(Uri, Line, Character, NewName) ->
- gen_server:call(?SERVER, {rename, {Uri, Line, Character, NewName}}).
+-spec prepare_rename(uri(), non_neg_integer(), non_neg_integer()) ->
+ ok.
+prepare_rename(Uri, Line, Character) ->
+ gen_server:call(?SERVER, {preparerename, {Uri, Line, Character}}).
-spec did_open(uri(), binary(), number(), binary()) -> ok.
did_open(Uri, LanguageId, Version, Text) ->
- gen_server:call(?SERVER, {did_open, {Uri, LanguageId, Version, Text}}).
+ gen_server:call(?SERVER, {did_open, {Uri, LanguageId, Version, Text}}).
-spec did_save(uri()) -> ok.
did_save(Uri) ->
- gen_server:call(?SERVER, {did_save, {Uri}}).
+ gen_server:call(?SERVER, {did_save, {Uri}}).
+
+-spec did_change_watched_files([{uri(), file_change_type()}]) -> ok.
+did_change_watched_files(Changes) ->
+ gen_server:call(?SERVER, {did_change_watched_files, {Changes}}).
-spec did_close(uri()) -> ok.
did_close(Uri) ->
- gen_server:call(?SERVER, {did_close, {Uri}}).
+ gen_server:call(?SERVER, {did_close, {Uri}}).
-spec document_symbol(uri()) ->
- ok.
+ ok.
document_symbol(Uri) ->
- gen_server:call(?SERVER, {document_symbol, {Uri}}).
+ gen_server:call(?SERVER, {document_symbol, {Uri}}).
-spec folding_range(uri()) -> ok.
folding_range(Uri) ->
- gen_server:call(?SERVER, {folding_range, {Uri}}).
+ gen_server:call(?SERVER, {folding_range, {Uri}}).
-spec initialize(uri()) -> map().
initialize(RootUri) ->
- initialize(RootUri, #{}).
+ initialize(RootUri, #{}).
-spec initialize(uri(), init_options()) -> map().
initialize(RootUri, InitOptions) ->
- gen_server:call(?SERVER, {initialize, {RootUri, InitOptions}}, ?TIMEOUT).
+ gen_server:call(?SERVER, {initialize, {RootUri, InitOptions}}, ?TIMEOUT).
-spec initialized() -> map().
initialized() ->
- gen_server:call(?SERVER, {initialized, {}}).
+ gen_server:call(?SERVER, {initialized, {}}).
-spec shutdown() -> map().
shutdown() ->
- gen_server:call(?SERVER, {shutdown}).
+ gen_server:call(?SERVER, {shutdown}).
-spec exit() -> ok.
exit() ->
- gen_server:call(?SERVER, {exit}).
+ gen_server:call(?SERVER, {exit}).
-spec start_link(any()) -> {ok, pid()}.
start_link(Args) ->
- gen_server:start_link({local, ?SERVER}, ?MODULE, Args, []).
+ gen_server:start_link({local, ?SERVER}, ?MODULE, Args, []).
-spec stop() -> ok.
stop() ->
- gen_server:stop(?SERVER).
+ gen_server:stop(?SERVER).
-spec workspace_symbol(string()) ->
- ok.
+ ok.
workspace_symbol(Query) ->
- gen_server:call(?SERVER, {workspace_symbol, {Query}}).
+ gen_server:call(?SERVER, {workspace_symbol, {Query}}).
-spec workspace_executecommand(string(), [map()]) ->
- ok.
+ ok.
workspace_executecommand(Command, Args) ->
- gen_server:call(?SERVER, {workspace_executecommand, {Command, Args}}).
+ gen_server:call(?SERVER, {workspace_executecommand, {Command, Args}}).
-spec preparecallhierarchy(uri(), non_neg_integer(), non_neg_integer()) -> ok.
preparecallhierarchy(Uri, Line, Char) ->
- Args = {Uri, Line, Char},
- gen_server:call(?SERVER, {preparecallhierarchy, Args}).
+ Args = {Uri, Line, Char},
+ gen_server:call(?SERVER, {preparecallhierarchy, Args}).
-spec callhierarchy_incomingcalls(els_call_hierarchy_item:item()) -> ok.
callhierarchy_incomingcalls(Item) ->
- Args = {Item},
- gen_server:call(?SERVER, {callhierarchy_incomingcalls, Args}).
+ Args = {Item},
+ gen_server:call(?SERVER, {callhierarchy_incomingcalls, Args}).
-spec callhierarchy_outgoingcalls(els_call_hierarchy_item:item()) -> ok.
callhierarchy_outgoingcalls(Item) ->
- Args = {Item},
- gen_server:call(?SERVER, {callhierarchy_outgoingcalls, Args}).
+ Args = {Item},
+ gen_server:call(?SERVER, {callhierarchy_outgoingcalls, Args}).
-spec get_notifications() -> [any()].
get_notifications() ->
- gen_server:call(?SERVER, {get_notifications}).
+ gen_server:call(?SERVER, {get_notifications}).
-spec handle_responses([map()]) -> ok.
handle_responses(Responses) ->
- gen_server:cast(?SERVER, {handle_responses, Responses}).
+ gen_server:cast(?SERVER, {handle_responses, Responses}).
+
+-spec inlay_hint(uri(), range()) -> ok.
+inlay_hint(Uri, Range) ->
+ gen_server:call(?SERVER, {inlay_hint, {Uri, Range}}).
%%==============================================================================
%% gen_server Callback Functions
%%==============================================================================
-spec init(any()) -> {ok, state()}.
init(#{io_device := IoDevice}) ->
- Args = [ []
- , IoDevice
- , fun handle_responses/1
- , els_jsonrpc:default_opts()
- ],
- _Pid = proc_lib:spawn_link(els_stdio, loop, Args),
- State = #state{io_device = IoDevice},
- {ok, State}.
+ Args = [
+ [],
+ IoDevice,
+ fun handle_responses/1,
+ fun els_utils:json_decode_with_atom_keys/1
+ ],
+ _Pid = proc_lib:spawn_link(els_stdio, loop, Args),
+ State = #state{io_device = IoDevice},
+ {ok, State}.
-spec handle_call(any(), any(), state()) -> {reply, any(), state()}.
-handle_call({Action, Opts}, _From, State) when Action =:= did_save
- orelse Action =:= did_close
- orelse Action =:= did_open
- orelse Action =:= initialized ->
- #state{io_device = IoDevice} = State,
- Method = method_lookup(Action),
- Params = notification_params(Opts),
- Content = els_protocol:notification(Method, Params),
- send(IoDevice, Content),
- {reply, ok, State};
+handle_call({Action, Opts}, _From, State) when
+ Action =:= did_save;
+ Action =:= did_close;
+ Action =:= did_open;
+ Action =:= did_change_watched_files;
+ Action =:= initialized
+->
+ #state{io_device = IoDevice} = State,
+ Method = method_lookup(Action),
+ Params = notification_params(Action, Opts),
+ Content = els_protocol:notification(Method, Params),
+ send(IoDevice, Content),
+ {reply, ok, State};
handle_call({exit}, _From, State) ->
- #state{io_device = IoDevice} = State,
- RequestId = State#state.request_id,
- Method = <<"exit">>,
- Params = #{},
- Content = els_protocol:request(RequestId, Method, Params),
- send(IoDevice, Content),
- {reply, ok, State};
+ #state{io_device = IoDevice} = State,
+ RequestId = State#state.request_id,
+ Method = <<"exit">>,
+ Params = #{},
+ Content = els_protocol:request(RequestId, Method, Params),
+ send(IoDevice, Content),
+ {reply, ok, State};
handle_call({shutdown}, From, State) ->
- #state{io_device = IoDevice} = State,
- RequestId = State#state.request_id,
- Method = <<"shutdown">>,
- Content = els_protocol:request(RequestId, Method),
- send(IoDevice, Content),
- {noreply, State#state{ request_id = RequestId + 1
- , pending = [{RequestId, From} | State#state.pending]
- }};
+ #state{io_device = IoDevice} = State,
+ RequestId = State#state.request_id,
+ Method = <<"shutdown">>,
+ Content = els_protocol:request(RequestId, Method),
+ send(IoDevice, Content),
+ {noreply, State#state{
+ request_id = RequestId + 1,
+ pending = [{RequestId, From} | State#state.pending]
+ }};
handle_call({'$_cancelrequest'}, _From, State) ->
- #state{request_id = Id} = State,
- do_cancel_request(Id - 1, State),
- {reply, ok, State};
+ #state{request_id = Id} = State,
+ do_cancel_request(Id - 1, State),
+ {reply, ok, State};
handle_call({'$_cancelrequest', Id}, _From, State) ->
- do_cancel_request(Id, State),
- {reply, ok, State};
+ do_cancel_request(Id, State),
+ {reply, ok, State};
handle_call({'$_settracenotification'}, _From, State) ->
- #state{io_device = IoDevice} = State,
- Method = <<"$/setTraceNotification">>,
- Params = #{value => <<"verbose">>},
- Content = els_protocol:notification(Method, Params),
- send(IoDevice, Content),
- {reply, ok, State};
+ #state{io_device = IoDevice} = State,
+ Method = <<"$/setTraceNotification">>,
+ Params = #{value => <<"verbose">>},
+ Content = els_protocol:notification(Method, Params),
+ send(IoDevice, Content),
+ {reply, ok, State};
handle_call({'$_unexpectedrequest'}, From, State) ->
- #state{io_device = IoDevice} = State,
- RequestId = State#state.request_id,
- Method = <<"$/unexpectedRequest">>,
- Params = #{},
- Content = els_protocol:request(RequestId, Method, Params),
- send(IoDevice, Content),
- {noreply, State#state{ request_id = RequestId + 1
- , pending = [{RequestId, From} | State#state.pending]
- }};
+ #state{io_device = IoDevice} = State,
+ RequestId = State#state.request_id,
+ Method = <<"$/unexpectedRequest">>,
+ Params = #{},
+ Content = els_protocol:request(RequestId, Method, Params),
+ send(IoDevice, Content),
+ {noreply, State#state{
+ request_id = RequestId + 1,
+ pending = [{RequestId, From} | State#state.pending]
+ }};
handle_call({get_notifications}, _From, State) ->
- #state{notifications = Notifications} = State,
- {reply, Notifications, State#state { notifications = []}};
+ #state{notifications = Notifications} = State,
+ {reply, Notifications, State#state{notifications = []}};
handle_call(Input = {Action, _}, From, State) ->
- #state{ io_device = IoDevice
- , request_id = RequestId
- } = State,
- Method = method_lookup(Action),
- Params = request_params(Input),
- Content = els_protocol:request(RequestId, Method, Params),
- send(IoDevice, Content),
- {noreply, State#state{ request_id = RequestId + 1
- , pending = [{RequestId, From} | State#state.pending]
- }}.
+ #state{
+ io_device = IoDevice,
+ request_id = RequestId
+ } = State,
+ Method = method_lookup(Action),
+ Params = request_params(Input),
+ Content = els_protocol:request(RequestId, Method, Params),
+ send(IoDevice, Content),
+ {noreply, State#state{
+ request_id = RequestId + 1,
+ pending = [{RequestId, From} | State#state.pending]
+ }}.
-spec handle_cast(any(), state()) -> {noreply, state()}.
handle_cast({handle_responses, Responses}, State) ->
- #state{ pending = Pending0
- , notifications = Notifications0
- , requests = Requests0
- } = State,
- {Pending, Notifications, Requests}
- = do_handle_messages(Responses, Pending0, Notifications0, Requests0),
- {noreply, State#state{ pending = Pending
- , notifications = Notifications
- , requests = Requests
- }};
+ #state{
+ pending = Pending0,
+ notifications = Notifications0,
+ requests = Requests0
+ } = State,
+ {Pending, Notifications, Requests} =
+ do_handle_messages(Responses, Pending0, Notifications0, Requests0),
+ {noreply, State#state{
+ pending = Pending,
+ notifications = Notifications,
+ requests = Requests
+ }};
handle_cast(_Request, State) ->
- {noreply, State}.
+ {noreply, State}.
-spec code_change(any(), state(), any()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
+ {ok, State}.
%%==============================================================================
%% Internal Functions
@@ -360,172 +397,236 @@ code_change(_OldVsn, State, _Extra) ->
%% @doc handle messages received from the transport layer
-spec do_handle_messages([map()], [any()], [any()], [any()]) ->
- {[any()], [any()], [any()]}.
+ {[any()], [any()], [any()]}.
do_handle_messages([], Pending, Notifications, Requests) ->
- {Pending, Notifications, Requests};
-do_handle_messages([Message|Messages], Pending, Notifications, Requests) ->
- case is_response(Message) of
- true ->
- RequestId = maps:get(id, Message),
- ?LOG_DEBUG("[CLIENT] Handling Response [response=~p]", [Message]),
- case lists:keyfind(RequestId, 1, Pending) of
- {RequestId, From} ->
- gen_server:reply(From, Message),
- do_handle_messages( Messages
- , lists:keydelete(RequestId, 1, Pending)
- , Notifications
- , Requests
- );
- false ->
- do_handle_messages(Messages, Pending, Notifications, Requests)
- end;
- false ->
- case is_notification(Message) of
+ {Pending, Notifications, Requests};
+do_handle_messages([Message | Messages], Pending, Notifications, Requests) ->
+ case is_response(Message) of
true ->
- ?LOG_DEBUG( "[CLIENT] Discarding Notification [message=~p]"
- , [Message]),
- do_handle_messages( Messages
- , Pending
- , [Message|Notifications]
- , Requests);
+ RequestId = maps:get(id, Message),
+ ?LOG_DEBUG("[CLIENT] Handling Response [response=~p]", [Message]),
+ case lists:keyfind(RequestId, 1, Pending) of
+ {RequestId, From} ->
+ gen_server:reply(From, Message),
+ do_handle_messages(
+ Messages,
+ lists:keydelete(RequestId, 1, Pending),
+ Notifications,
+ Requests
+ );
+ false ->
+ do_handle_messages(Messages, Pending, Notifications, Requests)
+ end;
false ->
- ?LOG_DEBUG( "[CLIENT] Discarding Server Request [message=~p]"
- , [Message]),
- do_handle_messages( Messages
- , Pending
- , Notifications
- , [Message|Requests])
- end
- end.
+ case is_notification(Message) of
+ true ->
+ ?LOG_DEBUG(
+ "[CLIENT] Discarding Notification [message=~p]",
+ [Message]
+ ),
+ do_handle_messages(
+ Messages,
+ Pending,
+ [Message | Notifications],
+ Requests
+ );
+ false ->
+ ?LOG_DEBUG(
+ "[CLIENT] Discarding Server Request [message=~p]",
+ [Message]
+ ),
+ do_handle_messages(
+ Messages,
+ Pending,
+ Notifications,
+ [Message | Requests]
+ )
+ end
+ end.
-spec method_lookup(atom()) -> binary().
-method_lookup(completion) -> <<"textDocument/completion">>;
-method_lookup(completionitem_resolve) -> <<"completionItem/resolve">>;
-method_lookup(definition) -> <<"textDocument/definition">>;
-method_lookup(document_symbol) -> <<"textDocument/documentSymbol">>;
-method_lookup(references) -> <<"textDocument/references">>;
-method_lookup(document_highlight) -> <<"textDocument/documentHighlight">>;
-method_lookup(document_codeaction) -> <<"textDocument/codeAction">>;
-method_lookup(document_codelens) -> <<"textDocument/codeLens">>;
-method_lookup(document_formatting) -> <<"textDocument/formatting">>;
+method_lookup(completion) -> <<"textDocument/completion">>;
+method_lookup(completionitem_resolve) -> <<"completionItem/resolve">>;
+method_lookup(signature_help) -> <<"textDocument/signatureHelp">>;
+method_lookup(definition) -> <<"textDocument/definition">>;
+method_lookup(document_symbol) -> <<"textDocument/documentSymbol">>;
+method_lookup(references) -> <<"textDocument/references">>;
+method_lookup(document_highlight) -> <<"textDocument/documentHighlight">>;
+method_lookup(document_codeaction) -> <<"textDocument/codeAction">>;
+method_lookup(document_codelens) -> <<"textDocument/codeLens">>;
+method_lookup(document_formatting) -> <<"textDocument/formatting">>;
method_lookup(document_rangeformatting) -> <<"textDocument/rangeFormatting">>;
method_lookup(document_ontypeormatting) -> <<"textDocument/onTypeFormatting">>;
-method_lookup(rename) -> <<"textDocument/rename">>;
-method_lookup(did_open) -> <<"textDocument/didOpen">>;
-method_lookup(did_save) -> <<"textDocument/didSave">>;
-method_lookup(did_close) -> <<"textDocument/didClose">>;
-method_lookup(hover) -> <<"textDocument/hover">>;
-method_lookup(implementation) -> <<"textDocument/implementation">>;
-method_lookup(folding_range) -> <<"textDocument/foldingRange">>;
+method_lookup(rename) -> <<"textDocument/rename">>;
+method_lookup(preparerename) -> <<"textDocument/prepareRename">>;
+method_lookup(did_open) -> <<"textDocument/didOpen">>;
+method_lookup(did_save) -> <<"textDocument/didSave">>;
+method_lookup(did_close) -> <<"textDocument/didClose">>;
+method_lookup(hover) -> <<"textDocument/hover">>;
+method_lookup(implementation) -> <<"textDocument/implementation">>;
+method_lookup(folding_range) -> <<"textDocument/foldingRange">>;
+method_lookup(semantic_tokens) -> <<"textDocument/semanticTokens/full">>;
method_lookup(preparecallhierarchy) -> <<"textDocument/prepareCallHierarchy">>;
+method_lookup(inlay_hint) -> <<"textDocument/inlayHint">>;
method_lookup(callhierarchy_incomingcalls) -> <<"callHierarchy/incomingCalls">>;
method_lookup(callhierarchy_outgoingcalls) -> <<"callHierarchy/outgoingCalls">>;
-method_lookup(workspace_symbol) -> <<"workspace/symbol">>;
+method_lookup(workspace_symbol) -> <<"workspace/symbol">>;
method_lookup(workspace_executecommand) -> <<"workspace/executeCommand">>;
-method_lookup(initialize) -> <<"initialize">>;
-method_lookup(initialized) -> <<"initialized">>.
+method_lookup(did_change_watched_files) -> <<"workspace/didChangeWatchedFiles">>;
+method_lookup(initialize) -> <<"initialize">>;
+method_lookup(initialized) -> <<"initialized">>.
-spec request_params(tuple()) -> any().
request_params({document_symbol, {Uri}}) ->
- TextDocument = #{ uri => Uri },
- #{ textDocument => TextDocument };
+ TextDocument = #{uri => Uri},
+ #{textDocument => TextDocument};
request_params({workspace_symbol, {Query}}) ->
- #{ query => Query };
+ #{query => Query};
+request_params({inlay_hint, {Uri, Range}}) ->
+ #{
+ textDocument => #{uri => Uri},
+ range => Range
+ };
request_params({workspace_executecommand, {Command, Args}}) ->
- #{ command => Command
- , arguments => Args };
-request_params({ completion
- , {Uri, Line, Char, TriggerKind, TriggerCharacter}}) ->
- #{ textDocument => #{ uri => Uri }
- , position => #{ line => Line - 1
- , character => Char - 1
- }
- , context => #{ triggerKind => TriggerKind
- , triggerCharacter => TriggerCharacter
- }
- };
+ #{
+ command => Command,
+ arguments => Args
+ };
+request_params({completion, {Uri, Line, Char, TriggerKind, TriggerCharacter}}) ->
+ #{
+ textDocument => #{uri => Uri},
+ position => #{
+ line => Line - 1,
+ character => Char - 1
+ },
+ context => #{
+ triggerKind => TriggerKind,
+ triggerCharacter => TriggerCharacter
+ }
+ };
request_params({completionitem_resolve, CompletionItem}) ->
- CompletionItem;
+ CompletionItem;
+request_params({signature_help, {Uri, Line, Char}}) ->
+ #{
+ textDocument => #{uri => Uri},
+ position => #{
+ line => Line - 1,
+ character => Char - 1
+ }
+ };
request_params({initialize, {RootUri, InitOptions}}) ->
- ContentFormat = [ ?MARKDOWN , ?PLAINTEXT ],
- TextDocument = #{ <<"completion">> =>
- #{ <<"contextSupport">> => 'true' }
- , <<"hover">> =>
- #{ <<"contentFormat">> => ContentFormat }
- },
- #{ <<"rootUri">> => RootUri
- , <<"initializationOptions">> => InitOptions
- , <<"capabilities">> => #{ <<"textDocument">> => TextDocument }
- };
-request_params({ document_codeaction, {Uri, Range, Diagnostics}}) ->
- #{ textDocument => #{ uri => Uri }
- , range => Range
- , context => #{ diagnostics => Diagnostics }
- };
-request_params({ document_codelens, {Uri}}) ->
- #{ textDocument => #{ uri => Uri }};
-request_params({ document_formatting
- , {Uri, TabSize, InsertSpaces}}) ->
- #{ textDocument => #{ uri => Uri }
- , options => #{ tabSize => TabSize
- , insertSpaces => InsertSpaces
- }
- };
+ ContentFormat = [?MARKDOWN, ?PLAINTEXT],
+ TextDocument = #{
+ <<"completion">> =>
+ #{
+ <<"contextSupport">> => 'true',
+ <<"completionItem">> =>
+ #{<<"snippetSupport">> => 'true'}
+ },
+ <<"hover">> =>
+ #{<<"contentFormat">> => ContentFormat},
+ <<"rename">> =>
+ #{<<"prepareSupport">> => 'true'}
+ },
+ #{
+ <<"rootUri">> => RootUri,
+ <<"initializationOptions">> => InitOptions,
+ <<"capabilities">> => #{<<"textDocument">> => TextDocument}
+ };
+request_params({document_codeaction, {Uri, Range, Diagnostics}}) ->
+ #{
+ textDocument => #{uri => Uri},
+ range => Range,
+ context => #{diagnostics => Diagnostics}
+ };
+request_params({document_codelens, {Uri}}) ->
+ #{textDocument => #{uri => Uri}};
+request_params({document_formatting, {Uri, TabSize, InsertSpaces}}) ->
+ #{
+ textDocument => #{uri => Uri},
+ options => #{
+ tabSize => TabSize,
+ insertSpaces => InsertSpaces
+ }
+ };
request_params({rename, {Uri, Line, Character, NewName}}) ->
- #{ textDocument => #{ uri => Uri }
- , position => #{ line => Line
- , character => Character
- }
- , newName => NewName
- };
+ #{
+ textDocument => #{uri => Uri},
+ position => #{
+ line => Line,
+ character => Character
+ },
+ newName => NewName
+ };
+request_params({preparerename, {Uri, Line, Character}}) ->
+ #{
+ textDocument => #{uri => Uri},
+ position => #{
+ line => Line,
+ character => Character
+ }
+ };
request_params({folding_range, {Uri}}) ->
- TextDocument = #{ uri => Uri },
- #{ textDocument => TextDocument };
+ TextDocument = #{uri => Uri},
+ #{textDocument => TextDocument};
request_params({callhierarchy_incomingcalls, {Item}}) ->
- #{item => Item};
+ #{item => Item};
request_params({callhierarchy_outgoingcalls, {Item}}) ->
- #{item => Item};
+ #{item => Item};
request_params({_Action, {Uri, Line, Char}}) ->
- #{ textDocument => #{ uri => Uri }
- , position => #{ line => Line - 1
- , character => Char - 1
- }
- }.
-
--spec notification_params(tuple()) -> map().
-notification_params({Uri}) ->
- TextDocument = #{ uri => Uri },
- #{textDocument => TextDocument};
-notification_params({Uri, LanguageId, Version, Text}) ->
- TextDocument = #{ uri => Uri
- , languageId => LanguageId
- , version => Version
- , text => Text
- },
- #{textDocument => TextDocument};
-notification_params({}) ->
- #{}.
+ #{
+ textDocument => #{uri => Uri},
+ position => #{
+ line => Line - 1,
+ character => Char - 1
+ }
+ }.
+
+-spec notification_params(atom(), tuple()) -> map().
+notification_params(did_change_watched_files, {Changes}) ->
+ #{
+ changes => [
+ #{
+ uri => Uri,
+ type => Type
+ }
+ || {Uri, Type} <- Changes
+ ]
+ };
+notification_params(_Action, {Uri}) ->
+ TextDocument = #{uri => Uri},
+ #{textDocument => TextDocument};
+notification_params(_Action, {Uri, LanguageId, Version, Text}) ->
+ TextDocument = #{
+ uri => Uri,
+ languageId => LanguageId,
+ version => Version,
+ text => Text
+ },
+ #{textDocument => TextDocument};
+notification_params(_Action, {}) ->
+ #{}.
-spec is_notification(map()) -> boolean().
is_notification(#{id := _Id}) ->
- false;
+ false;
is_notification(_) ->
- true.
+ true.
-spec is_response(map()) -> boolean().
is_response(#{method := _Method}) ->
- false;
+ false;
is_response(_) ->
- true.
+ true.
-spec do_cancel_request(request_id(), state()) -> ok.
do_cancel_request(Id, State) ->
- #state{io_device = IoDevice} = State,
- Method = <<"$/cancelRequest">>,
- Params = #{id => Id},
- Content = els_protocol:notification(Method, Params),
- send(IoDevice, Content).
+ #state{io_device = IoDevice} = State,
+ Method = <<"$/cancelRequest">>,
+ Params = #{id => Id},
+ Content = els_protocol:notification(Method, Params),
+ send(IoDevice, Content).
-spec send(atom() | pid(), binary()) -> ok.
send(IoDevice, Payload) ->
- els_stdio:send(IoDevice, Payload).
+ els_stdio:send(IoDevice, Payload).
diff --git a/apps/els_core/src/els_command.erl b/apps/els_core/src/els_command.erl
index b751ade32..8e7f13953 100644
--- a/apps/els_core/src/els_command.erl
+++ b/apps/els_core/src/els_command.erl
@@ -6,28 +6,31 @@
%%==============================================================================
%% API
%%==============================================================================
--export([ with_prefix/1
- , without_prefix/1
- ]).
+-export([
+ with_prefix/1,
+ without_prefix/1
+]).
%%==============================================================================
%% Constructors
%%==============================================================================
--export([ make_command/3 ]).
+-export([make_command/3]).
%%==============================================================================
%% Type Definitions
%%==============================================================================
--type command() :: #{ title := binary()
- , command := command_id()
- , arguments => [any()]
- }.
+-type command() :: #{
+ title := binary(),
+ command := command_id(),
+ arguments => [any()]
+}.
-type command_id() :: binary().
--export_type([ command/0
- , command_id/0
- ]).
+-export_type([
+ command/0,
+ command_id/0
+]).
%%==============================================================================
%% API
@@ -36,16 +39,16 @@
%% @doc Add a server-unique prefix to a command.
-spec with_prefix(command_id()) -> command_id().
with_prefix(Id) ->
- Prefix = server_prefix(),
- <>.
+ Prefix = server_prefix(),
+ <>.
%% @doc Strip a server-unique prefix from a command.
-spec without_prefix(command_id()) -> command_id().
without_prefix(Id0) ->
- case binary:split(Id0, <<":">>) of
- [_, Id] -> Id;
- [Id] -> Id
- end.
+ case binary:split(Id0, <<":">>) of
+ [_, Id] -> Id;
+ [Id] -> Id
+ end.
%%==============================================================================
%% Constructors
@@ -53,10 +56,11 @@ without_prefix(Id0) ->
-spec make_command(binary(), command_id(), [map()]) -> command().
make_command(Title, CommandId, Args) ->
- #{ title => Title
- , command => with_prefix(CommandId)
- , arguments => Args
- }.
+ #{
+ title => Title,
+ command => with_prefix(CommandId),
+ arguments => Args
+ }.
%%==============================================================================
%% Internal Functions
@@ -69,4 +73,4 @@ make_command(Title, CommandId, Args) ->
%% erlang_ls instances at the same time against a single client.
-spec server_prefix() -> binary().
server_prefix() ->
- els_utils:to_binary(os:getpid()).
+ els_utils:to_binary(os:getpid()).
diff --git a/apps/els_core/src/els_config.erl b/apps/els_core/src/els_config.erl
index 2fd3652ce..270c14188 100644
--- a/apps/els_core/src/els_config.erl
+++ b/apps/els_core/src/els_config.erl
@@ -1,19 +1,22 @@
-module(els_config).
%% API
--export([ do_initialize/4
- , initialize/3
- , initialize/4
- , get/1
- , set/2
- , start_link/0
- ]).
+-export([
+ do_initialize/4,
+ initialize/3,
+ initialize/4,
+ get/1,
+ set/2,
+ start_link/0,
+ is_dep/1
+]).
%% gen_server callbacks
--export([ init/1
- , handle_call/3
- , handle_cast/2
- ]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2
+]).
%%==============================================================================
%% Includes
@@ -26,57 +29,67 @@
%%==============================================================================
-define(DEFAULT_CONFIG_FILE, "erlang_ls.config").
-define(ALTERNATIVE_CONFIG_FILE, "erlang_ls.yaml").
--define( DEFAULT_EXCLUDED_OTP_APPS
- , [ "megaco"
- , "diameter"
- , "snmp"
- , "wx"
- ]
- ).
+-define(DEFAULT_EXCLUDED_OTP_APPS, [
+ "megaco",
+ "diameter",
+ "snmp",
+ "wx"
+]).
-define(SERVER, ?MODULE).
%% TODO: Refine names to avoid confusion
--type key() :: apps_dirs
- | apps_paths
- | capabilities
- | diagnostics
- | deps_dirs
- | deps_paths
- | include_dirs
- | include_paths
- | lenses
- | otp_path
- | otp_paths
- | otp_apps_exclude
- | plt_path
- | root_uri
- | search_paths
- | code_reload
- | elvis_config_path
- | indexing_enabled
- | bsp_enabled
- | compiler_telemetry_enabled.
-
--type path() :: file:filename().
--type state() :: #{ apps_dirs => [path()]
- , apps_paths => [path()]
- , lenses => [els_code_lens:lens_id()]
- , diagnostics => [els_diagnostics:diagnostic_id()]
- , deps_dirs => [path()]
- , deps_paths => [path()]
- , include_dirs => [path()]
- , include_paths => [path()]
- , otp_path => path()
- , otp_paths => [path()]
- , otp_apps_exclude => [string()]
- , plt_path => path()
- , root_uri => uri()
- , search_paths => [path()]
- , code_reload => map() | 'disabled'
- , indexing_enabled => boolean()
- , bsp_enabled => boolean() | auto
- , compiler_telemetry_enabled => boolean()
- }.
+-type key() ::
+ apps_dirs
+ | apps_paths
+ | capabilities
+ | diagnostics
+ | deps_dirs
+ | deps_paths
+ | include_dirs
+ | include_paths
+ | lenses
+ | otp_path
+ | otp_paths
+ | otp_apps_exclude
+ | plt_path
+ | root_uri
+ | search_paths
+ | code_reload
+ | elvis_config_path
+ | indexing_enabled
+ | compiler_telemetry_enabled
+ | refactorerl
+ | wrangler
+ | edoc_custom_tags
+ | providers
+ | formatting.
+
+-type path() :: file:filename().
+-type state() :: #{
+ apps_dirs => [path()],
+ apps_paths => [path()],
+ lenses => [els_code_lens:lens_id()],
+ diagnostics => [els_diagnostics:diagnostic_id()],
+ deps_dirs => [path()],
+ deps_paths => [path()],
+ include_dirs => [path()],
+ include_paths => [path()],
+ otp_path => path(),
+ otp_paths => [path()],
+ otp_apps_exclude => [string()],
+ plt_path => path(),
+ root_uri => uri(),
+ search_paths => [path()],
+ code_reload => map() | 'disabled',
+ indexing_enabled => boolean(),
+ compiler_telemetry_enabled => boolean(),
+ wrangler => map() | 'notconfigured',
+ refactorerl => map() | 'notconfigured',
+ providers => map(),
+ formatting => map()
+}.
+
+-type error_reporting() :: lsp_notification | log.
%%==============================================================================
%% Exported functions
@@ -84,100 +97,238 @@
-spec initialize(uri(), map(), map()) -> ok.
initialize(RootUri, Capabilities, InitOptions) ->
- initialize(RootUri, Capabilities, InitOptions, _ReportMissingConfig = false).
+ %% https://github.com/erlang-ls/erlang_ls/issues/1060
+ initialize(RootUri, Capabilities, InitOptions, _ErrorReporting = log).
--spec initialize(uri(), map(), map(), boolean()) -> ok.
-initialize(RootUri, Capabilities, InitOptions, ReportMissingConfig) ->
- RootPath = els_utils:to_list(els_uri:path(RootUri)),
- Config = consult_config(
- config_paths(RootPath, InitOptions), ReportMissingConfig),
- do_initialize(RootUri, Capabilities, InitOptions, Config).
+-spec initialize(uri(), map(), map(), error_reporting()) -> ok.
+initialize(RootUri, Capabilities, InitOptions, ErrorReporting) ->
+ RootPath = els_utils:to_list(els_uri:path(RootUri)),
+ ?LOG_INFO("Root path: ~s", [RootPath]),
+ ConfigPaths = config_paths(RootPath, InitOptions),
+ {GlobalConfigPath, MaybeGlobalConfig} = find_config(global_config_paths()),
+ {LocalConfigPath, MaybeLocalConfig} = find_config(ConfigPaths),
+ ConfigPath =
+ case LocalConfigPath of
+ undefined ->
+ report_missing_config(ErrorReporting),
+ GlobalConfigPath;
+ _ ->
+ LocalConfigPath
+ end,
+ Config =
+ case {MaybeGlobalConfig, MaybeLocalConfig} of
+ {{ok, GlobalConfig}, {ok, LocalConfig}} ->
+ %% Augment LocalConfig onto GlobalConfig, note that this
+ %% is not a deep merge of nested maps.
+ maps:merge(GlobalConfig, LocalConfig);
+ {{error, Reason}, _} ->
+ %% We should not continue if the config is broken, but would
+ %% need a bigger initialization refactor to make that work.
+ report_broken_config(ErrorReporting, GlobalConfigPath, Reason),
+ #{};
+ {_, {error, Reason}} ->
+ report_broken_config(ErrorReporting, LocalConfigPath, Reason),
+ #{}
+ end,
+ do_initialize(RootUri, Capabilities, InitOptions, {ConfigPath, Config}).
--spec do_initialize(uri(), map(), map(), {undefined|path(), map()}) -> ok.
+-spec do_initialize(uri(), map(), map(), {undefined | path(), map()}) -> ok.
do_initialize(RootUri, Capabilities, InitOptions, {ConfigPath, Config}) ->
- RootPath = els_utils:to_list(els_uri:path(RootUri)),
- OtpPath = maps:get("otp_path", Config, code:root_dir()),
- ?LOG_INFO("OTP Path: ~p", [OtpPath]),
- DepsDirs = maps:get("deps_dirs", Config, []),
- AppsDirs = maps:get("apps_dirs", Config, ["."]),
- IncludeDirs = maps:get("include_dirs", Config, ["include"]),
- ExcludeUnusedIncludes = maps:get("exclude_unused_includes", Config, []),
- Macros = maps:get("macros", Config, []),
- DialyzerPltPath = maps:get("plt_path", Config, undefined),
- OtpAppsExclude = maps:get( "otp_apps_exclude"
- , Config
- , ?DEFAULT_EXCLUDED_OTP_APPS
- ),
- Lenses = maps:get("lenses", Config, #{}),
- Diagnostics = maps:get("diagnostics", Config, #{}),
- ExcludePathsSpecs = [[OtpPath, "lib", P ++ "*"] || P <- OtpAppsExclude],
- ExcludePaths = els_utils:resolve_paths(ExcludePathsSpecs, RootPath, true),
- ?LOG_INFO("Excluded OTP Applications: ~p", [OtpAppsExclude]),
- CodeReload = maps:get("code_reload", Config, disabled),
- Runtime = maps:get("runtime", Config, #{}),
- CtRunTest = maps:get("ct-run-test", Config, #{}),
- CodePathExtraDirs = maps:get("code_path_extra_dirs", Config, []),
- ok = add_code_paths(CodePathExtraDirs, RootPath),
- ElvisConfigPath = maps:get("elvis_config_path", Config, undefined),
- BSPEnabled = maps:get("bsp_enabled", Config, auto),
- IncrementalSync = maps:get("incremental_sync", Config, true),
- CompilerTelemetryEnabled
- = maps:get("compiler_telemetry_enabled", Config, false),
-
- IndexingEnabled = maps:get(<<"indexingEnabled">>, InitOptions, true),
-
- %% Passed by the LSP client
- ok = set(root_uri , RootUri),
- %% Read from the configuration file
- ok = set(config_path , ConfigPath),
- ok = set(otp_path , OtpPath),
- ok = set(deps_dirs , DepsDirs),
- ok = set(apps_dirs , AppsDirs),
- ok = set(include_dirs , IncludeDirs),
- ok = set(exclude_unused_includes , ExcludeUnusedIncludes),
- ok = set(macros , Macros),
- ok = set(plt_path , DialyzerPltPath),
- ok = set(code_reload , CodeReload),
- ?LOG_INFO("Config=~p", [Config]),
- ok = set(runtime, maps:merge( els_config_runtime:default_config()
- , Runtime)),
- ok = set('ct-run-test', maps:merge( els_config_ct_run_test:default_config()
- , CtRunTest)),
- ok = set(elvis_config_path, ElvisConfigPath),
- ok = set(bsp_enabled, BSPEnabled),
- ok = set(compiler_telemetry_enabled, CompilerTelemetryEnabled),
- ok = set(incremental_sync, IncrementalSync),
- %% Calculated from the above
- ok = set(apps_paths , project_paths(RootPath, AppsDirs, false)),
- ok = set(deps_paths , project_paths(RootPath, DepsDirs, false)),
- ok = set(include_paths , include_paths(RootPath, IncludeDirs, false)),
- ok = set(otp_paths , otp_paths(OtpPath, false) -- ExcludePaths),
- ok = set(lenses , Lenses),
- ok = set(diagnostics , Diagnostics),
- %% All (including subdirs) paths used to search files with file:path_open/3
- ok = set( search_paths
- , lists:append([ project_paths(RootPath, AppsDirs, true)
- , project_paths(RootPath, DepsDirs, true)
- , include_paths(RootPath, IncludeDirs, false)
- , otp_paths(OtpPath, true)
- ])
- ),
- %% Init Options
- ok = set(capabilities , Capabilities),
- ok = set(indexing_enabled, IndexingEnabled),
- ok.
+ put(erls_dirs, maps:get("erls_dirs", Config, [])),
+ RootPath = els_utils:to_list(els_uri:path(RootUri)),
+ OtpPath = maps:get("otp_path", Config, code:root_dir()),
+ ?LOG_INFO("OTP Path: ~p", [OtpPath]),
+ {DefaultDepsDirs, DefaultAppsDirs, DefaultIncludeDirs} =
+ get_default_dirs(RootPath),
+ DepsDirs = maps:get("deps_dirs", Config, DefaultDepsDirs),
+ AppsDirs = maps:get("apps_dirs", Config, DefaultAppsDirs),
+ IncludeDirs = maps:get("include_dirs", Config, DefaultIncludeDirs),
+ ExcludeUnusedIncludes = maps:get("exclude_unused_includes", Config, []),
+ Macros = maps:get("macros", Config, []),
+ DialyzerPltPath = maps:get("plt_path", Config, undefined),
+ OtpAppsExclude = maps:get(
+ "otp_apps_exclude",
+ Config,
+ ?DEFAULT_EXCLUDED_OTP_APPS
+ ),
+ Lenses = maps:get("lenses", Config, #{}),
+ Diagnostics = maps:get("diagnostics", Config, #{}),
+ ExcludePathsSpecs = [[OtpPath, "lib", P ++ "*"] || P <- OtpAppsExclude],
+ ExcludePaths = els_utils:resolve_paths(ExcludePathsSpecs, true),
+ ?LOG_INFO("Excluded OTP Applications: ~p", [OtpAppsExclude]),
+ CodeReload = maps:get("code_reload", Config, disabled),
+ Runtime = maps:get("runtime", Config, #{}),
+ CtRunTest = maps:get("ct-run-test", Config, #{}),
+ CodePathExtraDirs = maps:get("code_path_extra_dirs", Config, []),
+ ok = add_code_paths(CodePathExtraDirs, RootPath),
+ ElvisConfigPath = maps:get("elvis_config_path", Config, undefined),
+ IncrementalSync = maps:get("incremental_sync", Config, true),
+ Indexing = maps:get("indexing", Config, #{}),
+ CompilerTelemetryEnabled =
+ maps:get("compiler_telemetry_enabled", Config, false),
+ EDocCustomTags = maps:get("edoc_custom_tags", Config, []),
+
+ IndexingEnabled = maps:get(<<"indexingEnabled">>, InitOptions, true),
+
+ RefactorErl = maps:get("refactorerl", Config, notconfigured),
+ Providers = maps:get("providers", Config, #{}),
+ EdocParseEnabled = maps:get("edoc_parse_enabled", Config, true),
+ InlayHintsEnabled = maps:get("inlay_hints_enabled", Config, false),
+ Formatting = maps:get("formatting", Config, #{}),
+ DocsMemo = maps:get("docs_memo", Config, false),
+
+ %% Initialize and start Wrangler
+ case maps:get("wrangler", Config, notconfigured) of
+ notconfigured ->
+ ok = set(wrangler, notconfigured);
+ Wrangler ->
+ ok = set(wrangler, Wrangler),
+ case maps:get("path", Wrangler, notconfigured) of
+ notconfigured ->
+ ?LOG_INFO(
+ "Wrangler path is not configured,\n"
+ " assuming it is installed system-wide."
+ );
+ Path ->
+ case code:add_path(Path) of
+ true ->
+ ok;
+ {error, bad_directory} ->
+ ?LOG_INFO(
+ "Wrangler path is configured but\n"
+ " not a valid ebin directory: ~p",
+ [Path]
+ )
+ end
+ end,
+ case application:load(wrangler) of
+ ok ->
+ case apply(api_wrangler, start, []) of
+ % Function defined in Wrangler.
+ % Using apply to circumvent tests resulting in 'unknown function'.
+ ok ->
+ ?LOG_INFO("Wrangler started successfully");
+ {error, Reason} ->
+ ?LOG_INFO("Wrangler could not be started: ~p", [Reason])
+ end;
+ {error, Reason} ->
+ ?LOG_INFO("Wrangler could not be loaded: ~p", [Reason])
+ end
+ end,
+
+ %% Passed by the LSP client
+ ok = set(root_uri, RootUri),
+ %% Read from the configuration file
+ ok = set(config_path, ConfigPath),
+ ok = set(otp_path, OtpPath),
+ ok = set(deps_dirs, DepsDirs),
+ ok = set(apps_dirs, AppsDirs),
+ ok = set(include_dirs, IncludeDirs),
+ ok = set(exclude_unused_includes, ExcludeUnusedIncludes),
+ ok = set(macros, Macros),
+ ok = set(plt_path, DialyzerPltPath),
+ ok = set(code_reload, CodeReload),
+ ok = set(providers, Providers),
+ ok = set(docs_memo, DocsMemo),
+ ?LOG_INFO("Config=~p", [Config]),
+ ok = set(
+ runtime,
+ maps:merge(
+ els_config_runtime:default_config(),
+ Runtime
+ )
+ ),
+ ok = set(
+ 'ct-run-test',
+ maps:merge(
+ els_config_ct_run_test:default_config(),
+ CtRunTest
+ )
+ ),
+ ok = set(elvis_config_path, ElvisConfigPath),
+ ok = set(compiler_telemetry_enabled, CompilerTelemetryEnabled),
+ ok = set(edoc_custom_tags, EDocCustomTags),
+ ok = set(edoc_parse_enabled, EdocParseEnabled),
+ ok = set(incremental_sync, IncrementalSync),
+ ok = set(inlay_hints_enabled, InlayHintsEnabled),
+ ok = set(
+ indexing,
+ maps:merge(
+ els_config_indexing:default_config(),
+ Indexing
+ )
+ ),
+ %% Calculated from the above
+ ok = set(apps_paths, project_paths(RootPath, AppsDirs, false)),
+ ok = set(deps_paths, project_paths(RootPath, DepsDirs, false)),
+ ok = set(include_paths, include_paths(RootPath, IncludeDirs, false)),
+ ok = set(otp_paths, otp_paths(OtpPath, false) -- ExcludePaths),
+ ok = set(lenses, Lenses),
+ ok = set(diagnostics, Diagnostics),
+ %% All (including subdirs) paths used to search files with file:path_open/3
+ ok = set(
+ search_paths,
+ lists:append([
+ project_paths(RootPath, AppsDirs, true),
+ project_paths(RootPath, DepsDirs, true),
+ include_paths(RootPath, IncludeDirs, false),
+ otp_paths(OtpPath, true)
+ ])
+ ),
+ %% Init Options
+ ok = set(capabilities, Capabilities),
+ ok = set(indexing_enabled, IndexingEnabled),
+
+ ok = set(refactorerl, RefactorErl),
+ ok = set(formatting, Formatting),
+ ok.
+
+-spec get_default_dirs(string()) ->
+ {DefaultDepsDirs, DefaultAppsDirs, DefaultIncludeDirs}
+when
+ DefaultDepsDirs :: [string()],
+ DefaultAppsDirs :: [string()],
+ DefaultIncludeDirs :: [string()].
+get_default_dirs(RootPath) ->
+ HasRebarConfig = filelib:is_file(filename:join(RootPath, "rebar.config")),
+ HasErlangMk = filelib:is_file(filename:join(RootPath, "erlang.mk")),
+ case {HasErlangMk, HasRebarConfig} of
+ {false, true} ->
+ ?LOG_INFO("Found rebar.config, using rebar3 default paths."),
+ {
+ _DefaultDepsDirs = [
+ "_build/default/lib/*",
+ "_build/test/lib/*"
+ ],
+ _DefaultAppsDirs = ["apps/*", "."],
+ _DefaultIncludeDirs = [
+ "src",
+ "include",
+ "apps",
+ "apps/*/include",
+ "_build/*/lib/",
+ "_build/*/lib/*/include"
+ ]
+ };
+ {_, _} ->
+ {
+ _DefaultDepsDirs = ["deps/*"],
+ _DefaultAppsDirs = ["apps/*", "."],
+ _DefaultIncludeDirs = ["src", "include"]
+ }
+ end.
-spec start_link() -> {ok, pid()}.
start_link() ->
- gen_server:start_link({local, ?SERVER}, ?MODULE, {}, []).
+ gen_server:start_link({local, ?SERVER}, ?MODULE, {}, []).
-spec get(key()) -> any().
get(Key) ->
- gen_server:call(?SERVER, {get, Key}).
+ gen_server:call(?SERVER, {get, Key}).
-spec set(key(), any()) -> ok.
set(Key, Value) ->
- gen_server:call(?SERVER, {set, Key, Value}).
+ gen_server:call(?SERVER, {set, Key, Value}).
%%==============================================================================
%% gen_server Callback Functions
@@ -185,16 +336,16 @@ set(Key, Value) ->
-spec init({}) -> {ok, state()}.
init({}) ->
- {ok, #{}}.
+ {ok, #{}}.
-spec handle_call(any(), any(), state()) ->
- {reply, any(), state()}.
+ {reply, any(), state()}.
handle_call({get, Key}, _From, State) ->
- Value = maps:get(Key, State, undefined),
- {reply, Value, State};
+ Value = maps:get(Key, State, undefined),
+ {reply, Value, State};
handle_call({set, Key, Value}, _From, State0) ->
- State = maps:put(Key, Value, State0),
- {reply, ok, State}.
+ State = maps:put(Key, Value, State0),
+ {reply, ok, State}.
-spec handle_cast(any(), state()) -> {noreply, state()}.
handle_cast(_Msg, State) -> {noreply, State}.
@@ -204,131 +355,308 @@ handle_cast(_Msg, State) -> {noreply, State}.
%%==============================================================================
-spec config_paths(path(), map()) -> [path()].
-config_paths( RootPath
- , #{<<"erlang">> := #{<<"config_path">> := ConfigPath0}}) ->
- ConfigPath = els_utils:to_list(ConfigPath0),
- lists:append([ possible_config_paths(ConfigPath)
- , possible_config_paths(filename:join([RootPath, ConfigPath]))
- , default_config_paths(RootPath)]);
+config_paths(
+ RootPath,
+ #{<<"erlang">> := #{<<"config_path">> := ConfigPath0}}
+) ->
+ ConfigPath = els_utils:to_list(ConfigPath0),
+ lists:append([
+ possible_config_paths(ConfigPath),
+ possible_config_paths(filename:join([RootPath, ConfigPath])),
+ default_config_paths(RootPath)
+ ]);
config_paths(RootPath, _Config) ->
- default_config_paths(RootPath).
+ default_config_paths(RootPath).
-spec default_config_paths(path()) -> [path()].
default_config_paths(RootPath) ->
- GlobalConfigDir = filename:basedir(user_config, "erlang_ls"),
- [ filename:join([RootPath, ?DEFAULT_CONFIG_FILE])
- , filename:join([RootPath, ?ALTERNATIVE_CONFIG_FILE])
- , filename:join([GlobalConfigDir, ?DEFAULT_CONFIG_FILE])
- , filename:join([GlobalConfigDir, ?ALTERNATIVE_CONFIG_FILE])
- ].
+ [
+ filename:join([RootPath, ?DEFAULT_CONFIG_FILE]),
+ filename:join([RootPath, ?ALTERNATIVE_CONFIG_FILE])
+ ].
+
+-spec global_config_paths() -> [path()].
+global_config_paths() ->
+ GlobalConfigDir = filename:basedir(user_config, "erlang_ls"),
+ [
+ filename:join([GlobalConfigDir, ?DEFAULT_CONFIG_FILE]),
+ filename:join([GlobalConfigDir, ?ALTERNATIVE_CONFIG_FILE])
+ ].
%% @doc Bare `Path' as well as with default config file name suffix.
-spec possible_config_paths(path()) -> [path()].
possible_config_paths(Path) ->
- [ Path
- , filename:join([Path, ?DEFAULT_CONFIG_FILE])
- , filename:join([Path, ?ALTERNATIVE_CONFIG_FILE])
- ].
-
--spec consult_config([path()], boolean()) -> {undefined|path(), map()}.
-consult_config([], ReportMissingConfig) ->
- ?LOG_INFO("No config file found."),
- case ReportMissingConfig of
- true ->
- report_missing_config();
- false ->
- ok
- end,
- {undefined, #{}};
-consult_config([Path | Paths], ReportMissingConfig) ->
- ?LOG_INFO("Reading config file. path=~p", [Path]),
- Options = [{map_node_format, map}],
- try yamerl:decode_file(Path, Options) of
- [] -> {Path, #{}};
- [Config] -> {Path, Config}
- catch
- Class:Error ->
- ?LOG_WARNING( "Could not read config file: path=~p class=~p error=~p"
- , [Path, Class, Error]),
- consult_config(Paths, ReportMissingConfig)
- end.
-
--spec report_missing_config() -> ok.
-report_missing_config() ->
- Msg =
- io_lib:format("The current project is missing an erlang_ls.config file. "
- "Need help configuring Erlang LS for your project? "
- "Visit: https://erlang-ls.github.io/configuration/", []),
- els_server:send_notification(<<"window/showMessage">>,
- #{ type => ?MESSAGE_TYPE_WARNING,
- message => els_utils:to_binary(Msg)
- }),
- ok.
+ [
+ Path,
+ filename:join([Path, ?DEFAULT_CONFIG_FILE]),
+ filename:join([Path, ?ALTERNATIVE_CONFIG_FILE])
+ ].
+
+-spec find_config([path()]) -> {FoundPath, OkConfig | Error} when
+ FoundPath :: path() | undefined,
+ OkConfig :: {ok, map()},
+ Error :: {error, term()}.
+find_config(Paths) ->
+ case lists:dropwhile(fun(P) -> not filelib:is_regular(P) end, Paths) of
+ [FoundPath | _] ->
+ {FoundPath, consult_config(FoundPath)};
+ _ ->
+ {undefined, {ok, #{}}}
+ end.
+
+-spec consult_config(path()) -> {ok, map()} | {error, term()}.
+consult_config(Path) ->
+ Options = [{map_node_format, map}],
+ case file:read_file(Path) of
+ {ok, FileBin} ->
+ ExpandedBin = expand_env_vars(FileBin),
+ try yamerl:decode(ExpandedBin, Options) of
+ [] ->
+ ?LOG_WARNING("Using empty configuration from ~s", [Path]),
+ {ok, #{}};
+ [Config] when is_map(Config) ->
+ {ok, Config};
+ _ ->
+ {error, {syntax_error, Path}}
+ catch
+ Class:Error ->
+ {error, {Class, Error}}
+ end;
+ {error, _} = Error ->
+ Error
+ end.
+
+-spec report_missing_config(error_reporting()) -> ok.
+report_missing_config(log) ->
+ ?LOG_INFO(
+ "The current project is missing an erlang_ls.config file. "
+ "Need help configuring Erlang LS for your project? "
+ "Visit: https://erlang-ls.github.io/configuration/"
+ );
+report_missing_config(lsp_notification) ->
+ Msg =
+ io_lib:format(
+ "The current project is missing an erlang_ls.config file. "
+ "Need help configuring Erlang LS for your project? "
+ "Visit: https://erlang-ls.github.io/configuration/",
+ []
+ ),
+ els_server:send_notification(
+ <<"window/showMessage">>,
+ #{
+ type => ?MESSAGE_TYPE_WARNING,
+ message => els_utils:to_binary(Msg)
+ }
+ ),
+ ok.
+
+-spec report_broken_config(
+ error_reporting(),
+ path(),
+ Reason :: term()
+) -> ok.
+report_broken_config(log, Path, Reason) ->
+ ?LOG_ERROR(
+ "The erlang_ls.config file at ~s can't be read (~p) "
+ "Need help configuring Erlang LS for your project? "
+ "Visit: https://erlang-ls.github.io/configuration/",
+ [Path, Reason]
+ );
+report_broken_config(lsp_notification, Path, Reason) ->
+ ?LOG_ERROR(
+ "Failed to parse configuration file at ~s: ~p",
+ [Path, Reason]
+ ),
+ Msg =
+ io_lib:format(
+ "The erlang_ls.config file at ~s can't be read "
+ "(check logs for details). "
+ "Need help configuring Erlang LS for your project? "
+ "Visit: https://erlang-ls.github.io/configuration/",
+ [Path]
+ ),
+ els_server:send_notification(
+ <<"window/showMessage">>,
+ #{
+ type => ?MESSAGE_TYPE_WARNING,
+ message => els_utils:to_binary(Msg)
+ }
+ ),
+ ok.
-spec include_paths(path(), string(), boolean()) -> [string()].
include_paths(RootPath, IncludeDirs, Recursive) ->
- Paths = [ els_utils:resolve_paths([[RootPath, Dir]], RootPath, Recursive)
- || Dir <- IncludeDirs
- ],
- lists:append(Paths).
+ Paths = [
+ els_utils:resolve_paths([[RootPath, Dir]], Recursive)
+ || Dir <- IncludeDirs
+ ],
+ lists:append(Paths).
-spec project_paths(path(), [string()], boolean()) -> [string()].
project_paths(RootPath, Dirs, Recursive) ->
- Paths = [ els_utils:resolve_paths( [ [RootPath, Dir, "src"]
- , [RootPath, Dir, "test"]
- , [RootPath, Dir, "include"]
- ]
- , RootPath
- , Recursive
- )
- || Dir <- Dirs
- ],
- case Recursive of
- false ->
- lists:append(Paths);
- true ->
- Filter = fun(Path) ->
- string:find(Path, "SUITE_data", trailing) =:= nomatch
- end,
- lists:filter(Filter, lists:append(Paths))
- end.
+ Paths = [
+ els_utils:resolve_paths(
+ [
+ [RootPath, Dir, "src"],
+ [RootPath, Dir, "test"],
+ [RootPath, Dir, "include"]
+ | [[RootPath, Dir, Src] || Src <- erlang:get(erls_dirs)]
+ ],
+ Recursive
+ )
+ || Dir <- Dirs
+ ],
+ case Recursive of
+ false ->
+ lists:append(Paths);
+ true ->
+ Filter = fun(Path) ->
+ string:find(Path, "SUITE_data", trailing) =:= nomatch
+ end,
+ lists:filter(Filter, lists:append(Paths))
+ end.
-spec otp_paths(path(), boolean()) -> [string()].
otp_paths(OtpPath, Recursive) ->
- els_utils:resolve_paths( [ [OtpPath, "lib", "*", "src"]
- , [OtpPath, "lib", "*", "include"]
- ]
- , OtpPath
- , Recursive
- ).
-
--spec add_code_paths(Dirs :: list(string()),
- RooDir :: string()) ->
- ok.
+ els_utils:resolve_paths(
+ [
+ [OtpPath, "lib", "*", "src"],
+ [OtpPath, "lib", "*", "include"]
+ ],
+ Recursive
+ ).
+
+-spec add_code_paths(
+ Dirs :: list(string()),
+ RooDir :: string()
+) ->
+ ok.
add_code_paths(WCDirs, RootDir) ->
- AddADir = fun(ADir) ->
- ?LOG_INFO("Adding code path: ~p", [ADir]),
- true = code:add_path(ADir)
- end,
- AllNames = lists:foldl(fun(Elem, AccIn) ->
- AccIn ++ filelib:wildcard(Elem, RootDir)
- end, [], WCDirs),
- Dirs = [ [$/ | safe_relative_path(Dir, RootDir)]
- || Name <- AllNames,
- filelib:is_dir([$/ | Dir] = filename:absname(Name, RootDir))
- ],
- lists:foreach(AddADir, Dirs).
+ AddADir = fun(ADir) ->
+ ?LOG_INFO("Adding code path: ~p", [ADir]),
+ true = code:add_path(ADir)
+ end,
+ AllNames = lists:foldl(
+ fun(Elem, AccIn) ->
+ AccIn ++ filelib:wildcard(Elem, RootDir)
+ end,
+ [],
+ WCDirs
+ ),
+ Dirs = [
+ RelativeDir
+ || Name <- AllNames,
+ begin
+ AbsDir = filename:absname(Name, RootDir),
+ RelativeDir =
+ case AbsDir of
+ [$/ | Dir] -> [$/ | safe_relative_path(Dir, RootDir)];
+ [D, $:, $/ | Dir] -> [D, $:, $/ | safe_relative_path(Dir, RootDir)];
+ _ -> error
+ end,
+ filelib:is_dir(AbsDir)
+ end
+ ],
+ lists:foreach(AddADir, Dirs).
+
+-spec expand_env_vars(binary()) -> binary().
+expand_env_vars(Bin) ->
+ expand_vars(Bin, get_env()).
+
+-spec expand_vars(binary(), [{string(), string()}]) -> binary().
+expand_vars(Bin, Env0) ->
+ %% Sort by longest key to ensure longest variable match.
+ Env = lists:sort(fun({A, _}, {B, _}) -> length(A) >= length(B) end, Env0),
+ case binary:split(Bin, <<"$">>, [global]) of
+ [_] ->
+ Bin;
+ [First | Rest] ->
+ iolist_to_binary([First] ++ [expand_var(B, Env) || B <- Rest])
+ end.
+
+-spec expand_var(binary(), [{string(), string()}]) -> iodata().
+expand_var(Bin, []) ->
+ [<<"$">>, Bin];
+expand_var(Bin, [{Var, Value} | RestEnv]) ->
+ case string:prefix(Bin, Var) of
+ nomatch ->
+ expand_var(Bin, RestEnv);
+ RestBin ->
+ [Value, RestBin]
+ end.
+
+-spec is_dep(string()) -> boolean().
+is_dep(Path) ->
+ lists:any(
+ fun(DepPath) ->
+ lists:prefix(DepPath, Path)
+ end,
+ els_config:get(deps_paths)
+ ).
+
+-spec get_env() -> [{string(), string()}].
+-if(?OTP_RELEASE >= 24).
+get_env() ->
+ os:env().
+-else.
+get_env() ->
+ [list_to_tuple(string:split(S, "=")) || S <- os:getenv()].
+-endif.
-if(?OTP_RELEASE >= 23).
--spec safe_relative_path(Dir :: file:name_all(),
- RootDir :: file:name_all()) ->
- Path :: file:name_all().
+-spec safe_relative_path(
+ Dir :: file:name_all(),
+ RootDir :: file:name_all()
+) ->
+ Path :: file:name_all().
safe_relative_path(Dir, RootDir) ->
- filelib:safe_relative_path(Dir, RootDir).
+ filelib:safe_relative_path(Dir, RootDir).
-else.
--spec safe_relative_path(FileName :: file:name_all(),
- RootDir :: file:name_all()) ->
- Path :: file:name_all().
+-spec safe_relative_path(
+ FileName :: file:name_all(),
+ RootDir :: file:name_all()
+) ->
+ Path :: file:name_all().
safe_relative_path(Dir, _) ->
- filename:safe_relative_path(Dir).
+ filename:safe_relative_path(Dir).
+-endif.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+expand_var_test_() ->
+ [
+ ?_assertEqual(
+ <<"foobar">>,
+ expand_vars(<<"foo$TEST">>, [{"TEST", "bar"}])
+ ),
+ ?_assertEqual(
+ <<"foobarbar">>,
+ expand_vars(<<"foo$TEST$TEST">>, [{"TEST", "bar"}])
+ ),
+ ?_assertEqual(
+ <<"foobarbaz">>,
+ expand_vars(<<"foo$TEST$TEST2">>, [
+ {"TEST", "bar"},
+ {"TEST2", "baz"}
+ ])
+ ),
+ ?_assertEqual(
+ <<"foo$TES">>,
+ expand_vars(<<"foo$TES">>, [{"TEST", "bar"}])
+ ),
+ ?_assertEqual(
+ <<"foobarBAZ">>,
+ expand_vars(<<"foo$TESTBAZ">>, [{"TEST", "bar"}])
+ ),
+ ?_assertEqual(
+ <<"foobar">>,
+ expand_vars(<<"foobar">>, [{"TEST", "bar"}])
+ ),
+ ?_assertEqual(
+ <<"foo$TEST">>,
+ expand_vars(<<"foo$TEST">>, [])
+ )
+ ].
+
-endif.
diff --git a/apps/els_core/src/els_config_ct_run_test.erl b/apps/els_core/src/els_config_ct_run_test.erl
index 8db423a10..e31701b61 100644
--- a/apps/els_core/src/els_config_ct_run_test.erl
+++ b/apps/els_core/src/els_config_ct_run_test.erl
@@ -1,39 +1,41 @@
-module(els_config_ct_run_test).
--include_lib("els_core/include/els_core.hrl").
-
%% We may introduce a behaviour for config modules in the future
--export([ default_config/0 ]).
+-export([default_config/0]).
%% Getters
--export([ get_module/0
- , get_function/0
- ]).
+-export([
+ get_module/0,
+ get_function/0
+]).
--type config() :: #{ string() => string() }.
+-type config() :: #{string() => string()}.
-spec default_config() -> config().
default_config() ->
- #{ "module" => default_module()
- , "function" => default_function()
- }.
+ #{
+ "module" => default_module(),
+ "function" => default_function()
+ }.
-spec get_module() -> atom().
get_module() ->
- Value = maps:get("module", els_config:get('ct-run-test'), default_module()),
- list_to_atom(Value).
+ Value = maps:get("module", els_config:get('ct-run-test'), default_module()),
+ list_to_atom(Value).
-spec get_function() -> atom().
get_function() ->
- Value = maps:get( "function"
- , els_config:get('ct-run-test')
- , default_function()),
- list_to_atom(Value).
+ Value = maps:get(
+ "function",
+ els_config:get('ct-run-test'),
+ default_function()
+ ),
+ list_to_atom(Value).
-spec default_module() -> string().
default_module() ->
- "rebar3_erlang_ls_agent".
+ "rebar3_erlang_ls_agent".
-spec default_function() -> string().
default_function() ->
- "run_ct_test".
+ "run_ct_test".
diff --git a/apps/els_core/src/els_config_indexing.erl b/apps/els_core/src/els_config_indexing.erl
new file mode 100644
index 000000000..0547647aa
--- /dev/null
+++ b/apps/els_core/src/els_config_indexing.erl
@@ -0,0 +1,50 @@
+-module(els_config_indexing).
+
+-include("els_core.hrl").
+
+-export([default_config/0]).
+
+%% Getters
+-export([
+ get_skip_generated_files/0,
+ get_generated_files_tag/0
+]).
+
+-type config() :: #{string() => string()}.
+
+-spec default_config() -> config().
+default_config() ->
+ #{
+ "skip_generated_files" => default_skip_generated_files(),
+ "generated_files_tag" => default_generated_files_tag()
+ }.
+
+-spec get_skip_generated_files() -> boolean().
+get_skip_generated_files() ->
+ Value = maps:get(
+ "skip_generated_files",
+ els_config:get(indexing),
+ default_skip_generated_files()
+ ),
+ normalize_boolean(Value).
+
+-spec get_generated_files_tag() -> string().
+get_generated_files_tag() ->
+ maps:get(
+ "generated_files_tag",
+ els_config:get(indexing),
+ default_generated_files_tag()
+ ).
+
+-spec default_skip_generated_files() -> string().
+default_skip_generated_files() ->
+ "false".
+
+-spec default_generated_files_tag() -> string().
+default_generated_files_tag() ->
+ "@generated".
+
+-spec normalize_boolean(boolean() | string()) -> boolean().
+normalize_boolean("true") -> true;
+normalize_boolean("false") -> false;
+normalize_boolean(Bool) when is_boolean(Bool) -> Bool.
diff --git a/apps/els_core/src/els_config_runtime.erl b/apps/els_core/src/els_config_runtime.erl
index e581599d4..dc83fb766 100644
--- a/apps/els_core/src/els_config_runtime.erl
+++ b/apps/els_core/src/els_config_runtime.erl
@@ -1,80 +1,106 @@
-module(els_config_runtime).
--include("els_core.hrl").
-
%% We may introduce a behaviour for config modules in the future
--export([ default_config/0 ]).
+-export([default_config/0]).
%% Getters
--export([ get_node_name/0
- , get_otp_path/0
- , get_start_cmd/0
- , get_start_args/0
- , get_name_type/0
- , get_cookie/0
- ]).
+-export([
+ get_node_name/0,
+ get_hostname/0,
+ get_domain/0,
+ get_otp_path/0,
+ get_start_cmd/0,
+ get_start_args/0,
+ get_name_type/0,
+ get_cookie/0
+]).
--type config() :: #{ string() => string() }.
+-type config() :: #{string() => string()}.
-spec default_config() -> config().
default_config() ->
- #{ "node_name" => default_node_name()
- , "otp_path" => default_otp_path()
- , "start_cmd" => default_start_cmd()
- , "start_args" => default_start_args()
- }.
+ #{
+ "hostname" => default_hostname(),
+ "domain" => default_domain(),
+ "node_name" => default_node_name(),
+ "otp_path" => default_otp_path(),
+ "start_cmd" => default_start_cmd(),
+ "start_args" => default_start_args()
+ }.
-spec get_node_name() -> atom().
get_node_name() ->
- Value = maps:get("node_name", els_config:get(runtime), default_node_name()),
- els_utils:compose_node_name(Value, get_name_type()).
+ Value = maps:get("node_name", els_config:get(runtime), default_node_name()),
+ els_utils:compose_node_name(Value, get_name_type()).
+
+-spec get_hostname() -> string().
+get_hostname() ->
+ case els_config:get(runtime) of
+ undefined -> default_hostname();
+ Runtime -> maps:get("hostname", Runtime, default_hostname())
+ end.
+
+-spec get_domain() -> string().
+get_domain() ->
+ maps:get("domain", els_config:get(runtime), default_domain()).
-spec get_otp_path() -> string().
get_otp_path() ->
- maps:get("otp_path", els_config:get(runtime), default_otp_path()).
+ maps:get("otp_path", els_config:get(runtime), default_otp_path()).
-spec get_start_cmd() -> string().
get_start_cmd() ->
- maps:get("start_cmd", els_config:get(runtime), default_start_cmd()).
+ maps:get("start_cmd", els_config:get(runtime), default_start_cmd()).
-spec get_start_args() -> [string()].
get_start_args() ->
- Value = maps:get("start_args", els_config:get(runtime), default_start_args()),
- string:tokens(Value, " ").
+ Value = maps:get("start_args", els_config:get(runtime), default_start_args()),
+ string:tokens(Value, " ").
-spec get_name_type() -> shortnames | longnames.
get_name_type() ->
- case maps:get("use_long_names", els_config:get(runtime), false) of
- false ->
- shortnames;
- true ->
- longnames
- end.
+ case maps:get("use_long_names", els_config:get(runtime), false) of
+ false ->
+ shortnames;
+ true ->
+ longnames
+ end.
-spec get_cookie() -> atom().
get_cookie() ->
- case maps:get("cookie", els_config:get(runtime), undefined) of
- undefined ->
- erlang:get_cookie();
- Cookie ->
- list_to_atom(Cookie)
+ case maps:get("cookie", els_config:get(runtime), undefined) of
+ undefined ->
+ erlang:get_cookie();
+ Cookie ->
+ list_to_atom(Cookie)
end.
-spec default_node_name() -> string().
default_node_name() ->
- RootUri = els_config:get(root_uri),
- {ok, Hostname} = inet:gethostname(),
- NodeName = els_utils:to_list(filename:basename(els_uri:path(RootUri))),
- NodeName ++ "@" ++ Hostname.
+ RootUri = els_config:get(root_uri),
+ Hostname = get_hostname(),
+ NodeName = els_distribution_server:normalize_node_name(
+ filename:basename(els_uri:path(RootUri))
+ ),
+ NodeName ++ "@" ++ Hostname.
+
+-spec default_hostname() -> string().
+default_hostname() ->
+ {ok, Hostname} = inet:gethostname(),
+ Hostname.
+
+-spec default_domain() -> string().
+default_domain() ->
+ proplists:get_value(domain, inet:get_rc(), "").
-spec default_otp_path() -> string().
default_otp_path() ->
- filename:dirname(filename:dirname(code:root_dir())).
+ filename:dirname(filename:dirname(code:root_dir())).
-spec default_start_cmd() -> string().
default_start_cmd() ->
- "rebar3".
+ "rebar3".
-spec default_start_args() -> string().
default_start_args() ->
- "erlang_ls".
+ "erlang_ls".
diff --git a/apps/els_core/src/els_core.app.src b/apps/els_core/src/els_core.app.src
index 30279e4ac..b3fbda2e4 100644
--- a/apps/els_core/src/els_core.app.src
+++ b/apps/els_core/src/els_core.app.src
@@ -5,9 +5,9 @@
{applications, [
kernel,
stdlib,
- jsx,
yamerl,
- redbug
+ redbug,
+ json_polyfill
]},
{env, [
{io_device, standard_io},
diff --git a/apps/els_core/src/els_distribution_server.erl b/apps/els_core/src/els_distribution_server.erl
index 2901e2a94..97efca4ba 100644
--- a/apps/els_core/src/els_distribution_server.erl
+++ b/apps/els_core/src/els_distribution_server.erl
@@ -8,27 +8,29 @@
%%==============================================================================
%% API
%%==============================================================================
--export([ start_link/0
- , start_distribution/1
- , start_distribution/4
- , connect/0
- , wait_connect_and_monitor/1
- , wait_connect_and_monitor/3
- , rpc_call/3
- , rpc_call/4
- , node_name/2
- , node_name/3
- ]).
+-export([
+ start_link/0,
+ start_distribution/1,
+ start_distribution/4,
+ connect/0,
+ wait_connect_and_monitor/1,
+ wait_connect_and_monitor/3,
+ rpc_call/3,
+ rpc_call/4,
+ node_name/2,
+ normalize_node_name/1
+]).
%%==============================================================================
%% Callbacks for the gen_server behaviour
%%==============================================================================
-behaviour(gen_server).
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , handle_info/2
- ]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2
+]).
%%==============================================================================
%% Includes
@@ -53,174 +55,194 @@
%%==============================================================================
-spec start_link() -> {ok, pid()}.
start_link() ->
- gen_server:start_link({local, ?SERVER}, ?MODULE, unused, []).
+ gen_server:start_link({local, ?SERVER}, ?MODULE, unused, []).
%% @doc Turns a non-distributed node into a distributed one
--spec start_distribution(atom()) -> ok.
+-spec start_distribution(atom()) -> ok | {error, any()}.
start_distribution(Name) ->
- Cookie = els_config_runtime:get_cookie(),
- RemoteNode = els_config_runtime:get_node_name(),
- NameType = els_config_runtime:get_name_type(),
- start_distribution(Name, RemoteNode, Cookie, NameType).
+ Cookie = els_config_runtime:get_cookie(),
+ RemoteNode = els_config_runtime:get_node_name(),
+ NameType = els_config_runtime:get_name_type(),
+ start_distribution(Name, RemoteNode, Cookie, NameType).
--spec start_distribution(atom(), atom(), atom(), shortnames | longnames) -> ok.
+-spec start_distribution(atom(), atom(), atom(), shortnames | longnames) ->
+ ok | {error, any()}.
start_distribution(Name, RemoteNode, Cookie, NameType) ->
- ?LOG_INFO("Enable distribution [name=~p]", [Name]),
- case net_kernel:start([Name, NameType]) of
- {ok, _Pid} ->
- case Cookie of
- nocookie ->
- ok;
- CustomCookie ->
- erlang:set_cookie(RemoteNode, CustomCookie)
- end,
- ?LOG_INFO("Distribution enabled [name=~p]", [Name]);
- {error, {already_started, _Pid}} ->
- ?LOG_INFO("Distribution already enabled [name=~p]", [Name]);
- {error, {{shutdown, {failed_to_start_child, net_kernel, E1}}, E2}} ->
- ?LOG_INFO("Distribution shutdown [errs=~p]", [{E1, E2}]),
- ?LOG_INFO("Distribution shut down [name=~p]", [Name])
- end.
+ ?LOG_INFO("Enable distribution [name=~p]", [Name]),
+ case net_kernel:start([Name, NameType]) of
+ {ok, _Pid} ->
+ case Cookie of
+ nocookie ->
+ ok;
+ CustomCookie ->
+ erlang:set_cookie(RemoteNode, CustomCookie)
+ end,
+ ?LOG_INFO("Distribution enabled [name=~p]", [Name]),
+ ok;
+ {error, {already_started, _Pid}} ->
+ ?LOG_INFO("Distribution already enabled [name=~p]", [Name]),
+ ok;
+ {error, Error} ->
+ ?LOG_WARNING("Distribution shutdown [error=~p] [name=~p]", [Error, Name]),
+ {error, Error}
+ end.
%% @doc Connect to an existing runtime node, if available, or start one.
-spec connect() -> ok.
connect() ->
- gen_server:call(?SERVER, {connect}, infinity).
+ gen_server:call(?SERVER, {connect}, infinity).
%% @doc Make a RPC call towards the runtime node.
-spec rpc_call(atom(), atom(), [any()]) -> {any(), binary()}.
rpc_call(M, F, A) ->
- rpc_call(M, F, A, ?RPC_TIMEOUT).
+ rpc_call(M, F, A, ?RPC_TIMEOUT).
%% @doc Make a RPC call towards the runtime node.
-spec rpc_call(atom(), atom(), [any()], timeout()) -> {any(), binary()}.
rpc_call(M, F, A, Timeout) ->
- gen_server:call(?SERVER, {rpc_call, M, F, A, Timeout}, Timeout).
+ gen_server:call(?SERVER, {rpc_call, M, F, A, Timeout}, Timeout).
%%==============================================================================
%% Callbacks for the gen_server behaviour
%%==============================================================================
-spec init(unused) -> {ok, state()}.
init(unused) ->
- ?LOG_INFO("Ensure EPMD is running", []),
- ok = ensure_epmd(),
- {ok, #{}}.
+ ?LOG_INFO("Ensure EPMD is running", []),
+ ok = ensure_epmd(),
+ {ok, #{}}.
-spec handle_call(any(), {pid(), any()}, state()) ->
- {reply, any(), state()} | {noreply, state()}.
+ {reply, any(), state()} | {noreply, state()}.
handle_call({connect}, _From, State) ->
- Node = els_config_runtime:get_node_name(),
- case connect_and_monitor(Node, not_hidden) of
- ok ->
- ok;
- error ->
- ok = start(Node)
- end,
- {reply, ok, State};
+ Node = els_config_runtime:get_node_name(),
+ case connect_and_monitor(Node, not_hidden) of
+ ok ->
+ ok;
+ error ->
+ ok = start(Node)
+ end,
+ {reply, ok, State};
handle_call({rpc_call, M, F, A, Timeout}, _From, State) ->
- {ok, P} = els_group_leader_server:new(),
- Node = els_config_runtime:get_node_name(),
- ?LOG_INFO("RPC Call [node=~p] [mfa=~p]", [Node, {M, F, A}]),
- Result = rpc:call(Node, M, F, A, Timeout),
- Output = els_group_leader_server:flush(P),
- ok = els_group_leader_server:stop(P),
- {reply, {Result, Output}, State};
+ {ok, P} = els_group_leader_server:new(),
+ Node = els_config_runtime:get_node_name(),
+ ?LOG_INFO("RPC Call [node=~p] [mfa=~p]", [Node, {M, F, A}]),
+ Result = rpc:call(Node, M, F, A, Timeout),
+ Output = els_group_leader_server:flush(P),
+ ok = els_group_leader_server:stop(P),
+ {reply, {Result, Output}, State};
handle_call(_Request, _From, State) ->
- {noreply, State}.
+ {noreply, State}.
-spec handle_cast(any(), state()) -> {noreply, state()}.
handle_cast(_Request, State) ->
- {noreply, State}.
+ {noreply, State}.
-spec handle_info(any(), state()) -> {noreply, state()}.
handle_info({nodedown, Node}, State) ->
- ?LOG_ERROR("Runtime node down [node=~p]", [Node]),
- {noreply, State};
+ ?LOG_ERROR("Runtime node down [node=~p]", [Node]),
+ {noreply, State};
handle_info(Request, State) ->
- ?LOG_WARNING("Unexpected request [request=~p]", [Request]),
- {noreply, State}.
+ ?LOG_WARNING("Unexpected request [request=~p]", [Request]),
+ {noreply, State}.
%%==============================================================================
%% Internal Functions
%%==============================================================================
-spec connect_and_monitor(atom(), hidden | not_hidden) -> ok | error.
connect_and_monitor(Node, Type) ->
- case connect_node(Node, Type) of
- true ->
- ?LOG_INFO("Connected to node [node=~p]", [Node]),
- erlang:monitor_node(Node, true),
- ok;
- false ->
- error
- end.
+ case connect_node(Node, Type) of
+ true ->
+ ?LOG_INFO("Connected to node [node=~p]", [Node]),
+ erlang:monitor_node(Node, true),
+ ok;
+ false ->
+ error;
+ ignored ->
+ error
+ end.
-spec start(atom()) -> ok.
start(Node) ->
- Cmd = els_config_runtime:get_start_cmd(),
- Args = els_config_runtime:get_start_args(),
- Path = els_config_runtime:get_otp_path(),
- ?LOG_INFO( "Starting new Erlang node [node=~p] [cmd=~p] [args=~p] [path=~p]"
- , [Node, Cmd, Args, Path]
- ),
- spawn_link(fun() -> els_utils:cmd(Cmd, Args, Path) end),
- wait_connect_and_monitor(Node),
- ok.
-
--spec wait_connect_and_monitor(atom()) -> ok | error.
+ Cmd = els_config_runtime:get_start_cmd(),
+ Args = els_config_runtime:get_start_args(),
+ Path = els_config_runtime:get_otp_path(),
+ ?LOG_INFO(
+ "Starting new Erlang node [node=~p] [cmd=~p] [args=~p] [path=~p]",
+ [Node, Cmd, Args, Path]
+ ),
+ spawn_link(fun() -> els_utils:cmd(Cmd, Args, Path) end),
+ wait_connect_and_monitor(Node),
+ ok.
+
+-spec wait_connect_and_monitor(atom()) -> ok | error.
wait_connect_and_monitor(Node) ->
- wait_connect_and_monitor(Node, ?WAIT_ATTEMPTS, not_hidden).
+ wait_connect_and_monitor(Node, ?WAIT_ATTEMPTS, not_hidden).
-spec wait_connect_and_monitor(
- Node :: atom(),
- Attempts :: pos_integer(),
- Type :: hidden | not_hidden
-) -> ok | error.
+ Node :: atom(),
+ Attempts :: pos_integer(),
+ Type :: hidden | not_hidden
+) -> ok | error.
wait_connect_and_monitor(Node, Attempts, Type) ->
- wait_connect_and_monitor(Node, Type, Attempts, Attempts).
+ wait_connect_and_monitor(Node, Type, Attempts, Attempts).
-spec wait_connect_and_monitor(
- Node :: atom(),
- Type :: hidden | not_hidden,
- Attempts :: pos_integer(),
- MaxAttempts :: pos_integer()
+ Node :: atom(),
+ Type :: hidden | not_hidden,
+ Attempts :: pos_integer(),
+ MaxAttempts :: pos_integer()
) -> ok | error.
wait_connect_and_monitor(Node, _, 0, MaxAttempts) ->
- ?LOG_ERROR( "Failed to connect to node ~p after ~p attempts"
- , [Node, MaxAttempts]),
- error;
+ ?LOG_ERROR(
+ "Failed to connect to node ~p after ~p attempts",
+ [Node, MaxAttempts]
+ ),
+ error;
wait_connect_and_monitor(Node, Type, Attempts, MaxAttempts) ->
- timer:sleep(?WAIT_INTERVAL),
- case connect_and_monitor(Node, Type) of
- ok ->
- ok;
- error ->
- ?LOG_WARNING( "Trying to connect to node ~p (~p/~p)"
- , [Node, MaxAttempts - Attempts + 1, MaxAttempts]),
- wait_connect_and_monitor(Node, Type, Attempts - 1, MaxAttempts)
- end.
+ timer:sleep(?WAIT_INTERVAL),
+ case connect_and_monitor(Node, Type) of
+ ok ->
+ ok;
+ error ->
+ ?LOG_WARNING(
+ "Trying to connect to node ~p (~p/~p)",
+ [Node, MaxAttempts - Attempts + 1, MaxAttempts]
+ ),
+ wait_connect_and_monitor(Node, Type, Attempts - 1, MaxAttempts)
+ end.
%% @doc Ensure the Erlang Port Mapper Daemon (EPMD) is up and running
-spec ensure_epmd() -> ok.
ensure_epmd() ->
- 0 = els_utils:cmd("epmd", ["-daemon"]),
- ok.
+ 0 = els_utils:cmd("epmd", ["-daemon"]),
+ ok.
-spec node_name(binary(), binary()) -> atom().
-node_name(Prefix, Name) ->
- Int = erlang:phash2(erlang:timestamp()),
- Id = lists:flatten(io_lib:format("~s_~s_~p", [Prefix, Name, Int])),
- {ok, HostName} = inet:gethostname(),
- node_name(Id, HostName, els_config_runtime:get_name_type()).
-
--spec node_name(string(), string(), 'longnames' | 'shortnames') -> atom().
-node_name(Id, HostName, shortnames) ->
- list_to_atom(Id ++ "@" ++ HostName);
-node_name(Id, HostName, longnames) ->
- Domain = proplists:get_value(domain, inet:get_rc(), ""),
- list_to_atom(Id ++ "@" ++ HostName ++ "." ++ Domain).
-
--spec connect_node(node(), hidden | not_hidden) -> boolean() | ignored.
+node_name(Prefix, Name0) ->
+ Name = normalize_node_name(Name0),
+ Int = erlang:phash2(erlang:timestamp()),
+ Id = lists:flatten(io_lib:format("~s_~s_~p", [Prefix, Name, Int])),
+ els_utils:compose_node_name(Id, els_config_runtime:get_name_type()).
+
+-spec normalize_node_name(string() | binary()) -> string().
+normalize_node_name(Name) ->
+ %% Replace invalid characters with _
+ re:replace(Name, "[^0-9A-Za-z_\\-]", "_", [global, {return, list}]).
+
+-spec connect_node(node(), hidden | not_hidden) -> boolean() | ignored.
connect_node(Node, not_hidden) ->
- net_kernel:connect_node(Node);
+ net_kernel:connect_node(Node);
connect_node(Node, hidden) ->
- net_kernel:hidden_connect_node(Node).
+ net_kernel:hidden_connect_node(Node).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+default_node_name_test_() ->
+ [
+ ?_assertEqual("foobar", normalize_node_name("foobar")),
+ ?_assertEqual("foo_bar", normalize_node_name("foo.bar")),
+ ?_assertEqual("_", normalize_node_name("&"))
+ ].
+
+-endif.
diff --git a/apps/els_core/src/els_distribution_sup.erl b/apps/els_core/src/els_distribution_sup.erl
index c91768657..0e71e4522 100644
--- a/apps/els_core/src/els_distribution_sup.erl
+++ b/apps/els_core/src/els_distribution_sup.erl
@@ -15,10 +15,10 @@
%%==============================================================================
%% API
--export([ start_link/0 ]).
+-export([start_link/0]).
%% Supervisor Callbacks
--export([ init/1 ]).
+-export([init/1]).
%%==============================================================================
%% Defines
@@ -30,23 +30,27 @@
%%==============================================================================
-spec start_link() -> {ok, pid()}.
start_link() ->
- supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+ supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%%==============================================================================
%% supervisors callbacks
%%==============================================================================
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init([]) ->
- SupFlags = #{ strategy => one_for_one
- , intensity => 5
- , period => 60
- },
- ChildSpecs = [ #{ id => els_distribution_server
- , start => {els_distribution_server, start_link, []}
- }
- , #{ id => els_group_leader_sup
- , start => {els_group_leader_sup, start_link, []}
- , type => supervisor
- }
- ],
- {ok, {SupFlags, ChildSpecs}}.
+ SupFlags = #{
+ strategy => one_for_one,
+ intensity => 5,
+ period => 60
+ },
+ ChildSpecs = [
+ #{
+ id => els_distribution_server,
+ start => {els_distribution_server, start_link, []}
+ },
+ #{
+ id => els_group_leader_sup,
+ start => {els_group_leader_sup, start_link, []},
+ type => supervisor
+ }
+ ],
+ {ok, {SupFlags, ChildSpecs}}.
diff --git a/apps/els_core/src/els_dodger.erl b/apps/els_core/src/els_dodger.erl
index ed695d26e..a9cd3fded 100644
--- a/apps/els_core/src/els_dodger.erl
+++ b/apps/els_core/src/els_dodger.erl
@@ -41,7 +41,6 @@
%% //stdlib/epp} before the parser sees them), an extended syntax tree
%% is created, using the {@link erl_syntax} module.
-
%% NOTES:
%%
%% * It's OK if the result does not parse - then at least nothing
@@ -77,12 +76,24 @@
-module(els_dodger).
--export([parse_file/1, quick_parse_file/1, parse_file/2,
- quick_parse_file/2, parse/1, quick_parse/1, parse/2,
- quick_parse/2, parse/3, parse/4, quick_parse/3, parse_form/2,
- parse_form/3, quick_parse_form/2, quick_parse_form/3,
- format_error/1, tokens_to_string/1, normal_parser/2]).
-
+-export([
+ parse_file/1,
+ quick_parse_file/1,
+ parse_file/2,
+ quick_parse_file/2,
+ parse/1,
+ quick_parse/1,
+ parse/2,
+ quick_parse/2,
+ parse/3, parse/4,
+ quick_parse/3,
+ parse_form/2,
+ parse_form/3,
+ quick_parse_form/2, quick_parse_form/3,
+ format_error/1,
+ tokens_to_string/1,
+ normal_parser/2
+]).
%% The following should be: 1) pseudo-uniquely identifiable, and 2)
%% cause nice looking error messages when the parser has to give up.
@@ -92,7 +103,6 @@
-define(var_prefix, "?,").
-define(pp_form, '?preprocessor declaration?').
-
%% @type errorinfo() = {ErrorLine::integer(),
%% Module::atom(),
%% Descriptor::term()}.
@@ -114,10 +124,10 @@
%% @equiv parse_file(File, [])
-spec parse_file(file:filename()) ->
- {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
+ {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
parse_file(File) ->
- parse_file(File, []).
+ parse_file(File, []).
%% @spec parse_file(File, Options) -> {ok, Forms} | {error, errorinfo()}
%% File = file:filename()
@@ -154,10 +164,10 @@ parse_file(File) ->
%% @see erl_syntax:is_form/1
-spec parse_file(file:filename(), [option()]) ->
- {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
+ {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
parse_file(File, Options) ->
- parse_file(File, fun parse/3, Options).
+ parse_file(File, fun parse/3, Options).
%% @spec quick_parse_file(File) -> {ok, Forms} | {error, errorinfo()}
%% File = file:filename()
@@ -166,10 +176,10 @@ parse_file(File, Options) ->
%% @equiv quick_parse_file(File, [])
-spec quick_parse_file(file:filename()) ->
- {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
+ {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
quick_parse_file(File) ->
- quick_parse_file(File, []).
+ quick_parse_file(File, []).
%% @spec quick_parse_file(File, Options) ->
%% {ok, Forms} | {error, errorinfo()}
@@ -192,52 +202,56 @@ quick_parse_file(File) ->
%% @see parse_file/2
-spec quick_parse_file(file:filename(), [option()]) ->
- {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
+ {'ok', erl_syntax:forms()} | {'error', errorinfo()}.
quick_parse_file(File, Options) ->
- parse_file(File, fun quick_parse/3, Options ++ [no_fail]).
+ parse_file(File, fun quick_parse/3, Options ++ [no_fail]).
-spec parse_file(file:filename(), function(), [option()]) -> any().
parse_file(File, Parser, Options) ->
- case do_parse_file(utf8, File, Parser, Options) of
- {ok, Forms}=Ret ->
- case find_invalid_unicode(Forms) of
- none ->
- Ret;
- invalid_unicode ->
- case epp:read_encoding(File) of
- utf8 ->
- Ret;
- _ ->
- do_parse_file(latin1, File, Parser, Options)
- end
- end;
- Else ->
- Else
- end.
+ case do_parse_file(utf8, File, Parser, Options) of
+ {ok, Forms} = Ret ->
+ case find_invalid_unicode(Forms) of
+ none ->
+ Ret;
+ invalid_unicode ->
+ case epp:read_encoding(File) of
+ utf8 ->
+ Ret;
+ _ ->
+ do_parse_file(latin1, File, Parser, Options)
+ end
+ end;
+ Else ->
+ Else
+ end.
-spec do_parse_file(utf8 | latin1, file:filename(), function(), [option()]) ->
- any().
+ any().
do_parse_file(DefEncoding, File, Parser, Options) ->
- case file:open(File, [read]) of
- {ok, Dev} ->
- _ = epp:set_encoding(Dev, DefEncoding),
- try Parser(Dev, 1, Options)
- after ok = file:close(Dev)
- end;
- {error, Error} ->
- {error, {0, file, Error}} % defer to file:format_error/1
- end.
+ case file:open(File, [read]) of
+ {ok, Dev} ->
+ _ = epp:set_encoding(Dev, DefEncoding),
+ try
+ Parser(Dev, 1, Options)
+ after
+ ok = file:close(Dev)
+ end;
+ {error, Error} ->
+ % defer to file:format_error/1
+ {error, {0, file, Error}}
+ end.
-spec find_invalid_unicode([any()]) -> invalid_unicode | none.
-find_invalid_unicode([H|T]) ->
- case H of
- {error, {_Line, file_io_server, invalid_unicode}} ->
- invalid_unicode;
- _Other ->
- find_invalid_unicode(T)
- end;
-find_invalid_unicode([]) -> none.
+find_invalid_unicode([H | T]) ->
+ case H of
+ {error, {_Line, file_io_server, invalid_unicode}} ->
+ invalid_unicode;
+ _Other ->
+ find_invalid_unicode(T)
+ end;
+find_invalid_unicode([]) ->
+ none.
%% =====================================================================
%% @spec parse(IODevice) -> {ok, Forms} | {error, errorinfo()}
@@ -246,7 +260,7 @@ find_invalid_unicode([]) -> none.
-spec parse(file:io_device()) -> {'ok', erl_syntax:forms()}.
parse(Dev) ->
- parse(Dev, 1).
+ parse(Dev, 1).
%% @spec parse(IODevice, StartLoc) -> {ok, Forms} | {error, errorinfo()}
%% IODevice = pid()
@@ -257,10 +271,10 @@ parse(Dev) ->
%% @see parse/1
-spec parse(file:io_device(), location()) ->
- {'ok', erl_syntax:forms()}.
+ {'ok', erl_syntax:forms()}.
parse(Dev, Loc) ->
- parse(Dev, Loc, []).
+ parse(Dev, Loc, []).
%% @spec parse(IODevice, StartLoc, Options) ->
%% {ok, Forms} | {error, errorinfo()}
@@ -280,19 +294,19 @@ parse(Dev, Loc) ->
%% @see quick_parse/3
-spec parse(file:io_device(), location(), [option()]) ->
- {'ok', erl_syntax:forms()}.
+ {'ok', erl_syntax:forms()}.
parse(Dev, Loc0, Options) ->
- parse(Dev, Loc0, fun parse_form/3, Options).
+ parse(Dev, Loc0, fun parse_form/3, Options).
%% @spec quick_parse(IODevice) -> {ok, Forms} | {error, errorinfo()}
%% @equiv quick_parse(IODevice, 1)
-spec quick_parse(file:io_device()) ->
- {'ok', erl_syntax:forms()}.
+ {'ok', erl_syntax:forms()}.
quick_parse(Dev) ->
- quick_parse(Dev, 1).
+ quick_parse(Dev, 1).
%% @spec quick_parse(IODevice, StartLoc) ->
%% {ok, Forms} | {error, errorinfo()}
@@ -304,10 +318,10 @@ quick_parse(Dev) ->
%% @see quick_parse/1
-spec quick_parse(file:io_device(), erl_anno:location()) ->
- {'ok', erl_syntax:forms()}.
+ {'ok', erl_syntax:forms()}.
quick_parse(Dev, Loc) ->
- quick_parse(Dev, Loc, []).
+ quick_parse(Dev, Loc, []).
%% @spec (IODevice, StartLoc, Options) ->
%% {ok, Forms} | {error, errorinfo()}
@@ -325,32 +339,36 @@ quick_parse(Dev, Loc) ->
%% @see parse/3
-spec quick_parse(file:io_device(), integer(), [option()]) ->
- {'ok', erl_syntax:forms()}.
+ {'ok', erl_syntax:forms()}.
quick_parse(Dev, L0, Options) ->
- parse(Dev, L0, fun quick_parse_form/3, Options).
+ parse(Dev, L0, fun quick_parse_form/3, Options).
-spec parse(file:io_device(), location(), function(), [option()]) ->
- {'ok', erl_syntax:forms()}.
+ {'ok', erl_syntax:forms()}.
parse(Dev, L0, Parser, Options) ->
- parse(Dev, L0, [], Parser, Options).
-
--spec parse(file:io_device(), location(),
- erl_syntax:forms(), function(), [option()]) ->
- {'ok', erl_syntax:forms()}.
+ parse(Dev, L0, [], Parser, Options).
+
+-spec parse(
+ file:io_device(),
+ location(),
+ erl_syntax:forms(),
+ function(),
+ [option()]
+) ->
+ {'ok', erl_syntax:forms()}.
parse(Dev, L0, Fs, Parser, Options) ->
- case Parser(Dev, L0, Options) of
- {ok, none, L1} ->
- parse(Dev, L1, Fs, Parser, Options);
- {ok, F, L1} ->
- parse(Dev, L1, [F | Fs], Parser, Options);
- {error, IoErr, L1} ->
- parse(Dev, L1, [{error, IoErr} | Fs], Parser, Options);
- {eof, _L1} ->
- {ok, lists:reverse(Fs)}
- end.
-
+ case Parser(Dev, L0, Options) of
+ {ok, none, L1} ->
+ parse(Dev, L1, Fs, Parser, Options);
+ {ok, F, L1} ->
+ parse(Dev, L1, [F | Fs], Parser, Options);
+ {error, IoErr, L1} ->
+ parse(Dev, L1, [{error, IoErr} | Fs], Parser, Options);
+ {eof, _L1} ->
+ {ok, lists:reverse(Fs)}
+ end.
%% =====================================================================
%% @spec parse_form(IODevice, StartLoc) -> {ok, Form, LineNo}
@@ -366,11 +384,12 @@ parse(Dev, L0, Fs, Parser, Options) ->
%% @see quick_parse_form/2
-spec parse_form(file:io_device(), erl_anno:location()) ->
- {'ok', erl_syntax:forms(), integer()}
- | {'eof', integer()} | {'error', errorinfo(), integer()}.
+ {'ok', erl_syntax:forms(), integer()}
+ | {'eof', integer()}
+ | {'error', errorinfo(), integer()}.
parse_form(Dev, Loc0) ->
- parse_form(Dev, Loc0, []).
+ parse_form(Dev, Loc0, []).
%% @spec parse_form(IODevice, StartLoc, Options) ->
%% {ok, Form, LineNo}
@@ -396,11 +415,12 @@ parse_form(Dev, Loc0) ->
%% @see quick_parse_form/3
-spec parse_form(file:io_device(), integer(), [option()]) ->
- {'ok', erl_syntax:forms(), integer()}
- | {'eof', integer()} | {'error', errorinfo(), integer()}.
+ {'ok', erl_syntax:forms(), integer()}
+ | {'eof', integer()}
+ | {'error', errorinfo(), integer()}.
parse_form(Dev, L0, Options) ->
- parse_form(Dev, L0, fun normal_parser/2, Options).
+ parse_form(Dev, L0, fun normal_parser/2, Options).
%% @spec quick_parse_form(IODevice, StartLine) ->
%% {ok, Form, LineNo}
@@ -416,11 +436,12 @@ parse_form(Dev, L0, Options) ->
%% @see parse_form/2
-spec quick_parse_form(file:io_device(), integer()) ->
- {'ok', erl_syntax:forms(), integer()}
- | {'eof', integer()} | {'error', errorinfo(), integer()}.
+ {'ok', erl_syntax:forms(), integer()}
+ | {'eof', integer()}
+ | {'error', errorinfo(), integer()}.
quick_parse_form(Dev, L0) ->
- quick_parse_form(Dev, L0, []).
+ quick_parse_form(Dev, L0, []).
%% @spec quick_parse_form(IODevice, StartLine, Options) ->
%% {ok, Form, LineNo}
@@ -441,420 +462,552 @@ quick_parse_form(Dev, L0) ->
%% @see parse_form/3
-spec quick_parse_form(file:io_device(), integer(), [option()]) ->
- {'ok', erl_syntax:forms() | none, integer()}
- | {'eof', integer()} | {'error', errorinfo(), integer()}.
+ {'ok', erl_syntax:forms() | none, integer()}
+ | {'eof', integer()}
+ | {'error', errorinfo(), integer()}.
quick_parse_form(Dev, L0, Options) ->
- parse_form(Dev, L0, fun quick_parser/2, Options).
+ parse_form(Dev, L0, fun quick_parser/2, Options).
-record(opt, {clever = false :: boolean()}).
-type opt() :: #opt{}.
-spec parse_form(file:io_device(), location(), function(), [option()]) ->
- {'ok', erl_syntax:forms() | none, integer()}
- | {'eof', integer()} | {'error', errorinfo(), integer()}.
+ {'ok', erl_syntax:forms() | none, integer()}
+ | {'eof', integer()}
+ | {'error', errorinfo(), integer()}.
parse_form(Dev, L0, Parser, Options) ->
- NoFail = proplists:get_bool(no_fail, Options),
- Opt = #opt{clever = proplists:get_bool(clever, Options)},
- case io:scan_erl_form(Dev, "", L0) of
- {ok, Ts, L1} ->
- case catch {ok, Parser(Ts, Opt)} of
- {'EXIT', Term} ->
- {error, io_error(L1, {unknown, Term}), L1};
- {error, Term} ->
- IoErr = io_error(L1, Term),
- {error, IoErr, L1};
- {parse_error, _IoErr} when NoFail ->
- {ok, erl_syntax:set_pos(
- erl_syntax:text(tokens_to_string(Ts)),
- start_pos(Ts, L1)),
- L1};
- {parse_error, IoErr} ->
- {error, IoErr, L1};
- {ok, F} ->
- {ok, F, L1}
- end;
- {error, _IoErr, _L1} = Err -> Err;
- {error, _Reason} -> {eof, L0}; % This is probably encoding problem
- {eof, _L1} = Eof -> Eof
- end.
+ NoFail = proplists:get_bool(no_fail, Options),
+ Opt = #opt{clever = proplists:get_bool(clever, Options)},
+ case io:scan_erl_form(Dev, "", L0) of
+ {ok, Ts, L1} ->
+ case catch {ok, Parser(Ts, Opt)} of
+ {'EXIT', Term} ->
+ {error, io_error(L1, {unknown, Term}), L1};
+ {error, Term} ->
+ IoErr = io_error(L1, Term),
+ {error, IoErr, L1};
+ {parse_error, _IoErr} when NoFail ->
+ {ok,
+ erl_syntax:set_pos(
+ erl_syntax:text(tokens_to_string(Ts)),
+ start_pos(Ts, L1)
+ ),
+ L1};
+ {parse_error, IoErr} ->
+ {error, IoErr, L1};
+ {ok, F} ->
+ {ok, F, L1}
+ end;
+ {error, _IoErr, _L1} = Err ->
+ Err;
+ % This is probably encoding problem
+ {error, _Reason} ->
+ {eof, L0};
+ {eof, _L1} = Eof ->
+ Eof
+ end.
-spec io_error(location(), any()) -> {location(), module(), any()}.
io_error(L, Desc) ->
- {L, ?MODULE, Desc}.
+ {L, ?MODULE, Desc}.
-spec start_pos([erl_scan:token()], location()) -> location().
start_pos([T | _Ts], _L) ->
- erl_anno:line(element(2, T));
+ erl_anno:line(element(2, T));
start_pos([], L) ->
- L.
+ L.
%% Exception-throwing wrapper for the standard Erlang parser stage
-spec parse_tokens([erl_scan:token()]) -> erl_syntax:syntaxTree().
parse_tokens(Ts) ->
- parse_tokens(Ts, fun fix_form/1).
+ parse_tokens(Ts, fun fix_form/1).
-spec parse_tokens([erl_scan:token()], function()) -> erl_syntax:syntaxTree().
parse_tokens(Ts, Fix) ->
- case erl_parse:parse_form(Ts) of
- {ok, Form} ->
- Form;
- {error, IoErr} ->
- case Fix(Ts) of
- {form, Form} ->
- Form;
- {retry, Ts1, Fix1} ->
- parse_tokens(Ts1, Fix1);
- error ->
- throw({parse_error, IoErr})
- end
- end.
+ case erl_parse:parse_form(Ts) of
+ {ok, Form} ->
+ Form;
+ {error, IoErr} ->
+ case Fix(Ts) of
+ {form, Form} ->
+ Form;
+ {retry, Ts1, Fix1} ->
+ parse_tokens(Ts1, Fix1);
+ error ->
+ throw({parse_error, IoErr})
+ end
+ end.
%% ---------------------------------------------------------------------
%% Quick scanning/parsing - deletes macro definitions and other
%% preprocessor directives, and replaces all macro calls with atoms.
-spec quick_parser([erl_scan:token()], [option()]) ->
- erl_syntax:syntaxTree() | none.
+ erl_syntax:syntaxTree() | none.
quick_parser(Ts, _Opt) ->
- filter_form(parse_tokens(quickscan_form(Ts))).
+ filter_form(parse_tokens(quickscan_form(Ts))).
-spec quickscan_form([erl_scan:token()]) -> [erl_scan:token()].
quickscan_form([{'-', _L}, {atom, La, define} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
quickscan_form([{'-', _L}, {atom, La, undef} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
quickscan_form([{'-', _L}, {atom, La, include} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
quickscan_form([{'-', _L}, {atom, La, include_lib} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
quickscan_form([{'-', _L}, {atom, La, ifdef} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
quickscan_form([{'-', _L}, {atom, La, ifndef} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
quickscan_form([{'-', _L}, {'if', La} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
quickscan_form([{'-', _L}, {atom, La, elif} | _Ts]) ->
- kill_form(La);
-quickscan_form([{'-', _L}, {atom, La, else} | _Ts]) ->
- kill_form(La);
+ kill_form(La);
+quickscan_form([{'-', _L}, {atom, La, 'else'} | _Ts]) ->
+ kill_form(La);
quickscan_form([{'-', _L}, {atom, La, endif} | _Ts]) ->
- kill_form(La);
-quickscan_form([{'-', L}, {'?', _}, {Type, _, _}=N | [{'(', _} | _]=Ts])
- when Type =:= atom; Type =:= var ->
- %% minus, macro and open parenthesis at start of form - assume that
- %% the macro takes no arguments; e.g. `-?foo(...).'
- quickscan_macros_1(N, Ts, [{'-', L}]);
-quickscan_form([{'?', _L}, {Type, _, _}=N | [{'(', _} | _]=Ts])
- when Type =:= atom; Type =:= var ->
- %% macro and open parenthesis at start of form - assume that the
- %% macro takes no arguments (see scan_macros for details)
- quickscan_macros_1(N, Ts, []);
+ kill_form(La);
+quickscan_form([{'-', L}, {'?', _}, {Type, _, _} = N | [{'(', _} | _] = Ts]) when
+ Type =:= atom; Type =:= var
+->
+ %% minus, macro and open parenthesis at start of form - assume that
+ %% the macro takes no arguments; e.g. `-?foo(...).'
+ quickscan_macros_1(N, Ts, [{'-', L}]);
+quickscan_form([{'?', _L}, {Type, _, _} = N | [{'(', _} | _] = Ts]) when
+ Type =:= atom; Type =:= var
+->
+ %% macro and open parenthesis at start of form - assume that the
+ %% macro takes no arguments (see scan_macros for details)
+ quickscan_macros_1(N, Ts, []);
quickscan_form(Ts) ->
- quickscan_macros(Ts).
+ quickscan_macros(Ts).
-spec kill_form(location()) -> [erl_scan:token()].
kill_form(L) ->
- [{atom, L, ?pp_form}, {'(', L}, {')', L}, {'->', L}, {atom, L, kill},
- {dot, L}].
+ [
+ {atom, L, ?pp_form},
+ {'(', L},
+ {')', L},
+ {'->', L},
+ {atom, L, kill},
+ {dot, L}
+ ].
-spec quickscan_macros([erl_scan:token()]) -> [erl_scan:token()].
quickscan_macros(Ts) ->
- quickscan_macros(Ts, []).
+ quickscan_macros(Ts, []).
-spec quickscan_macros([erl_scan:token()], [erl_scan:token()]) ->
- [erl_scan:token()].
-quickscan_macros([{'?', _}, {Type, _, A} | Ts], [{string, L, S} | As])
- when Type =:= atom; Type =:= var ->
- %% macro after a string literal: change to a single string
- {_, Ts1} = skip_macro_args(Ts),
- S1 = S ++ quick_macro_string(A),
- quickscan_macros(Ts1, [{string, L, S1} | As]);
-quickscan_macros([{'?', _}, {Type, _, _}=N | [{'(', _}|_]=Ts],
- [{':', _}|_]=As)
- when Type =:= atom; Type =:= var ->
- %% macro and open parenthesis after colon - check the token
- %% following the arguments (see scan_macros for details)
- Ts1 = case skip_macro_args(Ts) of
- {_, [{'->', _} | _] = Ts2} -> Ts2;
- {_, [{'when', _} | _] = Ts2} -> Ts2;
- _ -> Ts %% assume macro without arguments
+ [erl_scan:token()].
+quickscan_macros([{'?', _}, {Type, _, A} | Ts], [{string, L, S} | As]) when
+ Type =:= atom; Type =:= var
+->
+ %% macro after a string literal: change to a single string
+ {_, Ts1} = skip_macro_args(Ts),
+ S1 = S ++ quick_macro_string(A),
+ quickscan_macros(Ts1, [{string, L, S1} | As]);
+quickscan_macros(
+ [{'?', _}, {Type, _, _} = N | [{'(', _} | _] = Ts],
+ [{':', _} | _] = As
+) when
+ Type =:= atom; Type =:= var
+->
+ %% macro and open parenthesis after colon - check the token
+ %% following the arguments (see scan_macros for details)
+ Ts1 =
+ case skip_macro_args(Ts) of
+ {_, [{'->', _} | _] = Ts2} -> Ts2;
+ {_, [{'when', _} | _] = Ts2} -> Ts2;
+ %% assume macro without arguments
+ _ -> Ts
end,
- quickscan_macros_1(N, Ts1, As);
-quickscan_macros([{'?', _}, {Type, _, _}=N | Ts], As)
- when Type =:= atom; Type =:= var ->
- %% macro with or without arguments
- {_, Ts1} = skip_macro_args(Ts),
- quickscan_macros_1(N, Ts1, As);
+ quickscan_macros_1(N, Ts1, As);
+quickscan_macros([{'?', _}, {Type, _, _} = N | Ts], As) when
+ Type =:= atom; Type =:= var
+->
+ %% macro with or without arguments
+ {_, Ts1} = skip_macro_args(Ts),
+ quickscan_macros_1(N, Ts1, As);
quickscan_macros([T | Ts], As) ->
- quickscan_macros(Ts, [T | As]);
+ quickscan_macros(Ts, [T | As]);
quickscan_macros([], As) ->
- lists:reverse(As).
+ lists:reverse(As).
-spec quickscan_macros_1(tuple(), [erl_scan:token()], [erl_scan:token()]) ->
- [erl_scan:token()].
+ [erl_scan:token()].
%% (after a macro has been found and the arglist skipped, if any)
quickscan_macros_1({_Type, _, A}, [{string, L, S} | Ts], As) ->
- %% string literal following macro: change to single string
- S1 = quick_macro_string(A) ++ S,
- quickscan_macros(Ts, [{string, L, S1} | As]);
+ %% string literal following macro: change to single string
+ S1 = quick_macro_string(A) ++ S,
+ quickscan_macros(Ts, [{string, L, S1} | As]);
quickscan_macros_1({_Type, L, A}, Ts, As) ->
- %% normal case - just replace the macro with an atom
- quickscan_macros(Ts, [{atom, L, quick_macro_atom(A)} | As]).
+ %% normal case - just replace the macro with an atom
+ quickscan_macros(Ts, [{atom, L, quick_macro_atom(A)} | As]).
-spec quick_macro_atom(atom()) -> atom().
quick_macro_atom(A) ->
- list_to_atom("?" ++ atom_to_list(A)).
+ list_to_atom("?" ++ atom_to_list(A)).
-spec quick_macro_string(atom()) -> string().
quick_macro_string(A) ->
- "(?" ++ atom_to_list(A) ++ ")".
+ "(?" ++ atom_to_list(A) ++ ")".
%% Skipping to the end of a macro call, tracking open/close constructs.
%% @spec (Tokens) -> {Skipped, Rest}
-spec skip_macro_args([erl_scan:token()]) ->
- {[erl_scan:token()], [erl_scan:token()]}.
-skip_macro_args([{'(', _}=T | Ts]) ->
- skip_macro_args(Ts, [')'], [T]);
+ {[erl_scan:token()], [erl_scan:token()]}.
+skip_macro_args([{'(', _} = T | Ts]) ->
+ skip_macro_args(Ts, [')'], [T]);
skip_macro_args(Ts) ->
- {[], Ts}.
+ {[], Ts}.
-spec skip_macro_args([erl_scan:token()], [atom()], [erl_scan:token()]) ->
- {[erl_scan:token()], [erl_scan:token()]}.
-skip_macro_args([{'(', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, [')' | Es], [T | As]);
-skip_macro_args([{'{', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['}' | Es], [T | As]);
-skip_macro_args([{'[', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, [']' | Es], [T | As]);
-skip_macro_args([{'<<', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['>>' | Es], [T | As]);
-skip_macro_args([{'begin', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['end' | Es], [T | As]);
-skip_macro_args([{'if', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['end' | Es], [T | As]);
-skip_macro_args([{'case', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['end' | Es], [T | As]);
-skip_macro_args([{'receive', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['end' | Es], [T | As]);
-skip_macro_args([{'try', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['end' | Es], [T | As]);
-skip_macro_args([{'cond', _}=T | Ts], Es, As) ->
- skip_macro_args(Ts, ['end' | Es], [T | As]);
-skip_macro_args([{E, _}=T | Ts], [E], As) -> %final close
- {lists:reverse([T | As]), Ts};
-skip_macro_args([{E, _}=T | Ts], [E | Es], As) -> %matching close
- skip_macro_args(Ts, Es, [T | As]);
+ {[erl_scan:token()], [erl_scan:token()]}.
+skip_macro_args([{'(', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, [')' | Es], [T | As]);
+skip_macro_args([{'{', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['}' | Es], [T | As]);
+skip_macro_args([{'[', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, [']' | Es], [T | As]);
+skip_macro_args([{'<<', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['>>' | Es], [T | As]);
+skip_macro_args([{'begin', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['end' | Es], [T | As]);
+skip_macro_args([{'if', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['end' | Es], [T | As]);
+skip_macro_args([{'case', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['end' | Es], [T | As]);
+skip_macro_args([{'receive', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['end' | Es], [T | As]);
+skip_macro_args([{'try', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['end' | Es], [T | As]);
+skip_macro_args([{'cond', _} = T | Ts], Es, As) ->
+ skip_macro_args(Ts, ['end' | Es], [T | As]);
+%final close
+skip_macro_args([{E, _} = T | Ts], [E], As) ->
+ {lists:reverse([T | As]), Ts};
+%matching close
+skip_macro_args([{E, _} = T | Ts], [E | Es], As) ->
+ skip_macro_args(Ts, Es, [T | As]);
skip_macro_args([T | Ts], Es, As) ->
- skip_macro_args(Ts, Es, [T | As]);
+ skip_macro_args(Ts, Es, [T | As]);
skip_macro_args([], _Es, _As) ->
- throw({error, macro_args}).
+ throw({error, macro_args}).
-spec filter_form(erl_syntax:syntaxTree()) -> erl_syntax:syntaxTree() | none.
-filter_form({function, _, ?pp_form, _,
- [{clause, _, [], [], [{atom, _, kill}]}]}) ->
- none;
+filter_form({function, _, ?pp_form, _, [{clause, _, [], [], [{atom, _, kill}]}]}) ->
+ none;
filter_form(T) ->
- T.
-
+ T.
%% ---------------------------------------------------------------------
%% Normal parsing - try to preserve all information
-spec normal_parser([erl_scan:token()], opt()) -> erl_syntax:syntaxTree().
normal_parser(Ts0, Opt) ->
- case scan_form(Ts0, Opt) of
- Ts when is_list(Ts) ->
- rewrite_form(parse_tokens(Ts));
- Node ->
- Node
- end.
+ case scan_form(Ts0, Opt) of
+ Ts when is_list(Ts) ->
+ rewrite_form(parse_tokens(Ts));
+ Node ->
+ Node
+ end.
-spec scan_form([erl_scan:token()], opt()) ->
- [erl_scan:token()] | erl_syntax:syntaxTree().
+ [erl_scan:token()] | erl_syntax:syntaxTree().
scan_form([{'-', _L}, {atom, La, define} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, define} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, define}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, undef} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, undef} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, undef}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, include} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, include} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, include}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, include_lib} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, include_lib} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, include_lib}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, ifdef} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, ifdef} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, ifdef}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, ifndef} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, ifndef} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, ifndef}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {'if', La} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, 'if'} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, 'if'}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, elif} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, 'elif'} | scan_macros(Ts, Opt)];
-scan_form([{'-', _L}, {atom, La, else} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, else} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, 'elif'}
+ | scan_macros(Ts, Opt)
+ ];
+scan_form([{'-', _L}, {atom, La, 'else'} | Ts], Opt) ->
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, 'else'}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, endif} | Ts], Opt) ->
- [{atom, La, ?pp_form}, {'(', La}, {')', La}, {'->', La},
- {atom, La, endif} | scan_macros(Ts, Opt)];
+ [
+ {atom, La, ?pp_form},
+ {'(', La},
+ {')', La},
+ {'->', La},
+ {atom, La, endif}
+ | scan_macros(Ts, Opt)
+ ];
scan_form([{'-', _L}, {atom, La, error} | Ts], _Opt) ->
- Desc = build_info_string("-error", Ts),
- ErrorInfo = {La, ?MODULE, {error, Desc}},
- erl_syntax:error_marker(ErrorInfo);
+ Desc = build_info_string("-error", Ts),
+ ErrorInfo = {La, ?MODULE, {error, Desc}},
+ erl_syntax:error_marker(ErrorInfo);
scan_form([{'-', _L}, {atom, La, warning} | Ts], _Opt) ->
- Desc = build_info_string("-warning", Ts),
- ErrorInfo = {La, ?MODULE, {warning, Desc}},
- erl_syntax:error_marker(ErrorInfo);
-scan_form([{'-', L}, {'?', L1}, {Type, _, _}=N | [{'(', _} | _]=Ts], Opt)
- when Type =:= atom; Type =:= var ->
- %% minus, macro and open parenthesis at start of form - assume that
- %% the macro takes no arguments; e.g. `-?foo(...).'
- macro(L1, N, Ts, [{'-', L}], Opt);
-scan_form([{'?', L}, {Type, _, _}=N | [{'(', _} | _]=Ts], Opt)
- when Type =:= atom; Type =:= var ->
- %% macro and open parenthesis at start of form - assume that the
- %% macro takes no arguments; probably a function declaration on the
- %% form `?m(...) -> ...', which will not parse if it is rewritten as
- %% `(?m(...)) -> ...', so it must be handled as `(?m)(...) -> ...'
- macro(L, N, Ts, [], Opt);
+ Desc = build_info_string("-warning", Ts),
+ ErrorInfo = {La, ?MODULE, {warning, Desc}},
+ erl_syntax:error_marker(ErrorInfo);
+scan_form([{'-', L}, {'?', L1}, {Type, _, _} = N | [{'(', _} | _] = Ts], Opt) when
+ Type =:= atom; Type =:= var
+->
+ %% minus, macro and open parenthesis at start of form - assume that
+ %% the macro takes no arguments; e.g. `-?foo(...).'
+ macro(L1, N, Ts, [{'-', L}], Opt);
+scan_form([{'?', L}, {Type, _, _} = N | [{'(', _} | _] = Ts], Opt) when
+ Type =:= atom; Type =:= var
+->
+ %% macro and open parenthesis at start of form - assume that the
+ %% macro takes no arguments; probably a function declaration on the
+ %% form `?m(...) -> ...', which will not parse if it is rewritten as
+ %% `(?m(...)) -> ...', so it must be handled as `(?m)(...) -> ...'
+ macro(L, N, Ts, [], Opt);
scan_form(Ts, Opt) ->
- scan_macros(Ts, Opt).
+ scan_macros(Ts, Opt).
-spec build_info_string(string(), [erl_scan:token()]) -> string().
build_info_string(Prefix, Ts0) ->
- Ts = lists:droplast(Ts0),
- String = lists:droplast(tokens_to_string(Ts)),
- Prefix ++ " " ++ String ++ ".".
+ Ts = lists:droplast(Ts0),
+ String = lists:droplast(tokens_to_string(Ts)),
+ Prefix ++ " " ++ String ++ ".".
-spec scan_macros([erl_scan:token()], opt()) -> [erl_scan:token()].
scan_macros(Ts, Opt) ->
- scan_macros(Ts, [], Opt).
+ scan_macros(Ts, [], Opt).
-spec scan_macros([erl_scan:token()], [erl_scan:token()], opt()) ->
- [erl_scan:token()].
-scan_macros([{'?', _}=M, {Type, _, _}=N | Ts], [{string, L, _}=S | As],
- #opt{clever = true}=Opt)
- when Type =:= atom; Type =:= var ->
- %% macro after a string literal: be clever and insert ++
- scan_macros([M, N | Ts], [{'++', L}, S | As], Opt);
-scan_macros([{'?', L}, {Type, _, _}=N | [{'(', _}|_]=Ts],
- [{':', _}|_]=As, Opt)
- when Type =:= atom; Type =:= var ->
- %% macro and open parentheses after colon - probably a call
- %% `m:?F(...)' so the argument list might belong to the call, not
- %% the macro - but it could also be a try-clause pattern
- %% `...:?T(...) ->' - we need to check the token following the
- %% arguments to decide
- {Args, Rest} = skip_macro_args(Ts),
- case Rest of
- [{'->', _} | _] ->
- macro_call(Args, L, N, Rest, As, Opt);
- [{'when', _} | _] ->
- macro_call(Args, L, N, Rest, As, Opt);
- _ ->
- macro(L, N, Ts, As, Opt)
- end;
-scan_macros([{'?', L}, {Type, _, _}=N | [{'(', _}|_]=Ts], As, Opt)
- when Type =:= atom; Type =:= var ->
- %% macro with arguments
- {Args, Rest} = skip_macro_args(Ts),
- macro_call(Args, L, N, Rest, As, Opt);
-scan_macros([{'?', L }, {Type, _, _}=N | Ts], As, Opt)
- when Type =:= atom; Type =:= var ->
- %% macro without arguments
- macro(L, N, Ts, As, Opt);
+ [erl_scan:token()].
+scan_macros(
+ [{'?', _} = M, {Type, _, _} = N | Ts],
+ [{string, L, _} = S | As],
+ #opt{clever = true} = Opt
+) when
+ Type =:= atom; Type =:= var
+->
+ %% macro after a string literal: be clever and insert ++
+ scan_macros([M, N | Ts], [{'++', L}, S | As], Opt);
+scan_macros(
+ [{'?', L}, {Type, _, _} = N | [{'(', _} | _] = Ts],
+ [{':', _} | _] = As,
+ Opt
+) when
+ Type =:= atom; Type =:= var
+->
+ %% macro and open parentheses after colon - probably a call
+ %% `m:?F(...)' so the argument list might belong to the call, not
+ %% the macro - but it could also be a try-clause pattern
+ %% `...:?T(...) ->' - we need to check the token following the
+ %% arguments to decide
+ {Args, Rest} = skip_macro_args(Ts),
+ case Rest of
+ [{'->', _} | _] ->
+ macro_call(Args, L, N, Rest, As, Opt);
+ [{'when', _} | _] ->
+ macro_call(Args, L, N, Rest, As, Opt);
+ _ ->
+ macro(L, N, Ts, As, Opt)
+ end;
+scan_macros([{'?', L}, {Type, _, _} = N | [{'(', _} | _] = Ts], As, Opt) when
+ Type =:= atom; Type =:= var
+->
+ %% macro with arguments
+ {Args, Rest} = skip_macro_args(Ts),
+ macro_call(Args, L, N, Rest, As, Opt);
+scan_macros([{'?', L}, {Type, _, _} = N | Ts], As, Opt) when
+ Type =:= atom; Type =:= var
+->
+ %% macro without arguments
+ macro(L, N, Ts, As, Opt);
scan_macros([T | Ts], As, Opt) ->
- scan_macros(Ts, [T | As], Opt);
+ scan_macros(Ts, [T | As], Opt);
scan_macros([], As, _Opt) ->
- lists:reverse(As).
+ lists:reverse(As).
%% Rewriting to a call which will be recognized by the post-parse pass
%% (we insert parentheses to preserve the precedences when parsing).
--spec macro(location(), tuple(), [erl_scan:token()],
- [erl_scan:token()], opt()) ->
- [erl_scan:token()].
+-spec macro(
+ location(),
+ tuple(),
+ [erl_scan:token()],
+ [erl_scan:token()],
+ opt()
+) ->
+ [erl_scan:token()].
macro(L, {Type, _, A}, Rest, As, Opt) ->
- scan_macros_1([], Rest, [{atom, L, macro_atom(Type, A)} | As], Opt).
-
--spec macro_call([erl_scan:token()], location(), tuple(),
- [erl_scan:token()], [erl_scan:token()], opt()) ->
- [erl_scan:token()].
+ scan_macros_1([], Rest, [{atom, L, macro_atom(Type, A)} | As], Opt).
+
+-spec macro_call(
+ [erl_scan:token()],
+ location(),
+ tuple(),
+ [erl_scan:token()],
+ [erl_scan:token()],
+ opt()
+) ->
+ [erl_scan:token()].
macro_call([{'(', _}, {')', _}], L, {_, Ln, _} = N, Rest, As, Opt) ->
- {Open, Close} = parentheses(As),
- scan_macros_1([], Rest,
- %% {'?macro_call', N }
- lists:reverse(Open ++ [{'{', L},
- {atom, L, ?macro_call},
- {',', L},
- N,
- {'}', Ln}] ++ Close,
- As), Opt);
-macro_call([{'(', _} | Args], L, {_, Ln, _}=N, Rest, As, Opt) ->
- {Open, Close} = parentheses(As),
- %% drop closing parenthesis
- {')', _} = lists:last(Args), %% assert
- Args1 = lists:droplast(Args),
- %% note that we must scan the argument list; it may not be skipped
- scan_macros_1(Args1 ++ [{'}', Ln} | Close],
- Rest,
- %% {'?macro_call', N, Arg1, ... }
- lists:reverse(Open ++ [{'{', L},
- {atom, L, ?macro_call},
- {',', L},
- N,
- {',', Ln}],
- As), Opt).
+ {Open, Close} = parentheses(As),
+ scan_macros_1(
+ [],
+ Rest,
+ %% {'?macro_call', N }
+ lists:reverse(
+ Open ++
+ [
+ {'{', L},
+ {atom, L, ?macro_call},
+ {',', L},
+ N,
+ {'}', Ln}
+ ] ++ Close,
+ As
+ ),
+ Opt
+ );
+macro_call([{'(', _} | Args], L, {_, Ln, _} = N, Rest, As, Opt) ->
+ {Open, Close} = parentheses(As),
+ %% drop closing parenthesis
+
+ %% assert
+ {')', _} = lists:last(Args),
+ Args1 = lists:droplast(Args),
+ %% note that we must scan the argument list; it may not be skipped
+ scan_macros_1(
+ Args1 ++ [{'}', Ln} | Close],
+ Rest,
+ %% {'?macro_call', N, Arg1, ... }
+ lists:reverse(
+ Open ++
+ [
+ {'{', L},
+ {atom, L, ?macro_call},
+ {',', L},
+ N,
+ {',', Ln}
+ ],
+ As
+ ),
+ Opt
+ ).
-spec macro_atom(atom | var, atom()) -> atom().
macro_atom(atom, A) ->
- list_to_atom(?atom_prefix ++ atom_to_list(A));
+ list_to_atom(?atom_prefix ++ atom_to_list(A));
macro_atom(var, A) ->
- list_to_atom(?var_prefix ++ atom_to_list(A)).
+ list_to_atom(?var_prefix ++ atom_to_list(A)).
-spec parentheses([erl_scan:token()]) -> {[erl_scan:token()], [erl_scan:token()]}.
%% don't insert parentheses after a string token, to avoid turning
%% `"string" ?macro' into a "function application" `"string"(...)'
%% (see note at top of file)
parentheses([{string, _, _} | _]) ->
- {[], []};
+ {[], []};
parentheses(_) ->
- {[{'(', 0}], [{')', 0}]}.
-
--spec scan_macros_1([erl_scan:token()], [erl_scan:token()],
- [erl_scan:token()], opt()) ->
- [erl_scan:token()].
+ {[{'(', 0}], [{')', 0}]}.
+
+-spec scan_macros_1(
+ [erl_scan:token()],
+ [erl_scan:token()],
+ [erl_scan:token()],
+ opt()
+) ->
+ [erl_scan:token()].
%% (after a macro has been found and the arglist skipped, if any)
-scan_macros_1(Args, [{string, L, _} | _]=Rest, As,
- #opt{clever = true}=Opt) ->
- %% string literal following macro: be clever and insert ++
- scan_macros(Args ++ [{'++', L} | Rest], As, Opt);
+scan_macros_1(
+ Args,
+ [{string, L, _} | _] = Rest,
+ As,
+ #opt{clever = true} = Opt
+) ->
+ %% string literal following macro: be clever and insert ++
+ scan_macros(Args ++ [{'++', L} | Rest], As, Opt);
scan_macros_1(Args, Rest, As, Opt) ->
- %% normal case - continue scanning
- scan_macros(Args ++ Rest, As, Opt).
+ %% normal case - continue scanning
+ scan_macros(Args ++ Rest, As, Opt).
-spec rewrite_form(erl_syntax:syntaxTree()) -> erl_syntax:syntaxTree().
-rewrite_form({function, L, ?pp_form, _,
- [{clause, _, [], [], [{call, _, A, As}]}]}) ->
- erl_syntax:set_pos(erl_syntax:attribute(A, rewrite_list(As)), L);
+rewrite_form({function, L, ?pp_form, _, [{clause, _, [], [], [{call, _, A, As}]}]}) ->
+ erl_syntax:set_pos(erl_syntax:attribute(A, rewrite_list(As)), L);
rewrite_form({function, L, ?pp_form, _, [{clause, _, [], [], [A]}]}) ->
- erl_syntax:set_pos(erl_syntax:attribute(A), L);
+ erl_syntax:set_pos(erl_syntax:attribute(A), L);
rewrite_form(T) ->
- rewrite(T).
+ rewrite(T).
-spec rewrite_list([erl_syntax:syntaxTree()]) -> [erl_syntax:syntaxTree()].
rewrite_list([T | Ts]) ->
- [rewrite(T) | rewrite_list(Ts)];
+ [rewrite(T) | rewrite_list(Ts)];
rewrite_list([]) ->
- [].
+ [].
%% Note: as soon as we start using erl_syntax:subtrees/1 and similar
%% functions, we cannot assume that we know the exact representation of
@@ -863,60 +1016,66 @@ rewrite_list([]) ->
-spec rewrite(erl_syntax:syntaxTree()) -> erl_syntax:syntaxTree().
rewrite(Node) ->
- case erl_syntax:type(Node) of
- atom ->
- case atom_to_list(erl_syntax:atom_value(Node)) of
- ?atom_prefix ++ As ->
- A1 = list_to_atom(As),
- N = erl_syntax:copy_pos(Node, erl_syntax:atom(A1)),
- erl_syntax:copy_pos(Node, erl_syntax:macro(N));
- ?var_prefix ++ As ->
- A1 = list_to_atom(As),
- N = erl_syntax:copy_pos(Node, erl_syntax:variable(A1)),
- erl_syntax:copy_pos(Node, erl_syntax:macro(N));
- _ ->
- Node
- end;
- Type when Type =:= tuple;
- Type =:= tuple_type ->
- case tuple_elements(Node, Type) of
- [MagicWord, A | As] ->
- case erl_syntax:type(MagicWord) of
- atom ->
- case erl_syntax:atom_value(MagicWord) of
- ?macro_call ->
- M = erl_syntax:macro(A, rewrite_list(As)),
- erl_syntax:copy_pos(Node, M);
+ case erl_syntax:type(Node) of
+ atom ->
+ case atom_to_list(erl_syntax:atom_value(Node)) of
+ ?atom_prefix ++ As ->
+ A1 = list_to_atom(As),
+ N = erl_syntax:copy_pos(Node, erl_syntax:atom(A1)),
+ erl_syntax:copy_pos(Node, erl_syntax:macro(N));
+ ?var_prefix ++ As ->
+ A1 = list_to_atom(As),
+ N = erl_syntax:copy_pos(Node, erl_syntax:variable(A1)),
+ erl_syntax:copy_pos(Node, erl_syntax:macro(N));
+ _ ->
+ Node
+ end;
+ Type when
+ Type =:= tuple;
+ Type =:= tuple_type
+ ->
+ case tuple_elements(Node, Type) of
+ [MagicWord, A | As] ->
+ case erl_syntax:type(MagicWord) of
+ atom ->
+ case erl_syntax:atom_value(MagicWord) of
+ ?macro_call ->
+ M = erl_syntax:macro(A, rewrite_list(As)),
+ erl_syntax:copy_pos(Node, M);
+ _ ->
+ rewrite_1(Node)
+ end;
+ _ ->
+ rewrite_1(Node)
+ end;
_ ->
- rewrite_1(Node)
- end;
- _ ->
- rewrite_1(Node)
- end;
+ rewrite_1(Node)
+ end;
_ ->
- rewrite_1(Node)
- end;
- _ ->
- rewrite_1(Node)
- end.
+ rewrite_1(Node)
+ end.
-spec tuple_elements(erl_syntax:syntaxTree(), atom()) -> [erl_syntax:syntaxTree()].
tuple_elements(Node, tuple) ->
- erl_syntax:tuple_elements(Node);
+ erl_syntax:tuple_elements(Node);
tuple_elements(Node, tuple_type) ->
- erl_syntax:tuple_type_elements(Node).
+ erl_syntax:tuple_type_elements(Node).
-spec rewrite_1(erl_syntax:syntaxTree()) -> erl_syntax:syntaxTree().
rewrite_1(Node) ->
- case subtrees(Node) of
- [] ->
- Node;
- Gs ->
- Node1 = erl_syntax:make_tree(erl_syntax:type(Node),
- [[rewrite(T) || T <- Ts]
- || Ts <- Gs]),
- erl_syntax:copy_pos(Node, Node1)
- end.
+ case subtrees(Node) of
+ [] ->
+ Node;
+ Gs ->
+ Node1 = erl_syntax:make_tree(
+ erl_syntax:type(Node),
+ [
+ [rewrite(T) || T <- Ts]
+ || Ts <- Gs
+ ]
+ ),
+ erl_syntax:copy_pos(Node, Node1)
+ end.
%% @doc Return the list of all subtrees of a syntax tree with special handling
%% for type attributes.
@@ -932,83 +1091,113 @@ rewrite_1(Node) ->
%% special expressions representing macros.
-spec subtrees(erl_syntax:syntaxTree()) -> [[erl_syntax:syntaxTree()]].
subtrees(Node) ->
- case is_type_attribute(Node) of
- {true, AttrName} ->
- [[erl_syntax:attribute_name(Node)],
- type_attribute_arguments(Node, AttrName)];
- false ->
- erl_syntax:subtrees(Node)
- end.
+ case is_type_attribute(Node) of
+ {true, AttrName} ->
+ [
+ [erl_syntax:attribute_name(Node)],
+ type_attribute_arguments(Node, AttrName)
+ ];
+ false ->
+ erl_syntax:subtrees(Node)
+ end.
-spec is_type_attribute(erl_syntax:syntaxTree()) -> {true, atom()} | false.
is_type_attribute(Node) ->
- case erl_syntax:type(Node) of
- attribute ->
- NameNode = erl_syntax:attribute_name(Node),
- case erl_syntax:type(NameNode) of
- atom ->
- AttrName = erl_syntax:atom_value(NameNode),
- case lists:member(AttrName, [callback, spec, type, opaque]) of
- true ->
- {true, AttrName};
- false ->
- false
- end;
+ case erl_syntax:type(Node) of
+ attribute ->
+ NameNode = erl_syntax:attribute_name(Node),
+ case erl_syntax:type(NameNode) of
+ atom ->
+ AttrName = erl_syntax:atom_value(NameNode),
+ case lists:member(AttrName, [callback, spec, type, opaque]) of
+ true ->
+ {true, AttrName};
+ false ->
+ false
+ end;
+ _ ->
+ false
+ end;
_ ->
- false
- end;
- _ ->
- false
- end.
-
--spec type_attribute_arguments(erl_syntax:syntaxTree(), atom())
- -> [erl_syntax:syntaxTree()].
-type_attribute_arguments(Node, AttrName) when AttrName =:= callback;
- AttrName =:= spec ->
- [Arg] = erl_syntax:attribute_arguments(Node),
- {FA, DefinitionClauses} = erl_syntax:concrete(Arg),
- [erl_syntax:tuple([erl_syntax:abstract(FA),
- erl_syntax:list(DefinitionClauses)])];
-type_attribute_arguments(Node, AttrName) when AttrName =:= opaque;
- AttrName =:= type ->
- [Arg] = erl_syntax:attribute_arguments(Node),
- {TypeName, Definition, TypeArgs} = erl_syntax:concrete(Arg),
- [erl_syntax:tuple([erl_syntax:abstract(TypeName),
- Definition,
- erl_syntax:list(TypeArgs)])].
-
-
+ false
+ end.
+
+-spec type_attribute_arguments(erl_syntax:syntaxTree(), atom()) ->
+ [erl_syntax:syntaxTree()].
+type_attribute_arguments(Node, AttrName) when
+ AttrName =:= callback;
+ AttrName =:= spec
+->
+ [Arg] = erl_syntax:attribute_arguments(Node),
+ {FA, DefinitionClauses} = erl_syntax:concrete(Arg),
+ [
+ erl_syntax:tuple([
+ erl_syntax:abstract(FA),
+ erl_syntax:list(DefinitionClauses)
+ ])
+ ];
+type_attribute_arguments(Node, AttrName) when
+ AttrName =:= opaque;
+ AttrName =:= type
+->
+ [Arg] = erl_syntax:attribute_arguments(Node),
+ {TypeName, Definition, TypeArgs} = erl_syntax:concrete(Arg),
+ [
+ erl_syntax:tuple([
+ erl_syntax:abstract(TypeName),
+ Definition,
+ erl_syntax:list(TypeArgs)
+ ])
+ ].
%% attempting a rescue operation on a token sequence for a single form
%% if it could not be parsed after the normal treatment
-spec fix_form([erl_scan:token()]) ->
- error | {retry, [erl_scan:token()], function()}.
-fix_form([{atom, _, ?pp_form}, {'(', _}, {')', _}, {'->', _},
- {atom, _, define}, {'(', _} | _]=Ts) ->
- case lists:reverse(Ts) of
- [{dot, _}, {')', _} | _] ->
- {retry, Ts, fun fix_define/1};
- [{dot, L} | Ts1] ->
- Ts2 = lists:reverse([{dot, L}, {')', L} | Ts1]),
- {retry, Ts2, fun fix_define/1};
- _ ->
- error
- end;
+ error | {retry, [erl_scan:token()], function()}.
+fix_form(
+ [
+ {atom, _, ?pp_form},
+ {'(', _},
+ {')', _},
+ {'->', _},
+ {atom, _, define},
+ {'(', _}
+ | _
+ ] = Ts
+) ->
+ case lists:reverse(Ts) of
+ [{dot, _}, {')', _} | _] ->
+ {retry, Ts, fun fix_define/1};
+ [{dot, L} | Ts1] ->
+ Ts2 = lists:reverse([{dot, L}, {')', L} | Ts1]),
+ {retry, Ts2, fun fix_define/1};
+ _ ->
+ error
+ end;
fix_form(_Ts) ->
- error.
+ error.
-spec fix_define([erl_scan:token()]) ->
- error | {form, erl_syntax:syntaxTree()}.
-fix_define([{atom, L, ?pp_form}, {'(', _}, {')', _}, {'->', _},
- {atom, La, define}, {'(', _}, N, {',', _} | Ts]) ->
- [{dot, _}, {')', _} | Ts1] = lists:reverse(Ts),
- S = tokens_to_string(lists:reverse(Ts1)),
- A = erl_syntax:set_pos(erl_syntax:atom(define), La),
- Txt = erl_syntax:set_pos(erl_syntax:text(S), La),
- {form, erl_syntax:set_pos(erl_syntax:attribute(A, [N, Txt]), L)};
+ error | {form, erl_syntax:syntaxTree()}.
+fix_define([
+ {atom, L, ?pp_form},
+ {'(', _},
+ {')', _},
+ {'->', _},
+ {atom, La, define},
+ {'(', _},
+ N,
+ {',', _}
+ | Ts
+]) ->
+ [{dot, _}, {')', _} | Ts1] = lists:reverse(Ts),
+ S = tokens_to_string(lists:reverse(Ts1)),
+ A = erl_syntax:set_pos(erl_syntax:atom(define), La),
+ Txt = erl_syntax:set_pos(erl_syntax:text(S), La),
+ {form, erl_syntax:set_pos(erl_syntax:attribute(A, [N, Txt]), L)};
fix_define(_Ts) ->
- error.
+ error.
%% @spec tokens_to_string(Tokens::[term()]) -> string()
%%
@@ -1018,24 +1207,23 @@ fix_define(_Ts) ->
-spec tokens_to_string([term()]) -> string().
tokens_to_string([{atom, _, A} | Ts]) ->
- io_lib:write_atom(A) ++ " " ++ tokens_to_string(Ts);
+ io_lib:write_atom(A) ++ " " ++ tokens_to_string(Ts);
tokens_to_string([{string, _, S} | Ts]) ->
- io_lib:write_string(S) ++ " " ++ tokens_to_string(Ts);
+ io_lib:write_string(S) ++ " " ++ tokens_to_string(Ts);
tokens_to_string([{char, _, C} | Ts]) ->
- io_lib:write_char(C) ++ " " ++ tokens_to_string(Ts);
+ io_lib:write_char(C) ++ " " ++ tokens_to_string(Ts);
tokens_to_string([{float, _, F} | Ts]) ->
- float_to_list(F) ++ " " ++ tokens_to_string(Ts);
+ float_to_list(F) ++ " " ++ tokens_to_string(Ts);
tokens_to_string([{integer, _, N} | Ts]) ->
- integer_to_list(N) ++ " " ++ tokens_to_string(Ts);
+ integer_to_list(N) ++ " " ++ tokens_to_string(Ts);
tokens_to_string([{var, _, A} | Ts]) ->
- atom_to_list(A) ++ " " ++ tokens_to_string(Ts);
+ atom_to_list(A) ++ " " ++ tokens_to_string(Ts);
tokens_to_string([{dot, _} | Ts]) ->
- ".\n" ++ tokens_to_string(Ts);
+ ".\n" ++ tokens_to_string(Ts);
tokens_to_string([{A, _} | Ts]) ->
- atom_to_list(A) ++ " " ++ tokens_to_string(Ts);
+ atom_to_list(A) ++ " " ++ tokens_to_string(Ts);
tokens_to_string([]) ->
- "".
-
+ "".
%% @spec format_error(Descriptor::term()) -> string()
%% @hidden
@@ -1045,17 +1233,16 @@ tokens_to_string([]) ->
-spec format_error(term()) -> string().
format_error(macro_args) ->
- errormsg("macro call missing end parenthesis");
+ errormsg("macro call missing end parenthesis");
format_error({error, Error}) ->
- Error;
+ Error;
format_error({warning, Error}) ->
- Error;
+ Error;
format_error({unknown, Reason}) ->
- errormsg(io_lib:format("unknown error: ~tP", [Reason, 15])).
+ errormsg(io_lib:format("unknown error: ~tP", [Reason, 15])).
-spec errormsg(string()) -> string().
errormsg(String) ->
- io_lib:format("~s: ~ts", [?MODULE, String]).
-
+ io_lib:format("~s: ~ts", [?MODULE, String]).
%% =====================================================================
diff --git a/apps/els_core/src/els_escript.erl b/apps/els_core/src/els_escript.erl
index 667dd6544..4ad3d3d5d 100644
--- a/apps/els_core/src/els_escript.erl
+++ b/apps/els_core/src/els_escript.erl
@@ -26,12 +26,14 @@
-module(els_escript).
--export([ extract/1 ]).
+-export([extract/1]).
--record(state, {file :: file:filename(),
- module :: module(),
- forms_or_bin,
- exports_main :: boolean()}).
+-record(state, {
+ file :: file:filename(),
+ module :: module(),
+ forms_or_bin,
+ exports_main :: boolean()
+}).
-type state() :: #state{}.
-type shebang() :: string().
@@ -41,135 +43,146 @@
-spec extract(file:filename()) -> any().
extract(File) ->
- {HeaderSz, NextLineNo, Fd, _Sections} = parse_header(File),
- case compile_source(File, Fd, NextLineNo, HeaderSz) of
- {ok, _Bin, Warnings} ->
- {ok, Warnings};
- {error, Errors, Warnings} ->
- {error, Errors, Warnings}
- end.
+ {HeaderSz, NextLineNo, Fd, _Sections} = parse_header(File),
+ case compile_source(File, Fd, NextLineNo, HeaderSz) of
+ {ok, _Bin, Warnings} ->
+ {ok, Warnings};
+ {error, Errors, Warnings} ->
+ {error, Errors, Warnings}
+ end.
--spec compile_source( file:filename()
- , any()
- , pos_integer()
- , pos_integer()) ->
- any().
+-spec compile_source(
+ file:filename(),
+ any(),
+ pos_integer(),
+ pos_integer()
+) ->
+ any().
compile_source(File, Fd, NextLineNo, HeaderSz) ->
- Forms = do_parse_file(File, Fd, NextLineNo, HeaderSz),
- ok = file:close(Fd),
- case compile:forms(Forms, [return_warnings, return_errors, debug_info]) of
- {ok, _, BeamBin, Warnings} ->
- {ok, BeamBin, Warnings};
- {error, Errors, Warnings} ->
- {error, Errors, Warnings}
- end.
+ Forms = do_parse_file(File, Fd, NextLineNo, HeaderSz),
+ ok = file:close(Fd),
+ case compile:forms(Forms, [return_warnings, return_errors, debug_info]) of
+ {ok, _, BeamBin, Warnings} ->
+ {ok, BeamBin, Warnings};
+ {error, Errors, Warnings} ->
+ {error, Errors, Warnings}
+ end.
-spec do_parse_file(any(), any(), pos_integer(), any()) ->
- [any()].
+ [any()].
do_parse_file(File, Fd, NextLineNo, HeaderSz) ->
- S = initial_state(File),
- #state{forms_or_bin = FormsOrBin} =
- parse_source(S, File, Fd, NextLineNo, HeaderSz),
- FormsOrBin.
+ S = initial_state(File),
+ #state{forms_or_bin = FormsOrBin} =
+ parse_source(S, File, Fd, NextLineNo, HeaderSz),
+ FormsOrBin.
-spec initial_state(_) -> state().
initial_state(File) ->
- #state{file = File,
- exports_main = false}.
+ #state{
+ file = File,
+ exports_main = false
+ }.
%% Skip header and make a heuristic guess about the script type
-spec parse_header(file:filename()) -> {any(), any(), any(), sections()}.
parse_header(File) ->
- {ok, Fd} = file:open(File, [read]),
+ {ok, Fd} = file:open(File, [read]),
- %% Skip shebang on first line
- Line1 = get_line(Fd),
- case classify_line(Line1) of
- shebang ->
- find_first_body_line(Fd, #sections{shebang = Line1});
- _ ->
- find_first_body_line(Fd, #sections{})
- end.
+ %% Skip shebang on first line
+ Line1 = get_line(Fd),
+ case classify_line(Line1) of
+ shebang ->
+ find_first_body_line(Fd, #sections{shebang = Line1});
+ _ ->
+ find_first_body_line(Fd, #sections{})
+ end.
-spec find_first_body_line(_, sections()) -> {any(), any(), any(), sections()}.
find_first_body_line(Fd, Sections) ->
- {ok, HeaderSz1} = file:position(Fd, cur),
- %% Look for special comment on second line
- Line2 = get_line(Fd),
- {ok, HeaderSz2} = file:position(Fd, cur),
- case classify_line(Line2) of
- emu_args ->
- %% Skip special comment on second line
- {HeaderSz2, 3, Fd, Sections};
- comment ->
- %% Look for special comment on third line
- Line3 = get_line(Fd),
- {ok, HeaderSz3} = file:position(Fd, cur),
- Line3Type = classify_line(Line3),
- case Line3Type of
+ {ok, HeaderSz1} = file:position(Fd, cur),
+ %% Look for special comment on second line
+ Line2 = get_line(Fd),
+ {ok, HeaderSz2} = file:position(Fd, cur),
+ case classify_line(Line2) of
emu_args ->
- %% Skip special comment on third line
- {HeaderSz3, 4, Fd, Sections};
+ %% Skip special comment on second line
+ {HeaderSz2, 3, Fd, Sections};
+ comment ->
+ %% Look for special comment on third line
+ Line3 = get_line(Fd),
+ {ok, HeaderSz3} = file:position(Fd, cur),
+ Line3Type = classify_line(Line3),
+ case Line3Type of
+ emu_args ->
+ %% Skip special comment on third line
+ {HeaderSz3, 4, Fd, Sections};
+ _ ->
+ %% Skip shebang on first line and comment on second
+ {HeaderSz2, 3, Fd, Sections}
+ end;
_ ->
- %% Skip shebang on first line and comment on second
- {HeaderSz2, 3, Fd, Sections}
- end;
- _ ->
- %% Just skip shebang on first line
- {HeaderSz1, 2, Fd,
- Sections#sections{}}
- end.
+ %% Just skip shebang on first line
+ {HeaderSz1, 2, Fd, Sections#sections{}}
+ end.
-spec classify_line(_) -> atom().
classify_line(Line) ->
- case Line of
- "#!" ++ _ -> shebang;
- "PK" ++ _ -> archive;
- "FOR1" ++ _ -> beam;
- "%%!" ++ _ -> emu_args;
- "%" ++ _ -> comment;
- _ -> undefined
- end.
+ case Line of
+ "#!" ++ _ -> shebang;
+ "PK" ++ _ -> archive;
+ "FOR1" ++ _ -> beam;
+ "%%!" ++ _ -> emu_args;
+ "%" ++ _ -> comment;
+ _ -> undefined
+ end.
-spec get_line(_) -> any().
get_line(P) ->
- case io:get_line(P, '') of
- eof ->
- throw("Premature end of file reached");
- Line ->
- Line
- end.
+ case io:get_line(P, '') of
+ eof ->
+ throw("Premature end of file reached");
+ Line ->
+ Line
+ end.
-spec parse_source(state(), _, _, pos_integer(), _) -> state().
parse_source(S, File, Fd, StartLine, HeaderSz) ->
- {PreDefMacros, DefModule} = pre_def_macros(File),
- IncludePath = [],
- %% Read the encoding on the second line, if there is any:
- {ok, _} = file:position(Fd, 0),
- _ = io:get_line(Fd, ''),
- Encoding = epp:set_encoding(Fd),
- {ok, _} = file:position(Fd, HeaderSz),
- {ok, Epp} = epp_open(File, Fd, StartLine, IncludePath, PreDefMacros),
- _ = [io:setopts(Fd, [{encoding, Encoding}]) || Encoding =/= none],
- {ok, FileForm} = epp:parse_erl_form(Epp),
- OptModRes = epp:parse_erl_form(Epp),
- S2 =
- case OptModRes of
- {ok, {attribute, _, module, M} = Form} ->
- epp_parse_file(Epp, S#state{module = M}, [Form, FileForm]);
- {ok, _} ->
- ModForm = {attribute, erl_anno:new(1), module, DefModule},
- epp_parse_file2(Epp, S#state{module = DefModule}, [ModForm, FileForm],
- OptModRes);
- {error, _} ->
- epp_parse_file2(Epp, S#state{module = DefModule}, [FileForm],
- OptModRes);
- {eof, LastLine} ->
- S#state{forms_or_bin = [FileForm, {eof, LastLine}]}
- end,
- ok = epp:close(Epp),
- ok = file:close(Fd),
- check_source(S2).
+ {PreDefMacros, DefModule} = pre_def_macros(File),
+ IncludePath = [],
+ %% Read the encoding on the second line, if there is any:
+ {ok, _} = file:position(Fd, 0),
+ _ = io:get_line(Fd, ''),
+ Encoding = epp:set_encoding(Fd),
+ {ok, _} = file:position(Fd, HeaderSz),
+ {ok, Epp} = epp_open(File, Fd, StartLine, IncludePath, PreDefMacros),
+ _ = [io:setopts(Fd, [{encoding, Encoding}]) || Encoding =/= none],
+ {ok, FileForm} = epp:parse_erl_form(Epp),
+ OptModRes = epp:parse_erl_form(Epp),
+ S2 =
+ case OptModRes of
+ {ok, {attribute, _, module, M} = Form} ->
+ epp_parse_file(Epp, S#state{module = M}, [Form, FileForm]);
+ {ok, _} ->
+ ModForm = {attribute, erl_anno:new(1), module, DefModule},
+ epp_parse_file2(
+ Epp,
+ S#state{module = DefModule},
+ [ModForm, FileForm],
+ OptModRes
+ );
+ {error, _} ->
+ epp_parse_file2(
+ Epp,
+ S#state{module = DefModule},
+ [FileForm],
+ OptModRes
+ );
+ {eof, LastLine} ->
+ S#state{forms_or_bin = [FileForm, {eof, LastLine}]}
+ end,
+ ok = epp:close(Epp),
+ ok = file:close(Fd),
+ check_source(S2).
-spec epp_open(_, _, pos_integer(), _, _) -> {ok, term()}.
-if(?OTP_RELEASE < 24).
@@ -196,83 +209,94 @@ epp_open(File, Fd, StartLine, IncludePath, PreDefMacros) ->
-spec epp_open24(_, _, pos_integer(), _, _) -> {ok, term()}.
epp_open24(File, Fd, StartLine, IncludePath, PreDefMacros) ->
%% We use apply in order to fool dialyzer not not analyze this path
- apply(epp, open,
- [[{fd, Fd}, {name, File}, {location, StartLine},
- {includes, IncludePath}, {macros, PreDefMacros}]]).
-
-
+ apply(
+ epp,
+ open,
+ [
+ [
+ {fd, Fd},
+ {name, File},
+ {location, StartLine},
+ {includes, IncludePath},
+ {macros, PreDefMacros}
+ ]
+ ]
+ ).
-spec check_source(state()) -> state().
check_source(S) ->
- case S of
- #state{exports_main = ExpMain,
- forms_or_bin = [FileForm2, ModForm2 | Forms]} ->
- %% Optionally add export of main/1
- Forms2 =
+ #state{
+ exports_main = ExpMain,
+ forms_or_bin = [FileForm2, ModForm2 | Forms]
+ } = S,
+ %% Optionally add export of main/1
+ Forms2 =
case ExpMain of
- false -> [{attribute, erl_anno:new(0), export, [{main, 1}]} | Forms];
- true -> Forms
+ false -> [{attribute, erl_anno:new(0), export, [{main, 1}]} | Forms];
+ true -> Forms
end,
- Forms3 = [FileForm2, ModForm2 | Forms2],
- S#state{forms_or_bin = Forms3}
- end.
+ Forms3 = [FileForm2, ModForm2 | Forms2],
+ S#state{forms_or_bin = Forms3}.
-spec pre_def_macros(_) -> {any(), any()}.
pre_def_macros(File) ->
- {MegaSecs, Secs, MicroSecs} = erlang:timestamp(),
- Unique = erlang:unique_integer([positive]),
- Replace = fun(Char) ->
- case Char of
- $\. -> $\_;
- _ -> Char
- end
- end,
- CleanBase = lists:map(Replace, filename:basename(File)),
- ModuleStr =
- CleanBase ++ "__" ++
- "escript__" ++
- integer_to_list(MegaSecs) ++ "__" ++
- integer_to_list(Secs) ++ "__" ++
- integer_to_list(MicroSecs) ++ "__" ++
- integer_to_list(Unique),
- Module = list_to_atom(ModuleStr),
- PreDefMacros = [{'MODULE', Module, redefine},
- {'MODULE_STRING', ModuleStr, redefine}],
- {PreDefMacros, Module}.
+ {MegaSecs, Secs, MicroSecs} = erlang:timestamp(),
+ Unique = erlang:unique_integer([positive]),
+ Replace = fun(Char) ->
+ case Char of
+ $\. -> $\_;
+ _ -> Char
+ end
+ end,
+ CleanBase = lists:map(Replace, filename:basename(File)),
+ ModuleStr =
+ CleanBase ++ "__" ++
+ "escript__" ++
+ integer_to_list(MegaSecs) ++ "__" ++
+ integer_to_list(Secs) ++ "__" ++
+ integer_to_list(MicroSecs) ++ "__" ++
+ integer_to_list(Unique),
+ Module = list_to_atom(ModuleStr),
+ PreDefMacros = [
+ {'MODULE', Module, redefine},
+ {'MODULE_STRING', ModuleStr, redefine}
+ ],
+ {PreDefMacros, Module}.
-spec epp_parse_file(_, state(), [any()]) -> state().
epp_parse_file(Epp, S, Forms) ->
- Parsed = epp:parse_erl_form(Epp),
- epp_parse_file2(Epp, S, Forms, Parsed).
+ Parsed = epp:parse_erl_form(Epp),
+ epp_parse_file2(Epp, S, Forms, Parsed).
-spec epp_parse_file2(_, state(), [any()], any()) -> state().
epp_parse_file2(Epp, S, Forms, Parsed) ->
- case Parsed of
- {ok, {attribute, Ln, mode, Mode} = Form} ->
- case is_valid(Mode) of
- true ->
- epp_parse_file(Epp, S, [Form | Forms]);
- false ->
- Args = lists:flatten(
- io_lib:format("illegal mode attribute: ~p", [Mode])),
- Error = {error, {Ln, erl_parse, Args}},
- epp_parse_file(Epp, S, [Error | Forms])
- end;
- {ok, {attribute, _, export, Fs} = Form} ->
- case lists:member({main, 1}, Fs) of
- false ->
- epp_parse_file(Epp, S, [Form | Forms]);
- true ->
- epp_parse_file(Epp, S#state{exports_main = true}, [Form | Forms])
- end;
- {ok, Form} ->
- epp_parse_file(Epp, S, [Form | Forms]);
- {error, _} = Form ->
- epp_parse_file(Epp, S, [Form | Forms]);
- {eof, LastLine} ->
- S#state{forms_or_bin = lists:reverse([{eof, LastLine} | Forms])}
- end.
+ case Parsed of
+ {ok, {attribute, Ln, mode, Mode} = Form} ->
+ case is_valid(Mode) of
+ true ->
+ epp_parse_file(Epp, S, [Form | Forms]);
+ false ->
+ Args = lists:flatten(
+ io_lib:format("illegal mode attribute: ~p", [Mode])
+ ),
+ Error = {error, {Ln, erl_parse, Args}},
+ epp_parse_file(Epp, S, [Error | Forms])
+ end;
+ {ok, {attribute, _, export, Fs} = Form} ->
+ case lists:member({main, 1}, Fs) of
+ false ->
+ epp_parse_file(Epp, S, [Form | Forms]);
+ true ->
+ epp_parse_file(Epp, S#state{exports_main = true}, [Form | Forms])
+ end;
+ {ok, Form} ->
+ epp_parse_file(Epp, S, [Form | Forms]);
+ {error, _} = Form ->
+ epp_parse_file(Epp, S, [Form | Forms]);
+ {eof, LastLine} ->
+ S#state{forms_or_bin = lists:reverse([{eof, LastLine} | Forms])}
+ end.
-spec is_valid(atom()) -> boolean().
is_valid(Mode) ->
- lists:member(Mode, [compile, debug, interpret, native]).
+ lists:member(Mode, [compile, debug, interpret, native]).
diff --git a/apps/els_core/src/els_io_string.erl b/apps/els_core/src/els_io_string.erl
index edbbd4b06..7bed8f1a6 100644
--- a/apps/els_core/src/els_io_string.erl
+++ b/apps/els_core/src/els_io_string.erl
@@ -2,15 +2,17 @@
-export([new/1]).
--export([ start_link/1
- , init/1
- , loop/1
- , skip/3
- ]).
-
--type state() :: #{ buffer := string()
- , original := string()
- }.
+-export([
+ start_link/1,
+ init/1,
+ loop/1,
+ skip/3
+]).
+
+-type state() :: #{
+ buffer := string(),
+ original := string()
+}.
%%------------------------------------------------------------------------------
%% API
@@ -18,9 +20,9 @@
-spec new(string() | binary()) -> pid().
new(Str) when is_binary(Str) ->
- new(els_utils:to_list(Str));
+ new(els_utils:to_list(Str));
new(Str) ->
- start_link(Str).
+ start_link(Str).
%%------------------------------------------------------------------------------
%% IO server
@@ -31,109 +33,109 @@ new(Str) ->
-spec start_link(string()) -> pid().
start_link(Str) ->
- spawn_link(?MODULE, init, [Str]).
+ spawn_link(?MODULE, init, [Str]).
-spec init(string()) -> ok.
init(Str) ->
- State = #{buffer => Str, original => Str},
- ?MODULE:loop(State).
+ State = #{buffer => Str, original => Str},
+ ?MODULE:loop(State).
-spec loop(state()) -> ok.
loop(#{buffer := Str} = State) ->
- receive
- {io_request, From, ReplyAs, Request} ->
- {Reply, NewStr} = request(Request, Str),
- reply(From, ReplyAs, Reply),
- ?MODULE:loop(State#{buffer := NewStr});
- {file_request, From, Ref, close} ->
- file_reply(From, Ref, ok);
- {file_request, From, Ref, {position, Pos}} ->
- {Reply, NewState} = file_position(Pos, State),
- file_reply(From, Ref, Reply),
- ?MODULE:loop(NewState);
- _Unknown ->
- ?MODULE:loop(State)
- end.
+ receive
+ {io_request, From, ReplyAs, Request} ->
+ {Reply, NewStr} = request(Request, Str),
+ reply(From, ReplyAs, Reply),
+ ?MODULE:loop(State#{buffer := NewStr});
+ {file_request, From, Ref, close} ->
+ file_reply(From, Ref, ok);
+ {file_request, From, Ref, {position, Pos}} ->
+ {Reply, NewState} = file_position(Pos, State),
+ file_reply(From, Ref, Reply),
+ ?MODULE:loop(NewState);
+ _Unknown ->
+ ?MODULE:loop(State)
+ end.
-spec reply(pid(), pid(), any()) -> any().
reply(From, ReplyAs, Reply) ->
- From ! {io_reply, ReplyAs, Reply}.
+ From ! {io_reply, ReplyAs, Reply}.
-spec file_reply(pid(), pid(), any()) -> any().
file_reply(From, ReplyAs, Reply) ->
- From ! {file_reply, ReplyAs, Reply}.
+ From ! {file_reply, ReplyAs, Reply}.
-spec file_position(integer(), state()) -> {any(), state()}.
file_position(Pos, #{original := Original} = State) ->
- Buffer = lists:nthtail(Pos, Original),
- {{ok, Pos}, State#{buffer => Buffer}}.
+ Buffer = lists:nthtail(Pos, Original),
+ {{ok, Pos}, State#{buffer => Buffer}}.
-spec request(any(), string()) -> {string() | {error, request}, string()}.
request({get_chars, _Encoding, _Prompt, N}, Str) ->
- get_chars(N, Str);
+ get_chars(N, Str);
request({get_line, _Encoding, _Prompt}, Str) ->
- get_line(Str);
-request({get_until, _Encoding, _Prompt, Module, Function, Xargs}, Str) ->
- get_until(Module, Function, Xargs, Str);
+ get_line(Str);
+request({get_until, _Encoding, _Prompt, Module, Function, XArgs}, Str) ->
+ get_until(Module, Function, XArgs, Str);
request(_Other, State) ->
- {{error, request}, State}.
+ {{error, request}, State}.
-spec get_chars(integer(), string()) -> {string() | eof, string()}.
get_chars(_N, []) ->
- {eof, []};
+ {eof, []};
get_chars(1, [Ch | Str]) ->
- {[Ch], Str};
+ {[Ch], Str};
get_chars(N, Str) ->
- do_get_chars(N, Str, []).
+ do_get_chars(N, Str, []).
-spec do_get_chars(integer(), string(), string()) -> {string(), string()}.
do_get_chars(0, Str, Result) ->
- {lists:flatten(Result), Str};
+ {lists:flatten(Result), Str};
do_get_chars(_N, [], Result) ->
- {Result, []};
+ {Result, []};
do_get_chars(N, [Ch | NewStr], Result) ->
- do_get_chars(N - 1, NewStr, [Result, Ch]).
+ do_get_chars(N - 1, NewStr, [Result, Ch]).
-spec get_line(string()) -> {string() | eof, string()}.
get_line([]) ->
- {eof, []};
+ {eof, []};
get_line(Str) ->
- do_get_line(Str, []).
+ do_get_line(Str, []).
-spec do_get_line(string(), string()) -> {string() | eof, string()}.
do_get_line([], Result) ->
- {lists:flatten(Result), []};
+ {lists:flatten(Result), []};
do_get_line("\r\n" ++ RestStr, Result) ->
- {lists:flatten(Result), RestStr};
+ {lists:flatten(Result), RestStr};
do_get_line("\n" ++ RestStr, Result) ->
- {lists:flatten(Result), RestStr};
+ {lists:flatten(Result), RestStr};
do_get_line("\r" ++ RestStr, Result) ->
- {lists:flatten(Result), RestStr};
+ {lists:flatten(Result), RestStr};
do_get_line([Ch | RestStr], Result) ->
- do_get_line(RestStr, [Result, Ch]).
+ do_get_line(RestStr, [Result, Ch]).
-spec get_until(module(), atom(), list(), term()) ->
- {term(), string()}.
+ {term(), string()}.
get_until(Module, Function, XArgs, Str) ->
- apply_get_until(Module, Function, [], Str, XArgs).
+ apply_get_until(Module, Function, [], Str, XArgs).
-spec apply_get_until(module(), atom(), any(), string() | eof, list()) ->
- {term(), string()}.
+ {term(), string()}.
apply_get_until(Module, Function, State, String, XArgs) ->
- case apply(Module, Function, [State, String | XArgs]) of
- {done, Result, NewStr} ->
- {Result, NewStr};
- {more, NewState} ->
- apply_get_until(Module, Function, NewState, eof, XArgs)
- end.
+ case apply(Module, Function, [State, String | XArgs]) of
+ {done, Result, NewStr} ->
+ {Result, NewStr};
+ {more, NewState} ->
+ apply_get_until(Module, Function, NewState, eof, XArgs)
+ end.
-spec skip(string() | {cont, integer(), string()}, term(), integer()) ->
- {more, {cont, integer(), string()}} | {done, integer(), string()}.
+ {more, {cont, integer(), string()}} | {done, integer(), string()}.
skip(Str, _Data, Length) when is_list(Str) ->
- {more, {cont, Length, Str}};
+ {more, {cont, Length, Str}};
skip({cont, 0, Str}, _Data, Length) ->
- {done, Length, Str};
+ {done, Length, Str};
skip({cont, Length, []}, _Data, Length) ->
- {done, eof, []};
+ {done, eof, []};
skip({cont, Length, [_ | RestStr]}, _Data, _Length) ->
- {more, {cont, Length - 1, RestStr}}.
+ {more, {cont, Length - 1, RestStr}}.
diff --git a/apps/els_core/src/els_jsonrpc.erl b/apps/els_core/src/els_jsonrpc.erl
index 2f5d039c8..fa0117f29 100644
--- a/apps/els_core/src/els_jsonrpc.erl
+++ b/apps/els_core/src/els_jsonrpc.erl
@@ -6,20 +6,18 @@
%%==============================================================================
%% Exports
%%==============================================================================
--export([ default_opts/0
- , split/1
- , split/2
- ]).
+-export([
+ split/1,
+ split/2
+]).
%%==============================================================================
%% Includes
%%==============================================================================
--include("els_core.hrl").
-
%%==============================================================================
%% Types
%%==============================================================================
--type more() :: {more, undefined | non_neg_integer()}.
+-type more() :: {more, undefined | non_neg_integer()}.
-type header() :: {atom() | binary(), binary()}.
%%==============================================================================
@@ -27,55 +25,51 @@
%%==============================================================================
-spec split(binary()) -> {[map()], binary()}.
split(Data) ->
- split(Data, default_opts()).
+ split(Data, fun els_utils:json_decode_with_atom_keys/1).
--spec split(binary(), [any()]) -> {[map()], binary()}.
-split(Data, DecodeOpts) ->
- split(Data, DecodeOpts, []).
+-spec split(binary(), fun()) -> {[map()], binary()}.
+split(Data, Decoder) ->
+ split(Data, Decoder, []).
--spec split(binary(), [any()], [map()]) -> {[map()], binary()}.
-split(Data, DecodeOpts, Responses) ->
- case peel_content(Data) of
- {ok, Body, Rest} ->
- Response = jsx:decode(Body, DecodeOpts),
- split(Rest, DecodeOpts, [Response|Responses]);
- {more, _Length} ->
- {lists:reverse(Responses), Data}
- end.
+-spec split(binary(), fun(), [map()]) -> {[map()], binary()}.
+split(Data, Decoder, Responses) ->
+ case peel_content(Data) of
+ {ok, Body, Rest} ->
+ Response = Decoder(Body),
+ split(Rest, Decoder, [Response | Responses]);
+ {more, _Length} ->
+ {lists:reverse(Responses), Data}
+ end.
-spec peel_content(binary()) -> {ok, binary(), binary()} | more().
peel_content(Data) ->
- case peel_headers(Data) of
- {ok, Headers, Data1} ->
- BinLength = proplists:get_value('Content-Length', Headers),
- Length = binary_to_integer(BinLength),
- case Data1 of
- <> ->
- {ok, Body, Rest};
- Data1 ->
- {more, Length - byte_size(Data1)}
- end;
- {more, Length} ->
- {more, Length}
- end.
+ case peel_headers(Data) of
+ {ok, Headers, Data1} ->
+ BinLength = proplists:get_value('Content-Length', Headers),
+ Length = binary_to_integer(BinLength),
+ case Data1 of
+ <> ->
+ {ok, Body, Rest};
+ Data1 ->
+ {more, Length - byte_size(Data1)}
+ end;
+ {more, Length} ->
+ {more, Length}
+ end.
-spec peel_headers(binary()) -> {ok, [header()], binary()} | more().
peel_headers(Data) ->
- peel_headers(Data, []).
+ peel_headers(Data, []).
-spec peel_headers(binary(), [header()]) -> {ok, [header()], binary()} | more().
peel_headers(Data, Headers) ->
- case erlang:decode_packet(httph_bin, Data, []) of
- {ok, http_eoh, Rest} ->
- {ok, lists:reverse(Headers), Rest};
- {ok, {http_header, _Bit, Field, _UnmodifiedField, Value}, Rest} ->
- peel_headers(Rest, [{Field, Value}|Headers]);
- {more, Length} ->
- {more, Length};
- {error, Reason} ->
- erlang:error(Reason, [Data, Headers])
- end.
-
--spec default_opts() -> [any()].
-default_opts() ->
- [return_maps, {labels, atom}].
+ case erlang:decode_packet(httph_bin, Data, []) of
+ {ok, http_eoh, Rest} ->
+ {ok, lists:reverse(Headers), Rest};
+ {ok, {http_header, _Bit, Field, _UnmodifiedField, Value}, Rest} ->
+ peel_headers(Rest, [{Field, Value} | Headers]);
+ {more, Length} ->
+ {more, Length};
+ {error, Reason} ->
+ erlang:error(Reason, [Data, Headers])
+ end.
diff --git a/apps/els_core/src/els_poi.erl b/apps/els_core/src/els_poi.erl
new file mode 100644
index 000000000..a9661a50d
--- /dev/null
+++ b/apps/els_core/src/els_poi.erl
@@ -0,0 +1,169 @@
+%%==============================================================================
+%% The Point Of Interest (a.k.a. _poi_) Data Structure
+%%==============================================================================
+-module(els_poi).
+
+%% Constructor
+-export([
+ new/3,
+ new/4
+]).
+
+-export([
+ match_pos/2,
+ sort/1,
+ label/1,
+ symbol_kind/1,
+ to_symbol/2,
+ folding_range/1,
+ symbol_range/1
+]).
+
+%%==============================================================================
+%% Includes
+%%==============================================================================
+-include("els_core.hrl").
+
+%%==============================================================================
+%% Type Definitions
+%%==============================================================================
+-type poi_kind() ::
+ application
+ | atom
+ | behaviour
+ | callback
+ | define
+ | export
+ | export_entry
+ | export_type
+ | export_type_entry
+ | function
+ | function_clause
+ | implicit_fun
+ | import_entry
+ | include
+ | include_lib
+ | keyword_expr
+ | list_comp
+ | macro
+ | module
+ | nifs
+ | nifs_entry
+ | parse_transform
+ | record
+ | record_def_field
+ | record_expr
+ | record_field
+ | spec
+ | type_application
+ | type_definition
+ | variable.
+-type poi_range() :: #{from := pos(), to := pos()}.
+-type poi_id() ::
+ atom()
+ %% record_def_field, record_field
+ | {atom(), atom()}
+ %% include, include_lib
+ | string()
+ | {atom(), arity()}
+ | {module(), atom(), arity()}.
+-type poi_data() :: any().
+-type poi() :: #{
+ kind := poi_kind(),
+ id := poi_id(),
+ data := poi_data(),
+ range := poi_range()
+}.
+-export_type([poi/0, poi_range/0, poi_id/0, poi_kind/0]).
+
+%%==============================================================================
+%% Behaviour Definition
+%%==============================================================================
+-callback label(poi()) -> binary().
+-callback symbol_kind() -> symbol_kind().
+
+%%==============================================================================
+%% API
+%%==============================================================================
+
+%% @doc Constructor for a Point of Interest.
+-spec new(poi_range(), poi_kind(), any()) -> poi().
+new(Range, Kind, Id) ->
+ new(Range, Kind, Id, undefined).
+
+%% @doc Constructor for a Point of Interest.
+-spec new(poi_range(), poi_kind(), any(), any()) -> poi().
+new(Range, Kind, Id, Data) ->
+ #{
+ kind => Kind,
+ id => Id,
+ data => Data,
+ range => Range
+ }.
+
+-spec match_pos([poi()], pos()) -> [poi()].
+match_pos(POIs, Pos) ->
+ [
+ POI
+ || #{
+ range := #{
+ from := From,
+ to := To
+ }
+ } = POI <- POIs,
+ (From =< Pos) andalso (Pos =< To)
+ ].
+
+%% @doc Sorts pois based on their range
+%%
+%% Order is defined using els_range:compare/2.
+-spec sort([poi()]) -> [poi()].
+sort(POIs) ->
+ lists:sort(fun compare/2, POIs).
+
+-spec label(els_poi:poi()) -> binary().
+label(#{kind := Kind} = POI) ->
+ (callback_module(Kind)):label(POI).
+
+-spec symbol_kind(els_poi:poi()) -> symbol_kind().
+symbol_kind(#{kind := Kind}) ->
+ (callback_module(Kind)):symbol_kind().
+
+-spec to_symbol(uri(), els_poi:poi()) -> symbol_information().
+to_symbol(Uri, POI) ->
+ #{
+ name => label(POI),
+ kind => symbol_kind(POI),
+ location => #{
+ uri => Uri,
+ range => els_protocol:range(symbol_range(POI))
+ }
+ }.
+
+-spec folding_range(els_poi:poi()) -> poi_range().
+folding_range(#{data := #{folding_range := Range}}) ->
+ Range.
+
+-spec symbol_range(els_poi:poi()) -> poi_range().
+symbol_range(#{data := #{symbol_range := SymbolRange}}) ->
+ SymbolRange;
+symbol_range(#{range := Range}) ->
+ Range.
+
+%%==============================================================================
+%% Internal Functions
+%%==============================================================================
+
+-spec compare(poi(), poi()) -> boolean().
+compare(#{range := A}, #{range := B}) ->
+ els_range:compare(A, B).
+
+-spec callback_module(poi_kind()) -> atom().
+callback_module(define) ->
+ els_poi_define;
+callback_module(function) ->
+ els_poi_function;
+callback_module(record) ->
+ els_poi_record;
+callback_module(type_definition) ->
+ els_poi_type_definition.
diff --git a/apps/els_core/src/els_protocol.erl b/apps/els_core/src/els_protocol.erl
index e99c1ced0..5dafe568f 100644
--- a/apps/els_core/src/els_protocol.erl
+++ b/apps/els_core/src/els_protocol.erl
@@ -7,16 +7,16 @@
%% Exports
%%==============================================================================
%% Messaging API
--export([ notification/2
- , request/2
- , request/3
- , response/2
- , error/2
- ]).
+-export([
+ notification/2,
+ request/2,
+ request/3,
+ response/2,
+ error/2
+]).
%% Data Structures
--export([ range/1
- ]).
+-export([range/1]).
%%==============================================================================
%% Includes
@@ -29,63 +29,70 @@
%%==============================================================================
-spec notification(binary(), any()) -> binary().
notification(Method, Params) ->
- Message = #{ jsonrpc => ?JSONRPC_VSN
- , method => Method
- , params => Params
- },
- content(jsx:encode(Message)).
+ Message = #{
+ jsonrpc => ?JSONRPC_VSN,
+ method => Method,
+ params => Params
+ },
+ content(Message).
-spec request(number(), binary()) -> binary().
request(RequestId, Method) ->
- Message = #{ jsonrpc => ?JSONRPC_VSN
- , method => Method
- , id => RequestId
- },
- content(jsx:encode(Message)).
+ Message = #{
+ jsonrpc => ?JSONRPC_VSN,
+ method => Method,
+ id => RequestId
+ },
+ content(Message).
-spec request(number(), binary(), any()) -> binary().
request(RequestId, Method, Params) ->
- Message = #{ jsonrpc => ?JSONRPC_VSN
- , method => Method
- , id => RequestId
- , params => Params
- },
- content(jsx:encode(Message)).
+ Message = #{
+ jsonrpc => ?JSONRPC_VSN,
+ method => Method,
+ id => RequestId,
+ params => Params
+ },
+ content(Message).
-spec response(number(), any()) -> binary().
response(RequestId, Result) ->
- Message = #{ jsonrpc => ?JSONRPC_VSN
- , id => RequestId
- , result => Result
- },
- ?LOG_DEBUG("[Response] [message=~p]", [Message]),
- content(jsx:encode(Message)).
+ Message = #{
+ jsonrpc => ?JSONRPC_VSN,
+ id => RequestId,
+ result => Result
+ },
+ ?LOG_DEBUG("[Response] [message=~p]", [Message]),
+ content(Message).
-spec error(number(), any()) -> binary().
error(RequestId, Error) ->
- Message = #{ jsonrpc => ?JSONRPC_VSN
- , id => RequestId
- , error => Error
- },
- ?LOG_DEBUG("[Response] [message=~p]", [Message]),
- content(jsx:encode(Message)).
+ Message = #{
+ jsonrpc => ?JSONRPC_VSN,
+ id => RequestId,
+ error => Error
+ },
+ ?LOG_DEBUG("[Response] [message=~p]", [Message]),
+ content(Message).
%%==============================================================================
%% Data Structures
%%==============================================================================
--spec range(poi_range()) -> range().
-range(#{ from := {FromL, FromC}, to := {ToL, ToC} }) ->
- #{ start => #{line => FromL - 1, character => FromC - 1}
- , 'end' => #{line => ToL - 1, character => ToC - 1}
- }.
+-spec range(els_poi:poi_range()) -> range().
+range(#{from := {FromL, FromC}, to := {ToL, ToC}}) ->
+ #{
+ start => #{line => FromL - 1, character => FromC - 1},
+ 'end' => #{line => ToL - 1, character => ToC - 1}
+ }.
%%==============================================================================
%% Internal Functions
%%==============================================================================
--spec content(binary()) -> binary().
-content(Body) ->
-els_utils:to_binary([headers(Body), "\r\n", Body]).
+-spec content(map()) -> binary().
+content(Message) ->
+ Body = list_to_binary(json:encode(Message)),
+ els_utils:to_binary([headers(Body), "\r\n", Body]).
-spec headers(binary()) -> iolist().
headers(Body) ->
- io_lib:format("Content-Length: ~p\r\n", [byte_size(Body)]).
+ io_lib:format("Content-Length: ~p\r\n", [byte_size(Body)]).
diff --git a/apps/els_core/src/els_provider.erl b/apps/els_core/src/els_provider.erl
index 328a069dc..a95050e1e 100644
--- a/apps/els_core/src/els_provider.erl
+++ b/apps/els_core/src/els_provider.erl
@@ -1,143 +1,34 @@
-module(els_provider).
%% API
--export([ handle_request/2
- , start_link/1
- , available_providers/0
- , enabled_providers/0
- , cancel_request/2
- ]).
-
--behaviour(gen_server).
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , handle_info/2
- ]).
+-export([
+ handle_request/2
+]).
%%==============================================================================
%% Includes
%%==============================================================================
--include_lib("kernel/include/logger.hrl").
+-include("els_core.hrl").
--callback is_enabled() -> boolean().
--callback init() -> any().
--callback handle_request(request(), any()) -> {any(), any()}.
--callback handle_info(any(), any()) -> any().
--callback cancel_request(pid(), any()) -> any().
--optional_callbacks([init/0, handle_info/2, cancel_request/2]).
+-callback handle_request(provider_request()) -> provider_result().
--type config() :: any().
--type provider() :: els_completion_provider
- | els_definition_provider
- | els_document_symbol_provider
- | els_hover_provider
- | els_references_provider
- | els_formatting_provider
- | els_document_highlight_provider
- | els_workspace_symbol_provider
- | els_folding_range_provider
- | els_implementation_provider
- | els_code_action_provider
- | els_general_provider
- | els_code_lens_provider
- | els_execute_command_provider
- | els_rename_provider
- | els_bsp_provider.
--type request() :: {atom() | binary(), map()}.
--type state() :: #{ provider := provider()
- , internal_state := any()
- }.
+-type provider() :: module().
+-type provider_request() :: {atom(), map()}.
+-type provider_result() ::
+ {async, uri(), pid()}
+ | {response, any()}
+ | {diagnostics, uri(), [pid()]}
+ | noresponse.
--export_type([ config/0
- , provider/0
- , request/0
- , state/0
- ]).
+-export_type([
+ provider/0,
+ provider_request/0,
+ provider_result/0
+]).
%%==============================================================================
-%% External functions
+%% API
%%==============================================================================
-
--spec start_link(provider()) -> {ok, pid()}.
-start_link(Provider) ->
- gen_server:start_link({local, Provider}, ?MODULE, Provider, []).
-
--spec handle_request(provider(), request()) -> any().
+-spec handle_request(provider(), provider_request()) -> provider_result().
handle_request(Provider, Request) ->
- gen_server:call(Provider, {handle_request, Request}, infinity).
-
--spec cancel_request(provider(), pid()) -> any().
-cancel_request(Provider, Job) ->
- gen_server:cast(Provider, {cancel_request, Job}).
-
-%%==============================================================================
-%% gen_server callbacks
-%%==============================================================================
-
--spec init(els_provider:provider()) -> {ok, state()}.
-init(Provider) ->
- ?LOG_INFO("Starting provider ~p", [Provider]),
- InternalState = case erlang:function_exported(Provider, init, 0) of
- true ->
- Provider:init();
- false ->
- #{}
- end,
- {ok, #{provider => Provider, internal_state => InternalState}}.
-
--spec handle_call(any(), {pid(), any()}, state()) ->
- {reply, any(), state()}.
-handle_call({handle_request, Request}, _From, State) ->
- #{internal_state := InternalState, provider := Provider} = State,
- {Reply, NewInternalState} = Provider:handle_request(Request, InternalState),
- {reply, Reply, State#{internal_state => NewInternalState}}.
-
--spec handle_cast(any(), state()) -> {noreply, state()}.
-handle_cast({cancel_request, Job}, State) ->
- #{internal_state := InternalState, provider := Provider} = State,
- case erlang:function_exported(Provider, cancel_request, 2) of
- true ->
- NewInternalState = Provider:cancel_request(Job, InternalState),
- {noreply, State#{internal_state => NewInternalState}};
- false ->
- {noreply, State}
- end.
-
--spec handle_info(any(), state()) ->
- {noreply, state()}.
-handle_info(Request, State) ->
- #{provider := Provider, internal_state := InternalState} = State,
- case erlang:function_exported(Provider, handle_info, 2) of
- true ->
- NewInternalState = Provider:handle_info(Request, InternalState),
- {noreply, State#{internal_state => NewInternalState}};
- false ->
- {noreply, State}
- end.
-
--spec available_providers() -> [provider()].
-available_providers() ->
- [ els_completion_provider
- , els_definition_provider
- , els_document_symbol_provider
- , els_hover_provider
- , els_references_provider
- , els_formatting_provider
- , els_document_highlight_provider
- , els_workspace_symbol_provider
- , els_folding_range_provider
- , els_implementation_provider
- , els_code_action_provider
- , els_general_provider
- , els_code_lens_provider
- , els_execute_command_provider
- , els_diagnostics_provider
- , els_rename_provider
- , els_bsp_provider
- , els_call_hierarchy_provider
- ].
-
--spec enabled_providers() -> [provider()].
-enabled_providers() ->
- [Provider || Provider <- available_providers(), Provider:is_enabled()].
+ Provider:handle_request(Request).
diff --git a/apps/els_core/src/els_stdio.erl b/apps/els_core/src/els_stdio.erl
index 342b1b748..fa8264305 100644
--- a/apps/els_core/src/els_stdio.erl
+++ b/apps/els_core/src/els_stdio.erl
@@ -1,11 +1,12 @@
-module(els_stdio).
--export([ start_listener/1
- , init/1
- , send/2
- ]).
+-export([
+ start_listener/1,
+ init/1,
+ send/2
+]).
--export([ loop/4 ]).
+-export([loop/4]).
%%==============================================================================
%% Includes
@@ -17,51 +18,53 @@
%%==============================================================================
-spec start_listener(function()) -> {ok, pid()}.
start_listener(Cb) ->
- {ok, IoDevice} = application:get_env(els_core, io_device),
- {ok, proc_lib:spawn_link(?MODULE, init, [{Cb, IoDevice}])}.
+ IoDevice = application:get_env(els_core, io_device, standard_io),
+ {ok, proc_lib:spawn_link(?MODULE, init, [{Cb, IoDevice}])}.
-spec init({function(), atom() | pid()}) -> no_return().
init({Cb, IoDevice}) ->
- ?LOG_INFO("Starting stdio server..."),
- ok = io:setopts(IoDevice, [binary, {encoding, latin1}]),
- {ok, Server} = application:get_env(els_core, server),
- ok = Server:set_io_device(IoDevice),
- ?MODULE:loop([], IoDevice, Cb, [return_maps]).
+ ?LOG_INFO("Starting stdio server... [io_device=~p]", [IoDevice]),
+ ok = io:setopts(IoDevice, [binary, {encoding, latin1}]),
+ {ok, Server} = application:get_env(els_core, server),
+ ok = Server:set_io_device(IoDevice),
+ ?MODULE:loop([], IoDevice, Cb, fun json:decode/1).
-spec send(atom() | pid(), binary()) -> ok.
send(IoDevice, Payload) ->
- io:format(IoDevice, "~s", [Payload]).
+ io:format(IoDevice, "~s", [Payload]).
%%==============================================================================
%% Listener loop function
%%==============================================================================
--spec loop([binary()], any(), function(), [any()]) -> no_return().
-loop(Lines, IoDevice, Cb, JsonOpts) ->
- case io:get_line(IoDevice, "") of
- <<"\n">> ->
- Headers = parse_headers(Lines),
- BinLength = proplists:get_value(<<"content-length">>, Headers),
- Length = binary_to_integer(BinLength),
- %% Use file:read/2 since it reads bytes
- {ok, Payload} = file:read(IoDevice, Length),
- Request = jsx:decode(Payload, JsonOpts),
- Cb([Request]),
- ?MODULE:loop([], IoDevice, Cb, JsonOpts);
- eof ->
- Cb([#{
- <<"method">> => <<"exit">>,
- <<"params">> => []
- }]);
- Line ->
- ?MODULE:loop([Line | Lines], IoDevice, Cb, JsonOpts)
- end.
+-spec loop([binary()], any(), function(), fun()) -> no_return().
+loop(Lines, IoDevice, Cb, JsonDecoder) ->
+ case io:get_line(IoDevice, "") of
+ <<"\n">> ->
+ Headers = parse_headers(Lines),
+ BinLength = proplists:get_value(<<"content-length">>, Headers),
+ Length = binary_to_integer(BinLength),
+ %% Use file:read/2 since it reads bytes
+ {ok, Payload} = file:read(IoDevice, Length),
+ Request = JsonDecoder(Payload),
+ Cb([Request]),
+ ?MODULE:loop([], IoDevice, Cb, JsonDecoder);
+ eof ->
+ Cb([
+ #{
+ <<"method">> => <<"exit">>,
+ <<"params">> => []
+ }
+ ]);
+ Line ->
+ ?MODULE:loop([Line | Lines], IoDevice, Cb, JsonDecoder)
+ end.
-spec parse_headers([binary()]) -> [{binary(), binary()}].
parse_headers(Lines) ->
- [parse_header(Line) || Line <- Lines].
+ [parse_header(Line) || Line <- Lines].
-spec parse_header(binary()) -> {binary(), binary()}.
parse_header(Line) ->
- [Name, Value] = binary:split(Line, <<":">>),
- {string:trim(string:lowercase(Name)), string:trim(Value)}.
+ [Name, Value] = binary:split(Line, <<":">>),
+ {string:trim(string:lowercase(Name)), string:trim(Value)}.
diff --git a/apps/els_core/src/els_text.erl b/apps/els_core/src/els_text.erl
index 61585b52a..76589731b 100644
--- a/apps/els_core/src/els_text.erl
+++ b/apps/els_core/src/els_text.erl
@@ -3,117 +3,191 @@
%%==============================================================================
-module(els_text).
--export([ last_token/1
- , line/2
- , line/3
- , range/3
- , tokens/1
- , apply_edits/2
- ]).
+-export([
+ last_token/1,
+ line/2,
+ line/3,
+ get_char/3,
+ range/3,
+ split_at_line/2,
+ tokens/1,
+ tokens/2,
+ apply_edits/2,
+ is_keyword_expr/1
+]).
+-export([strip_comments/1]).
-export_type([edit/0]).
--include("els_core.hrl").
-
--type edit() :: {poi_range(), string()}.
--type lines() :: [string() | binary()].
--type text() :: binary().
--type line_num() :: non_neg_integer().
+-type edit() :: {els_poi:poi_range(), string()}.
+-type lines() :: [string() | binary()].
+-type text() :: binary().
+-type line_num() :: non_neg_integer().
-type column_num() :: pos_integer().
--type token() :: erl_scan:token().
+-type token() :: erl_scan:token().
%% @doc Extract the N-th line from a text.
-spec line(text(), line_num()) -> text().
line(Text, LineNum) ->
- Lines = binary:split(Text, <<"\n">>, [global]),
- lists:nth(LineNum + 1, Lines).
+ Lines = binary:split(Text, [<<"\r\n">>, <<"\n">>], [global]),
+ lists:nth(LineNum + 1, Lines).
%% @doc Extract the N-th line from a text, up to the given column number.
-spec line(text(), line_num(), column_num()) -> text().
line(Text, LineNum, ColumnNum) ->
- Line = line(Text, LineNum),
- binary:part(Line, {0, ColumnNum}).
+ Line = line(Text, LineNum),
+ binary:part(Line, {0, ColumnNum}).
+
+-spec get_char(text(), line_num(), column_num()) ->
+ {ok, char()} | {error, out_of_range}.
+get_char(Text, Line, Column) ->
+ LineStarts = line_starts(Text),
+ Pos = pos(LineStarts, {Line, Column}),
+ case Pos < size(Text) of
+ true ->
+ {ok, binary:at(Text, Pos)};
+ false ->
+ {error, out_of_range}
+ end.
%% @doc Extract a snippet from a text, from [StartLoc..EndLoc).
-spec range(text(), {line_num(), column_num()}, {line_num(), column_num()}) ->
- text().
+ text().
range(Text, StartLoc, EndLoc) ->
- LineStarts = line_starts(Text),
- StartPos = pos(LineStarts, StartLoc),
- EndPos = pos(LineStarts, EndLoc),
- binary:part(Text, StartPos, EndPos - StartPos).
+ LineStarts = line_starts(Text),
+ StartPos = pos(LineStarts, StartLoc),
+ EndPos = pos(LineStarts, EndLoc),
+ binary:part(Text, StartPos, EndPos - StartPos).
+
+-spec split_at_line(text(), line_num()) -> {text(), text()}.
+split_at_line(Text, Line) ->
+ StartPos = pos(line_starts(Text), {Line + 1, 1}),
+ <> = Text,
+ {Left, Right}.
%% @doc Return tokens from text.
-spec tokens(text()) -> [any()].
tokens(Text) ->
- case erl_scan:string(els_utils:to_list(Text)) of
- {ok, Tokens, _} -> Tokens;
- {error, _, _} -> []
- end.
+ case erl_scan:string(els_utils:to_list(Text)) of
+ {ok, Tokens, _} -> Tokens;
+ {error, _, _} -> []
+ end.
+
+-spec tokens(text(), {integer(), integer()}) -> [any()].
+tokens(Text, Pos) ->
+ case erl_scan:string(els_utils:to_list(Text), Pos) of
+ {ok, Tokens, _} ->
+ [unpack_anno(T) || T <- Tokens];
+ {error, _, _} ->
+ []
+ end.
+
+-spec unpack_anno(erl_scan:token()) ->
+ {Category :: atom(), Pos :: {integer(), integer()}, Symbol :: any()}
+ | {Category :: atom(), Pos :: {integer(), integer()}}.
+unpack_anno({Category, Anno, Symbol}) ->
+ Line = erl_anno:line(Anno),
+ Column = erl_anno:column(Anno),
+ {Category, {Line, Column}, Symbol};
+unpack_anno({Category, Anno}) ->
+ Line = erl_anno:line(Anno),
+ Column = erl_anno:column(Anno),
+ {Category, {Line, Column}}.
%% @doc Extract the last token from the given text.
-spec last_token(text()) -> token() | {error, empty}.
last_token(Text) ->
- case tokens(Text) of
- [] -> {error, empty};
- Tokens -> lists:last(Tokens)
- end.
+ case tokens(Text) of
+ [] -> {error, empty};
+ Tokens -> lists:last(Tokens)
+ end.
-spec apply_edits(text(), [edit()]) -> text().
apply_edits(Text, []) ->
- Text;
+ Text;
apply_edits(Text, Edits) when is_binary(Text) ->
- Lines = lists:foldl(fun(Edit, Acc) ->
- apply_edit(Acc, 0, Edit)
- end, bin_to_lines(Text), Edits),
- lines_to_bin(Lines).
+ lists:foldl(
+ fun(Edit, Acc) ->
+ lines_to_bin(apply_edit(bin_to_lines(Acc), 0, Edit))
+ end,
+ Text,
+ Edits
+ ).
-spec apply_edit(lines(), line_num(), edit()) -> lines().
apply_edit([], L, {#{from := {FromL, _}}, _} = Edit) when L < FromL ->
- %% End of lines
- %% Add empty line
- [[] | apply_edit([], L + 1, Edit)];
+ %% End of lines
+ %% Add empty line
+ [[] | apply_edit([], L + 1, Edit)];
apply_edit([], L, {#{from := {L, FromC}}, Insert}) ->
- %% End of lines
- Padding = lists:duplicate(FromC, $ ),
- string:split(Padding ++ Insert, "\n");
-apply_edit([CurrLine|RestLines], L, {#{from := {FromL, _}}, _} = Edit)
- when L < FromL ->
- %% Go to next line
- [CurrLine|apply_edit(RestLines, L + 1, Edit)];
-apply_edit([CurrLine0|RestLines], L,
- {#{from := {L, FromC}, to := {L, ToC}}, Insert}) ->
- CurrLine = ensure_string(CurrLine0),
- %% One line edit
- {Prefix, Rest} = lists:split(FromC, CurrLine),
- {_, Suffix} = lists:split(ToC - FromC, Rest),
- string:split(Prefix ++ Insert ++ Suffix, "\n") ++ RestLines;
-apply_edit([CurrLine0|RestLines], L,
- {#{from := {L, FromC}, to := {ToL, ToC}}, Insert}) ->
- %% Multiline edit
- CurrLine = ensure_string(CurrLine0),
- {Prefix, _} = lists:split(FromC, CurrLine),
- case lists:split(ToL - L - 1, RestLines) of
- {_, []} ->
- string:split(Prefix ++ Insert, "\n") ++ RestLines;
- {_, [CurrSuffix|SuffixLines]} ->
- {_, Suffix} = lists:split(ToC, ensure_string(CurrSuffix)),
- string:split(Prefix ++ Insert ++ Suffix, "\n") ++ SuffixLines
- end.
+ %% End of lines
+ Padding = lists:duplicate(FromC, $\s),
+ string:split(Padding ++ Insert, "\n");
+apply_edit([CurrLine | RestLines], L, {#{from := {FromL, _}}, _} = Edit) when
+ L < FromL
+->
+ %% Go to next line
+ [CurrLine | apply_edit(RestLines, L + 1, Edit)];
+apply_edit(
+ [CurrLine0 | RestLines],
+ L,
+ {#{from := {L, FromC}, to := {L, ToC}}, Insert}
+) ->
+ CurrLine = ensure_string(CurrLine0),
+ %% One line edit
+ {Prefix, Rest} = lists:split(FromC, CurrLine),
+ {_, Suffix} = lists:split(ToC - FromC, Rest),
+ string:split(Prefix ++ Insert ++ Suffix, "\n") ++ RestLines;
+apply_edit(
+ [CurrLine0 | RestLines],
+ L,
+ {#{from := {L, FromC}, to := {ToL, ToC}}, Insert}
+) ->
+ %% Multiline edit
+ CurrLine = ensure_string(CurrLine0),
+ {Prefix, _} = lists:split(FromC, CurrLine),
+ case lists:split(ToL - L - 1, RestLines) of
+ {_, []} ->
+ string:split(Prefix ++ Insert, "\n") ++ RestLines;
+ {_, [CurrSuffix | SuffixLines]} ->
+ {_, Suffix} = lists:split(ToC, ensure_string(CurrSuffix)),
+ string:split(Prefix ++ Insert ++ Suffix, "\n") ++ SuffixLines
+ end.
-spec lines_to_bin(lines()) -> text().
lines_to_bin(Lines) ->
- els_utils:to_binary(lists:join("\n", Lines)).
+ els_utils:to_binary(lists:join("\n", Lines)).
-spec bin_to_lines(text()) -> lines().
bin_to_lines(Text) ->
- [Bin || Bin <- binary:split(Text, <<"\n">>, [global])].
+ [Bin || Bin <- binary:split(Text, [<<"\r\n">>, <<"\n">>], [global])].
-spec ensure_string(binary() | string()) -> string().
ensure_string(Text) when is_binary(Text) ->
- els_utils:to_list(Text);
-ensure_string(Text) ->
- Text.
+ els_utils:to_list(Text).
+
+-spec strip_comments(binary()) -> binary().
+strip_comments(Text) ->
+ lines_to_bin(
+ lists:map(
+ fun(Line) ->
+ hd(string:split(Line, "%"))
+ end,
+ bin_to_lines(Text)
+ )
+ ).
+
+-spec is_keyword_expr(binary()) -> boolean().
+is_keyword_expr(Text) ->
+ lists:member(Text, [
+ <<"begin">>,
+ <<"case">>,
+ <<"fun">>,
+ <<"if">>,
+ <<"maybe">>,
+ <<"receive">>,
+ <<"try">>
+ ]).
%%==============================================================================
%% Internal functions
@@ -121,10 +195,10 @@ ensure_string(Text) ->
-spec line_starts(text()) -> [{integer(), any()}].
line_starts(Text) ->
- [{-1, 1} | binary:matches(Text, <<"\n">>)].
+ [{-1, 1} | binary:matches(Text, <<"\n">>)].
-spec pos([{integer(), any()}], {line_num(), column_num()}) ->
- pos_integer().
+ pos_integer().
pos(LineStarts, {LineNum, ColumnNum}) ->
- {LinePos, _} = lists:nth(LineNum, LineStarts),
- LinePos + ColumnNum.
+ {LinePos, _} = lists:nth(LineNum, LineStarts),
+ LinePos + ColumnNum.
diff --git a/apps/els_core/src/els_uri.erl b/apps/els_core/src/els_uri.erl
index 8f6fbd74a..14966829d 100644
--- a/apps/els_core/src/els_uri.erl
+++ b/apps/els_core/src/els_uri.erl
@@ -8,76 +8,136 @@
%%==============================================================================
%% Exports
%%==============================================================================
--export([ module/1
- , path/1
- , uri/1
- ]).
+-export([
+ module/1,
+ path/1,
+ uri/1,
+ app/1
+]).
%%==============================================================================
%% Types
%%==============================================================================
-type path() :: binary().
--export_type([ path/0 ]).
+-export_type([path/0]).
%%==============================================================================
%% Includes
%%==============================================================================
-include("els_core.hrl").
+-spec app(uri() | [binary()]) -> {ok, atom()} | error.
+app(Uri) when is_binary(Uri) ->
+ app(lists:reverse(filename:split(path(Uri))));
+app([]) ->
+ error;
+app([_File, <<"src">>, AppBin0 | _]) ->
+ case binary:split(AppBin0, <<"-">>) of
+ [AppBin, _Vsn] ->
+ {ok, binary_to_atom(AppBin)};
+ [AppBin] ->
+ {ok, binary_to_atom(AppBin)}
+ end;
+app([_ | Rest]) ->
+ app(Rest).
+
-spec module(uri()) -> atom().
module(Uri) ->
- binary_to_atom(filename:basename(path(Uri), <<".erl">>), utf8).
+ binary_to_atom(filename:basename(path(Uri), <<".erl">>), utf8).
-spec path(uri()) -> path().
path(Uri) ->
- #{ host := Host
- , path := Path
- , scheme := <<"file">>
- } = uri_string:normalize(Uri, [return_map]),
+ path(Uri, els_utils:is_windows()).
- case {is_windows(), Host} of
- {true, <<>>} ->
- % Windows drive letter, have to strip the initial slash
- re:replace(
- Path, "^/([a-zA-Z])(:|%3A)(.*)", "\\1:\\3", [{return, binary}]
- );
- {true, _} ->
- <<"//", Host/binary, Path/binary>>;
- {false, <<>>} ->
- Path;
- {false, _} ->
- error(badarg)
- end.
+-spec path(uri(), boolean()) -> path().
+path(Uri, IsWindows) ->
+ #{
+ host := Host,
+ path := Path0,
+ scheme := <<"file">>
+ } = uri_string:normalize(Uri, [return_map]),
+ Path = uri_string:percent_decode(Path0),
+ case {IsWindows, Host} of
+ {true, <<>>} ->
+ % Windows drive letter, have to strip the initial slash
+ Path1 = re:replace(
+ Path, "^/([a-zA-Z]:)(.*)", "\\1\\2", [{return, binary}]
+ ),
+ lowercase_drive_letter(Path1);
+ {true, _} ->
+ <<"//", Host/binary, Path/binary>>;
+ {false, <<>>} ->
+ Path;
+ {false, _} ->
+ error(badarg)
+ end.
-spec uri(path()) -> uri().
uri(Path) ->
- [Head | Tail] = filename:split(Path),
- {Host, Path1} = case {is_windows(), Head} of
- {false, <<"/">>} ->
- {<<>>, uri_join(Tail)};
- {true, X} when X =:= <<"//">> orelse X =:= <<"\\\\">> ->
- [H | T] = Tail,
- {H, uri_join(T)};
- {true, _} ->
- % Strip the trailing slash from the first component
- H1 = string:slice(Head, 0, 2),
- {<<>>, uri_join([H1|Tail])}
- end,
+ [Head | Tail] = filename:split(Path),
+ {Host, Path1} =
+ case {els_utils:is_windows(), Head} of
+ {false, <<"/">>} ->
+ {<<>>, uri_join(Tail)};
+ {true, X} when X =:= <<"//">> orelse X =:= <<"\\\\">> ->
+ [H | T] = Tail,
+ {H, uri_join(T)};
+ {true, _} ->
+ % Strip the trailing slash from the first component
+ H1 = string:slice(Head, 0, 2),
+ {<<>>, uri_join([H1 | Tail])}
+ end,
- els_utils:to_binary(
- uri_string:recompose(#{
- scheme => <<"file">>,
- host => Host,
- path => [$/, Path1]
- })
- ).
+ els_utils:to_binary(
+ uri_string:recompose(#{
+ scheme => <<"file">>,
+ host => Host,
+ path => [$/, Path1]
+ })
+ ).
-spec uri_join([path()]) -> iolist().
uri_join(List) ->
- lists:join(<<"/">>, List).
+ lists:join(<<"/">>, List).
+
+-spec lowercase_drive_letter(binary()) -> binary().
+lowercase_drive_letter(<>) ->
+ Drive = string:to_lower(Drive0),
+ <>;
+lowercase_drive_letter(Path) ->
+ Path.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+path_uri_test_() ->
+ [
+ ?_assertEqual(
+ <<"/foo/bar.erl">>,
+ path(<<"file:///foo/bar.erl">>)
+ ),
+ ?_assertEqual(
+ <<"/foo/bar baz.erl">>,
+ path(<<"file:///foo/bar%20baz.erl">>)
+ ),
+ ?_assertEqual(
+ <<"/foo/bar.erl">>,
+ path(uri(path(<<"file:///foo/bar.erl">>)))
+ ),
+ ?_assertEqual(
+ <<"/foo/bar baz.erl">>,
+ path(uri(<<"/foo/bar baz.erl">>))
+ ),
+ ?_assertEqual(
+ <<"file:///foo/bar%20baz.erl">>,
+ uri(<<"/foo/bar baz.erl">>)
+ )
+ ].
--spec is_windows() -> boolean().
-is_windows() ->
- {OS, _} = os:type(),
- OS =:= win32.
+path_windows_test() ->
+ ?assertEqual(
+ <<"C:/foo/bar.erl">>,
+ path(<<"file:///C%3A/foo/bar.erl">>, true)
+ ).
+-endif.
diff --git a/apps/els_core/src/els_utils.erl b/apps/els_core/src/els_utils.erl
index 8b87e4b37..6c26a15a7 100644
--- a/apps/els_core/src/els_utils.erl
+++ b/apps/els_core/src/els_utils.erl
@@ -1,27 +1,35 @@
-module(els_utils).
--export([ cmd/2
- , cmd/3
- , filename_to_atom/1
- , find_header/1
- , find_module/1
- , find_modules/1
- , fold_files/4
- , halt/1
- , lookup_document/1
- , include_id/1
- , include_lib_id/1
- , macro_string_to_term/1
- , project_relative/1
- , resolve_paths/3
- , to_binary/1
- , to_list/1
- , compose_node_name/2
- , function_signature/1
- , base64_encode_term/1
- , base64_decode_term/1
- ]).
-
+-export([
+ cmd/2,
+ cmd/3,
+ filename_to_atom/1,
+ find_header/1,
+ find_module/1,
+ find_modules/1,
+ fold_files/4,
+ halt/1,
+ lookup_document/1,
+ include_id/1,
+ include_lib_id/1,
+ macro_string_to_term/1,
+ project_relative/1,
+ resolve_paths/2,
+ to_binary/1,
+ to_list/1,
+ compose_node_name/2,
+ function_signature/1,
+ base64_encode_term/1,
+ base64_decode_term/1,
+ levenshtein_distance/2,
+ camel_case/1,
+ jaro_distance/2,
+ is_windows/0,
+ system_tmp_dir/0,
+ race/2,
+ uniq/1,
+ json_decode_with_atom_keys/1
+]).
%%==============================================================================
%% Includes
@@ -37,160 +45,167 @@
-spec cmd(string(), [string()]) -> integer() | no_return().
cmd(Cmd, Args) ->
- cmd(Cmd, Args, []).
+ cmd(Cmd, Args, []).
% @doc Replacement for os:cmd that allows for spaces in args and paths
-spec cmd(string(), [string()], string()) -> integer() | no_return().
cmd(Cmd, Args, Path) ->
- ?LOG_INFO("Running OS command [command=~p] [args=~p]", [Cmd, Args]),
- Executable = case filename:basename(Cmd) of
- Cmd ->
- cmd_path(Cmd);
- _ ->
- %% The command already contains a path
- Cmd
- end,
- Tag = make_ref(),
- F =
- fun() ->
- P = open_port(
- {spawn_executable, Executable},
- [ binary
- , use_stdio
- , stream
- , exit_status
- , hide
- , {args, Args}
- %% TODO: Windows-friendly version?
- , {env, [{"PATH", Path ++ ":" ++ os:getenv("PATH")}]}
- ]
- ),
- exit({Tag, cmd_receive(P)})
- end,
- {Pid, Ref} = erlang:spawn_monitor(F),
- receive
- {'DOWN', Ref, process, Pid, {Tag, Data}} ->
- Data;
- {'DOWN', Ref, process, Pid, Reason} ->
- exit(Reason)
- end.
+ ?LOG_INFO("Running OS command [command=~p] [args=~p]", [Cmd, Args]),
+ Executable =
+ case filename:basename(Cmd) of
+ Cmd ->
+ cmd_path(Cmd);
+ _ ->
+ %% The command already contains a path
+ Cmd
+ end,
+ Tag = make_ref(),
+ F =
+ fun() ->
+ P = open_port(
+ {spawn_executable, Executable},
+ [
+ binary,
+ use_stdio,
+ stream,
+ exit_status,
+ hide,
+ {args, Args},
+ %% TODO: Windows-friendly version?
+ {env, [{"PATH", Path ++ ":" ++ os:getenv("PATH")}]}
+ ]
+ ),
+ exit({Tag, cmd_receive(P)})
+ end,
+ {Pid, Ref} = erlang:spawn_monitor(F),
+ receive
+ {'DOWN', Ref, process, Pid, {Tag, Data}} ->
+ Data;
+ {'DOWN', Ref, process, Pid, Reason} ->
+ exit(Reason)
+ end.
%% @doc Return the path for a command
-spec cmd_path(string()) -> string().
cmd_path(Cmd) ->
- ErtsBinDir = filename:dirname(escript:script_name()),
- case os:find_executable(Cmd, ErtsBinDir) of
- false ->
- case os:find_executable(Cmd) of
+ ErtsBinDir = filename:dirname(escript:script_name()),
+ case os:find_executable(Cmd, ErtsBinDir) of
false ->
- Fmt = "Could not find command ~p",
- Args = [Cmd],
- Msg = lists:flatten(io_lib:format(Fmt, Args)),
- error(Msg);
- GlobalEpmd ->
- GlobalEpmd
- end;
- Epmd ->
- Epmd
- end.
+ case os:find_executable(Cmd) of
+ false ->
+ Fmt = "Could not find command ~p",
+ Args = [Cmd],
+ Msg = lists:flatten(io_lib:format(Fmt, Args)),
+ error(Msg);
+ GlobalEpmd ->
+ GlobalEpmd
+ end;
+ Epmd ->
+ Epmd
+ end.
%% @doc Convert an 'include'/'include_lib' POI ID to a document index ID
-spec filename_to_atom(string()) -> atom().
filename_to_atom(FileName) ->
- list_to_atom(filename:basename(FileName, filename:extension(FileName))).
+ list_to_atom(filename:basename(FileName, filename:extension(FileName))).
%% @doc Look for a header in the DB
-spec find_header(atom()) -> {ok, uri()} | {error, any()}.
find_header(Id) ->
- {ok, Candidates} = els_dt_document_index:lookup(Id),
- case [Uri || #{kind := header, uri := Uri} <- Candidates] of
- [Uri | _] ->
- {ok, Uri};
- [] ->
- FileName = atom_to_list(Id) ++ ".hrl",
- els_indexing:find_and_index_file(FileName)
- end.
+ {ok, Candidates} = els_dt_document_index:lookup(Id),
+ case [Uri || #{kind := header, uri := Uri} <- Candidates] of
+ [Uri | _] ->
+ {ok, Uri};
+ [] ->
+ FileName = atom_to_list(Id) ++ ".hrl",
+ els_indexing:find_and_deeply_index_file(FileName)
+ end.
%% @doc Look for a module in the DB
--spec find_module(atom()) -> {ok, uri()} | {error, any()}.
+-spec find_module(atom()) -> {ok, uri()} | {error, not_found}.
find_module(Id) ->
- case find_modules(Id) of
- {ok, [Uri | _]} ->
- {ok, Uri};
- Else ->
- Else
- end.
+ case find_modules(Id) of
+ {ok, [Uri | _]} ->
+ {ok, Uri};
+ {ok, []} ->
+ {error, not_found}
+ end.
%% @doc Look for all versions of a module in the DB
--spec find_modules(atom()) -> {ok, [uri()]} | {error, any()}.
+-spec find_modules(atom()) -> {ok, [uri()]}.
find_modules(Id) ->
- {ok, Candidates} = els_dt_document_index:lookup(Id),
- case [Uri || #{kind := module, uri := Uri} <- Candidates] of
- [] ->
- FileName = atom_to_list(Id) ++ ".erl",
- case els_indexing:find_and_index_file(FileName) of
- {ok, Uri} -> {ok, [Uri]};
- Error -> Error
- end;
- Uris ->
- {ok, prioritize_uris(Uris)}
- end.
+ {ok, Candidates} = els_dt_document_index:lookup(Id),
+ case [Uri || #{kind := module, uri := Uri} <- Candidates] of
+ [] ->
+ FileName = atom_to_list(Id) ++ ".erl",
+ case els_indexing:find_and_deeply_index_file(FileName) of
+ {ok, Uri} ->
+ {ok, [Uri]};
+ _Error ->
+ ?LOG_INFO("Finding module failed [filename=~p]", [FileName]),
+ {ok, []}
+ end;
+ Uris ->
+ {ok, prioritize_uris(Uris)}
+ end.
%% @doc Look for a document in the DB.
%%
%% Look for a given document in the DB and return it.
%% If the module is not in the DB, try to index it.
-spec lookup_document(uri()) ->
- {ok, els_dt_document:item()} | {error, any()}.
-lookup_document(Uri) ->
- case els_dt_document:lookup(Uri) of
- {ok, [Document]} ->
- {ok, Document};
- {ok, []} ->
- Path = els_uri:path(Uri),
- {ok, Uri} = els_indexing:index_file(Path),
- case els_dt_document:lookup(Uri) of
+ {ok, els_dt_document:item()} | {error, any()}.
+lookup_document(Uri0) ->
+ case els_dt_document:lookup(Uri0) of
{ok, [Document]} ->
- {ok, Document};
- Error ->
- ?LOG_INFO("Document lookup failed [error=~p] [uri=~p]", [Error, Uri]),
- {error, Error}
- end
- end.
+ {ok, Document};
+ {ok, []} ->
+ Path = els_uri:path(Uri0),
+ %% The returned Uri could be different from the original input
+ %% (e.g. if the original Uri would contain a query string)
+ {ok, Uri} = els_indexing:shallow_index(Path, app),
+ case els_dt_document:lookup(Uri) of
+ {ok, [Document]} ->
+ {ok, Document};
+ {ok, []} ->
+ ?LOG_INFO("Document lookup failed [uri=~p]", [Uri]),
+ {error, document_lookup_failed}
+ end
+ end.
%% @doc Convert path to an 'include' POI id
-spec include_id(file:filename_all()) -> string().
include_id(Path) ->
- els_utils:to_list(filename:basename(Path)).
+ els_utils:to_list(filename:basename(Path)).
%% @doc Convert path to an 'include_lib' POI id
-spec include_lib_id(file:filename_all()) -> string().
include_lib_id(Path) ->
- Components = filename:split(Path),
- Length = length(Components),
- End = Length - 1,
- Beginning = max(1, Length - 2),
- [H|T] = lists:sublist(Components, Beginning, End),
- %% Strip the app version number from the path
- Id = filename:join([re:replace(H, "-.*", "", [{return, list}]) | T]),
- els_utils:to_list(Id).
+ Components = filename:split(Path),
+ Length = length(Components),
+ End = Length - 1,
+ Beginning = max(1, Length - 2),
+ [H | T] = lists:sublist(Components, Beginning, End),
+ %% Strip the app version number from the path
+ Id = filename:join([re:replace(H, "-.*", "", [{return, list}]) | T]),
+ els_utils:to_list(Id).
-spec macro_string_to_term(list()) -> any().
macro_string_to_term(Value) ->
- try
- {ok, Tokens, _End} = erl_scan:string(Value ++ "."),
- {ok, Term} = erl_parse:parse_term(Tokens),
- Term
- catch
- _Class:Exception ->
- Fmt =
- "Error parsing custom defined macro, "
- "falling back to 'true'"
- "[value=~p] [exception=~p]",
- Args = [Value, Exception],
- ?LOG_ERROR(Fmt, Args),
- true
- end.
+ try
+ {ok, Tokens, _End} = erl_scan:string(Value ++ "."),
+ {ok, Term} = erl_parse:parse_term(Tokens),
+ Term
+ catch
+ _Class:Exception ->
+ Fmt =
+ "Error parsing custom defined macro, "
+ "falling back to 'true'"
+ "[value=~p] [exception=~p]",
+ Args = [Value, Exception],
+ ?LOG_ERROR(Fmt, Args),
+ true
+ end.
%% @doc Folds over all files in a directory
%%
@@ -198,178 +213,253 @@ macro_string_to_term(Value) ->
%% skipping all symlinks.
-spec fold_files(function(), function(), path(), any()) -> any().
fold_files(F, Filter, Dir, Acc) ->
- do_fold_dir(F, Filter, Dir, Acc).
+ do_fold_dir(F, Filter, Dir, Acc).
%% @doc Resolve paths based on path specs
%%
%% Gets a list of path specs and returns the expanded list of paths.
-%% Path specs can contains glob expressions. Resolved paths that contain
-%% symlinks will be ignored.
--spec resolve_paths([[path()]], path(), boolean()) -> [[path()]].
-resolve_paths(PathSpecs, RootPath, Recursive) ->
- lists:append([ resolve_path(PathSpec, RootPath, Recursive)
- || PathSpec <- PathSpecs
- ]).
+%% Path specs can contains glob expressions.
+-spec resolve_paths([[path()]], boolean()) -> [[path()]].
+resolve_paths(PathSpecs, Recursive) ->
+ lists:append([
+ resolve_path(PathSpec, Recursive)
+ || PathSpec <- PathSpecs
+ ]).
-spec halt(non_neg_integer()) -> ok.
halt(ExitCode) ->
- ok = init:stop(ExitCode).
+ ok = init:stop(ExitCode).
%% @doc Returns a project-relative file path for a given URI
-spec project_relative(uri()) -> file:filename() | {error, not_relative}.
project_relative(Uri) ->
- RootUri = els_config:get(root_uri),
- Size = byte_size(RootUri),
- case Uri of
- <> ->
- Trimmed = string:trim(Relative, leading, [$/, $\\ ]),
- to_list(Trimmed);
- _ ->
- {error, not_relative}
- end.
+ RootUri = els_config:get(root_uri),
+ Size = byte_size(RootUri),
+ case Uri of
+ <> ->
+ Trimmed = string:trim(Relative, leading, [$/, $\\]),
+ to_list(Trimmed);
+ _ ->
+ {error, not_relative}
+ end.
-spec to_binary(unicode:chardata()) -> binary().
to_binary(X) when is_binary(X) ->
- X;
+ X;
to_binary(X) when is_list(X) ->
- case unicode:characters_to_binary(X) of
- Result when is_binary(Result) -> Result;
- _ -> iolist_to_binary(X)
- end.
+ case unicode:characters_to_binary(X) of
+ Result when is_binary(Result) -> Result;
+ _ -> iolist_to_binary(X)
+ end.
-spec to_list(unicode:chardata()) -> string().
to_list(X) when is_list(X) ->
- X;
+ X;
to_list(X) when is_binary(X) ->
- case unicode:characters_to_list(X) of
- Result when is_list(Result) -> Result;
- _ -> binary_to_list(X)
- end.
+ case unicode:characters_to_list(X) of
+ Result when is_list(Result) -> Result;
+ _ -> binary_to_list(X)
+ end.
+
+-spec is_windows() -> boolean().
+is_windows() ->
+ {OS, _} = os:type(),
+ OS =:= win32.
+
+-spec system_tmp_dir() -> string().
+system_tmp_dir() ->
+ case is_windows() of
+ true ->
+ os:getenv("TEMP");
+ false ->
+ "/tmp"
+ end.
+
+%% @doc Run functions in parallel and return the result of the first function
+%% that terminates
+-spec race([fun(() -> Result)], timeout()) -> Result.
+race(Funs, Timeout) ->
+ Parent = self(),
+ Ref = make_ref(),
+ Pids = [spawn_link(fun() -> Parent ! {Ref, Fun()} end) || Fun <- Funs],
+ receive
+ {Ref, Result} ->
+ %% Ensure no lingering processes
+ [exit(Pid, kill) || Pid <- Pids],
+ %% Ensure no lingering messages
+ ok = flush(Ref),
+ Result
+ after Timeout ->
+ %% Ensure no lingering processes
+ [exit(Pid, kill) || Pid <- Pids],
+ %% Ensure no lingering messages
+ ok = flush(Ref),
+ error(timeout)
+ end.
+
+%% uniq/1: return a new list with the unique elements of the given list
+-spec uniq(List1) -> List2 when
+ List1 :: [T],
+ List2 :: [T],
+ T :: term().
+
+uniq(L) ->
+ uniq(L, #{}).
+
+-spec uniq(List1, Map) -> List2 when
+ Map :: map(),
+ List1 :: [T],
+ List2 :: [T],
+ T :: term().
+uniq([X | Xs], M) ->
+ case is_map_key(X, M) of
+ true ->
+ uniq(Xs, M);
+ false ->
+ [X | uniq(Xs, M#{X => true})]
+ end;
+uniq([], _) ->
+ [].
+
+-spec json_decode_with_atom_keys(binary()) -> map().
+json_decode_with_atom_keys(Binary) ->
+ Push = fun(Key, Value, Acc) -> [{binary_to_atom(Key), Value} | Acc] end,
+ {Result, ok, <<>>} = json:decode(Binary, ok, #{object_push => Push}),
+ Result.
%%==============================================================================
%% Internal functions
%%==============================================================================
+-spec flush(reference()) -> ok.
+flush(Ref) ->
+ receive
+ {Ref, _} ->
+ flush(Ref)
+ after 0 ->
+ ok
+ end.
%% Folding over files
-spec do_fold_files(function(), function(), path(), [path()], any()) -> any().
-do_fold_files(_F, _Filter, _Dir, [], Acc0) ->
- Acc0;
+do_fold_files(_F, _Filter, _Dir, [], Acc0) ->
+ Acc0;
do_fold_files(F, Filter, Dir, [File | Rest], Acc0) ->
- Path = filename:join(Dir, File),
- %% Symbolic links are not regular files
- Acc = case filelib:is_regular(Path) of
- true -> do_fold_file(F, Filter, Path, Acc0);
- false -> Acc0
- end,
- do_fold_files(F, Filter, Dir, Rest, Acc).
+ Path = filename:join(Dir, File),
+ %% Symbolic links are not regular files
+ Acc =
+ case filelib:is_regular(Path) of
+ true -> do_fold_file(F, Filter, Path, Acc0);
+ false -> do_fold_dir(F, Filter, Path, Acc0)
+ end,
+ do_fold_files(F, Filter, Dir, Rest, Acc).
-spec do_fold_file(function(), function(), path(), any()) ->
- any().
+ any().
do_fold_file(F, Filter, Path, Acc) ->
- case Filter(Path) of
- true -> F(Path, Acc);
- false -> Acc
- end.
+ case Filter(Path) of
+ true -> F(Path, Acc);
+ false -> Acc
+ end.
-spec do_fold_dir(function(), function(), path(), any()) ->
- any().
+ any().
do_fold_dir(F, Filter, Dir, Acc) ->
- case not is_symlink(Dir) andalso filelib:is_dir(Dir) of
- true ->
- {ok, Files} = file:list_dir(Dir),
- do_fold_files(F, Filter, Dir, Files, Acc);
- false ->
- Acc
- end.
+ case not is_symlink(Dir) andalso filelib:is_dir(Dir) of
+ true ->
+ {ok, Files} = file:list_dir(Dir),
+ do_fold_files(F, Filter, Dir, Files, Acc);
+ false ->
+ Acc
+ end.
-spec is_symlink(path()) -> boolean().
is_symlink(Path) ->
- case file:read_link(Path) of
- {ok, _} -> true;
- {error, _} -> false
- end.
+ case file:read_link(Path) of
+ {ok, _} -> true;
+ {error, _} -> false
+ end.
%% @doc Resolve paths recursively
--spec resolve_path([path()], path(), boolean()) -> [path()].
-resolve_path(PathSpec, RootPath, Recursive) ->
- Path = filename:join(PathSpec),
- Paths = filelib:wildcard(Path),
-
- case Recursive of
- true ->
- lists:append([ [make_normalized_path(P) | subdirs(P)]
- || P <- Paths, not contains_symlink(P, RootPath)
- ]);
- false ->
- [make_normalized_path(P) || P <- Paths, not contains_symlink(P, RootPath)]
- end.
+-spec resolve_path([path()], boolean()) -> [path()].
+resolve_path(PathSpec, Recursive) ->
+ Path = filename:join(PathSpec),
+ Paths = filelib:wildcard(Path),
+
+ case Recursive of
+ true ->
+ lists:append([
+ [make_normalized_path(P) | subdirs(P)]
+ || P <- Paths
+ ]);
+ false ->
+ [make_normalized_path(P) || P <- Paths]
+ end.
%% Returns all subdirectories for the provided path
-spec subdirs(path()) -> [path()].
subdirs(Path) ->
- subdirs(Path, []).
+ subdirs(Path, []).
-spec subdirs(path(), [path()]) -> [path()].
subdirs(Path, Subdirs) ->
- case file:list_dir(Path) of
- {ok, Files} -> subdirs_(Path, Files, Subdirs);
- {error, _} -> Subdirs
- end.
+ case file:list_dir(Path) of
+ {ok, Files} -> subdirs_(Path, Files, Subdirs);
+ {error, _} -> Subdirs
+ end.
-spec subdirs_(path(), [path()], [path()]) -> [path()].
subdirs_(Path, Files, Subdirs) ->
- Fold = fun(F, Acc) ->
- FullPath = filename:join([Path, F]),
- case
- not is_symlink(FullPath)
- andalso filelib:is_dir(FullPath)
- of
- true -> subdirs(FullPath, [FullPath | Acc]);
- false -> Acc
- end
- end,
- lists:foldl(Fold, Subdirs, Files).
-
--spec contains_symlink(path(), path()) -> boolean().
-contains_symlink(RootPath, RootPath) ->
- false;
-contains_symlink([], _RootPath) ->
- false;
-contains_symlink(Path, RootPath) ->
- Parts = filename:split(Path),
- case lists:droplast(Parts) of
- [] -> false;
- ParentParts ->
- Parent = filename:join(ParentParts),
- ((not (Parent == RootPath)) and (is_symlink(Parent)))
- orelse contains_symlink(Parent, RootPath)
- end.
+ Fold = fun(F, Acc) ->
+ FullPath = filename:join([Path, F]),
+ case
+ not is_symlink(FullPath) andalso
+ filelib:is_dir(FullPath)
+ of
+ true -> subdirs(FullPath, [FullPath | Acc]);
+ false -> Acc
+ end
+ end,
+ lists:foldl(Fold, Subdirs, Files).
-spec cmd_receive(port()) -> integer().
cmd_receive(Port) ->
- receive
- {Port, {exit_status, ExitCode}} ->
- ExitCode;
- {Port, _} ->
- cmd_receive(Port)
- end.
+ receive
+ {Port, {exit_status, ExitCode}} ->
+ ExitCode;
+ {Port, _} ->
+ cmd_receive(Port)
+ end.
%% @doc Prioritize files
%% Prefer files below root and prefer files in src dir.
-spec prioritize_uris([uri()]) -> [uri()].
prioritize_uris(Uris) ->
- Root = els_config:get(root_uri),
- Order = fun(nomatch) -> 3;
- (Cont) ->
- case string:find(Cont, "/src/") of
- nomatch -> 1;
- _ -> 0
- end
- end,
- Prio = [{Order(string:prefix(Uri, Root)), Uri} || Uri <- Uris],
- [Uri || {_, Uri} <- lists:sort(Prio)].
+ Root = els_config:get(root_uri),
+ AppsPaths = els_config:get(apps_paths),
+ Prio = [{score_uri(Uri, Root, AppsPaths), Uri} || Uri <- Uris],
+ [Uri || {_, Uri} <- lists:sort(Prio)].
+
+-spec score_uri(uri(), uri(), [file:name()]) -> tuple().
+score_uri(Uri, RootUri, AppsPaths) ->
+ Path = els_uri:path(Uri),
+ Prefix = string:prefix(Uri, RootUri),
+ %% prefer files under project root
+ S1 =
+ case Prefix of
+ nomatch -> 1;
+ _Rest -> 0
+ end,
+ %% among those, prefer files under some project app directory (e.g.
+ %% deprioritize dependencies and shadow copies)
+ S2 = length([
+ AP
+ || S1 == 0,
+ AP <- AppsPaths,
+ string:prefix(Path, AP) /= nomatch
+ ]),
+ {S1, -S2}.
%%==============================================================================
%% This section excerpted from the rebar3 sources, rebar_dir.erl
@@ -379,73 +469,330 @@ prioritize_uris(Uris) ->
%% @doc make a path absolute
-spec make_absolute_path(path()) -> path().
make_absolute_path(Path) ->
- case filename:pathtype(Path) of
- absolute ->
- Path;
- relative ->
- {ok, Dir} = file:get_cwd(),
- filename:join([Dir, Path]);
- volumerelative ->
- Volume = hd(filename:split(Path)),
- {ok, Dir} = file:get_cwd(Volume),
- filename:join([Dir, Path])
- end.
+ case filename:pathtype(Path) of
+ absolute ->
+ Path;
+ relative ->
+ {ok, Dir} = file:get_cwd(),
+ filename:join([Dir, Path]);
+ volumerelative ->
+ Volume = hd(filename:split(Path)),
+ {ok, Dir} = file:get_cwd(Volume),
+ filename:join([Dir, Path])
+ end.
%% @doc normalizing a path removes all of the `..' and the
%% `.' segments it may contain.
-spec make_normalized_path(path()) -> path().
make_normalized_path(Path) ->
- AbsPath = make_absolute_path(Path),
- Components = filename:split(AbsPath),
- make_normalized_path(Components, []).
+ AbsPath = make_absolute_path(Path),
+ Components = filename:split(AbsPath),
+ make_normalized_path(Components, []).
%% @private drops path fragments for normalization
-spec make_normalized_path([file:name_all()], [file:name_all()]) -> path().
make_normalized_path([], NormalizedPath) ->
- filename:join(lists:reverse(NormalizedPath));
+ filename:join(lists:reverse(NormalizedPath));
make_normalized_path(["." | []], []) ->
- ".";
+ ".";
make_normalized_path(["." | T], NormalizedPath) ->
- make_normalized_path(T, NormalizedPath);
+ make_normalized_path(T, NormalizedPath);
make_normalized_path([".." | T], []) ->
- make_normalized_path(T, [".."]);
+ make_normalized_path(T, [".."]);
make_normalized_path([".." | T], [Head | Tail]) when Head =/= ".." ->
- make_normalized_path(T, Tail);
+ make_normalized_path(T, Tail);
make_normalized_path([H | T], NormalizedPath) ->
- make_normalized_path(T, [H | NormalizedPath]).
+ make_normalized_path(T, [H | NormalizedPath]).
-spec compose_node_name(Name :: string(), Type :: shortnames | longnames) ->
- NodeName :: atom().
+ NodeName :: atom().
compose_node_name(Name, Type) ->
- NodeName = case lists:member($@, Name) of
- true ->
- Name;
- _ ->
- {ok, HostName} = inet:gethostname(),
- Name ++ [$@ | HostName]
- end,
- case Type of
- shortnames ->
- list_to_atom(NodeName);
- longnames ->
- Domain = proplists:get_value(domain, inet:get_rc(), ""),
- list_to_atom(NodeName ++ "." ++ Domain)
- end.
+ NodeName =
+ case lists:member($@, Name) of
+ true ->
+ Name;
+ _ ->
+ HostName = els_config_runtime:get_hostname(),
+ Name ++ [$@ | HostName]
+ end,
+ case Type of
+ shortnames ->
+ list_to_atom(NodeName);
+ longnames ->
+ Domain = els_config_runtime:get_domain(),
+ case Domain of
+ "" -> list_to_atom(NodeName);
+ _ -> list_to_atom(NodeName ++ "." ++ Domain)
+ end
+ end.
%% @doc Given an MFA or a FA, return a printable version of the
%% function signature, in binary format.
--spec function_signature( {atom(), atom(), non_neg_integer()} |
- {atom(), non_neg_integer()}) ->
- binary().
+-spec function_signature(
+ {atom(), atom(), non_neg_integer()}
+ | {atom(), non_neg_integer()}
+) ->
+ binary().
function_signature({M, F, A}) ->
- els_utils:to_binary(io_lib:format("~p:~ts/~p", [M, F, A]));
+ els_utils:to_binary(io_lib:format("~p:~ts/~p", [M, F, A]));
function_signature({F, A}) ->
- els_utils:to_binary(io_lib:format("~ts/~p", [F, A])).
+ els_utils:to_binary(io_lib:format("~ts/~p", [F, A])).
-spec base64_encode_term(any()) -> binary().
base64_encode_term(Term) ->
- els_utils:to_binary(base64:encode_to_string(term_to_binary(Term))).
+ els_utils:to_binary(base64:encode_to_string(term_to_binary(Term))).
-spec base64_decode_term(binary()) -> any().
base64_decode_term(Base64) ->
- binary_to_term(base64:decode(Base64)).
+ binary_to_term(base64:decode(Base64)).
+
+-spec camel_case(binary() | string()) -> binary().
+camel_case(Str0) ->
+ %% Remove ''
+ Str = string:trim(Str0, both, "'"),
+ Words = [string:titlecase(Word) || Word <- string:lexemes(Str, "_")],
+ iolist_to_binary(Words).
+
+-spec levenshtein_distance(binary(), binary()) -> integer().
+levenshtein_distance(S, T) ->
+ {Distance, _} = levenshtein_distance(to_list(S), to_list(T), #{}),
+ Distance.
+
+-spec levenshtein_distance(string(), string(), map()) -> {integer(), map()}.
+levenshtein_distance([] = S, T, Cache) ->
+ {length(T), maps:put({S, T}, length(T), Cache)};
+levenshtein_distance(S, [] = T, Cache) ->
+ {length(S), maps:put({S, T}, length(S), Cache)};
+levenshtein_distance([X | S], [X | T], Cache) ->
+ levenshtein_distance(S, T, Cache);
+levenshtein_distance([_SH | ST] = S, [_TH | TT] = T, Cache) ->
+ case maps:find({S, T}, Cache) of
+ {ok, Distance} ->
+ {Distance, Cache};
+ error ->
+ {L1, C1} = levenshtein_distance(S, TT, Cache),
+ {L2, C2} = levenshtein_distance(ST, T, C1),
+ {L3, C3} = levenshtein_distance(ST, TT, C2),
+ L = 1 + lists:min([L1, L2, L3]),
+ {L, maps:put({S, T}, L, C3)}
+ end.
+
+%%% Jaro distance
+
+%% @doc Computes the Jaro distance (similarity) between two strings.
+%%
+%% Returns a float value between 0.0 (equates to no similarity) and 1.0 (is an
+%% exact match) representing Jaro distance between String1 and String2.
+%%
+%% The Jaro distance metric is designed and best suited for short strings such
+%% as person names. Erlang LS uses this function to provide the "did you
+%% mean?" functionality.
+%%
+%% @end
+-spec jaro_distance(S, S) -> float() when S :: string() | binary().
+jaro_distance(Str, Str) ->
+ 1.0;
+jaro_distance(_, "") ->
+ 0.0;
+jaro_distance("", _) ->
+ 0.0;
+jaro_distance(Str1, Str2) when
+ is_binary(Str1),
+ is_binary(Str2)
+->
+ jaro_distance(
+ binary_to_list(Str1),
+ binary_to_list(Str2)
+ );
+jaro_distance(Str1, Str2) when
+ is_list(Str1),
+ is_list(Str2)
+->
+ Len1 = length(Str1),
+ Len2 = length(Str2),
+ case jaro_match(Str1, Len1, Str2, Len2) of
+ {0, _Trans} ->
+ 0.0;
+ {Comm, Trans} ->
+ (Comm / Len1 + Comm / Len2 + (Comm - Trans) / Comm) / 3
+ end.
+
+-type jaro_state() :: {integer(), integer(), integer()}.
+-type jaro_range() :: {integer(), integer()}.
+
+-spec jaro_match(string(), integer(), string(), integer()) ->
+ {integer(), integer()}.
+jaro_match(Chars1, Len1, Chars2, Len2) when Len1 < Len2 ->
+ jaro_match(Chars1, Chars2, (Len2 div 2) - 1);
+jaro_match(Chars1, Len1, Chars2, _Len2) ->
+ jaro_match(Chars2, Chars1, (Len1 div 2) - 1).
+
+-spec jaro_match(string(), string(), integer()) -> {integer(), integer()}.
+jaro_match(Chars1, Chars2, Lim) ->
+ jaro_match(Chars1, Chars2, {0, Lim}, {0, 0, -1}, 0).
+
+-spec jaro_match(string(), string(), jaro_range(), jaro_state(), integer()) ->
+ {integer(), integer()}.
+jaro_match([Char | Rest], Chars0, Range, State0, Idx) ->
+ {Chars, State} = jaro_submatch(Char, Chars0, Range, State0, Idx),
+ case Range of
+ {Lim, Lim} ->
+ jaro_match(Rest, tl(Chars), Range, State, Idx + 1);
+ {Pre, Lim} ->
+ jaro_match(Rest, Chars, {Pre + 1, Lim}, State, Idx + 1)
+ end;
+jaro_match([], _, _, {Comm, Trans, _}, _) ->
+ {Comm, Trans}.
+
+-spec jaro_submatch(char(), string(), jaro_range(), jaro_state(), integer()) ->
+ {string(), jaro_state()}.
+jaro_submatch(Char, Chars0, {Pre, _} = Range, State, Idx) ->
+ case jaro_detect(Char, Chars0, Range) of
+ undefined ->
+ {Chars0, State};
+ {SubIdx, Chars} ->
+ {Chars, jaro_proceed(State, Idx - Pre + SubIdx)}
+ end.
+
+-spec jaro_detect(char(), string(), jaro_range()) ->
+ {integer(), string()} | undefined.
+jaro_detect(Char, Chars, {Pre, Lim}) ->
+ jaro_detect(Char, Chars, Pre + 1 + Lim, 0, []).
+
+-spec jaro_detect(char(), string(), integer(), integer(), list()) ->
+ {integer(), string()} | undefined.
+jaro_detect(_Char, _Chars, 0, _Idx, _Acc) ->
+ undefined;
+jaro_detect(_Char, [], _Lim, _Idx, _Acc) ->
+ undefined;
+jaro_detect(Char, [Char | Rest], _Lim, Idx, Acc) ->
+ {Idx, lists:reverse(Acc) ++ [undefined | Rest]};
+jaro_detect(Char, [Other | Rest], Lim, Idx, Acc) ->
+ jaro_detect(Char, Rest, Lim - 1, Idx + 1, [Other | Acc]).
+
+-spec jaro_proceed(jaro_state(), integer()) -> jaro_state().
+jaro_proceed({Comm, Trans, Former}, Current) when Current < Former ->
+ {Comm + 1, Trans + 1, Current};
+jaro_proceed({Comm, Trans, _Former}, Current) ->
+ {Comm + 1, Trans, Current}.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+jaro_distance_test_() ->
+ [
+ ?_assertEqual(
+ jaro_distance("same", "same"),
+ 1.0
+ ),
+ ?_assertEqual(
+ jaro_distance("any", ""),
+ 0.0
+ ),
+ ?_assertEqual(
+ jaro_distance("", "any"),
+ 0.0
+ ),
+ ?_assertEqual(
+ jaro_distance("martha", "marhta"),
+ 0.9444444444444445
+ ),
+ ?_assertEqual(
+ jaro_distance("martha", "marhha"),
+ 0.888888888888889
+ ),
+ ?_assertEqual(
+ jaro_distance("marhha", "martha"),
+ 0.888888888888889
+ ),
+ ?_assertEqual(
+ jaro_distance("dwayne", "duane"),
+ 0.8222222222222223
+ ),
+ ?_assertEqual(
+ jaro_distance("dixon", "dicksonx"),
+ 0.7666666666666666
+ ),
+ ?_assertEqual(
+ jaro_distance("xdicksonx", "dixon"),
+ 0.7851851851851852
+ ),
+ ?_assertEqual(
+ jaro_distance("shackleford", "shackelford"),
+ 0.9696969696969697
+ ),
+ ?_assertEqual(
+ jaro_distance("dunningham", "cunnigham"),
+ 0.8962962962962964
+ ),
+ ?_assertEqual(
+ jaro_distance("nichleson", "nichulson"),
+ 0.9259259259259259
+ ),
+ ?_assertEqual(
+ jaro_distance("jones", "johnson"),
+ 0.7904761904761904
+ ),
+ ?_assertEqual(
+ jaro_distance("massey", "massie"),
+ 0.888888888888889
+ ),
+ ?_assertEqual(
+ jaro_distance("abroms", "abrams"),
+ 0.888888888888889
+ ),
+ ?_assertEqual(
+ jaro_distance("hardin", "martinez"),
+ 0.7222222222222222
+ ),
+ ?_assertEqual(
+ jaro_distance("itman", "smith"),
+ 0.4666666666666666
+ ),
+ ?_assertEqual(
+ jaro_distance("jeraldine", "geraldine"),
+ 0.9259259259259259
+ ),
+ ?_assertEqual(
+ jaro_distance("michelle", "michael"),
+ 0.8690476190476191
+ ),
+ ?_assertEqual(
+ jaro_distance("julies", "julius"),
+ 0.888888888888889
+ ),
+ ?_assertEqual(
+ jaro_distance("tanya", "tonya"),
+ 0.8666666666666667
+ ),
+ ?_assertEqual(
+ jaro_distance("sean", "susan"),
+ 0.7833333333333333
+ ),
+ ?_assertEqual(
+ jaro_distance("jon", "john"),
+ 0.9166666666666666
+ ),
+ ?_assertEqual(
+ jaro_distance("jon", "jan"),
+ 0.7777777777777777
+ ),
+ ?_assertEqual(
+ jaro_distance("семена", "стремя"),
+ 0.6666666666666666
+ )
+ ].
+
+camel_case_test() ->
+ ?assertEqual(<<"">>, camel_case(<<"">>)),
+ ?assertEqual(<<"F">>, camel_case(<<"f">>)),
+ ?assertEqual(<<"Foo">>, camel_case(<<"foo">>)),
+ ?assertEqual(<<"FooBar">>, camel_case(<<"foo_bar">>)),
+ ?assertEqual(<<"FooBarBaz">>, camel_case(<<"foo_bar_baz">>)),
+ ?assertEqual(<<"FooBarBaz">>, camel_case(<<"'foo_bar_baz'">>)).
+
+json_decode_with_atom_keys_test() ->
+ Json = list_to_binary(json:encode(#{foo => bar})),
+ ?assertEqual(#{foo => <<"bar">>}, json_decode_with_atom_keys(Json)).
+
+-endif.
diff --git a/apps/els_core/test/els_fake_stdio.erl b/apps/els_core/test/els_fake_stdio.erl
index d188bc2d3..93441ef7d 100644
--- a/apps/els_core/test/els_fake_stdio.erl
+++ b/apps/els_core/test/els_fake_stdio.erl
@@ -1,101 +1,103 @@
-module(els_fake_stdio).
--export([ start/0
- , connect/2
- , loop/1
- ]).
+-export([
+ start/0,
+ connect/2,
+ loop/1
+]).
--record(state, { buffer = <<>> :: binary()
- , connected :: pid()
- , pending = [] :: [any()]
- }).
+-record(state, {
+ buffer = <<>> :: binary(),
+ connected :: pid(),
+ pending = [] :: [any()]
+}).
-type state() :: #state{}.
-spec start() -> pid().
start() ->
- proc_lib:spawn_link(?MODULE, loop, [#state{}]).
+ proc_lib:spawn_link(?MODULE, loop, [#state{}]).
-spec connect(pid(), pid()) -> ok.
connect(Pid, IoDevice) ->
- Pid ! {connect, IoDevice},
- ok.
+ Pid ! {connect, IoDevice},
+ ok.
-spec loop(state()) -> ok.
loop(State0) ->
- State1 = process_pending(State0),
- receive
- {connect, IoDevice} ->
- loop(State1#state{connected = IoDevice});
- {custom_request, From, Ref, Request} ->
- State = handle_locally(From, Ref, Request, State1),
- loop(State);
- {io_request, From, Ref, Request} ->
- State = dispatch(From, Ref, Request, State1),
- loop(State)
- end.
+ State1 = process_pending(State0),
+ receive
+ {connect, IoDevice} ->
+ loop(State1#state{connected = IoDevice});
+ {custom_request, From, Ref, Request} ->
+ State = handle_locally(From, Ref, Request, State1),
+ loop(State);
+ {io_request, From, Ref, Request} ->
+ State = dispatch(From, Ref, Request, State1),
+ loop(State)
+ end.
-spec dispatch(pid(), any(), any(), state()) -> ok.
dispatch(From, Ref, Request, State0) ->
- case is_custom(Request) of
- true -> redirect(From, Ref, Request, State0);
- false -> handle_locally(From, Ref, Request, State0)
- end.
+ case is_custom(Request) of
+ true -> redirect(From, Ref, Request, State0);
+ false -> handle_locally(From, Ref, Request, State0)
+ end.
-spec is_custom(any()) -> boolean().
is_custom({put_chars, _Encoding, _Chars}) ->
- true;
+ true;
is_custom({put_chars, _Encoding, _M, _F, _Args}) ->
- true;
+ true;
is_custom(_) ->
- false.
+ false.
-spec redirect(pid(), any(), any(), state()) -> state().
redirect(From, Ref, Request, State) ->
- Connected = State#state.connected,
- Connected ! {custom_request, From, Ref, Request},
- State.
+ Connected = State#state.connected,
+ Connected ! {custom_request, From, Ref, Request},
+ State.
-spec handle_locally(pid(), any(), any(), state()) -> state().
handle_locally(From, Ref, Request, State0) ->
- case handle_request(Request, State0) of
- {noreply, State} ->
- pending(From, Ref, Request, State);
- {reply, Reply, State} ->
- reply(From, Ref, Reply),
- State
- end.
+ case handle_request(Request, State0) of
+ {noreply, State} ->
+ pending(From, Ref, Request, State);
+ {reply, Reply, State} ->
+ reply(From, Ref, Reply),
+ State
+ end.
-spec handle_request(any(), state()) ->
- {reply, any(), state()} | {noreply, state()}.
+ {reply, any(), state()} | {noreply, state()}.
handle_request({setopts, _Opts}, State) ->
- {reply, ok, State};
+ {reply, ok, State};
handle_request({put_chars, Encoding, M, F, Args}, State0) ->
- Chars = apply(M, F, Args),
- handle_request({put_chars, Encoding, Chars}, State0);
+ Chars = apply(M, F, Args),
+ handle_request({put_chars, Encoding, Chars}, State0);
handle_request({put_chars, Encoding, Chars}, State0) ->
- EncodedChars = unicode:characters_to_list(Chars, Encoding),
- CharsBin = els_utils:to_binary(EncodedChars),
- Buffer = State0#state.buffer,
- State = State0#state{buffer = <>},
- {reply, ok, State};
+ EncodedChars = unicode:characters_to_list(Chars, Encoding),
+ CharsBin = els_utils:to_binary(EncodedChars),
+ Buffer = State0#state.buffer,
+ State = State0#state{buffer = <>},
+ {reply, ok, State};
handle_request({get_line, _Encoding, _Prompt}, State0) ->
- case binary:split(State0#state.buffer, <<"\n">>, [trim]) of
- [Line0, Rest] ->
- Line = string:trim(Line0),
- {reply, <>, State0#state{buffer = Rest}};
- _ ->
- {noreply, State0}
- end;
+ case binary:split(State0#state.buffer, <<"\n">>, [trim]) of
+ [Line0, Rest] ->
+ Line = string:trim(Line0),
+ {reply, <>, State0#state{buffer = Rest}};
+ _ ->
+ {noreply, State0}
+ end;
handle_request({get_chars, Encoding, _Prompt, Count}, State) ->
- handle_request({get_chars, Encoding, Count}, State);
+ handle_request({get_chars, Encoding, Count}, State);
handle_request({get_chars, _Encoding, Count}, State0) ->
- case State0#state.buffer of
- <> ->
- {reply, Data, State0#state{buffer = Rest}};
- _ ->
- {noreply, State0}
- end.
+ case State0#state.buffer of
+ <> ->
+ {reply, Data, State0#state{buffer = Rest}};
+ _ ->
+ {noreply, State0}
+ end.
-spec reply(pid(), any(), any()) -> any().
reply(From, ReplyAs, Reply) ->
@@ -103,11 +105,11 @@ reply(From, ReplyAs, Reply) ->
-spec pending(pid(), any(), any(), state()) -> state().
pending(From, Ref, Request, #state{pending = Pending} = State) ->
- State#state{pending = [{From, Ref, Request} | Pending]}.
+ State#state{pending = [{From, Ref, Request} | Pending]}.
-spec process_pending(state()) -> state().
process_pending(#state{pending = Pending} = State) ->
- FoldFun = fun({From, Ref, Request}, Acc) ->
- handle_locally(From, Ref, Request, Acc)
- end,
- lists:foldl(FoldFun, State#state{pending = []}, Pending).
+ FoldFun = fun({From, Ref, Request}, Acc) ->
+ handle_locally(From, Ref, Request, Acc)
+ end,
+ lists:foldl(FoldFun, State#state{pending = []}, Pending).
diff --git a/apps/els_dap/include/els_dap.hrl b/apps/els_dap/include/els_dap.hrl
deleted file mode 100644
index cdf9100a6..000000000
--- a/apps/els_dap/include/els_dap.hrl
+++ /dev/null
@@ -1,8 +0,0 @@
--ifndef(__ELS_DAP_HRL__).
--define(__ELS_DAP_HRL__, 1).
-
--include_lib("els_core/include/els_core.hrl").
-
--define(APP, els_dap).
-
--endif.
diff --git a/apps/els_dap/src/els_dap.erl b/apps/els_dap/src/els_dap.erl
deleted file mode 100644
index 783087edf..000000000
--- a/apps/els_dap/src/els_dap.erl
+++ /dev/null
@@ -1,128 +0,0 @@
-%%=============================================================================
-%% @doc Erlang DAP's escript Entrypoint
-%%=============================================================================
--module(els_dap).
-
--export([ main/1 ]).
-
--export([ parse_args/1
- , log_root/0
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include("els_dap.hrl").
--include_lib("kernel/include/logger.hrl").
-
--define(DEFAULT_LOGGING_LEVEL, "debug").
-
--spec main([any()]) -> ok.
-main(Args) ->
- application:load(getopt),
- application:load(els_core),
- application:load(els_dap),
- ok = parse_args(Args),
- application:set_env(els_core, server, els_dap_server),
- configure_logging(),
- {ok, _} = application:ensure_all_started(?APP),
- patch_logging(),
- ?LOG_INFO("Started Erlang LS - DAP server", []),
- receive _ -> ok end.
-
--spec print_version() -> ok.
-print_version() ->
- {ok, Vsn} = application:get_key(?APP, vsn),
- io:format("Version: ~s~n", [Vsn]),
- ok.
-
-%%==============================================================================
-%% Argument parsing
-%%==============================================================================
-
--spec parse_args([string()]) -> ok.
-parse_args(Args) ->
- case getopt:parse(opt_spec_list(), Args) of
- {ok, {[version | _], _BadArgs}} ->
- print_version(),
- halt(1);
- {ok, {ParsedArgs, _BadArgs}} ->
- set_args(ParsedArgs);
- {error, {invalid_option, _}} ->
- getopt:usage(opt_spec_list(), "Erlang LS - DAP"),
- halt(1)
- end.
-
--spec opt_spec_list() -> [getopt:option_spec()].
-opt_spec_list() ->
- [ { version
- , $v
- , "version"
- , undefined
- , "Print the current version of Erlang LS - DAP"
- }
- , { log_dir
- , $d
- , "log-dir"
- , {string, filename:basedir(user_log, "els_dap")}
- , "Directory where logs will be written."
- }
- , { log_level
- , $l
- , "log-level"
- , {string, ?DEFAULT_LOGGING_LEVEL}
- , "The log level that should be used."
- }
- ].
-
--spec set_args([] | [getopt:compound_option()]) -> ok.
-set_args([]) -> ok;
-set_args([version | Rest]) -> set_args(Rest);
-set_args([{Arg, Val} | Rest]) ->
- set(Arg, Val),
- set_args(Rest).
-
--spec set(atom(), getopt:arg_value()) -> ok.
-set(log_dir, Dir) ->
- application:set_env(els_core, log_dir, Dir);
-set(log_level, Level) ->
- application:set_env(els_core, log_level, list_to_atom(Level)).
-
-%%==============================================================================
-%% Logger configuration
-%%==============================================================================
-
--spec configure_logging() -> ok.
-configure_logging() ->
- LogFile = filename:join([log_root(), "dap_server.log"]),
- {ok, LoggingLevel} = application:get_env(els_core, log_level),
- ok = filelib:ensure_dir(LogFile),
- Handler = #{ config => #{ file => LogFile }
- , level => LoggingLevel
- , formatter => { logger_formatter
- , #{ template => [ "[", time, "] "
- , file, ":", line, " "
- , pid, " "
- , "[", level, "] "
- , msg, "\n"
- ]
- }
- }
- },
- [logger:remove_handler(H) || H <- logger:get_handler_ids()],
- logger:add_handler(els_core_handler, logger_std_h, Handler),
- logger:set_primary_config(level, LoggingLevel),
- ok.
-
--spec patch_logging() -> ok.
-patch_logging() ->
- %% The ssl_handler is added by ranch -> ssl
- logger:remove_handler(ssl_handler),
- ok.
-
--spec log_root() -> string().
-log_root() ->
- {ok, LogDir} = application:get_env(els_core, log_dir),
- {ok, CurrentDir} = file:get_cwd(),
- Dirname = filename:basename(CurrentDir),
- filename:join([LogDir, Dirname]).
diff --git a/apps/els_dap/src/els_dap_agent.erl b/apps/els_dap/src/els_dap_agent.erl
deleted file mode 100644
index a3613ff91..000000000
--- a/apps/els_dap/src/els_dap_agent.erl
+++ /dev/null
@@ -1,22 +0,0 @@
-%%=============================================================================
-%% @doc Code for the Erlang DAP Agent
-%%
-%% This module is injected in the Erlang node that the DAP server launches.
-%% @end
-%%=============================================================================
--module(els_dap_agent).
-
--export([ int_cb/2, meta_eval/2 ]).
-
--spec int_cb(pid(), pid()) -> ok.
-int_cb(Thread, ProviderPid) ->
- ProviderPid ! {int_cb, Thread},
- ok.
-
--spec meta_eval(pid(), string()) -> any().
-meta_eval(Meta, Command) ->
- _ = int:meta(Meta, eval, {ignored_module, Command}),
- receive
- {Meta, {eval_rsp, Return}} ->
- Return
- end.
diff --git a/apps/els_dap/src/els_dap_app.erl b/apps/els_dap/src/els_dap_app.erl
deleted file mode 100644
index fe37dc1bc..000000000
--- a/apps/els_dap/src/els_dap_app.erl
+++ /dev/null
@@ -1,28 +0,0 @@
-%%==============================================================================
-%% Application Callback Module
-%%==============================================================================
--module(els_dap_app).
-
-%%==============================================================================
-%% Behaviours
-%%==============================================================================
--behaviour(application).
-
-%%==============================================================================
-%% Exports
-%%==============================================================================
-%% Application Callbacks
--export([ start/2
- , stop/1
- ]).
-
-%%==============================================================================
-%% Application Callbacks
-%%==============================================================================
--spec start(normal, any()) -> {ok, pid()}.
-start(_StartType, _StartArgs) ->
- els_dap_sup:start_link().
-
--spec stop(any()) -> ok.
-stop(_State) ->
- ok.
diff --git a/apps/els_dap/src/els_dap_breakpoints.erl b/apps/els_dap/src/els_dap_breakpoints.erl
deleted file mode 100644
index a5dc3959b..000000000
--- a/apps/els_dap/src/els_dap_breakpoints.erl
+++ /dev/null
@@ -1,125 +0,0 @@
--module(els_dap_breakpoints).
--export([ build_source_breakpoints/1
- , get_function_breaks/2
- , get_line_breaks/2
- , do_line_breakpoints/4
- , do_function_breaks/4
- , type/3]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Types
-%%==============================================================================
-
--type breakpoints() :: #{
- module() => #{
- line => #{
- line() => line_breaks()
- },
- function => [function_break()]
- }
-}.
--type line() :: non_neg_integer().
--type line_breaks() :: #{ condition => expression()
- , hitcond => expression()
- , logexpr => expression()
- }.
--type expression() :: string().
--type function_break() :: {atom(), non_neg_integer()}.
-
--export_type([ breakpoints/0
- , line_breaks/0]).
-
--spec type(breakpoints(), module(), line()) -> line_breaks().
-type(Breakpoints, Module, Line) ->
- ?LOG_DEBUG("checking breakpoint type for ~s:~b", [Module, Line]),
- case Breakpoints of
- #{Module := #{line := #{Line := Break}}} ->
- Break;
- _ ->
- %% function breaks get handled like regular ones
- #{}
- end.
-
-%% @doc build regular, conditional, hit and log breakpoints from setBreakpoint
-%% request
--spec build_source_breakpoints(Params :: map()) ->
- {module(), #{line() => line_breaks()}}.
-build_source_breakpoints(Params) ->
- #{<<"source">> := #{<<"path">> := Path}} = Params,
- Module = els_uri:module(els_uri:uri(Path)),
- SourceBreakpoints = maps:get(<<"breakpoints">>, Params, []),
- _SourceModified = maps:get(<<"sourceModified">>, Params, false),
- {Module, maps:from_list(lists:map(fun build_source_breakpoint/1,
- SourceBreakpoints))}.
-
--spec build_source_breakpoint(map()) ->
- { line()
- , #{ condition => expression()
- , hitcond => expression()
- , logexpr => expression()
- }
- }.
-build_source_breakpoint(#{<<"line">> := Line} = Breakpoint) ->
- Cond = case Breakpoint of
- #{<<"condition">> := CondExpr} when CondExpr =/= <<>> ->
- #{condition => CondExpr};
- _ -> #{}
- end,
- Hit = case Breakpoint of
- #{<<"hitCondition">> := HitExpr} when HitExpr =/= <<>> ->
- #{hitcond => HitExpr};
- _ -> #{}
- end,
- Log = case Breakpoint of
- #{<<"logMessage">> := LogExpr} when LogExpr =/= <<>> ->
- #{logexpr => LogExpr};
- _ -> #{}
- end,
- {Line, lists:foldl(fun maps:merge/2, #{}, [Cond, Hit, Log])}.
-
--spec get_function_breaks(module(), breakpoints()) -> [function_break()].
-get_function_breaks(Module, Breaks) ->
- case Breaks of
- #{Module := #{function := Functions}} -> Functions;
- _ -> []
- end.
-
--spec get_line_breaks(module(), breakpoints()) -> #{line() => line_breaks()}.
-get_line_breaks(Module, Breaks) ->
- case Breaks of
- #{Module := #{line := Lines}} -> Lines;
- _ -> []
- end.
-
--spec do_line_breakpoints(node(), module(),
- #{line() => line_breaks()}, breakpoints()) ->
- breakpoints().
-do_line_breakpoints(Node, Module, LineBreakPoints, Breaks) ->
- maps:map(
- fun
- (Line, _) -> els_dap_rpc:break(Node, Module, Line)
- end,
- LineBreakPoints
- ),
- case Breaks of
- #{Module := ModBreaks} ->
- Breaks#{Module => ModBreaks#{line => LineBreakPoints}};
- _ ->
- Breaks#{Module => #{line => LineBreakPoints, function => []}}
- end.
-
--spec do_function_breaks(node(), module(), [function_break()], breakpoints()) ->
- breakpoints().
-do_function_breaks(Node, Module, FBreaks, Breaks) ->
- [els_dap_rpc:break_in(Node, Module, Func, Arity) || {Func, Arity} <- FBreaks],
- case Breaks of
- #{Module := ModBreaks} ->
- Breaks#{Module => ModBreaks#{function => FBreaks}};
- _ ->
- Breaks#{Module => #{line => #{}, function => FBreaks}}
- end.
diff --git a/apps/els_dap/src/els_dap_general_provider.erl b/apps/els_dap/src/els_dap_general_provider.erl
deleted file mode 100644
index e1534e815..000000000
--- a/apps/els_dap/src/els_dap_general_provider.erl
+++ /dev/null
@@ -1,928 +0,0 @@
-%%==============================================================================
-%% @doc Erlang DAP General Provider
-%%
-%% Implements the logic for hanlding all of the commands in the protocol.
-%%
-%% The functionality in this module will eventually be broken into several
-%% different providers.
-%% @end
-%%==============================================================================
--module(els_dap_general_provider).
-
--behaviour(els_provider).
--export([ handle_request/2
- , handle_info/2
- , is_enabled/0
- , init/0
- ]).
-
--export([ capabilities/0
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Types
-%%==============================================================================
-
-%% Protocol
--type capabilities() :: #{}.
--type request() :: {Command :: binary(), Params :: map()}.
--type result() :: #{}.
-
-%% Internal
--type frame_id() :: pos_integer().
--type frame() :: #{ module := module()
- , function := atom()
- , arguments := [any()]
- , source := binary()
- , line := line()
- , bindings := any()
- }.
--type thread() :: #{ pid := pid()
- , frames := #{frame_id() => frame()}
- }.
--type thread_id() :: integer().
--type mode() :: undefined | running | stepping.
--type state() :: #{ threads => #{thread_id() => thread()}
- , project_node => atom()
- , launch_params => #{}
- , scope_bindings =>
- #{pos_integer() => {binding_type(), bindings()}}
- , breakpoints := els_dap_breakpoints:breakpoints()
- , hits => #{line() => non_neg_integer()}
- , timeout := timeout()
- , mode := mode()
- }.
--type bindings() :: [{varname(), term()}].
--type varname() :: atom() | string().
-%% extendable bindings type for customized pretty printing
--type binding_type() :: generic | map_assoc.
--type line() :: non_neg_integer().
-
-%%==============================================================================
-%% els_provider functions
-%%==============================================================================
-
--spec is_enabled() -> boolean().
-is_enabled() -> true.
-
--spec init() -> state().
-init() ->
- #{ threads => #{}
- , launch_params => #{}
- , scope_bindings => #{}
- , breakpoints => #{}
- , hits => #{}
- , timeout => 30
- , mode => undefined}.
-
--spec handle_request(request(), state()) -> {result(), state()}.
-handle_request({<<"initialize">>, _Params}, State) ->
- %% quick fix to satisfy els_config initialization
- {ok, RootPath} = file:get_cwd(),
- RootUri = els_uri:uri(els_utils:to_binary(RootPath)),
- InitOptions = #{},
- Capabilities = capabilities(),
- ok = els_config:initialize(RootUri, Capabilities, InitOptions),
- {Capabilities, State};
-handle_request({<<"launch">>, #{<<"cwd">> := Cwd} = Params}, State) ->
- #{ <<"projectnode">> := ProjectNode
- , <<"cookie">> := Cookie
- , <<"timeout">> := TimeOut
- , <<"use_long_names">> := UseLongNames} = start_distribution(Params),
- case Params of
- #{ <<"runinterminal">> := Cmd
- } ->
- ParamsR
- = #{ <<"kind">> => <<"integrated">>
- , <<"title">> => ProjectNode
- , <<"cwd">> => Cwd
- , <<"args">> => Cmd
- },
- ?LOG_INFO("Sending runinterminal request: [~p]", [ParamsR]),
- els_dap_server:send_request(<<"runInTerminal">>, ParamsR),
- ok;
- _ ->
- NameTypeParam = case UseLongNames of
- true ->
- "--name";
- false ->
- "--sname"
- end,
- ?LOG_INFO("launching 'rebar3 shell`", []),
- spawn(fun() ->
- els_utils:cmd(
- "rebar3",
- [ "shell"
- , NameTypeParam
- , atom_to_list(ProjectNode)
- , "--setcookie"
- , erlang:binary_to_list(Cookie)
- ]
- )
- end)
- end,
-
- els_dap_server:send_event(<<"initialized">>, #{}),
-
- {#{}, State#{ project_node => ProjectNode
- , launch_params => Params
- , timeout => TimeOut
- }};
-handle_request({<<"attach">>, Params}, State) ->
- #{ <<"projectnode">> := ProjectNode
- , <<"timeout">> := TimeOut} = start_distribution(Params),
-
- els_dap_server:send_event(<<"initialized">>, #{}),
-
- {#{}, State#{ project_node => ProjectNode
- , launch_params => Params
- , timeout => TimeOut
- }};
-handle_request( {<<"configurationDone">>, _Params}
- , #{ project_node := ProjectNode
- , launch_params := LaunchParams
- , timeout := Timeout} = State
- ) ->
- ensure_connected(ProjectNode, Timeout),
- %% TODO: Fetch stack_trace mode from Launch Config
- els_dap_rpc:stack_trace(ProjectNode, all),
- MFA = {els_dap_agent, int_cb, [self()]},
- els_dap_rpc:auto_attach(ProjectNode, [break], MFA),
-
- case LaunchParams of
- #{ <<"module">> := Module
- , <<"function">> := Function
- , <<"args">> := Args
- } ->
- M = binary_to_atom(Module, utf8),
- F = binary_to_atom(Function, utf8),
- A = els_dap_rpc:eval(ProjectNode, Args, []),
- ?LOG_INFO("Launching MFA: [~p]", [{M, F, A}]),
- rpc:cast(ProjectNode, M, F, A);
- _ -> ok
- end,
- {#{}, State#{mode => running}};
-handle_request( {<<"setBreakpoints">>, Params}
- , #{ project_node := ProjectNode
- , breakpoints := Breakpoints0
- , timeout := Timeout} = State
- ) ->
- ensure_connected(ProjectNode, Timeout),
- {Module, LineBreaks} = els_dap_breakpoints:build_source_breakpoints(Params),
-
-
- {module, Module} = els_dap_rpc:i(ProjectNode, Module),
-
- %% purge all breakpoints from the module
- els_dap_rpc:no_break(ProjectNode, Module),
- Breakpoints1 =
- els_dap_breakpoints:do_line_breakpoints(ProjectNode, Module,
- LineBreaks, Breakpoints0),
- BreakpointsRsps = [
- #{<<"verified">> => true, <<"line">> => Line}
- || {{_, Line}, _} <- els_dap_rpc:all_breaks(ProjectNode, Module)
- ],
-
- FunctionBreaks =
- els_dap_breakpoints:get_function_breaks(Module, Breakpoints1),
- Breakpoints2 =
- els_dap_breakpoints:do_function_breaks(ProjectNode, Module,
- FunctionBreaks, Breakpoints1),
-
- { #{<<"breakpoints">> => BreakpointsRsps}
- , State#{ breakpoints => Breakpoints2}
- };
-handle_request({<<"setExceptionBreakpoints">>, _Params}, State) ->
- {#{}, State};
-handle_request({<<"setFunctionBreakpoints">>, Params}
- , #{ project_node := ProjectNode
- , breakpoints := Breakpoints0
- , timeout := Timeout} = State
- ) ->
- ensure_connected(ProjectNode, Timeout),
- FunctionBreakPoints = maps:get(<<"breakpoints">>, Params, []),
- MFAs = [
- begin
- Spec = {Mod, _, _} = parse_mfa(MFA),
- els_dap_rpc:i(ProjectNode, Mod),
- Spec
- end
- || #{<<"name">> := MFA, <<"enabled">> := Enabled} <- FunctionBreakPoints,
- Enabled andalso parse_mfa(MFA) =/= error
- ],
-
- ModFuncBreaks = lists:foldl(
- fun({M, F, A}, Acc) ->
- case Acc of
- #{M := FBreaks} -> Acc#{M => [{F, A} | FBreaks]};
- _ -> Acc#{M => [{F, A}]}
- end
- end,
- #{},
- MFAs
- ),
-
- els_dap_rpc:no_break(ProjectNode),
- Breakpoints1 = maps:fold(
- fun (Mod, Breaks, Acc) ->
- Acc#{Mod => Breaks#{function => []}}
- end,
- #{},
- Breakpoints0
- ),
-
- Breakpoints2 = maps:fold(
- fun(Module, FunctionBreaks, Acc) ->
- els_dap_breakpoints:do_function_breaks(ProjectNode, Module,
- FunctionBreaks, Acc)
- end,
- Breakpoints1,
- ModFuncBreaks
- ),
- BreakpointsRsps = [
- #{
- <<"verified">> => true,
- <<"line">> => Line,
- <<"source">> => #{<<"path">> => source(Module, ProjectNode)}
- }
- || {{Module, Line}, [Status, _, _, _]} <-
- els_dap_rpc:all_breaks(ProjectNode),
- Status =:= active
- ],
-
- %% replay line breaks
- Breakpoints3 = maps:fold(
- fun(Module, _, Acc) ->
- Lines = els_dap_breakpoints:get_line_breaks(Module, Acc),
- els_dap_breakpoints:do_line_breakpoints(ProjectNode, Module,
- Lines, Acc)
- end,
- Breakpoints2,
- Breakpoints2
- ),
-
- { #{<<"breakpoints">> => BreakpointsRsps}
- , State#{breakpoints => Breakpoints3}
- };
-handle_request({<<"threads">>, _Params}, #{threads := Threads0} = State) ->
- Threads =
- [ #{ <<"id">> => Id
- , <<"name">> => format_term(Pid)
- } || {Id, #{pid := Pid} = _Thread} <- maps:to_list(Threads0)
- ],
- {#{<<"threads">> => Threads}, State};
-handle_request({<<"stackTrace">>, Params}, #{threads := Threads} = State) ->
- #{<<"threadId">> := ThreadId} = Params,
- Thread = maps:get(ThreadId, Threads),
- Frames = maps:get(frames, Thread),
- StackFrames =
- [ #{ <<"id">> => Id
- , <<"name">> => format_mfa(M, F, length(A))
- , <<"source">> => #{<<"path">> => Source}
- , <<"line">> => Line
- , <<"column">> => 0
- }
- || { Id
- , #{ module := M
- , function := F
- , arguments := A
- , line := Line
- , source := Source
- }
- } <- maps:to_list(Frames)
- ],
- {#{<<"stackFrames">> => StackFrames}, State};
-handle_request({<<"scopes">>, #{<<"frameId">> := FrameId} }
- , #{ threads := Threads
- , scope_bindings := ExistingScopes} = State) ->
- case frame_by_id(FrameId, maps:values(Threads)) of
- undefined -> {#{<<"scopes">> => []}, State};
- Frame ->
- Bindings = maps:get(bindings, Frame),
- Ref = erlang:unique_integer([positive]),
- {#{<<"scopes">> => [
- #{
- <<"name">> => <<"Locals">>,
- <<"presentationHint">> => <<"locals">>,
- <<"variablesReference">> => Ref
- }
- ]}, State#{scope_bindings => ExistingScopes#{Ref => {generic, Bindings}}}}
- end;
-handle_request( {<<"next">>, Params}
- , #{ threads := Threads
- , project_node := ProjectNode
- } = State
- ) ->
- #{<<"threadId">> := ThreadId} = Params,
- Pid = to_pid(ThreadId, Threads),
- ok = els_dap_rpc:next(ProjectNode, Pid),
- {#{}, State};
-handle_request( {<<"pause">>, _}
- , State
- ) ->
- %% pause is not supported by the OTP debugger
- %% but we cannot disable it in the UI either
- {#{}, State};
-handle_request( {<<"continue">>, Params}
- , #{ threads := Threads
- , project_node := ProjectNode
- } = State
- ) ->
- #{<<"threadId">> := ThreadId} = Params,
- Pid = to_pid(ThreadId, Threads),
- ok = els_dap_rpc:continue(ProjectNode, Pid),
- { #{<<"allThreadsContinued">> => false}
- , State#{mode => running}
- };
-handle_request( {<<"stepIn">>, Params}
- , #{ threads := Threads
- , project_node := ProjectNode
- } = State
- ) ->
- #{<<"threadId">> := ThreadId} = Params,
- Pid = to_pid(ThreadId, Threads),
- ok = els_dap_rpc:step(ProjectNode, Pid),
- {#{}, State};
-handle_request( {<<"stepOut">>, Params}
- , #{ threads := Threads
- , project_node := ProjectNode
- } = State
- ) ->
- #{<<"threadId">> := ThreadId} = Params,
- Pid = to_pid(ThreadId, Threads),
- ok = els_dap_rpc:next(ProjectNode, Pid),
- {#{}, State};
-handle_request({<<"evaluate">>, #{ <<"context">> := <<"hover">>
- , <<"frameId">> := FrameId
- , <<"expression">> := Input
- } = _Params}
- , #{ threads := Threads } = State
-) ->
- %% hover makes only sense for variables
- %% use the expression as fallback
- case frame_by_id(FrameId, maps:values(Threads)) of
- undefined -> {#{<<"result">> => <<"not available">>}, State};
- Frame ->
- Bindings = maps:get(bindings, Frame),
- VarName = erlang:list_to_atom(els_utils:to_list(Input)),
- case proplists:lookup(VarName, Bindings) of
- {VarName, VarValue} ->
- build_evaluate_response(VarValue, State);
- none ->
- {#{<<"result">> => <<"not available">>}, State}
- end
- end;
-handle_request({<<"evaluate">>, #{ <<"context">> := Context
- , <<"frameId">> := FrameId
- , <<"expression">> := Input
- } = _Params}
- , #{ threads := Threads
- , project_node := ProjectNode
- } = State
-) when Context =:= <<"watch">> orelse Context =:= <<"repl">> ->
- %% repl and watch can use whole expressions,
- %% but we still want structured variable scopes
- case pid_by_frame_id(FrameId, maps:values(Threads)) of
- undefined ->
- {#{<<"result">> => <<"not available">>}, State};
- Pid ->
- Update =
- case Context of
- <<"watch">> -> no_update;
- <<"repl">> -> update
- end,
- Return = safe_eval(ProjectNode, Pid, Input, Update),
- build_evaluate_response(Return, State)
- end;
-handle_request({<<"variables">>, #{<<"variablesReference">> := Ref
- } = _Params}
- , #{ scope_bindings := AllBindings
- } = State) ->
- #{Ref := {Type, Bindings}} = AllBindings,
- RestBindings = maps:remove(Ref, AllBindings),
- {Variables, MoreBindings} = build_variables(Type, Bindings),
- { #{<<"variables">> => Variables}
- , State#{ scope_bindings => maps:merge(RestBindings, MoreBindings)}};
-handle_request({<<"disconnect">>, _Params}
- , #{project_node := ProjectNode,
- threads := Threads,
- launch_params := #{<<"request">> := Request} = State}) ->
- case Request of
- <<"attach">> ->
- els_dap_rpc:no_break(ProjectNode),
- [els_dap_rpc:continue(ProjectNode, Pid) ||
- {_ThreadID, #{pid := Pid}} <- maps:to_list(Threads)],
- [els_dap_rpc:n(ProjectNode, Module) ||
- Module <- els_dap_rpc:interpreted(ProjectNode)];
- <<"launch">> ->
- els_dap_rpc:halt(ProjectNode)
- end,
- els_utils:halt(0),
- {#{}, State}.
-
--spec evaluate_condition(els_dap_breakpoints:line_breaks(), module(),
- integer(), atom(), pid()) -> boolean().
-evaluate_condition(Breakpt, Module, Line, ProjectNode, ThreadPid) ->
- %% evaluate condition if exists, otherwise treat as 'true'
- case Breakpt of
- #{condition := CondExpr} ->
- CondEval = safe_eval(ProjectNode, ThreadPid, CondExpr, no_update),
- case CondEval of
- true -> true;
- false -> false;
- _ ->
- WarnCond = unicode:characters_to_binary(
- io_lib:format(
- "~s:~b - Breakpoint condition evaluated to non-Boolean: ~w~n",
- [source(Module, ProjectNode), Line, CondEval])),
- els_dap_server:send_event( <<"output">>
- , #{ <<"output">> => WarnCond
- , <<"category">> => <<"stdout">>
- }),
- false
- end;
- _ -> true
- end.
-
--spec evaluate_hitcond(els_dap_breakpoints:line_breaks(), integer(), module(),
- integer(), atom(), pid()) -> boolean().
-evaluate_hitcond(Breakpt, HitCount, Module, Line, ProjectNode, ThreadPid) ->
- %% evaluate condition if exists, otherwise treat as 'true'
- case Breakpt of
- #{hitcond := HitExpr} ->
- HitEval = safe_eval(ProjectNode, ThreadPid, HitExpr, no_update),
- case HitEval of
- N when is_integer(N), N>0 -> (HitCount rem N =:= 0);
- _ ->
- WarnHit = unicode:characters_to_binary(
- io_lib:format(
- "~s:~b - Breakpoint hit condition not a non-negative int: ~w~n",
- [source(Module, ProjectNode), Line, HitEval])),
- els_dap_server:send_event( <<"output">>
- , #{ <<"output">> => WarnHit
- , <<"category">> => <<"stdout">>
- }),
- true
- end;
- _ -> true
- end.
-
--spec check_stop(els_dap_breakpoints:line_breaks(), boolean(), module(),
- integer(), atom(), pid()) -> boolean().
-check_stop(Breakpt, IsHit, Module, Line, ProjectNode, ThreadPid) ->
- case Breakpt of
- #{logexpr := LogExpr} ->
- case IsHit of
- true ->
- Return = safe_eval(ProjectNode, ThreadPid, LogExpr, no_update),
- LogMessage = unicode:characters_to_binary(
- io_lib:format("~s:~b - ~p~n",
- [source(Module, ProjectNode), Line, Return])),
- els_dap_server:send_event( <<"output">>
- , #{ <<"output">> => LogMessage
- , <<"category">> => <<"stdout">>
- }),
- false;
- false -> false
- end;
- _ -> IsHit
- end.
-
--spec debug_stop(thread_id()) -> mode().
-debug_stop(ThreadId) ->
- els_dap_server:send_event( <<"stopped">>
- , #{ <<"reason">> => <<"breakpoint">>
- , <<"threadId">> => ThreadId
- }),
- stepping.
-
--spec debug_previous_mode(mode(), atom(), pid(), thread_id()) -> mode().
-debug_previous_mode(Mode0, ProjectNode, ThreadPid, ThreadId) ->
- case Mode0 of
- running ->
- els_dap_rpc:continue(ProjectNode, ThreadPid),
- Mode0;
- _ ->
- debug_stop(ThreadId)
- end.
-
--spec handle_info(any(), state()) -> state() | no_return().
-handle_info( {int_cb, ThreadPid}
- , #{ threads := Threads
- , project_node := ProjectNode
- , breakpoints := Breakpoints
- , hits := Hits0
- , mode := Mode0
- } = State
- ) ->
- ?LOG_DEBUG("Int CB called. thread=~p", [ThreadPid]),
- ThreadId = id(ThreadPid),
- Thread = #{ pid => ThreadPid
- , frames => stack_frames(ThreadPid, ProjectNode)
- },
- {Module, Line} = break_module_line(ThreadPid, ProjectNode),
- Breakpt = els_dap_breakpoints:type(Breakpoints, Module, Line),
- Condition = evaluate_condition(Breakpt, Module, Line, ProjectNode, ThreadPid),
- %% update hit count for current line if condition is true
- HitCount = maps:get(Line, Hits0, 0) + 1,
- Hits1 = case Condition of
- true -> maps:put(Line, HitCount, Hits0);
- false -> Hits0
- end,
- %% check if there is hit expression, if yes check along with condition
- IsHit = Condition andalso
- evaluate_hitcond(Breakpt, HitCount, Module, Line, ProjectNode, ThreadPid),
- %% finally, either stop or log
- Stop = check_stop(Breakpt, IsHit, Module, Line, ProjectNode, ThreadPid),
- Mode1 = case Stop of
- true -> debug_stop(ThreadId);
- false -> debug_previous_mode(Mode0, ProjectNode, ThreadPid, ThreadId)
- end,
- State#{
- threads => maps:put(ThreadId, Thread, Threads),
- mode => Mode1,
- hits => Hits1
- };
-handle_info({nodedown, Node}, State) ->
- %% the project node is down, there is nothing left to do then to exit
- ?LOG_NOTICE("project node ~p terminated, ending debug session", [Node]),
- stop_debugger(),
- State.
-
-%%==============================================================================
-%% API
-%%==============================================================================
-
--spec capabilities() -> capabilities().
-capabilities() ->
- #{ <<"supportsConfigurationDoneRequest">> => true
- , <<"supportsEvaluateForHovers">> => true
- , <<"supportsFunctionBreakpoints">> => true
- , <<"supportsConditionalBreakpoints">> => true
- , <<"supportsHitConditionalBreakpoints">> => true
- , <<"supportsLogPoints">> => true}.
-
-%%==============================================================================
-%% Internal Functions
-%%==============================================================================
--spec inject_dap_agent(atom()) -> ok.
-inject_dap_agent(Node) ->
- Module = els_dap_agent,
- {Module, Bin, File} = code:get_object_code(Module),
- {_Replies, _} = els_dap_rpc:load_binary(Node, Module, File, Bin),
- ok.
-
--spec id(pid()) -> integer().
-id(Pid) ->
- erlang:phash2(Pid).
-
--spec stack_frames(pid(), atom()) -> #{frame_id() => frame()}.
-stack_frames(Pid, Node) ->
- {ok, Meta} = els_dap_rpc:get_meta(Node, Pid),
- [{Level, {M, F, A}} | Rest] =
- els_dap_rpc:meta(Node, Meta, backtrace, all),
- Bindings = els_dap_rpc:meta(Node, Meta, bindings, Level),
- StackFrameId = erlang:unique_integer([positive]),
- StackFrame = #{ module => M
- , function => F
- , arguments => A
- , source => source(M, Node)
- , line => break_line(Pid, Node)
- , bindings => Bindings},
- collect_frames(Node, Meta, Level, Rest, #{StackFrameId => StackFrame}).
-
--spec break_module_line(pid(), atom()) -> {module(), integer()}.
-break_module_line(Pid, Node) ->
- Snapshots = els_dap_rpc:snapshot(Node),
- {Pid, _Function, break, Location} = lists:keyfind(Pid, 1, Snapshots),
- Location.
-
--spec break_line(pid(), atom()) -> integer().
-break_line(Pid, Node) ->
- {_, Line} = break_module_line(Pid, Node),
- Line.
-
--spec source(atom(), atom()) -> binary().
-source(Module, Node) ->
- Source = els_dap_rpc:file(Node, Module),
- els_dap_rpc:clear(Node),
- unicode:characters_to_binary(Source).
-
--spec to_pid(pos_integer(), #{thread_id() => thread()}) -> pid().
-to_pid(ThreadId, Threads) ->
- Thread = maps:get(ThreadId, Threads),
- maps:get(pid, Thread).
-
--spec frame_by_id(frame_id(), [thread()]) -> frame() | undefined.
-frame_by_id(FrameId, Threads) ->
- case [ maps:get(FrameId, Frames)
- || #{frames := Frames} <- Threads, maps:is_key(FrameId, Frames)
- ] of
- [Frame] -> Frame;
- _ -> undefined
- end.
-
--spec pid_by_frame_id(frame_id(), [thread()]) -> pid() | undefined.
-pid_by_frame_id(FrameId, Threads) ->
- case [ Pid
- || #{frames := Frames, pid := Pid} <- Threads
- , maps:is_key(FrameId, Frames)
- ] of
- [Proc] -> Proc;
- _ ->
- undefined
- end.
-
--spec format_mfa(module(), atom(), integer()) -> binary().
-format_mfa(M, F, A) ->
- els_utils:to_binary(io_lib:format("~p:~p/~p", [M, F, A])).
-
--spec parse_mfa(string()) -> {module(), atom(), non_neg_integer()} | error.
-parse_mfa(MFABinary) ->
- MFA = unicode:characters_to_list(MFABinary),
- case erl_scan:string(MFA) of
- {ok, [ {'fun', _}
- , {atom, _, Module}
- , {':', _}
- , {atom, _, Function}
- , {'/', _}
- , {integer, _, Arity}], _} when Arity >= 0 ->
- {Module, Function, Arity};
- {ok, [ {atom, _, Module}
- , {':', _}
- , {atom, _, Function}
- , {'/', _}
- , {integer, _, Arity}], _} when Arity >= 0 ->
- {Module, Function, Arity};
- _ ->
- error
- end.
-
--spec build_variables(binding_type(), bindings()) ->
- {[any()], #{pos_integer() => bindings()}}.
-build_variables(Type, Bindings) ->
- build_variables(Type, Bindings, {[], #{}}).
-
--spec build_variables(binding_type(), bindings(), Acc) -> Acc
- when Acc :: {[any()], #{pos_integer() => bindings()}}.
-build_variables(_, [], Acc) ->
- Acc;
-build_variables(generic, [{Name, Value} | Rest], Acc) when is_list(Value) ->
- build_variables(
- generic,
- Rest,
- add_var_to_acc(Name, Value, build_list_bindings(Value), Acc)
- );
-build_variables(generic, [{Name, Value} | Rest], Acc) when is_tuple(Value) ->
- build_variables(
- generic,
- Rest,
- add_var_to_acc(Name, Value, build_tuple_bindings(Value), Acc)
- );
-build_variables(generic, [{Name, Value} | Rest], Acc) when is_map(Value) ->
- build_variables(
- generic,
- Rest,
- add_var_to_acc(Name, Value, build_map_bindings(Value), Acc)
- );
-build_variables(generic, [{Name, Value} | Rest], Acc) ->
- build_variables(
- generic,
- Rest,
- add_var_to_acc(Name, Value, none, Acc)
- );
-build_variables(map_assoc, [{Name, Assocs} | Rest], Acc) ->
- {_, [{'Value', Value}, {'Key', Key}]} = Assocs,
- build_variables(
- map_assoc,
- Rest,
- add_var_to_acc(Name, {Key, Value}, Assocs, Acc)
- ).
-
--spec add_var_to_acc(
- varname(),
- term(),
- none | {binding_type(), bindings()},
- Acc
-) -> Acc
- when Acc :: {[any()], #{non_neg_integer() => bindings()}}.
-add_var_to_acc(Name, Value, none, {VarAcc, BindAcc}) ->
- { [build_variable(Name, Value, 0) | VarAcc]
- , BindAcc};
-add_var_to_acc(Name, Value, Bindings, {VarAcc, BindAcc}) ->
- Ref = erlang:unique_integer([positive]),
- { [build_variable(Name, Value, Ref) | VarAcc]
- , BindAcc#{ Ref => Bindings}
- }.
-
--spec build_variable(varname(), term(), non_neg_integer()) -> any().
-build_variable(Name, Value, Ref) ->
- %% print whole term to enable copying if the value
- #{ <<"name">> => unicode:characters_to_binary(io_lib:format("~s", [Name]))
- , <<"value">> => format_term(Value)
- , <<"variablesReference">> => Ref }.
-
--spec build_list_bindings(
- maybe_improper_list()
-) -> {binding_type(), bindings()}.
-build_list_bindings(List) ->
- build_maybe_improper_list_bindings(List, 0, []).
-
--spec build_tuple_bindings(tuple()) -> {binding_type(), bindings()}.
-build_tuple_bindings(Tuple) ->
- build_list_bindings(erlang:tuple_to_list(Tuple)).
-
--spec build_map_bindings(map()) -> {binding_type(), bindings()}.
-build_map_bindings(Map) ->
- {_, Bindings} =
- lists:foldl(
- fun ({Key, Value}, {Cnt, Acc}) ->
- Name =
- unicode:characters_to_binary(
- io_lib:format("~s => ~s", [format_term(Key), format_term(Value)])),
- { Cnt + 1
- , [{ Name
- , {generic, [{'Value', Value}, {'Key', Key}]}
- } | Acc]
- }
- end,
- {0, []}, maps:to_list(Map)),
- {map_assoc, Bindings}.
-
--spec build_maybe_improper_list_bindings(
- maybe_improper_list(),
- non_neg_integer(),
- bindings()
-) -> {binding_type(), bindings()}.
-build_maybe_improper_list_bindings([], _, Acc) ->
- {generic, Acc};
-build_maybe_improper_list_bindings([E | Tail], Cnt, Acc) ->
- Binding = {erlang:integer_to_list(Cnt), E},
- build_maybe_improper_list_bindings(Tail, Cnt + 1, [Binding | Acc]);
-build_maybe_improper_list_bindings(ImproperTail, _Cnt, Acc) ->
- Binding = {"improper tail", ImproperTail},
- build_maybe_improper_list_bindings([], 0, [Binding | Acc]).
-
--spec is_structured(term()) -> boolean().
-is_structured(Term) when
- is_list(Term) orelse
- is_map(Term) orelse
- is_tuple(Term) -> true;
-is_structured(_) -> false.
-
--spec build_evaluate_response(term(), state()) -> {any(), state()}.
-build_evaluate_response(
- ResultValue,
- State = #{scope_bindings := ExistingScopes}
-) ->
- ResultBinary = format_term(ResultValue),
- case is_structured(ResultValue) of
- true ->
- {_, SubScope} = build_variables(generic, [{undefined, ResultValue}]),
- %% there is onlye one sub-scope returned
- [Ref] = maps:keys(SubScope),
- NewScopes = maps:merge(ExistingScopes, SubScope),
- { #{<<"result">> => ResultBinary, <<"variablesReference">> => Ref}
- , State#{scope_bindings => NewScopes}
- };
- false ->
- { #{<<"result">> => ResultBinary}
- , State
- }
- end.
-
--spec format_term(term()) -> binary().
-format_term(T) ->
- %% print on one line and print strings
- %% as printable characters (if possible)
- els_utils:to_binary(
- [ string:trim(Line)
- || Line <- string:split(io_lib:format("~tp", [T]), "\n", all)]).
-
--spec collect_frames(node(), pid(), pos_integer(), Backtrace, Acc) -> Acc
- when Acc :: #{frame_id() => frame()},
- Backtrace :: [{pos_integer(), {module(), atom(), non_neg_integer()}}].
-collect_frames(_, _, _, [], Acc) -> Acc;
-collect_frames(Node, Meta, Level, [{NextLevel, {M, F, A}} | Rest], Acc) ->
- case els_dap_rpc:meta(Node, Meta, stack_frame, {up, Level}) of
- {NextLevel, {_, Line}, Bindings} ->
- StackFrameId = erlang:unique_integer([positive]),
- StackFrame = #{ module => M
- , function => F
- , arguments => A
- , source => source(M, Node)
- , line => Line
- , bindings => Bindings},
- collect_frames( Node
- , Meta
- , NextLevel
- , Rest
- , Acc#{StackFrameId => StackFrame}
- );
- BadFrame ->
- ?LOG_ERROR( "Received a bad frame: ~p expected level ~p and module ~p"
- , [BadFrame, NextLevel, M]),
- Acc
- end.
-
--spec ensure_connected(node(), timeout()) -> ok.
-ensure_connected(Node, Timeout) ->
- case is_node_connected(Node) of
- true -> ok;
- false ->
- % connect and monitore project node
- case els_distribution_server:wait_connect_and_monitor( Node
- , Timeout
- , hidden) of
- ok -> inject_dap_agent(Node);
- _ -> stop_debugger()
- end
- end.
-
--spec stop_debugger() -> no_return().
-stop_debugger() ->
- %% the project node is down, there is nothing left to do then to exit
- els_dap_server:send_event(<<"terminated">>, #{}),
- els_dap_server:send_event(<<"exited">>, #{ <<"exitCode">> => <<"0">>}),
- ?LOG_NOTICE("terminating debug adapter"),
- els_utils:halt(0).
-
--spec is_node_connected(node()) -> boolean().
-is_node_connected(Node) ->
- lists:member(Node, erlang:nodes(connected)).
-
--spec safe_eval(node(), pid(), string(), update | no_update) -> term().
-safe_eval(ProjectNode, Debugged, Expression, Update) ->
- {ok, Meta} = els_dap_rpc:get_meta(ProjectNode, Debugged),
- Command = els_utils:to_list(Expression),
- Return = els_dap_rpc:meta_eval(ProjectNode, Meta, Command),
- case Update of
- update -> ok;
- no_update ->
- receive
- {int_cb, Debugged} -> ok
- end
- end,
- Return.
-
--spec check_project_node_name(binary(), boolean()) -> atom().
-check_project_node_name(ProjectNode, false) ->
- binary_to_atom(ProjectNode, utf8);
-check_project_node_name(ProjectNode, true) ->
- case binary:match(ProjectNode, <<"@">>) of
- nomatch ->
- {ok, HostName} = inet:gethostname(),
- BinHostName = list_to_binary(HostName),
- DomainStr = proplists:get_value(domain, inet:get_rc(), ""),
- Domain = list_to_binary(DomainStr),
- BinName = <>,
- binary_to_atom(BinName, utf8);
- _ ->
- binary_to_atom(ProjectNode, utf8)
- end.
-
--spec start_distribution(map()) -> map().
-start_distribution(Params) ->
- #{<<"cwd">> := Cwd} = Params,
- ok = file:set_cwd(Cwd),
- Name = filename:basename(Cwd),
-
- %% get default and final launch config
- DefaultConfig = #{
- <<"projectnode">> =>
- atom_to_binary(
- els_distribution_server:node_name(<<"erlang_ls_dap_project">>, Name),
- utf8
- ),
- <<"cookie">> => atom_to_binary(erlang:get_cookie(), utf8),
- <<"timeout">> => 30,
- <<"use_long_names">> => false
- },
- Config = maps:merge(DefaultConfig, Params),
- #{ <<"projectnode">> := RawProjectNode
- , <<"cookie">> := ConfCookie
- , <<"use_long_names">> := UseLongNames} = Config,
- ConfProjectNode = check_project_node_name(RawProjectNode, UseLongNames),
- ?LOG_INFO("Configured Project Node Name: ~p", [ConfProjectNode]),
- Cookie = binary_to_atom(ConfCookie, utf8),
-
- NameType = case UseLongNames of
- true ->
- longnames;
- false ->
- shortnames
- end,
- %% start distribution
- LocalNode = els_distribution_server:node_name("erlang_ls_dap",
- binary_to_list(Name), NameType),
- els_distribution_server:start_distribution(LocalNode, ConfProjectNode,
- Cookie, NameType),
- ?LOG_INFO("Distribution up on: [~p]", [LocalNode]),
-
- Config#{ <<"projectnode">> => ConfProjectNode}.
diff --git a/apps/els_dap/src/els_dap_methods.erl b/apps/els_dap/src/els_dap_methods.erl
deleted file mode 100644
index 2b6fe1739..000000000
--- a/apps/els_dap/src/els_dap_methods.erl
+++ /dev/null
@@ -1,65 +0,0 @@
-%%=============================================================================
-%% @doc DAP Methods Dispatcher
-%%
-%% Dispatches the handling of a command to the corresponding provider.
-%% @end
-%%=============================================================================
--module(els_dap_methods).
-
--export([ dispatch/4 ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include("els_dap.hrl").
--include_lib("kernel/include/logger.hrl").
-
--type method_name() :: binary().
--type state() :: map().
--type params() :: map().
--type result() :: {response, params() | null, state()}
- | {error_response, binary(), state()}
- | {noresponse, state()}
- | {notification, binary(), params(), state()}.
--type request_type() :: notification | request.
-
-%%==============================================================================
-%% @doc Dispatch the handling of the method to els_method
-%%==============================================================================
--spec dispatch(method_name(), params(), request_type(), state()) -> result().
-dispatch(Command, Args, Type, State) ->
- ?LOG_DEBUG("Dispatching request [command=~p] [args=~p]", [Command, Args]),
- try do_dispatch(Command, Args, State)
- catch
- error:function_clause ->
- not_implemented_method(Command, State);
- Type:Reason:Stack ->
- ?LOG_ERROR( "Unexpected error [type=~p] [error=~p] [stack=~p]"
- , [Type, Reason, Stack]),
- Error = #{ code => ?ERR_UNKNOWN_ERROR_CODE
- , message => <<"Unexpected error while ", Command/binary>>
- },
- {error_response, Error, State}
- end.
-
--spec do_dispatch(atom(), params(), state()) -> result().
-do_dispatch(Command, Args, #{status := initialized} = State) ->
- Request = {Command, Args},
- Result = els_provider:handle_request(els_dap_general_provider, Request),
- {response, Result, State};
-do_dispatch(<<"initialize">>, Args, State) ->
- Request = {<<"initialize">>, Args},
- Result = els_provider:handle_request(els_dap_general_provider, Request),
- {response, Result, State#{status => initialized}};
-do_dispatch(_Command, _Args, State) ->
- Message = <<"The server is not fully initialized yet, please wait.">>,
- Result = #{ code => ?ERR_SERVER_NOT_INITIALIZED
- , message => Message
- },
- {error, Result, State}.
-
--spec not_implemented_method(method_name(), state()) -> result().
-not_implemented_method(Command, State) ->
- ?LOG_WARNING("[Command not implemented] [command=~s]", [Command]),
- Error = <<"Command not implemented: ", Command/binary>>,
- {error_response, Error, State}.
diff --git a/apps/els_dap/src/els_dap_protocol.erl b/apps/els_dap/src/els_dap_protocol.erl
deleted file mode 100644
index 936f8b78f..000000000
--- a/apps/els_dap/src/els_dap_protocol.erl
+++ /dev/null
@@ -1,93 +0,0 @@
-%%==============================================================================
-%% @doc Debug Adapter Protocol
-%%
-%% Handles the building and encoding of the messages supported by the
-%% protocol.
-%% @end
-%%==============================================================================
--module(els_dap_protocol).
-
-%%==============================================================================
-%% Exports
-%%==============================================================================
-%% Messaging API
--export([ event/3
- , request/3
- , response/3
- , error_response/3
- ]).
-
-%% Data Structures
--export([ range/1
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include("els_dap.hrl").
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Messaging API
-%%==============================================================================
--spec event(number(), binary(), any()) -> binary().
-%% TODO: Body is optional
-event(Seq, EventType, Body) ->
- Message = #{ type => <<"event">>
- , seq => Seq
- , event => EventType
- , body => Body
- },
- content(jsx:encode(Message)).
-
--spec request(number(), binary(), any()) -> binary().
-request(RequestSeq, Method, Params) ->
- Message = #{ type => <<"request">>
- , seq => RequestSeq
- , command => Method
- , arguments => Params
- },
- content(jsx:encode(Message)).
-
--spec response(number(), any(), any()) -> binary().
-response(Seq, Command, Result) ->
- Message = #{ type => <<"response">>
- , request_seq => Seq
- , success => true
- , command => Command
- , body => Result
- },
- ?LOG_DEBUG("[Response] [message=~p]", [Message]),
- content(jsx:encode(Message)).
-
--spec error_response(number(), any(), any()) -> binary().
-error_response(Seq, Command, Error) ->
- Message = #{ type => <<"response">>
- , request_seq => Seq
- , success => false
- , command => Command
- , body => #{ error => Error
- }
- },
- ?LOG_DEBUG("[Response] [message=~p]", [Message]),
- content(jsx:encode(Message)).
-
-%%==============================================================================
-%% Data Structures
-%%==============================================================================
--spec range(poi_range()) -> range().
-range(#{ from := {FromL, FromC}, to := {ToL, ToC} }) ->
- #{ start => #{line => FromL - 1, character => FromC - 1}
- , 'end' => #{line => ToL - 1, character => ToC - 1}
- }.
-
-%%==============================================================================
-%% Internal Functions
-%%==============================================================================
--spec content(binary()) -> binary().
-content(Body) ->
-els_utils:to_binary([headers(Body), "\r\n", Body]).
-
--spec headers(binary()) -> iolist().
-headers(Body) ->
- io_lib:format("Content-Length: ~p\r\n", [byte_size(Body)]).
diff --git a/apps/els_dap/src/els_dap_provider.erl b/apps/els_dap/src/els_dap_provider.erl
deleted file mode 100644
index 5e6963064..000000000
--- a/apps/els_dap/src/els_dap_provider.erl
+++ /dev/null
@@ -1,103 +0,0 @@
-%%==============================================================================
-%% @doc Erlang DAP Provider Behaviour
-%% @end
-%%==============================================================================
--module(els_dap_provider).
-
-%% API
--export([ handle_request/2
- , start_link/1
- , available_providers/0
- , enabled_providers/0
- ]).
-
--behaviour(gen_server).
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , handle_info/2
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include_lib("kernel/include/logger.hrl").
-
--callback is_enabled() -> boolean().
--callback init() -> any().
--callback handle_request(request(), any()) -> {any(), any()}.
--callback handle_info(any(), any()) -> any().
--optional_callbacks([init/0, handle_info/2]).
-
--type config() :: any().
--type provider() :: els_dap_general_provider.
--type request() :: {atom(), map()}.
--type state() :: #{ provider := provider()
- , internal_state := any()
- }.
-
--export_type([ config/0
- , provider/0
- , request/0
- , state/0
- ]).
-
-%%==============================================================================
-%% External functions
-%%==============================================================================
-
--spec start_link(provider()) -> {ok, pid()}.
-start_link(Provider) ->
- gen_server:start_link({local, Provider}, ?MODULE, Provider, []).
-
--spec handle_request(provider(), request()) -> any().
-handle_request(Provider, Request) ->
- gen_server:call(Provider, {handle_request, Provider, Request}, infinity).
-
--spec available_providers() -> [provider()].
-available_providers() ->
- [ els_dap_general_provider
- ].
-
--spec enabled_providers() -> [provider()].
-enabled_providers() ->
- [Provider || Provider <- available_providers(), Provider:is_enabled()].
-
-%%==============================================================================
-%% gen_server callbacks
-%%==============================================================================
-
--spec init(els_provider:provider()) -> {ok, state()}.
-init(Provider) ->
- ?LOG_INFO("Starting provider ~p", [Provider]),
- InternalState = case erlang:function_exported(Provider, init, 0) of
- true ->
- Provider:init();
- false ->
- #{}
- end,
- {ok, #{provider => Provider, internal_state => InternalState}}.
-
--spec handle_call(any(), {pid(), any()}, state()) ->
- {reply, any(), state()}.
-handle_call({handle_request, Request}, _From, State) ->
- #{internal_state := InternalState, provider := Provider} = State,
- {Reply, NewInternalState} = Provider:handle_request(Request, InternalState),
- {reply, Reply, State#{internal_state => NewInternalState}}.
-
--spec handle_cast(any(), state()) ->
- {noreply, state()}.
-handle_cast(_Request, State) ->
- {noreply, State}.
-
--spec handle_info(any(), state()) ->
- {noreply, state()}.
-handle_info(Request, State) ->
- #{provider := Provider, internal_state := InternalState} = State,
- case erlang:function_exported(Provider, handle_info, 2) of
- true ->
- NewInternalState = Provider:handle_info(Request, InternalState),
- {noreply, State#{internal_state => NewInternalState}};
- false ->
- {noreply, State}
- end.
diff --git a/apps/els_dap/src/els_dap_providers_sup.erl b/apps/els_dap/src/els_dap_providers_sup.erl
deleted file mode 100644
index 57941e7da..000000000
--- a/apps/els_dap/src/els_dap_providers_sup.erl
+++ /dev/null
@@ -1,63 +0,0 @@
-%%==============================================================================
-%% @doc Erlang DAP Providers' Supervisor
-%% @end
-%%==============================================================================
--module(els_dap_providers_sup).
-
-%%==============================================================================
-%% Behaviours
-%%==============================================================================
--behaviour(supervisor).
-
-%%==============================================================================
-%% Exports
-%%==============================================================================
-
-%% API
--export([ start_link/0 ]).
-
-%% Supervisor Callbacks
--export([ init/1 ]).
-
-%%==============================================================================
-%% Defines
-%%==============================================================================
--define(SERVER, ?MODULE).
-
-%%==============================================================================
-%% Type Definitions
-%%==============================================================================
--type provider_spec() ::
- #{ id := els_dap_provider:provider()
- , start := { els_dap_provider
- , start_link
- , [els_dap_provider:provider()]
- }
- , shutdown := brutal_kill
- }.
-
-%%==============================================================================
-%% API
-%%==============================================================================
--spec start_link() -> {ok, pid()}.
-start_link() ->
- supervisor:start_link({local, ?SERVER}, ?MODULE, []).
-
-%%==============================================================================
-%% Supervisor callbacks
-%%==============================================================================
--spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
-init([]) ->
- SupFlags = #{ strategy => one_for_one
- , intensity => 1
- , period => 5
- },
- ChildSpecs = [provider_specs(P) || P <- els_dap_provider:enabled_providers()],
- {ok, {SupFlags, ChildSpecs}}.
-
--spec provider_specs(els_dap_provider:provider()) -> provider_spec().
-provider_specs(Provider) ->
- #{ id => Provider
- , start => {els_dap_provider, start_link, [Provider]}
- , shutdown => brutal_kill
- }.
diff --git a/apps/els_dap/src/els_dap_rpc.erl b/apps/els_dap/src/els_dap_rpc.erl
deleted file mode 100644
index bf269e325..000000000
--- a/apps/els_dap/src/els_dap_rpc.erl
+++ /dev/null
@@ -1,146 +0,0 @@
--module(els_dap_rpc).
-
--export([ interpreted/1
- , n/2
- , all_breaks/1
- , all_breaks/2
- , auto_attach/3
- , break/3
- , break_in/4
- , clear/1
- , continue/2
- , eval/3
- , file/2
- , get_meta/2
- , halt/1
- , i/2
- , load_binary/4
- , meta/4
- , meta_eval/3
- , module_info/3
- , next/2
- , no_break/1
- , no_break/2
- , snapshot/1
- , stack_trace/2
- , step/2
- ]).
-
--spec interpreted(node()) -> any().
-interpreted(Node) ->
- rpc:call(Node, int, interpreted, []).
-
--spec n(node(), any()) -> any().
-n(Node, Module) ->
- rpc:call(Node, int, n, [Module]).
-
--spec all_breaks(node()) -> any().
-all_breaks(Node) ->
- rpc:call(Node, int, all_breaks, []).
-
--spec all_breaks(node(), atom()) -> any().
-all_breaks(Node, Module) ->
- rpc:call(Node, int, all_breaks, [Module]).
-
--spec auto_attach(node(), [atom()], {module(), atom(), [any()]}) -> any().
-auto_attach(Node, Flags, MFA) ->
- rpc:call(Node, int, auto_attach, [Flags, MFA]).
-
--spec break(node(), module(), integer()) -> any().
-break(Node, Module, Line) ->
- rpc:call(Node, int, break, [Module, Line]).
-
--spec break_in(node(), module(), atom(), non_neg_integer()) -> any().
-break_in(Node, Module, Func, Arity) ->
- rpc:call(Node, int, break_in, [ Module, Func, Arity]).
-
--spec clear(node()) -> ok.
-clear(Node) ->
- rpc:call(Node, int, clear, []).
-
--spec continue(node(), pid()) -> any().
-continue(Node, Pid) ->
- rpc:call(Node, int, continue, [Pid]).
-
--spec eval(node(), string(), [any()]) -> any().
-eval(Node, Input, Bindings) ->
- {ok, Tokens, _} = erl_scan:string(unicode:characters_to_list(Input) ++ "."),
- {ok, Exprs} = erl_parse:parse_exprs(Tokens),
-
- case rpc:call(Node, erl_eval, exprs, [Exprs, Bindings]) of
- {value, Value, _NewBindings} -> Value;
- {badrpc, Error} -> Error
- end.
-
--spec file(node(), module()) -> file:filename().
-file(Node, Module) ->
- MaybeSource =
- case rpc:call(Node, int, file, [Module]) of
- {error, not_loaded} ->
- BeamName = atom_to_list(Module) ++ ".beam",
- case rpc:call(Node, code, where_is_file, [BeamName]) of
- non_existing -> {error, not_found};
- BeamFile -> rpc:call(Node, filelib, find_source, [BeamFile])
- end;
- IntSource ->
- {ok, IntSource}
- end,
- case MaybeSource of
- {ok, Source} ->
- Source;
- {error, not_found} ->
- CompileOpts = module_info(Node, Module, compile),
- proplists:get_value(source, CompileOpts)
- end.
-
--spec get_meta(node(), pid()) -> {ok, pid()}.
-get_meta(Node, Pid) ->
- rpc:call(Node, dbg_iserver, safe_call, [{get_meta, Pid}]).
-
--spec halt(node()) -> true.
-halt(Node) ->
- rpc:cast(Node, erlang, halt, []).
-
--spec i(node(), module()) -> any().
-i(Node, Module) ->
- rpc:call(Node, int, i, [Module]).
-
--spec load_binary(node(), module(), string(), binary()) -> any().
-load_binary(Node, Module, File, Bin) ->
- rpc:call(Node, code, load_binary, [Module, File, Bin]).
-
--spec meta(node(), pid(), atom(), any()) -> any().
-meta(Node, Meta, Flag, Opt) ->
- rpc:call(Node, int, meta, [Meta, Flag, Opt]).
-
--spec meta_eval(node(), pid(), string()) -> any().
-meta_eval(Node, Meta, Command) ->
- rpc:call(Node, els_dap_agent, meta_eval, [Meta, Command]).
-
--spec next(node(), pid()) -> any().
-next(Node, Pid) ->
- rpc:call(Node, int, next, [Pid]).
-
--spec no_break(node()) -> ok.
-no_break(Node) ->
- rpc:call(Node, int, no_break, []).
-
--spec no_break(node(), atom()) -> ok.
-no_break(Node, Module) ->
- rpc:call(Node, int, no_break, [Module]).
-
--spec module_info(node(), module(), atom()) -> any().
-module_info(Node, Module, What) ->
- rpc:call(Node, Module, module_info, [What]).
-
--spec snapshot(node()) -> any().
-snapshot(Node) ->
- rpc:call(Node, int, snapshot, []).
-
--spec stack_trace(node(), any()) -> any().
-stack_trace(Node, Flag) ->
- rpc:call(Node, int, stack_trace, [Flag]).
-
--spec step(node(), pid()) -> any().
-step(Node, Pid) ->
- rpc:call(Node, int, step, [Pid]).
diff --git a/apps/els_dap/src/els_dap_server.erl b/apps/els_dap/src/els_dap_server.erl
deleted file mode 100644
index e1cfca48b..000000000
--- a/apps/els_dap/src/els_dap_server.erl
+++ /dev/null
@@ -1,175 +0,0 @@
-%%==============================================================================
-%% @doc Erlang DAP Server
-%%
-%% This process is the middleware that receives the protocol messages,
-%% forwards them to the dispatcher and sends the result back to the
-%% client using the configured transport.
-%% @end
-%%==============================================================================
--module(els_dap_server).
-
-%%==============================================================================
-%% Behaviours
-%%==============================================================================
--behaviour(gen_server).
-
-%%==============================================================================
-%% Exports
-%%==============================================================================
-
--export([ start_link/0
- ]).
-
-%% gen_server callbacks
--export([ init/1
- , handle_call/3
- , handle_cast/2
- ]).
-
-%% API
--export([ process_requests/1
- , set_io_device/1
- , send_event/2
- , send_request/2
- ]).
-
-%% Testing
--export([ reset_internal_state/0
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Macros
-%%==============================================================================
--define(SERVER, ?MODULE).
-
-%%==============================================================================
-%% Record Definitions
-%%==============================================================================
--record(state, { io_device :: any()
- , seq :: number()
- , internal_state :: map()
- }).
-
-%%==============================================================================
-%% Type Definitions
-%%==============================================================================
--type state() :: #state{}.
-
-%%==============================================================================
-%% API
-%%==============================================================================
--spec start_link() -> {ok, pid()}.
-start_link() ->
- {ok, Pid} = gen_server:start_link({local, ?SERVER}, ?MODULE, [], []),
- Cb = fun(Requests) ->
- gen_server:cast(Pid, {process_requests, Requests})
- end,
- {ok, _} = els_stdio:start_listener(Cb),
- {ok, Pid}.
-
--spec process_requests([any()]) -> ok.
-process_requests(Requests) ->
- gen_server:cast(?SERVER, {process_requests, Requests}).
-
--spec set_io_device(atom() | pid()) -> ok.
-set_io_device(IoDevice) ->
- gen_server:call(?SERVER, {set_io_device, IoDevice}).
-
--spec send_event(binary(), map()) -> ok.
-send_event(EventType, Body) ->
- gen_server:cast(?SERVER, {event, EventType, Body}).
-
--spec send_request(binary(), map()) -> ok.
-send_request(Method, Params) ->
- gen_server:cast(?SERVER, {request, Method, Params}).
-
-%%==============================================================================
-%% Testing
-%%==============================================================================
--spec reset_internal_state() -> ok.
-reset_internal_state() ->
- gen_server:call(?MODULE, {reset_internal_state}).
-
-%%==============================================================================
-%% gen_server callbacks
-%%==============================================================================
--spec init([]) -> {ok, state()}.
-init([]) ->
- ?LOG_INFO("Starting els_dap_server..."),
- State = #state{ seq = 0
- , internal_state = #{}
- },
- {ok, State}.
-
--spec handle_call(any(), any(), state()) -> {reply, any(), state()}.
-handle_call({set_io_device, IoDevice}, _From, State) ->
- {reply, ok, State#state{io_device = IoDevice}};
-handle_call({reset_internal_state}, _From, State) ->
- {reply, ok, State#state{internal_state = #{}}}.
-
--spec handle_cast(any(), state()) -> {noreply, state()}.
-handle_cast({process_requests, Requests}, State0) ->
- State = lists:foldl(fun handle_request/2, State0, Requests),
- {noreply, State};
-handle_cast({event, EventType, Body}, State0) ->
- State = do_send_event(EventType, Body, State0),
- {noreply, State};
-handle_cast({request, Method, Params}, State0) ->
- State = do_send_request(Method, Params, State0),
- {noreply, State};
-handle_cast(_, State) ->
- {noreply, State}.
-
-%%==============================================================================
-%% Internal Functions
-%%==============================================================================
--spec handle_request(map(), state()) -> state().
-handle_request(#{ <<"seq">> := Seq
- , <<"type">> := <<"request">>
- , <<"command">> := Command
- } = Request, #state{internal_state = InternalState} = State0) ->
- Args = maps:get(<<"arguments">>, Request, #{}),
- case els_dap_methods:dispatch(Command, Args, request, InternalState) of
- {response, Result, NewInternalState} ->
- Response = els_dap_protocol:response(Seq, Command, Result),
- ?LOG_DEBUG("[SERVER] Sending response [response=~s]", [Response]),
- send(Response, State0),
- State0#state{internal_state = NewInternalState};
- {error_response, Error, NewInternalState} ->
- Response = els_dap_protocol:error_response(Seq, Command, Error),
- ?LOG_DEBUG("[SERVER] Sending error response [response=~s]", [Response]),
- send(Response, State0),
- State0#state{internal_state = NewInternalState}
- end;
-handle_request(Response, State0) ->
- ?LOG_DEBUG( "[SERVER] got request response [response=~p]"
- , [Response]
- ),
- State0.
-
--spec do_send_event(binary(), map(), state()) -> state().
-do_send_event(EventType, Body, #state{seq = Seq0} = State0) ->
- Seq = Seq0 + 1,
- Event = els_dap_protocol:event(Seq, EventType, Body),
- ?LOG_DEBUG( "[SERVER] Sending event [type=~s]", [EventType]),
- send(Event, State0),
- State0#state{seq = Seq}.
-
--spec do_send_request(binary(), map(), state()) -> state().
-do_send_request(Method, Params, #state{seq = RequestId0} = State0) ->
- RequestId = RequestId0 + 1,
- Request = els_dap_protocol:request(RequestId, Method, Params),
- ?LOG_DEBUG( "[SERVER] Sending request [request=~p]"
- , [Request]
- ),
- send(Request, State0),
- State0#state{seq = RequestId}.
-
--spec send(binary(), state()) -> ok.
-send(Payload, #state{io_device = IoDevice}) ->
- els_stdio:send(IoDevice, Payload).
diff --git a/apps/els_dap/src/els_dap_sup.erl b/apps/els_dap/src/els_dap_sup.erl
deleted file mode 100644
index db85ea309..000000000
--- a/apps/els_dap/src/els_dap_sup.erl
+++ /dev/null
@@ -1,105 +0,0 @@
-%%==============================================================================
-%% @doc Erlang DAP Top Level Supervisor
-%% @end
-%%==============================================================================
--module(els_dap_sup).
-
-%%==============================================================================
-%% Behaviours
-%%==============================================================================
--behaviour(supervisor).
-
-%%==============================================================================
-%% Exports
-%%==============================================================================
-
-%% API
--export([ start_link/0 ]).
-
-%% Supervisor Callbacks
--export([ init/1 ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Defines
-%%==============================================================================
--define(SERVER, ?MODULE).
-
-%%==============================================================================
-%% API
-%%==============================================================================
--spec start_link() -> {ok, pid()}.
-start_link() ->
- supervisor:start_link({local, ?SERVER}, ?MODULE, []).
-
-%%==============================================================================
-%% supervisors callbacks
-%%==============================================================================
--spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
-init([]) ->
- SupFlags = #{ strategy => rest_for_one
- , intensity => 5
- , period => 60
- },
- {ok, Vsn} = application:get_key(vsn),
- ?LOG_INFO("Starting session (version ~p)", [Vsn]),
- restrict_stdio_access(),
- ChildSpecs = [ #{ id => els_config
- , start => {els_config, start_link, []}
- , shutdown => brutal_kill
- }
- , #{ id => els_dap_providers_sup
- , start => {els_dap_providers_sup, start_link, []}
- , type => supervisor
- }
- , #{ id => els_dap_server
- , start => {els_dap_server, start_link, []}
- }
- ],
- {ok, {SupFlags, ChildSpecs}}.
-
-%% @doc Restrict access to standard I/O
-%%
-%% Sets the `io_device' application variable to the current group
-%% leaders and replaces the group leader process of this supervisor,
-%% for a fake one. This fake group leader is propagated to all of this
-%% supervisor's children.
-%%
-%% This prevents any library that decides to write anything to
-%% standard output from corrupting the messages sent through JSONRPC.
-%% This problem is happening for example when calling `edoc:get_doc/2',
-%% which can print warnings to standard output.
--spec restrict_stdio_access() -> ok.
-restrict_stdio_access() ->
- ?LOG_INFO("Use group leader as io_device"),
- case application:get_env(els_core, io_device, standard_io) of
- standard_io ->
- application:set_env(els_core, io_device, erlang:group_leader());
- _ -> ok
- end,
-
- ?LOG_INFO("Replace group leader to avoid unwanted output to stdout"),
- Pid = erlang:spawn(fun noop_group_leader/0),
- erlang:group_leader(Pid, self()),
- ok.
-
-%% @doc Simulate a group leader but do nothing
--spec noop_group_leader() -> no_return().
-noop_group_leader() ->
- receive
- Message ->
- ?LOG_INFO("noop_group_leader got [message=~p]", [Message]),
- case Message of
- {io_request, From, ReplyAs, getopts} ->
- From ! {io_reply, ReplyAs, []};
- {io_request, From, ReplyAs, _} ->
- From ! {io_reply, ReplyAs, ok};
- _ ->
- ok
- end,
- noop_group_leader()
- end.
diff --git a/apps/els_dap/test/els_dap_SUITE.erl b/apps/els_dap/test/els_dap_SUITE.erl
deleted file mode 100644
index 3155a7a03..000000000
--- a/apps/els_dap/test/els_dap_SUITE.erl
+++ /dev/null
@@ -1,106 +0,0 @@
--module(els_dap_SUITE).
-
--include("els_dap.hrl").
-
-%% CT Callbacks
--export([ suite/0
- , init_per_suite/1
- , end_per_suite/1
- , init_per_testcase/2
- , end_per_testcase/2
- , groups/0
- , all/0
- ]).
-
-%% Test cases
--export([ parse_args/1
- , log_root/1
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include_lib("common_test/include/ct.hrl").
--include_lib("stdlib/include/assert.hrl").
-
-%%==============================================================================
-%% Types
-%%==============================================================================
--type config() :: [{atom(), any()}].
-
-%%==============================================================================
-%% CT Callbacks
-%%==============================================================================
--spec suite() -> [tuple()].
-suite() ->
- [{timetrap, {seconds, 30}}].
-
--spec all() -> [atom()].
-all() ->
- els_test_utils:all(?MODULE).
-
--spec groups() -> [atom()].
-groups() ->
- [].
-
--spec init_per_suite(config()) -> config().
-init_per_suite(_Config) ->
- [].
-
--spec end_per_suite(config()) -> ok.
-end_per_suite(_Config) ->
- ok.
-
--spec init_per_testcase(atom(), config()) -> config().
-init_per_testcase(_TestCase, _Config) ->
- [].
-
--spec end_per_testcase(atom(), config()) -> ok.
-end_per_testcase(_TestCase, _Config) ->
- unset_all_env(els_core),
- ok.
-
-%%==============================================================================
-%% Helpers
-%%==============================================================================
--spec unset_all_env(atom()) -> ok.
-unset_all_env(Application) ->
- Envs = application:get_all_env(Application),
- unset_env(Application, Envs).
-
--spec unset_env(atom(), list({atom(), term()})) -> ok.
-unset_env(_Application, []) ->
- ok;
-unset_env(Application, [{Par, _Val} | Rest]) ->
- application:unset_env(Application, Par),
- unset_env(Application, Rest).
-
-%%==============================================================================
-%% Testcases
-%%==============================================================================
--spec parse_args(config()) -> ok.
-parse_args(_Config) ->
- Args =
- [ "--log-dir"
- , "/test"
- , "--log-level"
- , "error"
- ],
- els_dap:parse_args(Args),
- ?assertEqual('error', application:get_env(els_core, log_level, undefined)),
- ok.
-
--spec log_root(config()) -> ok.
-log_root(_Config) ->
- meck:new(file, [unstick]),
- meck:expect(file, get_cwd, fun() -> {ok, "/root/els_dap"} end),
-
- Args =
- [ "--log-dir"
- , "/somewhere_else/logs"
- ],
- els_dap:parse_args(Args),
- ?assertEqual("/somewhere_else/logs/els_dap", els_dap:log_root()),
-
- meck:unload(file),
- ok.
diff --git a/apps/els_dap/test/els_dap_general_provider_SUITE.erl b/apps/els_dap/test/els_dap_general_provider_SUITE.erl
deleted file mode 100644
index 47f77d0fc..000000000
--- a/apps/els_dap/test/els_dap_general_provider_SUITE.erl
+++ /dev/null
@@ -1,662 +0,0 @@
--module(els_dap_general_provider_SUITE).
-
-%% CT Callbacks
--export([ suite/0
- , init_per_suite/1
- , end_per_suite/1
- , init_per_testcase/2
- , end_per_testcase/2
- , groups/0
- , all/0
- ]).
-
-%% Test cases
--export([ initialize/1
- , launch_mfa/1
- , launch_mfa_with_cookie/1
- , configuration_done/1
- , configuration_done_with_long_names/1
- , configuration_done_with_long_names_using_host/1
- , configuration_done_with_breakpoint/1
- , frame_variables/1
- , navigation_and_frames/1
- , set_variable/1
- , breakpoints/1
- , project_node_exit/1
- , breakpoints_with_cond/1
- , breakpoints_with_hit/1
- , breakpoints_with_cond_and_hit/1
- , log_points/1
- , log_points_with_lt_condition/1
- , log_points_with_eq_condition/1
- , log_points_with_hit/1
- , log_points_with_hit1/1
- , log_points_with_cond_and_hit/1
- , log_points_empty_cond/1
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include_lib("common_test/include/ct.hrl").
--include_lib("stdlib/include/assert.hrl").
-
-%%==============================================================================
-%% Types
-%%==============================================================================
--type config() :: [{atom(), any()}].
-
-%%==============================================================================
-%% CT Callbacks
-%%==============================================================================
--spec suite() -> [tuple()].
-suite() ->
- [{timetrap, {seconds, 30}}].
-
--spec all() -> [atom()].
-all() ->
- els_dap_test_utils:all(?MODULE).
-
--spec groups() -> [atom()].
-groups() ->
- [].
-
--spec init_per_suite(config()) -> config().
-init_per_suite(Config) ->
- Config.
-
--spec end_per_suite(config()) -> ok.
-end_per_suite(_Config) ->
- meck:unload().
-
--spec init_per_testcase(atom(), config()) -> config().
-init_per_testcase(TestCase, Config) when
- TestCase =:= undefined orelse
- TestCase =:= initialize orelse
- TestCase =:= launch_mfa orelse
- TestCase =:= launch_mfa_with_cookie orelse
- TestCase =:= configuration_done orelse
- TestCase =:= configuration_done_with_long_names orelse
- TestCase =:= configuration_done_with_long_names_using_host orelse
- TestCase =:= configuration_done_with_breakpoint orelse
- TestCase =:= log_points orelse
- TestCase =:= log_points_with_lt_condition orelse
- TestCase =:= log_points_with_eq_condition orelse
- TestCase =:= log_points_with_hit orelse
- TestCase =:= log_points_with_hit1 orelse
- TestCase =:= log_points_with_cond_and_hit orelse
- TestCase =:= log_points_empty_cond orelse
- TestCase =:= breakpoints_with_cond orelse
- TestCase =:= breakpoints_with_hit orelse
- TestCase =:= breakpoints_with_cond_and_hit ->
- {ok, DAPProvider} = els_provider:start_link(els_dap_general_provider),
- {ok, _} = els_config:start_link(),
- meck:expect(els_dap_server, send_event, 2, meck:val(ok)),
- [{provider, DAPProvider}, {node, node_name()} | Config];
-init_per_testcase(_TestCase, Config0) ->
- Config1 = init_per_testcase(undefined, Config0),
- %% initialize dap, equivalent to configuration_done_with_breakpoint
- try configuration_done_with_breakpoint(Config1) of
- ok -> Config1;
- R -> {user_skip, {error, dap_initialization, R}}
- catch
- Class:Reason ->
- {user_skip, {error, dap_initialization, Class, Reason}}
- end.
-
--spec end_per_testcase(atom(), config()) -> ok.
-end_per_testcase(_TestCase, Config) ->
- NodeName = ?config(node, Config),
- Node = binary_to_atom(NodeName, utf8),
- unset_all_env(els_core),
- ok = gen_server:stop(?config(provider, Config)),
- gen_server:stop(els_config),
- %% kill the project node
- rpc:cast(Node, erlang, halt, []),
- ok.
-
-%%==============================================================================
-%% Helpers
-%%==============================================================================
--spec unset_all_env(atom()) -> ok.
-unset_all_env(Application) ->
- Envs = application:get_all_env(Application),
- unset_env(Application, Envs).
-
--spec unset_env(atom(), list({atom(), term()})) -> ok.
-unset_env(_Application, []) ->
- ok;
-unset_env(Application, [{Par, _Val} | Rest]) ->
- application:unset_env(Application, Par),
- unset_env(Application, Rest).
-
--spec node_name() -> node().
-node_name() ->
- unicode:characters_to_binary(
- io_lib:format("~s~p@localhost", [?MODULE, erlang:unique_integer()])
- ).
-
--spec path_to_test_module(file:name(), module()) -> file:name().
-path_to_test_module(AppDir, Module) ->
- unicode:characters_to_binary(
- io_lib:format("~s.erl", [filename:join([AppDir, "src", Module])])
- ).
-
--spec wait_for_break(binary(), module(), non_neg_integer()) -> boolean().
-wait_for_break(NodeName, WantModule, WantLine) ->
- Node = binary_to_atom(NodeName, utf8),
- Checker =
- fun() ->
- Snapshots = rpc:call(Node, int, snapshot, []),
- lists:any(
- fun
- ({_, _, break, {Module, Line}}) when
- Module =:= WantModule andalso Line =:= WantLine
- ->
- true;
- (_) ->
- false
- end,
- Snapshots
- )
- end,
- els_dap_test_utils:wait_for_fun(Checker, 200, 20).
-
-%%==============================================================================
-%% Testcases
-%%==============================================================================
-
--spec initialize(config()) -> ok.
-initialize(Config) ->
- Provider = ?config(provider, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- ok.
-
--spec launch_mfa(config()) -> ok.
-launch_mfa(Config) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- Node = ?config(node, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, els_dap_test_module, entry, [])
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- ok.
-
--spec launch_mfa_with_cookie(config()) -> ok.
-launch_mfa_with_cookie(Config) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- Node = ?config(node, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, <<"some_cookie">>,
- els_dap_test_module, entry, [])
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- ok.
-
--spec configuration_done(config()) -> ok.
-configuration_done(Config) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- Node = ?config(node, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, els_dap_test_module, entry, [])
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- els_provider:handle_request(Provider, request_configuration_done(#{})),
- ok.
-
--spec configuration_done_with_long_names(config()) -> ok.
-configuration_done_with_long_names(Config) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- NodeStr = io_lib:format("~s~p", [?MODULE, erlang:unique_integer()]),
- Node = unicode:characters_to_binary(NodeStr),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, <<"some_cookie">>,
- els_dap_test_module, entry, [], use_long_names)
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- ok.
-
--spec configuration_done_with_long_names_using_host(config()) -> ok.
-configuration_done_with_long_names_using_host(Config) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- Node = ?config(node, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, <<"some_cookie">>,
- els_dap_test_module, entry, [], use_long_names)
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- ok.
-
--spec configuration_done_with_breakpoint(config()) -> ok.
-configuration_done_with_breakpoint(Config) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- Node = ?config(node, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, els_dap_test_module, entry, [5])
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
-
- els_provider:handle_request(
- Provider,
- request_set_breakpoints( path_to_test_module(DataDir, els_dap_test_module)
- , [9, 29])
- ),
- els_provider:handle_request(Provider, request_configuration_done(#{})),
- ?assertEqual(ok, wait_for_break(Node, els_dap_test_module, 9)),
- ok.
-
--spec frame_variables(config()) -> ok.
-frame_variables(Config) ->
- Provider = ?config(provider, Config),
- %% get thread ID from mocked DAP response
- #{ <<"reason">> := <<"breakpoint">>
- , <<"threadId">> := ThreadId} =
- meck:capture(last, els_dap_server, send_event, [<<"stopped">>, '_'], 2),
- %% get stackframe
- #{<<"stackFrames">> := [#{<<"id">> := FrameId}]} =
- els_provider:handle_request( Provider
- , request_stack_frames(ThreadId)
- ),
- %% get scope
- #{<<"scopes">> := [#{<<"variablesReference">> := VariableRef}]} =
- els_provider:handle_request(Provider, request_scope(FrameId)),
- %% extract variable
- #{<<"variables">> := [NVar]} =
- els_provider:handle_request(Provider, request_variable(VariableRef)),
- %% at this point there should be only one variable present,
- ?assertMatch(#{ <<"name">> := <<"N">>
- , <<"value">> := <<"5">>
- , <<"variablesReference">> := 0
- }
- , NVar),
- ok.
-
--spec navigation_and_frames(config()) -> ok.
-navigation_and_frames(Config) ->
- %% test next, stepIn, continue and check aginst expeted stack frames
- Provider = ?config(provider, Config),
- #{<<"threads">> := [#{<<"id">> := ThreadId}]} =
- els_provider:handle_request( Provider
- , request_threads()
- ),
- %% next
- %%, reset meck history, to capture next call
- meck:reset([els_dap_server]),
- els_provider:handle_request(Provider, request_next(ThreadId)),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- %% check
- #{<<"stackFrames">> := Frames1} =
- els_provider:handle_request( Provider
- , request_stack_frames(ThreadId)
- ),
- ?assertMatch([#{ <<"line">> := 11
- , <<"name">> := <<"els_dap_test_module:entry/1">>}], Frames1),
- %% continue
- meck:reset([els_dap_server]),
- els_provider:handle_request(Provider, request_continue(ThreadId)),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- %% check
- #{<<"stackFrames">> := Frames2} =
- els_provider:handle_request( Provider
- , request_stack_frames(ThreadId)
- ),
- ?assertMatch( [ #{ <<"line">> := 9
- , <<"name">> := <<"els_dap_test_module:entry/1">>}
- , #{ <<"line">> := 11
- , <<"name">> := <<"els_dap_test_module:entry/1">>}
- ]
- , Frames2
- ),
- %% stepIn
- meck:reset([els_dap_server]),
- els_provider:handle_request(Provider, request_step_in(ThreadId)),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- %% check
- #{<<"stackFrames">> := Frames3} =
- els_provider:handle_request( Provider
- , request_stack_frames(ThreadId)
- ),
- ?assertMatch( [ #{ <<"line">> := 15
- , <<"name">> := <<"els_dap_test_module:ds/0">>
- },
- #{ <<"line">> := 9
- , <<"name">> := <<"els_dap_test_module:entry/1">>},
- #{ <<"line">> := 11
- , <<"name">> := <<"els_dap_test_module:entry/1">>}
- ]
- , Frames3
- ),
- ok.
-
--spec set_variable(config()) -> ok.
-set_variable(Config) ->
- Provider = ?config(provider, Config),
- #{<<"threads">> := [#{<<"id">> := ThreadId}]} =
- els_provider:handle_request( Provider
- , request_threads()
- ),
- #{<<"stackFrames">> := [#{<<"id">> := FrameId1}]} =
- els_provider:handle_request( Provider
- , request_stack_frames(ThreadId)
- ),
- meck:reset([els_dap_server]),
- Result1 =
- els_provider:handle_request( Provider
- , request_evaluate( <<"repl">>
- , FrameId1
- , <<"N=1">>
- )
- ),
- ?assertEqual(#{<<"result">> => <<"1">>}, Result1),
-
- %% get variable value through hover evaluate
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- #{<<"stackFrames">> := [#{<<"id">> := FrameId2}]} =
- els_provider:handle_request( Provider
- , request_stack_frames(ThreadId)
- ),
- ?assertNotEqual(FrameId1, FrameId2),
- Result2 =
- els_provider:handle_request( Provider
- , request_evaluate( <<"hover">>
- , FrameId2
- , <<"N">>
- )
- ),
- ?assertEqual(#{<<"result">> => <<"1">>}, Result2),
- %% get variable value through scopes
- #{ <<"scopes">> := [ #{<<"variablesReference">> := VariableRef} ] } =
- els_provider:handle_request(Provider, request_scope(FrameId2)),
- %% extract variable
- #{<<"variables">> := [NVar]} =
- els_provider:handle_request( Provider
- , request_variable(VariableRef)
- ),
- %% at this point there should be only one variable present
- ?assertMatch( #{ <<"name">> := <<"N">>
- , <<"value">> := <<"1">>
- , <<"variablesReference">> := 0
- }
- , NVar
- ),
- ok.
-
--spec breakpoints(config()) -> ok.
-breakpoints(Config) ->
- Provider = ?config(provider, Config),
- NodeName = ?config(node, Config),
- Node = binary_to_atom(NodeName, utf8),
- DataDir = ?config(data_dir, Config),
- els_provider:handle_request(
- Provider,
- request_set_breakpoints( path_to_test_module(DataDir, els_dap_test_module)
- , [9])
- ),
- ?assertMatch([{{els_dap_test_module, 9}, _}], els_dap_rpc:all_breaks(Node)),
- els_provider:handle_request(
- Provider,
- request_set_function_breakpoints([<<"els_dap_test_module:entry/1">>])
- ),
- ?assertMatch(
- [{{els_dap_test_module, 7}, _}, {{els_dap_test_module, 9}, _}],
- els_dap_rpc:all_breaks(Node)
- ),
- els_provider:handle_request(
- Provider,
- request_set_breakpoints(path_to_test_module(DataDir, els_dap_test_module)
- , [])
- ),
- ?assertMatch(
- [{{els_dap_test_module, 7}, _}, {{els_dap_test_module, 9}, _}],
- els_dap_rpc:all_breaks(Node)
- ),
- els_provider:handle_request(
- Provider,
- request_set_breakpoints(path_to_test_module(DataDir, els_dap_test_module)
- , [9])
- ),
- els_provider:handle_request(
- Provider,
- request_set_function_breakpoints([])
- ),
- ?assertMatch([{{els_dap_test_module, 9}, _}], els_dap_rpc:all_breaks(Node)),
- ok.
-
--spec project_node_exit(config()) -> ok.
-project_node_exit(Config) ->
- NodeName = ?config(node, Config),
- Node = binary_to_atom(NodeName, utf8),
- meck:expect(els_utils, halt, 1, meck:val(ok)),
- meck:reset(els_dap_server),
- erlang:monitor_node(Node, true),
- %% kill node and wait for nodedown message
- rpc:cast(Node, erlang, halt, []),
- receive
- {nodedown, Node} -> ok
- end,
- %% wait until els_utils:halt has been called
- els_test_utils:wait_until_mock_called(els_utils, halt).
- %% there is a race condition in CI, important is that the process stops
- % ?assert(meck:called(els_dap_server, send_event, [<<"terminated">>, '_'])),
- % ?assert(meck:called(els_dap_server, send_event, [<<"exited">>, '_'])).
-
--spec breakpoints_with_cond(config()) -> ok.
-breakpoints_with_cond(Config) ->
- breakpoints_base(Config, 9, #{condition => <<"N =:= 5">>}, <<"5">>).
-
--spec breakpoints_with_hit(config()) -> ok.
-breakpoints_with_hit(Config) ->
- breakpoints_base(Config, 9, #{hitcond => <<"3">>}, <<"8">>).
-
--spec breakpoints_with_cond_and_hit(config()) -> ok.
-breakpoints_with_cond_and_hit(Config) ->
- Params = #{condition => <<"N < 7">>, hitcond => <<"3">>},
- breakpoints_base(Config, 9, Params, <<"4">>).
-
-%% Parameterizable base test for breakpoints: sets up a breakpoint with given
-%% parameters and checks the value of N when first hit
-breakpoints_base(Config, BreakLine, Params, NExp) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- Node = ?config(node, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, els_dap_test_module, entry, [10])
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- meck:reset([els_dap_server]),
-
- els_provider:handle_request(
- Provider,
- request_set_breakpoints(
- path_to_test_module(DataDir, els_dap_test_module),
- [{BreakLine, Params}]
- )
- ),
- %% hit breakpoint
- els_provider:handle_request(Provider, request_configuration_done(#{})),
- ?assertEqual(ok, wait_for_break(Node, els_dap_test_module, BreakLine)),
- %% check value of N
- #{<<"threads">> := [#{<<"id">> := ThreadId}]} =
- els_provider:handle_request( Provider
- , request_threads()
- ),
- #{<<"stackFrames">> := [#{<<"id">> := FrameId}|_]} =
- els_provider:handle_request( Provider
- , request_stack_frames(ThreadId)
- ),
- #{<<"scopes">> := [#{<<"variablesReference">> := VariableRef}]} =
- els_provider:handle_request(Provider, request_scope(FrameId)),
- #{<<"variables">> := [NVar]} =
- els_provider:handle_request(Provider, request_variable(VariableRef)),
- ?assertMatch(#{ <<"name">> := <<"N">>
- , <<"value">> := NExp
- , <<"variablesReference">> := 0
- }
- , NVar),
- ok.
-
--spec log_points(config()) -> ok.
-log_points(Config) ->
- log_points_base(Config, 9, #{log => <<"N">>}, 11, 1).
-
--spec log_points_with_lt_condition(config()) -> ok.
-log_points_with_lt_condition(Config) ->
- log_points_base(Config, 9, #{log => <<"N">>, condition => <<"N < 5">>}, 7, 4).
-
--spec log_points_with_eq_condition(config()) -> ok.
-log_points_with_eq_condition(Config) ->
- Params = #{log => <<"N">>, condition => <<"N =:= 5">>},
- log_points_base(Config, 9, Params, 7, 1).
-
--spec log_points_with_hit(config()) -> ok.
-log_points_with_hit(Config) ->
- log_points_base(Config, 9, #{log => <<"N">>, hitcond => <<"3">>}, 7, 3).
-
--spec log_points_with_hit1(config()) -> ok.
-log_points_with_hit1(Config) ->
- log_points_base(Config, 9, #{log => <<"N">>, hitcond => <<"1">>}, 7, 10).
-
--spec log_points_with_cond_and_hit(config()) -> ok.
-log_points_with_cond_and_hit(Config) ->
- Params = #{log => <<"N">>, condition => <<"N < 5">>, hitcond => <<"2">>},
- log_points_base(Config, 9, Params, 7, 2).
-
--spec log_points_empty_cond(config()) -> ok.
-log_points_empty_cond(Config) ->
- log_points_base(Config, 9, #{log => <<"N">>, condition => <<>>}, 11, 1).
-
-%% Parameterizable base test for logpoints: sets up a logpoint with given
-%% parameters and checks how many hits it gets before hitting a given breakpoint
-log_points_base(Config, LogLine, Params, BreakLine, NumCalls) ->
- Provider = ?config(provider, Config),
- DataDir = ?config(data_dir, Config),
- Node = ?config(node, Config),
- els_provider:handle_request(Provider, request_initialize(#{})),
- els_provider:handle_request(
- Provider,
- request_launch(DataDir, Node, els_dap_test_module, entry, [10])
- ),
- els_test_utils:wait_until_mock_called(els_dap_server, send_event),
- meck:reset([els_dap_server]),
-
- els_provider:handle_request(
- Provider,
- request_set_breakpoints(
- path_to_test_module(DataDir, els_dap_test_module),
- [{LogLine, Params}, BreakLine]
- )
- ),
- els_provider:handle_request(Provider, request_configuration_done(#{})),
- ?assertEqual(ok, wait_for_break(Node, els_dap_test_module, BreakLine)),
- ?assertEqual(NumCalls,
- meck:num_calls(els_dap_server, send_event, [<<"output">>, '_'])),
- ok.
-
-%%==============================================================================
-%% Requests
-%%==============================================================================
-
-request_initialize(Params) ->
- {<<"initialize">>, Params}.
-
-request_launch(Params) ->
- {<<"launch">>, Params}.
-
-request_launch(AppDir, Node, M, F, A) ->
- request_launch(
- #{ <<"projectnode">> => Node
- , <<"cwd">> => list_to_binary(AppDir)
- , <<"module">> => atom_to_binary(M, utf8)
- , <<"function">> => atom_to_binary(F, utf8)
- , <<"args">> => unicode:characters_to_binary(io_lib:format("~w", [A]))
- }).
-
-request_launch(AppDir, Node, Cookie, M, F, A) ->
- {<<"launch">>, Params} = request_launch(AppDir, Node, M, F, A),
- {<<"launch">>, Params#{<<"cookie">> => Cookie}}.
-
-request_launch(AppDir, Node, Cookie, M, F, A, use_long_names) ->
- {<<"launch">>, Params} = request_launch(AppDir, Node, M, F, A),
- {<<"launch">>, Params#{<<"cookie">> => Cookie,
- <<"use_long_names">> => true}}.
-
-request_configuration_done(Params) ->
- {<<"configurationDone">>, Params}.
-
-request_set_breakpoints(File, Specs) ->
- { <<"setBreakpoints">>
- , #{ <<"source">> => #{<<"path">> => File}
- , <<"sourceModified">> => false
- , <<"breakpoints">> => lists:map(fun map_spec/1, Specs)
- }}.
-
-map_spec({Line, Params}) ->
- Cond = case Params of
- #{condition := CondExpr} -> #{<<"condition">> => CondExpr};
- _ -> #{}
- end,
- Hit = case Params of
- #{hitcond := HitExpr} -> #{<<"hitCondition">> => HitExpr};
- _ -> #{}
- end,
- Log = case Params of
- #{log := LogMsg} -> #{<<"logMessage">> => LogMsg};
- _ -> #{}
- end,
- lists:foldl(fun maps:merge/2, #{<<"line">> => Line}, [Cond, Hit, Log]);
-map_spec(Line) -> #{<<"line">> => Line}.
-
-request_set_function_breakpoints(MFAs) ->
- {<<"setFunctionBreakpoints">>, #{
- <<"breakpoints">> => [#{ <<"name">> => MFA
- , <<"enabled">> => true} || MFA <- MFAs]
- }}.
-
-request_stack_frames(ThreadId) ->
- {<<"stackTrace">>, #{<<"threadId">> => ThreadId}}.
-
-request_scope(FrameId) ->
- {<<"scopes">>, #{<<"frameId">> => FrameId}}.
-
-request_variable(Ref) ->
- {<<"variables">>, #{<<"variablesReference">> => Ref}}.
-
-request_threads() ->
- {<<"threads">>, #{}}.
-
-request_step_in(ThreadId) ->
- {<<"stepIn">>, #{<<"threadId">> => ThreadId}}.
-
-request_next(ThreadId) ->
- {<<"next">>, #{<<"threadId">> => ThreadId}}.
-
-request_continue(ThreadId) ->
- {<<"continue">>, #{<<"threadId">> => ThreadId}}.
-
-request_evaluate(Context, FrameId, Expression) ->
- {<<"evaluate">>,
- #{ <<"context">> => Context
- , <<"frameId">> => FrameId
- , <<"expression">> => Expression
- }
- }.
diff --git a/apps/els_dap/test/els_dap_general_provider_SUITE_data/src/els_dap_test.app.src b/apps/els_dap/test/els_dap_general_provider_SUITE_data/src/els_dap_test.app.src
deleted file mode 100644
index c46ad3ae1..000000000
--- a/apps/els_dap/test/els_dap_general_provider_SUITE_data/src/els_dap_test.app.src
+++ /dev/null
@@ -1,9 +0,0 @@
-{application, els_dap_test, [
- {description, "test application"},
- {vsn, "0.1.0"},
- {applications, [
- kernel,
- stdlib
- ]},
- {modules, []}
-]}.
diff --git a/apps/els_dap/test/els_dap_general_provider_SUITE_data/src/els_dap_test_module.erl b/apps/els_dap/test/els_dap_general_provider_SUITE_data/src/els_dap_test_module.erl
deleted file mode 100644
index 965325eaf..000000000
--- a/apps/els_dap/test/els_dap_general_provider_SUITE_data/src/els_dap_test_module.erl
+++ /dev/null
@@ -1,32 +0,0 @@
--module(els_dap_test_module).
-
--export([entry/1]).
-
--spec entry(non_neg_integer()) -> ok.
-entry(0) ->
- ok;
-entry(N) ->
- ds(),
- %% tail recursive call
- entry(N - 1).
-
--spec ds() -> ok.
-ds() ->
- Atom = '42',
- Int = 42,
- Float = 42.0,
- Binary = <<"42">>,
- CharList = "42",
- Pid = self(),
- Ref = make_ref(),
- Tuple = {Binary, CharList},
- Map = #{
- Int => Float,
- Atom => Binary,
- CharList => Pid,
- Ref => Tuple
- },
- dummy(Map).
-
--spec dummy(map()) -> ok.
-dummy(_) -> ok.
diff --git a/apps/els_dap/test/els_dap_test_utils.erl b/apps/els_dap/test/els_dap_test_utils.erl
deleted file mode 100644
index 17f80ac77..000000000
--- a/apps/els_dap/test/els_dap_test_utils.erl
+++ /dev/null
@@ -1,91 +0,0 @@
--module(els_dap_test_utils).
-
--export([ all/1
- , all/2
- , end_per_suite/1
- , end_per_testcase/2
- , init_per_suite/1
- , init_per_testcase/2
- , wait_for/2
- , wait_for_fun/3
- ]).
-
--include_lib("common_test/include/ct.hrl").
-
-%%==============================================================================
-%% Defines
-%%==============================================================================
--define(TEST_APP, <<"code_navigation">>).
-
-%%==============================================================================
-%% Types
-%%==============================================================================
--type config() :: [{atom(), any()}].
-
-%%==============================================================================
-%% API
-%%==============================================================================
-
--spec all(module()) -> [atom()].
-all(Module) -> all(Module, []).
-
--spec all(module(), [atom()]) -> [atom()].
-all(Module, Functions) ->
- ExcludedFuns = [init_per_suite, end_per_suite, all, module_info | Functions],
- Exports = Module:module_info(exports),
- [F || {F, 1} <- Exports, not lists:member(F, ExcludedFuns)].
-
--spec init_per_suite(config()) -> config().
-init_per_suite(Config) ->
- PrivDir = code:priv_dir(els_dap),
- RootPath = filename:join([ els_utils:to_binary(PrivDir)
- , ?TEST_APP]),
- RootUri = els_uri:uri(RootPath),
- application:load(els_core),
- [ {root_uri, RootUri}
- , {root_path, RootPath}
- | Config ].
-
--spec end_per_suite(config()) -> ok.
-end_per_suite(_Config) ->
- ok.
-
--spec init_per_testcase(atom(), config()) -> config().
-init_per_testcase(_TestCase, Config) ->
- meck:new(els_distribution_server, [no_link, passthrough]),
- meck:expect(els_distribution_server, connect, 0, ok),
- Started = els_test_utils:start(),
- RootUri = ?config(root_uri, Config),
- els_client:initialize(RootUri, #{indexingEnabled => false}),
- els_client:initialized(),
-[ {started, Started}
- | Config].
-
--spec end_per_testcase(atom(), config()) -> ok.
-end_per_testcase(_TestCase, Config) ->
- meck:unload(els_distribution_server),
- [application:stop(App) || App <- ?config(started, Config)],
- ok.
-
--spec wait_for(any(), non_neg_integer()) -> ok.
-wait_for(_Message, Timeout) when Timeout =< 0 ->
- timeout;
-wait_for(Message, Timeout) ->
- receive Message -> ok
- after 10 -> wait_for(Message, Timeout - 10)
- end.
-
--spec wait_for_fun(term(), non_neg_integer(), non_neg_integer()) ->
- {ok, any()} | ok | timeout.
-wait_for_fun(_CheckFun, _WaitTime, 0) ->
- timeout;
-wait_for_fun(CheckFun, WaitTime, Retries) ->
- case CheckFun() of
- true ->
- ok;
- {true, Value} ->
- {ok, Value};
- false ->
- timer:sleep(WaitTime),
- wait_for_fun(CheckFun, WaitTime, Retries - 1)
- end.
diff --git a/apps/els_lsp/include/els_lsp.hrl b/apps/els_lsp/include/els_lsp.hrl
index b99d27981..cc3e3b7b0 100644
--- a/apps/els_lsp/include/els_lsp.hrl
+++ b/apps/els_lsp/include/els_lsp.hrl
@@ -5,6 +5,8 @@
-define(APP, els_lsp).
--define(LSP_LOG_FORMAT, ["[", time, "] ", "[", level, "] ", msg, " [", mfa, " L", line, "] ", pid, "\n"]).
+-define(LSP_LOG_FORMAT, [
+ "[", time, "] ", "[", level, "] ", msg, " [", mfa, " L", line, "] ", pid, "\n"
+]).
-endif.
diff --git a/apps/els_lsp/priv/code_navigation/erlang_ls.config b/apps/els_lsp/priv/code_navigation/erlang_ls.config
index 395df7171..d01a177b5 100644
--- a/apps/els_lsp/priv/code_navigation/erlang_ls.config
+++ b/apps/els_lsp/priv/code_navigation/erlang_ls.config
@@ -2,3 +2,6 @@ macros:
- name: DEFINED_WITHOUT_VALUE
- name: DEFINED_WITH_VALUE
value: 1
+edoc_custom_tags:
+ - edoc
+ - generated
diff --git a/apps/els_lsp/priv/code_navigation/include/builtin_macros.hrl b/apps/els_lsp/priv/code_navigation/include/builtin_macros.hrl
new file mode 100644
index 000000000..760285324
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/include/builtin_macros.hrl
@@ -0,0 +1,12 @@
+-export([f/0]).
+
+-spec f() -> any().
+f() ->
+ ?MODULE,
+ ?MODULE_STRING,
+ ?FILE,
+ ?LINE,
+ ?MACHINE,
+ ?FUNCTION_NAME,
+ ?FUNCTION_ARITY,
+ ?OTP_RELEASE.
diff --git a/apps/els_lsp/priv/code_navigation/src/atom_typo.erl b/apps/els_lsp/priv/code_navigation/src/atom_typo.erl
new file mode 100644
index 000000000..152a6587d
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/atom_typo.erl
@@ -0,0 +1,19 @@
+-module(atom_typo).
+-export([f/0]).
+
+f() ->
+ %% typos
+ ture,
+ falsee,
+ fales,
+ undifened,
+ udefined,
+ errorr,
+ %% ok
+ true,
+ false,
+ fails,
+ undefined,
+ unified,
+ error,
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/code_action.erl b/apps/els_lsp/priv/code_navigation/src/code_action.erl
new file mode 100644
index 000000000..920d04021
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/code_action.erl
@@ -0,0 +1,26 @@
+%% Important: this file feeds the tests in the els_code_action_SUITE.erl.
+%% Please only add new cases from bottom, otherwise it might break those tests.
+-module(code_action_oops).
+
+-export([function_a/0, function_d/0]).
+
+function_a() ->
+ A = 123,
+ function_b().
+
+function_b() ->
+ ok.
+
+function_c() ->
+ Foo = 1,
+ Bar = 2,
+ Foo + Barf.
+
+-define(TIMEOUT, 200).
+
+-include_lib("stdlib/include/assert.hrl").
+function_d() ->
+ foobar(),
+ foobar(x,y,z),
+ foobar(Foo, #foo_bar{}, Bar = 123, #foo_bar{} = Baz),
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/code_action_browse_docs.erl b/apps/els_lsp/priv/code_navigation/src/code_action_browse_docs.erl
new file mode 100644
index 000000000..4491e11cc
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/code_action_browse_docs.erl
@@ -0,0 +1,10 @@
+-module(code_action_browse_docs).
+
+-spec function_a(file:filename()) -> pid().
+function_e(L) ->
+ lists:sort(L),
+ self().
+
+-spec function_b() -> my_dep_mod:my_type().
+function_f() ->
+ my_dep_mod:my_function().
diff --git a/apps/els_lsp/priv/code_navigation/src/code_completion_fail.erl b/apps/els_lsp/priv/code_navigation/src/code_completion_fail.erl
new file mode 100644
index 000000000..aafcecdea
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/code_completion_fail.erl
@@ -0,0 +1,2 @@
+-module(s).
+a
\ No newline at end of file
diff --git a/apps/els_dap/src/els_dap.app.src b/apps/els_lsp/priv/code_navigation/src/code_navigation.app.src
similarity index 51%
rename from apps/els_dap/src/els_dap.app.src
rename to apps/els_lsp/priv/code_navigation/src/code_navigation.app.src
index 8749e3116..2de3adc12 100644
--- a/apps/els_dap/src/els_dap.app.src
+++ b/apps/els_lsp/priv/code_navigation/src/code_navigation.app.src
@@ -1,16 +1,14 @@
-{application, els_dap, [
- {description, "Erlang LS - Debug Adapter Protocol"},
+{application, code_navigation, [
+ {description, "Erlang LS - Test App"},
{vsn, git},
{registered, []},
- {mod, {els_dap_app, []}},
{applications, [
kernel,
- stdlib,
- getopt,
- els_core
+ stdlib
]},
{env, []},
{modules, []},
+ {maintainers, []},
{licenses, ["Apache 2.0"]},
{links, []}
]}.
diff --git a/apps/els_lsp/priv/code_navigation/src/code_navigation.erl b/apps/els_lsp/priv/code_navigation/src/code_navigation.erl
index 56fb6247c..939b8cf60 100644
--- a/apps/els_lsp/priv/code_navigation/src/code_navigation.erl
+++ b/apps/els_lsp/priv/code_navigation/src/code_navigation.erl
@@ -17,7 +17,7 @@
-define(MACRO_A, macro_a).
-define(MACRO_A(X), erlang:display(X)).
-
+-define('MACRO A', macro_a).
function_a() ->
function_b(),
#record_a{}.
@@ -122,3 +122,13 @@ macro_b(_X, _Y) ->
function_mb() ->
?MACRO_B(m, b).
+
+code_navigation() -> code_navigation.
+
+code_navigation(X) -> X.
+
+multiple_instances_same_file() -> {code_navigation, [simple_list], "abc"}.
+
+code_navigation_extra(X, Y, Z) -> [code_navigation_extra, X, Y, Z].
+
+multiple_instances_diff_file() -> code_navigation_extra.
diff --git a/apps/els_lsp/priv/code_navigation/src/code_navigation_broken.erl b/apps/els_lsp/priv/code_navigation/src/code_navigation_broken.erl
new file mode 100644
index 000000000..7b7237ff0
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/code_navigation_broken.erl
@@ -0,0 +1,19 @@
+-module(code_navigation_broken).
+
+function_a() ->
+ ok.
+
+function_b() ->
+ function_a() % missing comma, breaks parsing of this function!
+ function_a(),
+ case function_a() of
+ ok ->
+ function_a(),
+ case function_a() of
+ ok ->
+ ok
+ end
+ end,
+ function_a(
+ ),
+ function_a().
diff --git a/apps/els_lsp/priv/code_navigation/src/code_navigation_extra.erl b/apps/els_lsp/priv/code_navigation/src/code_navigation_extra.erl
index 115be3374..a7f41c41f 100644
--- a/apps/els_lsp/priv/code_navigation/src/code_navigation_extra.erl
+++ b/apps/els_lsp/priv/code_navigation/src/code_navigation_extra.erl
@@ -1,6 +1,6 @@
-module(code_navigation_extra).
--export([ do/1, do_2/0, 'DO_LOUDER'/0 ]).
+-export([ do/1, do_2/0, 'DO_LOUDER'/0, function_a/2 ]).
do(_Config) ->
do_4(1, foo).
@@ -21,3 +21,8 @@ do_4(_, _) ->
'DO_LOUDER'() ->
'Code.Navigation.Elixirish':do('Atom').
+
+function_a(Arg1, Arg2) ->
+ funct(),
+ code_navigation:().
+ code_navigation:funct().
diff --git a/apps/els_lsp/priv/code_navigation/src/code_navigation_types.erl b/apps/els_lsp/priv/code_navigation/src/code_navigation_types.erl
index b3e1e103d..daca03226 100644
--- a/apps/els_lsp/priv/code_navigation/src/code_navigation_types.erl
+++ b/apps/els_lsp/priv/code_navigation/src/code_navigation_types.erl
@@ -2,7 +2,7 @@
-type type_a() :: atom().
--export_type([ type_a/0 ]).
+-export_type([ type_a/0, type_b/0, user_type_c/0 ]).
-opaque opaque_type_a() :: atom().
@@ -13,3 +13,16 @@
-include("transitive.hrl").
-type user_type_b() :: type_b().
+-type user_type_c() :: user_type_b().
+-record(record_a,
+ {field_a = an_atom :: user_type_a()}).
+
+-type user_type_c() :: #{
+ key_a := user_type_a()
+ }.
+
+-spec function_a(A :: user_type_a()) -> B :: user_type_b().
+function_a(type_a) ->
+ type_b.
+
+-type user_type_d() :: type() | code_navigation:().
diff --git a/apps/els_lsp/priv/code_navigation/src/completion_more.erl b/apps/els_lsp/priv/code_navigation/src/completion_more.erl
new file mode 100644
index 000000000..16859a750
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/completion_more.erl
@@ -0,0 +1,9 @@
+-module(completion_more).
+
+lc() ->
+ [
+ [].
+
+mc() ->
+ #{
+ #{}.
diff --git a/apps/els_lsp/priv/code_navigation/src/completion_records.erl b/apps/els_lsp/priv/code_navigation/src/completion_records.erl
new file mode 100644
index 000000000..803da9938
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/completion_records.erl
@@ -0,0 +1,18 @@
+-module(completion_records).
+
+-record(record_a, {field_a, field_b, 'Field C'}).
+-record(record_b, {field_x, field_y}).
+
+function_a(#record_a{field_a = a, field_b = b}) ->
+ #record_b{field_x = #record_a{},
+ %% #record_a{
+ field_y = y},
+ {}.
+
+-spec function_b(#record_b{}) -> #record_a{}.
+function_b(R) ->
+ function_a(R).
+
+-define(A, #record_a{}).
+function_c(R) ->
+ function_a(R).
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics module name check.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics module name check.erl
new file mode 100644
index 000000000..872e39d66
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics module name check.erl
@@ -0,0 +1 @@
+-module('diagnostics module name check').
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_behaviour_recursive.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_behaviour_recursive.erl
new file mode 100644
index 000000000..07bf3c5bc
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_behaviour_recursive.erl
@@ -0,0 +1,12 @@
+-module(diagnostics_behaviour_recursive).
+-behaviour(diagnostics_behaviour).
+
+-export([one/0]).
+-export([two/0]).
+
+-callback three() -> ok.
+
+one() ->
+ ok.
+two() ->
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_behaviour_recursive_impl.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_behaviour_recursive_impl.erl
new file mode 100644
index 000000000..da92e2aad
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_behaviour_recursive_impl.erl
@@ -0,0 +1,5 @@
+-module(diagnostics_behaviour_recursive_impl).
+-behaviour(diagnostics_behaviour_recursive).
+-export([three/0]).
+three() ->
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_bound_var_in_pattern_cannot_parse.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_bound_var_in_pattern_cannot_parse.erl
new file mode 100644
index 000000000..3f4b261d3
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_bound_var_in_pattern_cannot_parse.erl
@@ -0,0 +1,6 @@
+-module(diagnostics_bound_var_in_pattern_cannot_parse).
+
+f(Var1) ->
+ Var1 = 1.
+
+g() ->'
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_eqwalizer.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_eqwalizer.erl
new file mode 100644
index 000000000..15aa112af
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_eqwalizer.erl
@@ -0,0 +1,7 @@
+-module(diagnostics_eqwalizer).
+
+-export([ main/0] ).
+
+-spec main() -> ok.
+main() ->
+ not_ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_module_name_check.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_module_name_check.erl
new file mode 100644
index 000000000..09898048f
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_module_name_check.erl
@@ -0,0 +1 @@
+-module(module_name_check).
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_unused_includes_broken.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_unused_includes_broken.erl
new file mode 100644
index 000000000..64adf47f1
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_unused_includes_broken.erl
@@ -0,0 +1,5 @@
+-module(diagnostics_unused_includes_broken).
+
+-include_lib(
+ "foo"-include_lib("foo")
+).
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_xref.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_xref.erl
index 564317b5f..1e716ea2b 100644
--- a/apps/els_lsp/priv/code_navigation/src/diagnostics_xref.erl
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_xref.erl
@@ -4,7 +4,11 @@
main() ->
lists:map(1, 2, 3),
- non_existing() ++ existing().
+ non_existing() ++ existing() ++ dynamic_call(foo, bar).
existing() ->
lists:seq(1, 3).
+
+dynamic_call(Foo, Bar) ->
+ Foo:bar(),
+ foo:Bar().
diff --git a/apps/els_lsp/priv/code_navigation/src/diagnostics_xref_pseudo.erl b/apps/els_lsp/priv/code_navigation/src/diagnostics_xref_pseudo.erl
index 44b102db0..a52adcea9 100644
--- a/apps/els_lsp/priv/code_navigation/src/diagnostics_xref_pseudo.erl
+++ b/apps/els_lsp/priv/code_navigation/src/diagnostics_xref_pseudo.erl
@@ -14,6 +14,7 @@ main() ->
unknown_module:module_info(module),
?MODULE:behaviour_info(callbacks),
lager:debug("log message", []),
+ lager:debug_unsafe("log message", []),
lager:info("log message", []),
lager:notice("log message", []),
lager:warning("log message", []),
@@ -23,6 +24,7 @@ main() ->
lager:emergency("log message", []),
lager:debug("log message"),
+ lager:debug_unsafe("log message", []),
lager:info("log message"),
lager:notice("log message"),
lager:warning("log message"),
@@ -33,4 +35,6 @@ main() ->
% At lease one failure so we know the diagnostic is running
unknown_module:nonexistent(),
+ Mod:module_info(),
+ Mod:module_info(module),
ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/docs_memo.erl b/apps/els_lsp/priv/code_navigation/src/docs_memo.erl
new file mode 100644
index 000000000..ee08f40a5
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/docs_memo.erl
@@ -0,0 +1,6 @@
+-module(docs_memo).
+
+-type type() -> any().
+
+-spec function() -> ok.
+function() -> ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/edoc_diagnostics.erl b/apps/els_lsp/priv/code_navigation/src/edoc_diagnostics.erl
new file mode 100644
index 000000000..25c2d0c02
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/edoc_diagnostics.erl
@@ -0,0 +1,15 @@
+-module(edoc_diagnostics).
+
+-export([main/0]).
+
+%% @mydoc Main function
+main() ->
+ internal().
+
+%% @docc internal
+internal() ->
+ ok.
+
+%% @doc `
+unused() ->
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/edoc_diagnostics_custom_tags.erl b/apps/els_lsp/priv/code_navigation/src/edoc_diagnostics_custom_tags.erl
new file mode 100644
index 000000000..c05c12f8a
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/edoc_diagnostics_custom_tags.erl
@@ -0,0 +1,18 @@
+-module(edoc_diagnostics_custom_tags).
+
+-export([ a/0 ]).
+
+%% @edoc
+%% `edoc' is a custom alias for `doc'
+a() ->
+ ok.
+
+%% @docc
+%% `docc' is not an existing or custom tag
+b() ->
+ ok.
+
+%% @generated
+%% The `generated' tag is used for generated code
+c() ->
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/extract_function.erl b/apps/els_lsp/priv/code_navigation/src/extract_function.erl
new file mode 100644
index 000000000..3fa34ceb0
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/extract_function.erl
@@ -0,0 +1,17 @@
+-module(extract_function).
+-export([f/2]).
+
+f(A, B) ->
+ C = 1,
+ F = A + B + C,
+ G = case A of
+ 1 -> one;
+ _ -> other
+ end,
+ H = [X || X <- [A, B, C], X > 1],
+ I = {A, B, A},
+ other_function(),
+ [X || X <- [A, B, C], X > 1].
+
+other_function() ->
+ hello.
diff --git a/apps/els_lsp/priv/code_navigation/src/folding_ranges.erl b/apps/els_lsp/priv/code_navigation/src/folding_ranges.erl
new file mode 100644
index 000000000..fdd63d3fb
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/folding_ranges.erl
@@ -0,0 +1,11 @@
+-module(folding_ranges).
+
+function_foldable() ->
+ ?assertEqual(2.0, 4/2).
+
+-record(unfoldable_record, { field_a }).
+
+-record(foldable_record, { field_a
+ , field_b
+ , field_c
+ }).
diff --git a/apps/els_lsp/priv/code_navigation/src/hover_nonexisting.erl b/apps/els_lsp/priv/code_navigation/src/hover_nonexisting.erl
new file mode 100644
index 000000000..780e06404
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/hover_nonexisting.erl
@@ -0,0 +1,6 @@
+-module(hover_nonexisting).
+
+-export([main/0]).
+
+main() ->
+ nonexisting:main().
diff --git a/apps/els_lsp/priv/code_navigation/src/implementation.erl b/apps/els_lsp/priv/code_navigation/src/implementation.erl
index 70245480e..c88df7246 100644
--- a/apps/els_lsp/priv/code_navigation/src/implementation.erl
+++ b/apps/els_lsp/priv/code_navigation/src/implementation.erl
@@ -1,3 +1,6 @@
-module(implementation).
-
+-export([call/1]).
-callback to_be_implemented() -> ok.
+
+call(Mod) ->
+ Mod:to_be_implemented().
diff --git a/apps/els_lsp/priv/code_navigation/src/inlay_hint.erl b/apps/els_lsp/priv/code_navigation/src/inlay_hint.erl
new file mode 100644
index 000000000..39b0d798b
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/inlay_hint.erl
@@ -0,0 +1,41 @@
+-module(inlay_hint).
+-export([test/0]).
+
+-record(foo, {}).
+
+test() ->
+ a(1, 2),
+ b(1, 2),
+ c(1),
+ d(1, 2),
+ e(1, 2),
+ f(1, 2),
+ g(1, 2, 3),
+ lists:append([], []).
+
+a(A1, A2) ->
+ A1 + A2.
+
+b(x, y) ->
+ 0;
+b(B1, _B2) ->
+ B1.
+
+c(#foo{}) ->
+ ok.
+
+d([1,2,3] = D1,
+ D2 = #{hej := 123}) ->
+ ok.
+
+-spec e(E1 :: any(), E2 :: any()) -> ok.
+e(_, _) ->
+ ok.
+
+-spec f(F1, F2) -> ok when F1 :: any(), F2 :: any().
+f(_, _) ->
+ ok.
+
+-spec g(G1, any(), _) -> ok when G1 :: any().
+g(_, G2, _G3) ->
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/rename_module_a.erl b/apps/els_lsp/priv/code_navigation/src/rename_module_a.erl
new file mode 100644
index 000000000..be174d088
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/rename_module_a.erl
@@ -0,0 +1,16 @@
+-module(rename_module_a).
+
+-export([ function_a/0
+ , function_b/0
+ ]).
+-export_type([type_a/0]).
+
+-type type_a() :: any().
+
+-callback function_a() -> type_a().
+
+function_a() ->
+ a.
+
+function_b() ->
+ b.
diff --git a/apps/els_lsp/priv/code_navigation/src/rename_module_b.erl b/apps/els_lsp/priv/code_navigation/src/rename_module_b.erl
new file mode 100644
index 000000000..2da5b6186
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/rename_module_b.erl
@@ -0,0 +1,14 @@
+-module(rename_module_b).
+
+-behaviour(rename_module_a).
+-import(rename_module_a, [function_b/0]).
+
+-export([function_a/0]).
+
+-type type_a() :: rename_module_a:type_a().
+
+-spec function_a() -> type_a().
+function_a() ->
+ rename_module_a:function_a(),
+ F = fun rename_module_a:function_a/0,
+ F().
diff --git a/apps/els_lsp/priv/code_navigation/src/rename_variable.erl b/apps/els_lsp/priv/code_navigation/src/rename_variable.erl
index ef6d1c9b7..f7ee13403 100644
--- a/apps/els_lsp/priv/code_navigation/src/rename_variable.erl
+++ b/apps/els_lsp/priv/code_navigation/src/rename_variable.erl
@@ -1,5 +1,5 @@
-module(rename_variable).
-
+-callback name(Var) -> Var.
foo(Var) ->
Var < 0;
foo(Var) ->
@@ -10,3 +10,23 @@ foo(_Var) ->
bar(Var) ->
Var.
+
+-spec baz(Var) -> Var
+ when Var :: atom().
+baz(Var) ->
+ Var.
+
+-record(foo, {a :: Var,
+ b :: [Var]}).
+
+-define(MACRO(Var), Var + Var).
+
+-type type(Var) :: Var.
+-opaque opaque(Var) :: Var.
+
+foo(Var) ->
+ Var.
+
+-if(Var == Var).
+
+-endif.
diff --git a/apps/els_lsp/priv/code_navigation/src/signature_help.erl b/apps/els_lsp/priv/code_navigation/src/signature_help.erl
new file mode 100644
index 000000000..ac27d0ae5
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/signature_help.erl
@@ -0,0 +1,18 @@
+-module(signature_help).
+
+-export([min/2,maps_get/0,record_max/0,multiline/0]).
+
+min(A, B) ->
+ erlang:min(A, B).
+
+maps_get() ->
+ maps:get(key, #{}, false).
+
+record_max() ->
+ erlang:max({a, b, c}, {d, e, f}).
+
+multiline() ->
+ erlang:min(
+ 1,
+ 2
+ ).
diff --git a/apps/els_lsp/priv/code_navigation/src/undefined_record_suggest.erl b/apps/els_lsp/priv/code_navigation/src/undefined_record_suggest.erl
new file mode 100644
index 000000000..a01ee33bb
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/undefined_record_suggest.erl
@@ -0,0 +1,7 @@
+-module(undefined_record_suggest).
+
+-record(foobar, {foobar}).
+
+function_a(R) ->
+ #foo_bar{} = R,
+ R#foobar.foobaz.
diff --git a/apps/els_lsp/priv/code_navigation/src/variable_list_comp.erl b/apps/els_lsp/priv/code_navigation/src/variable_list_comp.erl
new file mode 100644
index 000000000..e49fe06d7
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/variable_list_comp.erl
@@ -0,0 +1,20 @@
+-module(variable_list_comp).
+
+one() ->
+ Var = 1,
+ [ Var || Var <- [1, 2, 3] ],
+ Var.
+
+two() ->
+ [ Var || Var <- [1, 2, 3] ],
+ [ Var || Var <- [4, 5, 6] ].
+
+three() ->
+ Var = 1,
+ [ Var || _ <- [1, 2, 3] ],
+ Var.
+
+four() ->
+ [ {Var, Var2} || Var <- [4, 5, 6],
+ Var2 <- [ Var || Var <- [1, 2, 3] ]
+ ].
diff --git a/apps/els_lsp/priv/code_navigation/src/watched_file_a.erl b/apps/els_lsp/priv/code_navigation/src/watched_file_a.erl
new file mode 100644
index 000000000..22c990bef
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/watched_file_a.erl
@@ -0,0 +1,6 @@
+-module(watched_file_a).
+
+-export([ main/0 ]).
+
+main() ->
+ ok.
diff --git a/apps/els_lsp/priv/code_navigation/src/watched_file_b.erl b/apps/els_lsp/priv/code_navigation/src/watched_file_b.erl
new file mode 100644
index 000000000..30b1287b6
--- /dev/null
+++ b/apps/els_lsp/priv/code_navigation/src/watched_file_b.erl
@@ -0,0 +1,6 @@
+-module(watched_file_b).
+
+-export([ main/0 ]).
+
+main() ->
+ watched_file_a:main().
diff --git a/apps/els_lsp/priv/snippets/case b/apps/els_lsp/priv/snippets/case
new file mode 100644
index 000000000..d1ba3cca6
--- /dev/null
+++ b/apps/els_lsp/priv/snippets/case
@@ -0,0 +1,4 @@
+case ${1:Exprs} of
+ ${2:Pattern} ->
+ ${3:Body}
+end
\ No newline at end of file
diff --git a/apps/els_lsp/src/edoc_report.erl b/apps/els_lsp/src/edoc_report.erl
new file mode 100644
index 000000000..6963a8512
--- /dev/null
+++ b/apps/els_lsp/src/edoc_report.erl
@@ -0,0 +1,134 @@
+%% =============================================================================
+%% An erlang_ls fork of Erlang/OTP's edoc_report
+%% =============================================================================
+%% The main reasons for the fork:
+%% * The edoc application does not offer an API to return a
+%% list of warnings and errors
+%% * Support for custom EDoc tags
+%% =====================================================================
+%% Licensed under the Apache License, Version 2.0 (the "License"); you may
+%% not use this file except in compliance with the License. You may obtain
+%% a copy of the License at
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% Alternatively, you may use this file under the terms of the GNU Lesser
+%% General Public License (the "LGPL") as published by the Free Software
+%% Foundation; either version 2.1, or (at your option) any later version.
+%% If you wish to allow use of your version of this file only under the
+%% terms of the LGPL, you should delete the provisions above and replace
+%% them with the notice and other provisions required by the LGPL; see
+%% . If you do not delete the provisions
+%% above, a recipient may use your version of this file under the terms of
+%% either the Apache License or the LGPL.
+%%
+%% @private
+%% @copyright 2001-2003 Richard Carlsson
+%% @author Richard Carlsson
+%% @see edoc
+%% @end
+%% =====================================================================
+
+%% @doc EDoc verbosity/error reporting.
+
+-module(edoc_report).
+
+-compile({no_auto_import, [error/1, error/2, error/3]}).
+-export([
+ error/1,
+ error/2,
+ error/3,
+ report/2,
+ report/3,
+ report/4,
+ warning/1,
+ warning/2,
+ warning/3,
+ warning/4
+]).
+
+-type where() :: any().
+-type what() :: any().
+-type line() :: non_neg_integer().
+-type severity() :: warning | error.
+
+-define(APPLICATION, edoc).
+-define(DICT_KEY, edoc_diagnostics).
+
+-spec error(what()) -> ok.
+error(What) ->
+ error([], What).
+
+-spec error(where(), what()) -> ok.
+error(Where, What) ->
+ error(0, Where, What).
+
+-spec error(line(), where(), any()) -> ok.
+error(Line, Where, S) when is_list(S) ->
+ report(Line, Where, S, [], error);
+error(Line, Where, {S, D}) when is_list(S) ->
+ report(Line, Where, S, D, error);
+error(Line, Where, {format_error, M, D}) ->
+ report(Line, Where, M:format_error(D), [], error).
+
+-spec warning(string()) -> ok.
+warning(S) ->
+ warning(S, []).
+
+-spec warning(string(), [any()]) -> ok.
+warning(S, Vs) ->
+ warning([], S, Vs).
+
+-spec warning(where(), string(), [any()]) -> ok.
+warning(Where, S, Vs) ->
+ warning(0, Where, S, Vs).
+
+-spec warning(line(), where(), string(), [any()]) -> ok.
+warning(L, Where, "tag @~s not recognized." = S, [Tag] = Vs) ->
+ CustomTags = els_config:get(edoc_custom_tags),
+ case lists:member(atom_to_list(Tag), CustomTags) of
+ true ->
+ ok;
+ false ->
+ report(L, Where, S, Vs, warning)
+ end;
+warning(L, Where, S, Vs) ->
+ report(L, Where, S, Vs, warning).
+
+-spec report(string(), [any()]) -> ok.
+report(S, Vs) ->
+ report([], S, Vs).
+
+-spec report(where(), string(), [any()]) -> ok.
+report(Where, S, Vs) ->
+ report(0, Where, S, Vs).
+
+-spec report(line(), where(), string(), [any()]) -> ok.
+report(L, Where, S, Vs) ->
+ report(L, Where, S, Vs, error).
+
+-spec report(line(), where(), string(), [any()], severity()) -> ok.
+report(L, Where, S, Vs, Severity) ->
+ put(?DICT_KEY, [{L, where(Where), S, Vs, Severity} | get(?DICT_KEY)]).
+
+-spec where(
+ [any()]
+ | {string(), module | footer | header | {atom(), non_neg_integer()}}
+) ->
+ string().
+where({File, module}) ->
+ io_lib:fwrite("~ts, in module header: ", [File]);
+where({File, footer}) ->
+ io_lib:fwrite("~ts, in module footer: ", [File]);
+where({File, header}) ->
+ io_lib:fwrite("~ts, in header file: ", [File]);
+where({File, {F, A}}) ->
+ io_lib:fwrite("~ts, function ~ts/~w: ", [File, F, A]);
+where([]) ->
+ io_lib:fwrite("~s: ", [?APPLICATION]);
+where(File) when is_list(File) ->
+ File ++ ": ".
diff --git a/apps/els_lsp/src/els_app.erl b/apps/els_lsp/src/els_app.erl
index a8b66be29..054863c39 100644
--- a/apps/els_lsp/src/els_app.erl
+++ b/apps/els_lsp/src/els_app.erl
@@ -12,18 +12,19 @@
%% Exports
%%==============================================================================
%% Application Callbacks
--export([ start/2
- , stop/1
- ]).
+-export([
+ start/2,
+ stop/1
+]).
%%==============================================================================
%% Application Callbacks
%%==============================================================================
-spec start(normal, any()) -> {ok, pid()}.
start(_StartType, _StartArgs) ->
- ok = application:set_env(elvis_core, no_output, true),
- els_sup:start_link().
+ ok = application:set_env(elvis_core, no_output, true),
+ els_sup:start_link().
-spec stop(any()) -> ok.
stop(_State) ->
- ok.
+ ok.
diff --git a/apps/els_lsp/src/els_arg.erl b/apps/els_lsp/src/els_arg.erl
new file mode 100644
index 000000000..bb3ece511
--- /dev/null
+++ b/apps/els_lsp/src/els_arg.erl
@@ -0,0 +1,72 @@
+-module(els_arg).
+-export([new/2]).
+-export([name/1]).
+-export([name/2]).
+-export([index/1]).
+-export([merge_args/2]).
+-export([get_args/2]).
+
+-export_type([arg/0]).
+-export_type([args/0]).
+
+-include_lib("els_core/include/els_core.hrl").
+
+-type args() :: [arg()].
+-type arg() :: #{
+ index := pos_integer(),
+ name := string() | undefined | {type, string() | undefined},
+ range => els_poi:poi_range()
+}.
+
+-spec new(pos_integer(), string()) -> arg().
+new(Index, Name) ->
+ #{index => Index, name => Name}.
+
+-spec get_args(uri(), els_poi:poi()) -> els_arg:args().
+get_args(Uri, #{
+ id := {F, A},
+ data := #{args := Args}
+}) ->
+ M = els_uri:module(Uri),
+ case els_dt_signatures:lookup({M, F, A}) of
+ {ok, []} ->
+ Args;
+ {ok, [#{args := []} | _]} ->
+ Args;
+ {ok, [#{args := SpecArgs} | _]} ->
+ merge_args(SpecArgs, Args)
+ end.
+
+-spec name(arg()) -> string().
+name(Arg) ->
+ name("Arg", Arg).
+
+-spec name(string(), arg()) -> string().
+name(Prefix, #{index := N, name := undefined}) ->
+ Prefix ++ integer_to_list(N);
+name(_Prefix, #{name := {type, Name}}) ->
+ Name;
+name(_Prefix, #{name := Name}) ->
+ Name.
+
+-spec index(arg()) -> string().
+index(#{index := Index}) ->
+ integer_to_list(Index).
+
+-spec merge_args(args(), args()) -> args().
+merge_args([], []) ->
+ [];
+merge_args([#{name := undefined} | T1], [Arg | T2]) ->
+ [Arg | merge_args(T1, T2)];
+merge_args(
+ [#{name := {type, Name}} = Arg | T1],
+ [#{name := undefined} | T2]
+) ->
+ [Arg#{name := Name} | merge_args(T1, T2)];
+merge_args(
+ [#{name := {type, _}} | T1],
+ [Arg | T2]
+) ->
+ [Arg | merge_args(T1, T2)];
+merge_args([Arg | T1], [_ | T2]) ->
+ [Arg | merge_args(T1, T2)].
diff --git a/apps/els_lsp/src/els_atom_typo_diagnostics.erl b/apps/els_lsp/src/els_atom_typo_diagnostics.erl
new file mode 100644
index 000000000..dd2cc42d6
--- /dev/null
+++ b/apps/els_lsp/src/els_atom_typo_diagnostics.erl
@@ -0,0 +1,72 @@
+%%==============================================================================
+%% AtomTypo diagnostics
+%% Catch common atom typos
+%%==============================================================================
+-module(els_atom_typo_diagnostics).
+
+%%==============================================================================
+%% Behaviours
+%%==============================================================================
+-behaviour(els_diagnostics).
+
+%%==============================================================================
+%% Exports
+%%==============================================================================
+-export([
+ is_default/0,
+ run/1,
+ source/0
+]).
+
+%%==============================================================================
+%% Includes
+%%==============================================================================
+-include("els_lsp.hrl").
+
+%%==============================================================================
+%% Callback Functions
+%%==============================================================================
+
+-spec is_default() -> boolean().
+is_default() ->
+ false.
+
+-spec run(uri()) -> [els_diagnostics:diagnostic()].
+run(Uri) ->
+ case els_utils:lookup_document(Uri) of
+ {error, _Error} ->
+ [];
+ {ok, Document} ->
+ Atoms = [<<"false">>, <<"true">>, <<"undefined">>, <<"error">>],
+ POIs = els_dt_document:pois(Document, [atom]),
+ [
+ make_diagnostic(POI, Atom)
+ || #{id := Id} = POI <- POIs,
+ Atom <- Atoms,
+ atom_to_binary(Id, utf8) =/= Atom,
+ els_utils:jaro_distance(atom_to_binary(Id, utf8), Atom) > 0.9
+ ]
+ end.
+
+-spec source() -> binary().
+source() ->
+ <<"AtomTypo">>.
+
+%%==============================================================================
+%% Internal Functions
+%%==============================================================================
+-spec make_diagnostic(els_poi:poi(), binary()) -> els_diagnostics:diagnostic().
+make_diagnostic(#{range := Range}, Atom) ->
+ Message = els_utils:to_binary(
+ io_lib:format(
+ "Atom typo? Did you mean: ~s",
+ [Atom]
+ )
+ ),
+ Severity = ?DIAGNOSTIC_WARNING,
+ els_diagnostics:make_diagnostic(
+ els_protocol:range(Range),
+ Message,
+ Severity,
+ source()
+ ).
diff --git a/apps/els_lsp/src/els_background_job.erl b/apps/els_lsp/src/els_background_job.erl
index d58b71908..2ab8b949d 100644
--- a/apps/els_lsp/src/els_background_job.erl
+++ b/apps/els_lsp/src/els_background_job.erl
@@ -6,25 +6,27 @@
%%==============================================================================
%% API
%%==============================================================================
--export([ new/1
- , list/0
- , stop/1
- , stop_all/0
- ]).
+-export([
+ new/1,
+ list/0,
+ list_titles/0,
+ stop/1,
+ stop_all/0
+]).
--export([ start_link/1
- ]).
+-export([start_link/1]).
%%==============================================================================
%% Callbacks for gen_server
%%==============================================================================
-behaviour(gen_server).
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , handle_info/2
- , terminate/2
- ]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2
+]).
%%==============================================================================
%% Includes
@@ -34,30 +36,34 @@
%%==============================================================================
%% Macro Definitions
%%==============================================================================
--define(SPINNING_WHEEL_INTERVAL, 100). %% ms
+
+%% ms
+-define(SPINNING_WHEEL_INTERVAL, 100).
%%==============================================================================
%% Types
%%==============================================================================
-type entry() :: any().
--type config() :: #{ task := fun((entry(), any()) -> any())
- , entries := [any()]
- , on_complete => fun()
- , on_error => fun()
- , title := binary()
- , show_percentages => boolean()
- , initial_state => any()
- }.
--type state() :: #{ config := config()
- , progress_enabled := boolean()
- , show_percentages := boolean()
- , token := els_progress:token()
- , current := non_neg_integer()
- , step := pos_integer()
- , total := non_neg_integer()
- , internal_state := any()
- , spinning_wheel := pid() | undefined
- }.
+-type config() :: #{
+ task := fun((entry(), any()) -> any()),
+ entries := [any()],
+ on_complete => fun(),
+ on_error => fun(),
+ title := binary(),
+ show_percentages => boolean(),
+ initial_state => any()
+}.
+-type state() :: #{
+ config := config(),
+ progress_enabled := boolean(),
+ show_percentages := boolean(),
+ token := els_progress:token(),
+ current := non_neg_integer(),
+ step := pos_integer(),
+ total := non_neg_integer(),
+ internal_state := any(),
+ spinning_wheel := pid() | undefined
+}.
%%==============================================================================
%% API
@@ -66,137 +72,196 @@
%% @doc Create a new background job
-spec new(config()) -> {ok, pid()}.
new(Config) ->
- supervisor:start_child(els_background_job_sup, [Config]).
+ supervisor:start_child(els_background_job_sup, [Config]).
%% @doc Return the list of running background jobs
-spec list() -> [pid()].
list() ->
- [Pid || {_Id, Pid, _Type, _Modules}
- <- supervisor:which_children(els_background_job_sup)].
+ [
+ Pid
+ || {_Id, Pid, _Type, _Modules} <-
+ supervisor:which_children(els_background_job_sup)
+ ].
+
+%% @doc Return the list of running background jobs
+-spec list_titles() -> [any()].
+list_titles() ->
+ Children = supervisor:which_children(els_background_job_sup),
+ lists:flatmap(
+ fun({_Id, Pid, _Type, _Modules}) ->
+ case catch gen_server:call(Pid, get_title) of
+ {ok, Title} ->
+ [Title];
+ _ ->
+ []
+ end
+ end,
+ Children
+ ).
%% @doc Terminate a background job
-spec stop(pid()) -> ok.
stop(Pid) ->
- supervisor:terminate_child(els_background_job_sup, Pid).
+ supervisor:terminate_child(els_background_job_sup, Pid).
%% @doc Terminate all background jobs
-spec stop_all() -> ok.
stop_all() ->
- [ok = supervisor:terminate_child(els_background_job_sup, Pid) ||
- Pid <- list()],
- ok.
+ [
+ ok = supervisor:terminate_child(els_background_job_sup, Pid)
+ || Pid <- list()
+ ],
+ ok.
%% @doc Start the server responsible for a background job
%%
%% To be used by the supervisor
-spec start_link(config()) -> {ok, pid()}.
start_link(Config) ->
- gen_server:start_link(?MODULE, Config, []).
+ gen_server:start_link(?MODULE, Config, []).
%%==============================================================================
%% Callbacks for gen_server
%%==============================================================================
-spec init(config()) -> {ok, state()}.
init(#{entries := Entries, title := Title} = Config) ->
- ?LOG_DEBUG("Background job started ~s", [Title]),
- %% Ensure the terminate function is called on shutdown, allowing the
- %% job to clean up.
- process_flag(trap_exit, true),
- ProgressEnabled = els_work_done_progress:is_supported(),
- Total = length(Entries),
- Step = step(Total),
- Token = els_work_done_progress:send_create_request(),
- OnComplete = maps:get(on_complete, Config, fun noop/1),
- OnError = maps:get(on_error, Config, fun noop/1),
- ShowPercentages = maps:get(show_percentages, Config, true),
- notify_begin(Token, Title, Total, ProgressEnabled, ShowPercentages),
- SpinningWheel = case {ProgressEnabled, ShowPercentages} of
- {true, false} ->
- spawn_link(fun() -> spinning_wheel(Token) end);
- {_, _} ->
- undefined
- end,
- self() ! exec,
- {ok, #{ config => Config#{ on_complete => OnComplete
- , on_error => OnError
- }
- , progress_enabled => ProgressEnabled
- , show_percentages => ShowPercentages
- , token => Token
- , current => 0
- , step => Step
- , total => Total
- , internal_state => maps:get(initial_state, Config, undefined)
- , spinning_wheel => SpinningWheel
- }}.
+ ?LOG_DEBUG("Background job started ~s", [Title]),
+ %% Ensure the terminate function is called on shutdown, allowing the
+ %% job to clean up.
+ process_flag(trap_exit, true),
+ ProgressEnabled = els_work_done_progress:is_supported(),
+ Total = length(Entries),
+ Step = step(Total),
+ Token = els_work_done_progress:send_create_request(),
+ OnComplete = maps:get(on_complete, Config, fun noop/1),
+ OnError = maps:get(on_error, Config, fun noop/1),
+ ShowPercentages = maps:get(show_percentages, Config, true),
+ notify_begin(Token, Title, Total, ProgressEnabled, ShowPercentages),
+ SpinningWheel =
+ case {ProgressEnabled, ShowPercentages} of
+ {true, false} ->
+ spawn_link(fun() -> spinning_wheel(Token) end);
+ {_, _} ->
+ undefined
+ end,
+ self() ! exec,
+ {ok, #{
+ config => Config#{
+ on_complete => OnComplete,
+ on_error => OnError
+ },
+ progress_enabled => ProgressEnabled,
+ show_percentages => ShowPercentages,
+ token => Token,
+ current => 0,
+ step => Step,
+ total => Total,
+ internal_state => maps:get(initial_state, Config, undefined),
+ spinning_wheel => SpinningWheel
+ }}.
-spec handle_call(any(), {pid(), any()}, state()) ->
- {noreply, state()}.
+ {noreply, state()}.
+handle_call(
+ get_title,
+ _From,
+ #{config := #{title := Title}} = State
+) ->
+ {reply, {ok, Title}, State};
handle_call(_Request, _From, State) ->
- {noreply, State}.
+ {noreply, State}.
-spec handle_cast(any(), any()) ->
- {noreply, state()}.
+ {noreply, state()}.
handle_cast(_Request, State) ->
- {noreply, State}.
+ {noreply, State}.
-spec handle_info(any(), any()) ->
- {noreply, state()}.
+ {noreply, state()}.
+handle_info({exec, InternalState}, State) ->
+ handle_info(exec, State#{internal_state => InternalState});
handle_info(exec, State) ->
- #{ config := #{ entries := Entries, task := Task} = Config
- , progress_enabled := ProgressEnabled
- , show_percentages := ShowPercentages
- , token := Token
- , current := Current
- , step := Step
- , total := Total
- , internal_state := InternalState
- } = State,
- case Entries of
- [] ->
- notify_end(Token, Total, ProgressEnabled),
- {stop, normal, State};
- [Entry|Rest] ->
- NewInternalState = Task(Entry, InternalState),
- notify_report( Token
- , Current
- , Step
- , Total
- , ProgressEnabled
- , ShowPercentages),
- self() ! exec,
- {noreply, State#{ config => Config#{ entries => Rest }
- , current => Current + 1
- , internal_state => NewInternalState
- }}
- end;
+ #{
+ config := #{entries := Entries, task := Task} = Config,
+ progress_enabled := ProgressEnabled,
+ show_percentages := ShowPercentages,
+ token := Token,
+ current := Current,
+ step := Step,
+ total := Total,
+ internal_state := InternalState
+ } = State,
+ case Entries of
+ [] ->
+ notify_end(Token, Total, ProgressEnabled),
+ {stop, normal, State};
+ [Entry | Rest] ->
+ MainPid = self(),
+ %% Run the task in a separate process so main process
+ %% is not blocked from receiving messages, needed for stopping
+ %% job.
+ spawn_link(
+ fun() ->
+ NewInternalState = Task(Entry, InternalState),
+ notify_report(
+ Token,
+ Current,
+ Step,
+ Total,
+ ProgressEnabled,
+ ShowPercentages
+ ),
+ MainPid ! {exec, NewInternalState}
+ end
+ ),
+ {noreply, State#{
+ config => Config#{entries => Rest},
+ current => Current + 1
+ }}
+ end;
+%% Allow the terminate function to be called if the spawned child processes dies
+handle_info({'EXIT', _Sender, Reason}, State) when Reason /= normal ->
+ {stop, Reason, State};
handle_info(_Request, State) ->
- {noreply, State}.
+ {noreply, State}.
-spec terminate(any(), state()) -> ok.
-terminate(normal, #{ config := #{on_complete := OnComplete}
- , internal_state := InternalState
- , spinning_wheel := SpinningWheel
- }) ->
- case SpinningWheel of
- undefined ->
- ok;
- Pid ->
- exit(Pid, kill)
- end,
- ?LOG_DEBUG("Background job completed.", []),
- OnComplete(InternalState),
- ok;
-terminate(Reason, #{ config := #{on_error := OnError}
- , internal_state := InternalState
- , token := Token
- , total := Total
- , progress_enabled := ProgressEnabled
- }) ->
- ?LOG_WARNING( "Background job aborted. [reason=~p]", [Reason]),
- notify_end(Token, Total, ProgressEnabled),
- OnError(InternalState),
- ok.
+terminate(normal, #{
+ config := #{on_complete := OnComplete},
+ internal_state := InternalState,
+ spinning_wheel := SpinningWheel
+}) ->
+ case SpinningWheel of
+ undefined ->
+ ok;
+ Pid ->
+ exit(Pid, kill)
+ end,
+ ?LOG_DEBUG("Background job completed.", []),
+ OnComplete(InternalState),
+ ok;
+terminate(Reason, #{
+ config := #{
+ on_error := OnError,
+ title := Title
+ },
+ internal_state := InternalState,
+ token := Token,
+ total := Total,
+ progress_enabled := ProgressEnabled
+}) ->
+ case Reason of
+ shutdown ->
+ ?LOG_DEBUG("Background job terminated.", []);
+ _ ->
+ ?LOG_ERROR(
+ "Background job aborted. [reason=~p] [title=~p",
+ [Reason, Title]
+ )
+ end,
+ notify_end(Token, Total, ProgressEnabled),
+ OnError(InternalState),
+ ok.
%%==============================================================================
%% Internal functions
@@ -207,50 +272,65 @@ step(N) -> 100 / N.
-spec progress_msg(non_neg_integer(), pos_integer()) -> binary().
progress_msg(Current, Total) ->
- list_to_binary(io_lib:format("~p / ~p", [Current, Total])).
+ list_to_binary(io_lib:format("~p / ~p", [Current, Total])).
-spec noop(any()) -> ok.
noop(_) ->
- ok.
+ ok.
--spec notify_begin( els_progress:token()
- , binary()
- , pos_integer()
- , boolean()
- , boolean()) ->
- ok.
+-spec notify_begin(
+ els_progress:token(),
+ binary(),
+ pos_integer(),
+ boolean(),
+ boolean()
+) ->
+ ok.
notify_begin(Token, Title, Total, true, ShowPercentages) ->
- BeginMsg = progress_msg(0, Total),
- Begin = case ShowPercentages of
+ BeginMsg = progress_msg(0, Total),
+ Begin =
+ case ShowPercentages of
true -> els_work_done_progress:value_begin(Title, BeginMsg, 0);
false -> els_work_done_progress:value_begin(Title, BeginMsg)
- end,
- els_progress:send_notification(Token, Begin);
+ end,
+ els_progress:send_notification(Token, Begin);
notify_begin(_Token, _Title, _Total, false, _ShowPercentages) ->
- ok.
+ ok.
--spec notify_report( els_progress:token(), pos_integer(), pos_integer()
- , pos_integer(), boolean(), boolean()) -> ok.
+-spec notify_report(
+ els_progress:token(),
+ pos_integer(),
+ pos_integer(),
+ pos_integer(),
+ boolean(),
+ boolean()
+) -> ok.
notify_report(Token, Current, Step, Total, true, true) ->
- Percentage = floor(Current * Step),
- ReportMsg = progress_msg(Current, Total),
- Report = els_work_done_progress:value_report(ReportMsg, Percentage),
- els_progress:send_notification(Token, Report);
-notify_report( _Token, _Current, _Step
- , _Total, _ProgressEnabled, _ShowPercentages) ->
- ok.
+ Percentage = floor(Current * Step),
+ ReportMsg = progress_msg(Current, Total),
+ Report = els_work_done_progress:value_report(ReportMsg, Percentage),
+ els_progress:send_notification(Token, Report);
+notify_report(
+ _Token,
+ _Current,
+ _Step,
+ _Total,
+ _ProgressEnabled,
+ _ShowPercentages
+) ->
+ ok.
-spec notify_end(els_progress:token(), pos_integer(), boolean()) -> ok.
notify_end(Token, Total, true) ->
- EndMsg = progress_msg(Total, Total),
- End = els_work_done_progress:value_end(EndMsg),
- els_progress:send_notification(Token, End);
+ EndMsg = progress_msg(Total, Total),
+ End = els_work_done_progress:value_end(EndMsg),
+ els_progress:send_notification(Token, End);
notify_end(_Token, _Total, false) ->
- ok.
+ ok.
-spec spinning_wheel(els_progress:token()) -> no_return().
spinning_wheel(Token) ->
- Report = els_work_done_progress:value_report(<<>>),
- els_progress:send_notification(Token, Report),
- timer:sleep(?SPINNING_WHEEL_INTERVAL),
- spinning_wheel(Token).
+ Report = els_work_done_progress:value_report(<<>>),
+ els_progress:send_notification(Token, Report),
+ timer:sleep(?SPINNING_WHEEL_INTERVAL),
+ spinning_wheel(Token).
diff --git a/apps/els_lsp/src/els_background_job_sup.erl b/apps/els_lsp/src/els_background_job_sup.erl
index 30024608f..96b148f22 100644
--- a/apps/els_lsp/src/els_background_job_sup.erl
+++ b/apps/els_lsp/src/els_background_job_sup.erl
@@ -13,10 +13,10 @@
%%==============================================================================
%% API
--export([ start_link/0 ]).
+-export([start_link/0]).
%% Supervisor Callbacks
--export([ init/1 ]).
+-export([init/1]).
%%==============================================================================
%% Defines
@@ -28,20 +28,24 @@
%%==============================================================================
-spec start_link() -> {ok, pid()}.
start_link() ->
- supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+ supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%%==============================================================================
%% Supervisor callbacks
%%==============================================================================
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init([]) ->
- SupFlags = #{ strategy => simple_one_for_one
- , intensity => 5
- , period => 60
- },
- ChildSpecs = [#{ id => els_background_job
- , start => {els_background_job, start_link, []}
- , restart => temporary
- , shutdown => 5000
- }],
- {ok, {SupFlags, ChildSpecs}}.
+ SupFlags = #{
+ strategy => simple_one_for_one,
+ intensity => 10,
+ period => 10
+ },
+ ChildSpecs = [
+ #{
+ id => els_background_job,
+ start => {els_background_job, start_link, []},
+ restart => temporary,
+ shutdown => 5000
+ }
+ ],
+ {ok, {SupFlags, ChildSpecs}}.
diff --git a/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl b/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl
index 75e303409..4e61a8ce4 100644
--- a/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl
+++ b/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl
@@ -8,10 +8,11 @@
%%==============================================================================
-behaviour(els_diagnostics).
--export([ is_default/0
- , run/1
- , source/0
- ]).
+-export([
+ is_default/0,
+ run/1,
+ source/0
+]).
%%==============================================================================
%% Includes
@@ -29,115 +30,136 @@
-spec is_default() -> boolean().
is_default() ->
- true.
+ true.
-spec run(uri()) -> [els_diagnostics:diagnostic()].
run(Uri) ->
- case filename:extension(Uri) of
- <<".erl">> ->
- BoundVarsInPatterns = find_vars(Uri),
- [make_diagnostic(POI) || POI <- BoundVarsInPatterns];
- _ ->
- []
- end.
+ case filename:extension(Uri) of
+ <<".erl">> ->
+ BoundVarsInPatterns = find_vars(Uri),
+ [make_diagnostic(POI) || POI <- BoundVarsInPatterns];
+ _ ->
+ []
+ end.
-spec source() -> binary().
source() ->
- <<"BoundVarInPattern">>.
+ <<"BoundVarInPattern">>.
%%==============================================================================
%% Internal Functions
%%==============================================================================
--spec find_vars(uri()) -> [poi()].
+-spec find_vars(uri()) -> [els_poi:poi()].
find_vars(Uri) ->
- {ok, #{text := Text}} = els_utils:lookup_document(Uri),
- {ok, Forms} = els_parser:parse_text(Text),
- lists:flatmap(fun find_vars_in_form/1, Forms).
-
--spec find_vars_in_form(erl_syntax:forms()) -> [poi()].
+ {ok, #{text := Text}} = els_utils:lookup_document(Uri),
+ case els_parser:parse_text(Text) of
+ {ok, Forms} ->
+ lists:flatmap(fun find_vars_in_form/1, Forms);
+ {error, Error} ->
+ ?LOG_DEBUG("Cannot parse text [text=~p] [error=~p]", [Text, Error]),
+ []
+ end.
+
+-spec find_vars_in_form(erl_syntax:forms()) -> [els_poi:poi()].
find_vars_in_form(Form) ->
- case erl_syntax:type(Form) of
- function ->
- AnnotatedForm = erl_syntax_lib:annotate_bindings(Form, []),
- %% There are no bound variables in function heads or guards
- %% so lets descend straight into the bodies
- Clauses = erl_syntax:function_clauses(AnnotatedForm),
- ClauseBodies = lists:map(fun erl_syntax:clause_body/1, Clauses),
- fold_subtrees(ClauseBodies, []);
- _ ->
- []
- end.
-
--spec fold_subtrees([[tree()]], [poi()]) -> [poi()].
+ case erl_syntax:type(Form) of
+ function ->
+ %% #1288: The try catch should allow us to understand the root cause
+ %% of the occasional crashes, which could be due to an incorrect mapping
+ %% between the erlfmt AST and erl_syntax AST
+ try
+ AnnotatedForm = erl_syntax_lib:annotate_bindings(Form, []),
+ %% There are no bound variables in function heads or guards
+ %% so lets descend straight into the bodies
+ Clauses = erl_syntax:function_clauses(AnnotatedForm),
+ ClauseBodies = lists:map(fun erl_syntax:clause_body/1, Clauses),
+ fold_subtrees(ClauseBodies, [])
+ catch
+ C:E:St ->
+ ?LOG_ERROR(
+ "Error annotating bindings "
+ "[form=~p] [class=~p] [error=~p] [stacktrace=~p]",
+ [Form, C, E, St]
+ ),
+ []
+ end;
+ _ ->
+ []
+ end.
+
+-spec fold_subtrees([[tree()]], [els_poi:poi()]) -> [els_poi:poi()].
fold_subtrees(Subtrees, Acc) ->
- erl_syntax_lib:foldl_listlist(fun find_vars_in_tree/2, Acc, Subtrees).
+ erl_syntax_lib:foldl_listlist(fun find_vars_in_tree/2, Acc, Subtrees).
--spec find_vars_in_tree(tree(), [poi()]) -> [poi()].
+-spec find_vars_in_tree(tree(), [els_poi:poi()]) -> [els_poi:poi()].
find_vars_in_tree(Tree, Acc) ->
- case erl_syntax:type(Tree) of
- Type when Type =:= fun_expr;
- Type =:= named_fun_expr ->
- %% There is no bound variables in fun expression heads,
- %% because they shadow whatever is in the input env
- %% so lets descend straight into the bodies
- %% (This is a workaround for erl_syntax_lib not considering
- %% shadowing in fun expressions)
- Clauses = case Type of
- fun_expr -> erl_syntax:fun_expr_clauses(Tree);
- named_fun_expr -> erl_syntax:named_fun_expr_clauses(Tree)
+ case erl_syntax:type(Tree) of
+ Type when
+ Type =:= fun_expr;
+ Type =:= named_fun_expr
+ ->
+ %% There is no bound variables in fun expression heads,
+ %% because they shadow whatever is in the input env
+ %% so lets descend straight into the bodies
+ %% (This is a workaround for erl_syntax_lib not considering
+ %% shadowing in fun expressions)
+ Clauses =
+ case Type of
+ fun_expr -> erl_syntax:fun_expr_clauses(Tree);
+ named_fun_expr -> erl_syntax:named_fun_expr_clauses(Tree)
end,
- ClauseBodies = lists:map(fun erl_syntax:clause_body/1, Clauses),
- fold_subtrees(ClauseBodies, Acc);
- match_expr ->
- Pattern = erl_syntax:match_expr_pattern(Tree),
- NewAcc = fold_pattern(Pattern, Acc),
- find_vars_in_tree(erl_syntax:match_expr_body(Tree), NewAcc);
- clause ->
- Patterns = erl_syntax:clause_patterns(Tree),
- NewAcc = fold_pattern_list(Patterns, Acc),
- fold_subtrees([erl_syntax:clause_body(Tree)], NewAcc);
- _ ->
- fold_subtrees(erl_syntax:subtrees(Tree), Acc)
- end.
-
--spec fold_pattern(tree(), [poi()]) -> [poi()].
+ ClauseBodies = lists:map(fun erl_syntax:clause_body/1, Clauses),
+ fold_subtrees(ClauseBodies, Acc);
+ match_expr ->
+ Pattern = erl_syntax:match_expr_pattern(Tree),
+ NewAcc = fold_pattern(Pattern, Acc),
+ find_vars_in_tree(erl_syntax:match_expr_body(Tree), NewAcc);
+ clause ->
+ Patterns = erl_syntax:clause_patterns(Tree),
+ NewAcc = fold_pattern_list(Patterns, Acc),
+ fold_subtrees([erl_syntax:clause_body(Tree)], NewAcc);
+ _ ->
+ fold_subtrees(erl_syntax:subtrees(Tree), Acc)
+ end.
+
+-spec fold_pattern(tree(), [els_poi:poi()]) -> [els_poi:poi()].
fold_pattern(Pattern, Acc) ->
- erl_syntax_lib:fold(fun find_vars_in_pattern/2, Acc, Pattern).
+ erl_syntax_lib:fold(fun find_vars_in_pattern/2, Acc, Pattern).
--spec fold_pattern_list([tree()], [poi()]) -> [poi()].
+-spec fold_pattern_list([tree()], [els_poi:poi()]) -> [els_poi:poi()].
fold_pattern_list(Patterns, Acc) ->
- lists:foldl(fun fold_pattern/2, Acc, Patterns).
+ lists:foldl(fun fold_pattern/2, Acc, Patterns).
--spec find_vars_in_pattern(tree(), [poi()]) -> [poi()].
+-spec find_vars_in_pattern(tree(), [els_poi:poi()]) -> [els_poi:poi()].
find_vars_in_pattern(Tree, Acc) ->
- case erl_syntax:type(Tree) of
- variable ->
- Var = erl_syntax:variable_name(Tree),
- Anno = erl_syntax:get_ann(Tree),
- case lists:keyfind(free, 1, Anno) of
- {free, Free} when Free =:= [Var] ->
- %% Using already bound variable in pattern
- [variable(Tree) | Acc];
+ case erl_syntax:type(Tree) of
+ variable ->
+ Var = erl_syntax:variable_name(Tree),
+ Anno = erl_syntax:get_ann(Tree),
+ case lists:keyfind(free, 1, Anno) of
+ {free, Free} when Free =:= [Var] ->
+ %% Using already bound variable in pattern
+ [variable(Tree) | Acc];
+ _ ->
+ Acc
+ end;
_ ->
- Acc
- end;
- _ ->
- Acc
- end.
+ Acc
+ end.
--spec variable(tree()) -> poi().
+-spec variable(tree()) -> els_poi:poi().
variable(Tree) ->
- Id = erl_syntax:variable_name(Tree),
- Pos = erl_syntax:get_pos(Tree),
- Range = els_range:range(Pos, variable, Id, undefined),
- els_poi:new(Range, variable, Id, undefined).
+ Id = erl_syntax:variable_name(Tree),
+ Pos = erl_syntax:get_pos(Tree),
+ Range = els_range:range(Pos, variable, Id, undefined),
+ els_poi:new(Range, variable, Id, undefined).
--spec make_diagnostic(poi()) -> els_diagnostics:diagnostic().
+-spec make_diagnostic(els_poi:poi()) -> els_diagnostics:diagnostic().
make_diagnostic(#{id := Id, range := POIRange}) ->
- Range = els_protocol:range(POIRange),
- VariableName = atom_to_binary(Id, utf8),
- Message = <<"Bound variable in pattern: ", VariableName/binary>>,
- Severity = ?DIAGNOSTIC_HINT,
- Source = source(),
- els_diagnostics:make_diagnostic(Range, Message, Severity, Source).
+ Range = els_protocol:range(POIRange),
+ VariableName = atom_to_binary(Id, utf8),
+ Message = <<"Bound variable in pattern: ", VariableName/binary>>,
+ Severity = ?DIAGNOSTIC_HINT,
+ Source = source(),
+ els_diagnostics:make_diagnostic(Range, Message, Severity, Source).
diff --git a/apps/els_lsp/src/els_bsp_client.erl b/apps/els_lsp/src/els_bsp_client.erl
deleted file mode 100644
index 425e0b962..000000000
--- a/apps/els_lsp/src/els_bsp_client.erl
+++ /dev/null
@@ -1,305 +0,0 @@
-%%==============================================================================
-%% A client for the Build Server Protocol using the STDIO transport
-%%==============================================================================
-%% https://build-server-protocol.github.io/docs/specification.html
-%%==============================================================================
-
--module(els_bsp_client).
-
-%%==============================================================================
-%% Behaviours
-%%==============================================================================
-
--behaviour(gen_server).
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , handle_info/2
- , terminate/2
- ]).
-
-%%==============================================================================
-%% Exports
-%%==============================================================================
--export([ start_link/0
- , start_server/1
- , stop/0
- , request/1
- , request/2
- , notification/1
- , notification/2
- , wait_response/2
- , check_response/2
- ]).
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include("els_lsp.hrl").
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Defines
-%%==============================================================================
--define(SERVER, ?MODULE).
--define(BSP_WILDCARD, "*.json").
--define(BSP_CONF_DIR, ".bsp").
-
-%%==============================================================================
-%% Record Definitions
-%%==============================================================================
--record(state, { request_id = 1 :: request_id()
- , pending = [] :: [pending_request()]
- , port :: port() | 'undefined'
- , buffer = <<>> :: binary()
- }).
-
-%%==============================================================================
-%% Type Definitions
-%%==============================================================================
--type state() :: #state{}.
--type request_id() :: pos_integer().
--type params() :: #{}.
--type method() :: binary().
--type pending_request() :: [{request_id(), from()}].
--type from() :: {pid(), any()}.
-
-
-%%==============================================================================
-%% Compat Stuff
-%% Since the build server can take arbitrary amounts of time to process things
-%% we really would like to use gen_server:send_request et co that are added in
-%% OTP 23/24, but we also need to support older OTP versions for now - implement
-%% rudimentary scaffolding to fake the new functionality.
-%% Remove whenever only OTP > 23 is supported.
-%%==============================================================================
--type server_ref() :: atom() | pid().
-
--if(?OTP_RELEASE > 23).
--spec do_send_request(server_ref(), any()) -> any().
-do_send_request(ServerRef, Request) ->
- gen_server:send_request(ServerRef, Request).
-
--spec do_wait_response(any(), timeout()) ->
- {reply, any()} |
- timeout |
- {error, {any(), server_ref()}}.
-do_wait_response(RequestId, Timeout) ->
- gen_server:wait_response(RequestId, Timeout).
-
--spec do_check_response(any(), any()) ->
- {reply, any()} |
- no_reply |
- {error, {any(), server_ref()}}.
-do_check_response(Msg, RequestId) ->
- gen_server:check_response(Msg, RequestId).
--else.
--spec do_send_request(server_ref(), any()) -> any().
-do_send_request(ServerRef, Request) ->
- Self = self(),
- Ref = erlang:make_ref(),
- F = fun() ->
- Result = gen_server:call(ServerRef, Request, infinity),
- try Self ! {Ref, Result} catch _:_ -> ok end
- end,
- {Pid, Mon} = erlang:spawn_monitor(F),
- {Pid, Mon, Ref, ServerRef}.
-
--spec do_wait_response(any(), timeout()) ->
- {reply, any()} |
- timeout |
- {error, {any(), server_ref()}}.
-do_wait_response({_Pid, Mon, Ref, ServerRef}, Timeout) ->
- receive
- {Ref, Result} ->
- erlang:demonitor(Mon, [flush]),
- {reply, Result};
- {'DOWN', Mon, _Type, _Object, Info} ->
- {error, {Info, ServerRef}}
- after Timeout ->
- timeout
- end.
-
--spec do_check_response(any(), any()) ->
- {reply, any()} |
- no_reply |
- {error, {any(), server_ref()}}.
-do_check_response(Msg, {_Pid, Mon, Ref, ServerRef}) ->
- case Msg of
- {Ref, Result} ->
- erlang:demonitor(Mon, [flush]),
- {reply, Result};
- {'DOWN', Mon, _Type, _Object, Info} ->
- {error, {Info, ServerRef}};
- _ ->
- no_reply
- end.
--endif.
-
-%%==============================================================================
-%% API
-%%==============================================================================
--spec start_link() -> {ok, pid()}.
-start_link() ->
- gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-
--spec start_server(uri()) -> {ok, map()} | {error, any()}.
-start_server(RootUri) ->
- gen_server:call(?SERVER, {start_server, RootUri}).
-
--spec stop() -> ok.
-stop() ->
- gen_server:stop(?SERVER).
-
--spec notification(method()) -> any().
-notification(Method) ->
- notification(Method, #{}).
-
--spec notification(method(), params()) -> any().
-notification(Method, Params) ->
- gen_server:cast(?SERVER, {notification, Method, Params}).
-
--spec request(method()) -> any().
-request(Method) ->
- request(Method, #{}).
-
--spec request(method(), params()) -> any().
-request(Method, Params) ->
- do_send_request(?SERVER, {request, Method, Params}).
-
--spec wait_response(any(), timeout()) ->
- {reply, any()} | timeout | {error, {any(), any()}}.
-wait_response(RequestId, Timeout) ->
- do_wait_response(RequestId, Timeout).
-
--spec check_response(any(), any()) ->
- {reply, any()} | no_reply | {error, {any(), any()}}.
-check_response(Msg, RequestId) ->
- do_check_response(Msg, RequestId).
-
-%%==============================================================================
-%% gen_server Callback Functions
-%%==============================================================================
--spec init([]) -> {ok, state()}.
-init([]) ->
- process_flag(trap_exit, true),
- {ok, #state{}}.
-
--spec handle_call(any(), any() , state()) ->
- {noreply, state()} | {reply, any(), state()}.
-handle_call({start_server, RootUri}, _From, State) ->
- RootPath = els_uri:path(RootUri),
- case find_config(RootPath) of
- undefined ->
- ?LOG_INFO("Found no BSP configuration. [root=~p]", [RootPath]),
- {reply, {error, noconfig}, State};
- #{ argv := [Cmd|Params] } = Config ->
- Executable = os:find_executable(binary_to_list(Cmd)),
- Args = [binary_to_list(P) || P <- Params],
- Opts = [{args, Args}, use_stdio, binary],
- ?LOG_INFO( "Start BSP Server [executable=~p] [args=~p]"
- , [Executable, Args]
- ),
- Port = open_port({spawn_executable, Executable}, Opts),
- {reply, {ok, Config}, State#state{port = Port}}
- end;
-handle_call({request, Method, Params}, From, State) ->
- #state{port = Port, request_id = RequestId, pending = Pending} = State,
- ?LOG_INFO( "Sending BSP Request [id=~p] [method=~p] [params=~p]"
- , [RequestId, Method, Params]
- ),
- Payload = els_protocol:request(RequestId, Method, Params),
- port_command(Port, Payload),
- {noreply, State#state{ request_id = RequestId + 1
- , pending = [{RequestId, From} | Pending]
- }}.
-
--spec handle_cast(any(), state()) -> {noreply, state()}.
-handle_cast({notification, Method, Params}, State) ->
- ?LOG_INFO( "Sending BSP Notification [method=~p] [params=~p]"
- , [Method, Params]
- ),
- #state{port = Port} = State,
- Payload = els_protocol:notification(Method, Params),
- port_command(Port, Payload),
- {noreply, State}.
-
--spec handle_info(any(), state()) -> {noreply, state()}.
-handle_info({Port, {data, Data}}, #state{port = Port} = State) ->
- NewState = handle_data(Data, State),
- {noreply, NewState};
-handle_info(_Request, State) ->
- {noreply, State}.
-
--spec terminate(any(), state()) -> ok.
-terminate(_Reason, #state{port = Port} = _State) ->
- case Port of
- undefined ->
- ok;
- _ ->
- port_close(Port)
- end,
- ok.
-
-%%==============================================================================
-%% Internal Functions
-%%==============================================================================
--spec find_config(els_uri:path()) -> map() | undefined.
-find_config(RootDir) ->
- Wildcard = filename:join([RootDir, ?BSP_CONF_DIR, ?BSP_WILDCARD]),
- Candidates = filelib:wildcard(els_utils:to_list(Wildcard)),
- choose_config(Candidates).
-
--spec choose_config([file:filename()]) -> map() | undefined.
-choose_config([]) ->
- undefined;
-choose_config([F|Fs]) ->
- try
- {ok, Content} = file:read_file(F),
- Config = jsx:decode(Content, [return_maps, {labels, atom}]),
- Languages = maps:get(languages, Config),
- case lists:member(<<"erlang">>, Languages) of
- true ->
- Config;
- false ->
- choose_config(Fs)
- end
- catch
- C:E:S ->
- ?LOG_ERROR( "Bad BSP config file. [file=~p] [error=~p]"
- , [F, {C, E, S}]
- ),
- choose_config(Fs)
- end.
-
--spec handle_data(binary(), state()) -> state().
-handle_data(Data, State) ->
- #state{buffer = Buffer, pending = Pending} = State,
- NewData = <>,
- ?LOG_DEBUG( "Received BSP Data [buffer=~p] [data=~p]"
- , [Buffer, Data]
- ),
- {Messages, NewBuffer} = els_jsonrpc:split(NewData),
- NewPending = lists:foldl(fun handle_message/2, Pending, Messages),
- State#state{buffer = NewBuffer, pending = NewPending}.
-
--spec handle_message(message(), [pending_request()]) -> [pending_request()].
-handle_message(#{ id := Id
- , method := Method
- , params := Params
- } = _Request, Pending) ->
- ?LOG_INFO( "Received BSP Request [id=~p] [method=~p] [params=~p]"
- , [Id, Method, Params]
- ),
- %% TODO: Handle server-initiated request
- Pending;
-handle_message(#{id := Id} = Response, Pending) ->
- From = proplists:get_value(Id, Pending),
- gen_server:reply(From, Response),
- lists:keydelete(Id, 1, Pending);
-handle_message(#{ method := Method, params := Params}, State) ->
- ?LOG_INFO( "Received BSP Notification [method=~p] [params=~p]"
- , [Method, Params]
- ),
- %% TODO: Handle server-initiated notification
- State.
diff --git a/apps/els_lsp/src/els_bsp_provider.erl b/apps/els_lsp/src/els_bsp_provider.erl
deleted file mode 100644
index 137cad884..000000000
--- a/apps/els_lsp/src/els_bsp_provider.erl
+++ /dev/null
@@ -1,262 +0,0 @@
--module(els_bsp_provider).
-
--behaviour(els_provider).
-
-%% API
--export([ start/1
- , maybe_start/1
- , info/1
- , request/2
- ]).
-
-%% els_provider functions
--export([ is_enabled/0
- , init/0
- , handle_request/2
- , handle_info/2
- ]).
-
-
-%%==============================================================================
-%% Includes
-%%==============================================================================
--include("els_lsp.hrl").
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Types
-%%==============================================================================
--type state() :: #{ running := boolean() % BSP server running?
- , root_uri := uri() | undefined % the root uri
- , pending_requests := list() % pending requests
- , pending_sources := list() % pending source info
- }.
--type method() :: binary().
--type params() :: map() | null.
--type request_response() :: {reply, any()} | {error, any()}.
--type request_id() :: {reference(), reference(), atom() | pid()}.
--type from() :: {pid(), reference()} | self.
--type config() :: map().
--type info_item() :: is_running.
-
-%%==============================================================================
-%% API
-%%==============================================================================
--spec start(uri()) -> {ok, config()} | {error, term()}.
-start(RootUri) ->
- els_provider:handle_request(?MODULE, {start, #{ root => RootUri }}).
-
--spec maybe_start(uri()) -> {ok, config()} | {error, term()} | disabled.
-maybe_start(RootUri) ->
- case els_config:get(bsp_enabled) of
- false ->
- disabled;
- X when X =:= true orelse X =:= auto ->
- start(RootUri)
- end.
-
--spec info(info_item()) -> any().
-info(Item) ->
- case els_provider:handle_request(?MODULE, {info, #{ item => Item}}) of
- {ok, Result} ->
- Result;
- {error, badarg} ->
- erlang:error(badarg, [Item])
- end.
-
--spec request(method(), params()) -> request_response().
-request(Method, Params) ->
- RequestId = send_request(Method, Params),
- wait_response(RequestId, infinity).
-
--spec send_request(method(), params()) -> request_id().
-send_request(Method, Params) ->
- Mon = erlang:monitor(process, ?MODULE),
- Ref = erlang:make_ref(),
- From = {self(), Ref},
- Request = #{ from => From, method => Method, params => Params },
- ok = els_provider:handle_request(?MODULE, {send_request, Request}),
- {Ref, Mon, ?MODULE}.
-
--spec wait_response(request_id(), timeout()) -> request_response() | timeout.
-wait_response({Ref, Mon, ServerRef}, Timeout) ->
- receive
- {Ref, Response} ->
- erlang:demonitor(Mon, [flush]),
- Response;
- {'DOWN', Mon, _Type, _Object, Info} ->
- erlang:demonitor(Mon, [flush]),
- {error, {Info, ServerRef}}
- after Timeout ->
- timeout
- end.
-
-%%==============================================================================
-%% els_provider functions
-%%==============================================================================
--spec init() -> state().
-init() ->
- #{ running => false
- , root_uri => undefined
- , pending_requests => []
- , pending_sources => [ apps_paths, deps_paths ]
- }.
-
--spec is_enabled() -> true.
-is_enabled() -> true.
-
--spec handle_request({start, #{ root := uri() }}, state())
- -> {{ok, config()}, state()} | {{error, any()}, state()};
- ({send_request, #{ from := from()
- , method := method()
- , params := params() }}, state())
- -> {ok, state()};
- ({info, info_item()}, state())
- -> {{ok, any()}, state()} | {{error, badarg}, state()}.
-handle_request({start, #{ root := RootUri }}, #{ running := false } = State) ->
- ?LOG_INFO("Starting BSP server in ~p", [RootUri]),
- case els_bsp_client:start_server(RootUri) of
- {ok, Config} ->
- ?LOG_INFO("BSP server started from config ~p", [Config]),
- {{ok, Config}, initialize_bsp(RootUri, State)};
- {error, Reason} ->
- ?LOG_INFO("BSP server startup failed: ~p", [Reason]),
- {{error, Reason}, State}
- end;
-handle_request({send_request, #{ from := From
- , method := Method
- , params := Params }}, State) ->
- case State of
- #{ running := false } ->
- reply_request(From, {error, not_running}),
- {ok, State};
- #{ running := true } ->
- {ok, request(From, Method, Params, State)}
- end;
-handle_request({info, #{ item := Item }}, State) ->
- case Item of
- is_running ->
- {{ok, maps:get(running, State)}, State};
- _ ->
- {{error, badarg}, State}
- end.
-
--spec handle_info(any(), state()) -> state().
-handle_info(Msg, State) ->
- case check_response(Msg, State) of
- {ok, NewState} ->
- NewState;
- no_reply ->
- ?LOG_WARNING("Discarding unrecognized message: ~p", [Msg]),
- State
- end.
-
-%%==============================================================================
-%% Internal functions
-%%==============================================================================
--spec initialize_bsp(uri(), state()) -> state().
-initialize_bsp(Root, State) ->
- {ok, Vsn} = application:get_key(els_lsp, vsn),
- Params = #{ <<"displayName">> => <<"Erlang LS BSP Client">>
- , <<"version">> => list_to_binary(Vsn)
- , <<"bspVersion">> => <<"2.0.0">>
- , <<"rootUri">> => Root
- , <<"capabilities">> => #{ <<"languageIds">> => [<<"erlang">>] }
- , <<"data">> => #{}
- },
- request(<<"build/initialize">>, Params, State#{ running => true
- , root_uri => Root }).
-
--spec request(method(), params(), state()) -> state().
-request(Method, Params, State) ->
- request(self, Method, Params, State).
-
--spec request(from(), method(), params(), state()) -> state().
-request(From, Method, Params, #{ pending_requests := Pending } = State) ->
- RequestId = els_bsp_client:request(Method, Params),
- PendingRequest = {RequestId, From, {Method, Params}},
- State#{ pending_requests => [PendingRequest | Pending] }.
-
--spec handle_response({binary(), any()}, any(), state()) -> state().
-handle_response({<<"build/initialize">>, _}, Response, State) ->
- ?LOG_INFO("BSP Server initialized: ~p", [Response]),
- ok = els_bsp_client:notification(<<"build/initialized">>),
- request(<<"workspace/buildTargets">>, #{}, State);
-handle_response({<<"workspace/buildTargets">>, _}, Response, State0) ->
- Result = maps:get(result, Response, #{}),
- Targets = maps:get(targets, Result, []),
- TargetIds = lists:flatten([ maps:get(id, Target, []) || Target <- Targets ]),
- Params = #{ <<"targets">> => TargetIds },
- State1 = request(<<"buildTarget/sources">>, Params, State0),
- State2 = request(<<"buildTarget/dependencySources">>, Params, State1),
- State2;
-handle_response({<<"buildTarget/sources">>, _}, Response, State) ->
- handle_sources(apps_paths,
- fun(Source) -> maps:get(uri, Source, []) end,
- Response,
- State);
-handle_response({<<"buildTarget/dependencySources">>, _}, Response, State) ->
- handle_sources(deps_paths,
- fun(Source) -> Source end,
- Response,
- State);
-handle_response(Request, Response, State) ->
- ?LOG_WARNING("Unhandled response. [request=~p] [response=~p]",
- [Request, Response]),
- State.
-
--spec handle_sources(atom(), fun((any()) -> uri()), map(), state()) -> state().
-handle_sources(ConfigKey, SourceFun, Response, State) ->
- Result = maps:get(result, Response, #{}),
- Items = maps:get(items, Result, []),
- Sources = lists:flatten([ maps:get(sources, Item, []) || Item <- Items ]),
- Uris = lists:flatten([ SourceFun(Source) || Source <- Sources ]),
- UriMaps = [ uri_string:parse(Uri) || Uri <- Uris ],
- NewPaths = lists:flatten([ maps:get(path, UM, []) || UM <- UriMaps ]),
- OldPaths = els_config:get(ConfigKey),
- AllPaths = lists:usort([ els_utils:to_list(P) || P <- OldPaths ++ NewPaths]),
- els_config:set(ConfigKey, AllPaths),
- PendingSources = maps:get(pending_sources, State) -- [ConfigKey],
- case PendingSources of
- [] ->
- els_indexing:maybe_start();
- _ ->
- ok
- end,
- State#{ pending_sources => PendingSources }.
-
--spec check_response(any(), state()) -> {ok, state()} | no_reply.
-check_response(Msg, #{ pending_requests := Pending } = State) ->
- F = fun({RequestId, From, Request}) ->
- case els_bsp_client:check_response(Msg, RequestId) of
- no_reply ->
- true;
- {reply, _Reply} ->
- false;
- {error, Reason} ->
- ?LOG_ERROR("BSP request error. [from=~p] [request~p] [error=~p]",
- [From, Request, Reason]),
- false
- end
- end,
- case lists:splitwith(F, Pending) of
- {_, []} ->
- no_reply;
- {Left, [{RequestId, From, Request} | Right]} ->
- Result = els_bsp_client:check_response(Msg, RequestId),
- NewState = State#{ pending_requests => Left ++ Right },
- case {From, Result} of
- {self, {reply, Reply}} ->
- {ok, handle_response(Request, Reply, NewState)};
- {self, {error, _Reason}} ->
- {ok, NewState};
- {From, Result} ->
- ok = reply_request(From, Result),
- {ok, NewState}
- end
- end.
-
--spec reply_request(from(), any()) -> ok.
-reply_request({Pid, Ref}, Result) ->
- try Pid ! {Ref, Result} catch _:_ -> ok end,
- ok.
diff --git a/apps/els_lsp/src/els_call_hierarchy_item.erl b/apps/els_lsp/src/els_call_hierarchy_item.erl
index e5b9abdcb..f36ccc978 100644
--- a/apps/els_lsp/src/els_call_hierarchy_item.erl
+++ b/apps/els_lsp/src/els_call_hierarchy_item.erl
@@ -2,51 +2,56 @@
-include("els_lsp.hrl").
--include_lib("kernel/include/logger.hrl").
-
--export([ new/5
- , poi/1
- ]).
+-export([
+ new/5,
+ poi/1
+]).
-type data() :: any().
--type item() :: #{ name := binary()
- , kind := symbol_kind()
- , tags => [symbol_tag()]
- , detail => binary()
- , uri := uri()
- , range := range()
- , selectionRange := range()
- , data => data()
- }.
--type incoming_call() :: #{ from := item()
- , fromRanges := [range()]
- }.
--type outgoing_call() :: #{ to := item()
- , fromRanges := [range()]
- }.
--export_type([ item/0
- , incoming_call/0
- , outgoing_call/0
- ]).
+-type item() :: #{
+ name := binary(),
+ kind := symbol_kind(),
+ tags => [symbol_tag()],
+ detail => binary(),
+ uri := uri(),
+ range := range(),
+ selectionRange := range(),
+ data => data()
+}.
+-type incoming_call() :: #{
+ from := item(),
+ fromRanges := [range()]
+}.
+-type outgoing_call() :: #{
+ to := item(),
+ fromRanges := [range()]
+}.
+-export_type([
+ item/0,
+ incoming_call/0,
+ outgoing_call/0
+]).
%% @doc Extract and decode the POI from the data
--spec poi(item()) -> poi().
+-spec poi(item()) -> els_poi:poi().
poi(#{<<"data">> := Data}) ->
- maps:get(poi, els_utils:base64_decode_term(Data)).
+ maps:get(poi, els_utils:base64_decode_term(Data)).
--spec new(binary(), uri(), poi_range(), poi_range(), data()) -> item().
+-spec new(binary(), uri(), els_poi:poi_range(), els_poi:poi_range(), data()) -> item().
new(Name, Uri, Range, SelectionRange, Data) ->
- #{from := {StartLine, _}} = Range,
- Detail = <<(atom_to_binary(els_uri:module(Uri), utf8))/binary,
- " [L",
- (integer_to_binary(StartLine))/binary,
- "]"
- >>,
- #{ name => Name
- , kind => ?SYMBOLKIND_FUNCTION
- , detail => Detail
- , uri => Uri
- , range => els_protocol:range(Range)
- , selectionRange => els_protocol:range(SelectionRange)
- , data => els_utils:base64_encode_term(Data)
- }.
+ #{from := {StartLine, _}} = Range,
+ Detail = <<
+ (atom_to_binary(els_uri:module(Uri), utf8))/binary,
+ " [L",
+ (integer_to_binary(StartLine))/binary,
+ "]"
+ >>,
+ #{
+ name => Name,
+ kind => ?SYMBOLKIND_FUNCTION,
+ detail => Detail,
+ uri => Uri,
+ range => els_protocol:range(Range),
+ selectionRange => els_protocol:range(SelectionRange),
+ data => els_utils:base64_encode_term(Data)
+ }.
diff --git a/apps/els_lsp/src/els_call_hierarchy_provider.erl b/apps/els_lsp/src/els_call_hierarchy_provider.erl
index b3caf61a8..7f440fe84 100644
--- a/apps/els_lsp/src/els_call_hierarchy_provider.erl
+++ b/apps/els_lsp/src/els_call_hierarchy_provider.erl
@@ -2,111 +2,109 @@
-behaviour(els_provider).
--export([ handle_request/2
- , is_enabled/0
- ]).
+-export([
+ handle_request/1
+]).
%%==============================================================================
%% Includes
%%==============================================================================
-include("els_lsp.hrl").
--include_lib("kernel/include/logger.hrl").
-
-%%==============================================================================
-%% Defines
-%%==============================================================================
-
-%%==============================================================================
-%% Types
-%%==============================================================================
--type state() :: any().
-
%%==============================================================================
%% els_provider functions
%%==============================================================================
--spec is_enabled() -> boolean().
-is_enabled() -> true.
-
--spec handle_request(any(), state()) -> {any(), state()}.
-handle_request({prepare, Params}, State) ->
- {Uri, Line, Char} =
- els_text_document_position_params:uri_line_character(Params),
- {ok, Document} = els_utils:lookup_document(Uri),
- Functions = els_dt_document:wrapping_functions(Document, Line + 1, Char + 1),
- Items = [function_to_item(Uri, F) || F <- Functions],
- {Items, State};
-handle_request({incoming_calls, Params}, State) ->
- #{ <<"item">> := #{<<"uri">> := Uri} = Item } = Params,
- POI = els_call_hierarchy_item:poi(Item),
- References = els_references_provider:find_references(Uri, POI),
- Items = [reference_to_item(Reference) || Reference <- References],
- {incoming_calls(Items), State};
-handle_request({outgoing_calls, Params}, State) ->
- #{ <<"item">> := Item } = Params,
- #{ <<"uri">> := Uri } = Item,
- POI = els_call_hierarchy_item:poi(Item),
- Applications = applications_in_function_range(Uri, POI),
- Items = lists:foldl(fun(Application, Acc) ->
- case application_to_item(Uri, Application) of
- {error, not_found} ->
- %% The function may contain a reference
- %% to a not yet implemented function
- Acc;
- {ok, I} ->
- [I|Acc]
- end
- end, [], Applications),
- {outgoing_calls(lists:reverse(Items)), State}.
+-spec handle_request(any()) -> {response, any()}.
+handle_request({prepare, Params}) ->
+ {Uri, Line, Char} =
+ els_text_document_position_params:uri_line_character(Params),
+ {ok, Document} = els_utils:lookup_document(Uri),
+ Functions = els_dt_document:wrapping_functions(Document, Line + 1, Char + 1),
+ Items = [function_to_item(Uri, F) || F <- Functions],
+ {response, Items};
+handle_request({incoming_calls, Params}) ->
+ #{<<"item">> := #{<<"uri">> := Uri} = Item} = Params,
+ POI = els_call_hierarchy_item:poi(Item),
+ References = els_references_provider:find_references(Uri, POI),
+ Items = lists:flatten([reference_to_item(Reference) || Reference <- References]),
+ {response, incoming_calls(Items)};
+handle_request({outgoing_calls, Params}) ->
+ #{<<"item">> := Item} = Params,
+ #{<<"uri">> := Uri} = Item,
+ POI = els_call_hierarchy_item:poi(Item),
+ Applications = applications_in_function_range(Uri, POI),
+ Items = lists:foldl(
+ fun(Application, Acc) ->
+ case application_to_item(Uri, Application) of
+ {error, not_found} ->
+ %% The function may contain a reference
+ %% to a not yet implemented function
+ Acc;
+ {ok, I} ->
+ [I | Acc]
+ end
+ end,
+ [],
+ Applications
+ ),
+ {response, outgoing_calls(lists:reverse(Items))}.
%%==============================================================================
%% Internal functions
%%==============================================================================
-spec incoming_calls([els_call_hierarchy_item:item()]) ->
- [els_call_hierarchy_item:incoming_call()].
+ [els_call_hierarchy_item:incoming_call()].
incoming_calls(Items) ->
- [#{from => Item, fromRanges => [Range]} || #{range := Range} = Item <- Items].
+ [#{from => Item, fromRanges => [Range]} || #{range := Range} = Item <- Items].
-spec outgoing_calls([els_call_hierarchy_item:item()]) ->
- [els_call_hierarchy_item:outgoing_call()].
+ [els_call_hierarchy_item:outgoing_call()].
outgoing_calls(Items) ->
- [#{to => Item, fromRanges => [Range]} || #{range := Range} = Item <- Items].
+ [#{to => Item, fromRanges => [Range]} || #{range := Range} = Item <- Items].
--spec function_to_item(uri(), poi()) -> els_call_hierarchy_item:item().
+-spec function_to_item(uri(), els_poi:poi()) -> els_call_hierarchy_item:item().
function_to_item(Uri, Function) ->
- #{id := Id, range := Range} = Function,
- Name = els_utils:function_signature(Id),
- Data = #{poi => Function},
- els_call_hierarchy_item:new(Name, Uri, Range, Range, Data).
+ #{id := Id, range := Range} = Function,
+ Name = els_utils:function_signature(Id),
+ Data = #{poi => Function},
+ els_call_hierarchy_item:new(Name, Uri, Range, Range, Data).
--spec reference_to_item(location()) -> els_call_hierarchy_item:item().
+-spec reference_to_item(location()) -> [els_call_hierarchy_item:item()].
reference_to_item(Reference) ->
- #{uri := RefUri, range := RefRange} = Reference,
- {ok, RefDoc} = els_utils:lookup_document(RefUri),
- [WrappingPOI] = els_dt_document:wrapping_functions(RefDoc, RefRange),
- Name = els_utils:function_signature(maps:get(id, WrappingPOI)),
- POIRange = els_range:to_poi_range(RefRange),
- Data = #{poi => WrappingPOI},
- els_call_hierarchy_item:new(Name, RefUri, POIRange, POIRange, Data).
+ #{uri := RefUri, range := RefRange} = Reference,
+ {ok, RefDoc} = els_utils:lookup_document(RefUri),
+ case els_dt_document:wrapping_functions(RefDoc, RefRange) of
+ [WrappingPOI] ->
+ els_dt_document:wrapping_functions(RefDoc, RefRange),
+ Name = els_utils:function_signature(maps:get(id, WrappingPOI)),
+ POIRange = els_range:to_poi_range(RefRange),
+ Data = #{poi => WrappingPOI},
+ [els_call_hierarchy_item:new(Name, RefUri, POIRange, POIRange, Data)];
+ _ ->
+ []
+ end.
--spec application_to_item(uri(), poi()) ->
- {ok, els_call_hierarchy_item:item()} | {error, not_found}.
+-spec application_to_item(uri(), els_poi:poi()) ->
+ {ok, els_call_hierarchy_item:item()} | {error, not_found}.
application_to_item(Uri, Application) ->
- #{id := Id} = Application,
- Name = els_utils:function_signature(Id),
- case els_code_navigation:goto_definition(Uri, Application) of
- {ok, DefUri, DefPOI} ->
- DefRange = maps:get(range, DefPOI),
- Data = #{poi => DefPOI},
- {ok, els_call_hierarchy_item:new(Name, DefUri, DefRange, DefRange, Data)};
- {error, Reason} ->
- {error, Reason}
- end.
+ #{id := Id} = Application,
+ Name = els_utils:function_signature(Id),
+ case els_code_navigation:goto_definition(Uri, Application) of
+ {ok, [{DefUri, DefPOI} | _]} ->
+ DefRange = maps:get(range, DefPOI),
+ Data = #{poi => DefPOI},
+ {ok, els_call_hierarchy_item:new(Name, DefUri, DefRange, DefRange, Data)};
+ {error, Reason} ->
+ {error, Reason}
+ end.
--spec applications_in_function_range(uri(), poi()) ->
- [poi()].
+-spec applications_in_function_range(uri(), els_poi:poi()) ->
+ [els_poi:poi()].
applications_in_function_range(Uri, Function) ->
- {ok, Document} = els_utils:lookup_document(Uri),
- #{data := #{wrapping_range := WrappingRange}} = Function,
- AllApplications = els_dt_document:pois(Document, ['application']),
- [A || #{range := AppRange} = A <- AllApplications
- , els_range:in(AppRange, WrappingRange)].
+ {ok, Document} = els_utils:lookup_document(Uri),
+ #{data := #{wrapping_range := WrappingRange}} = Function,
+ AllApplications = els_dt_document:pois(Document, ['application']),
+ [
+ A
+ || #{range := AppRange} = A <- AllApplications,
+ els_range:in(AppRange, WrappingRange)
+ ].
diff --git a/apps/els_lsp/src/els_code_action_provider.erl b/apps/els_lsp/src/els_code_action_provider.erl
index 8acd202ed..a2631f0a2 100644
--- a/apps/els_lsp/src/els_code_action_provider.erl
+++ b/apps/els_lsp/src/els_code_action_provider.erl
@@ -2,106 +2,93 @@
-behaviour(els_provider).
--export([ handle_request/2
- , is_enabled/0
- ]).
+-export([
+ handle_request/1
+]).
-include("els_lsp.hrl").
--type state() :: any().
-
%%==============================================================================
%% els_provider functions
%%==============================================================================
-
--spec is_enabled() -> boolean().
-is_enabled() -> true.
-
--spec handle_request(any(), state()) -> {any(), state()}.
-handle_request({document_codeaction, Params}, State) ->
- #{ <<"textDocument">> := #{ <<"uri">> := Uri}
- , <<"range">> := RangeLSP
- , <<"context">> := Context } = Params,
- Result = code_actions(Uri, RangeLSP, Context),
- {Result, State}.
+-spec handle_request(any()) -> {response, any()}.
+handle_request({document_codeaction, Params}) ->
+ %% TODO: Make code actions run async?
+ %% TODO: Extract document here
+ #{
+ <<"textDocument">> := #{<<"uri">> := Uri},
+ <<"range">> := RangeLSP,
+ <<"context">> := Context
+ } = Params,
+ Result = code_actions(Uri, RangeLSP, Context),
+ {response, Result}.
%%==============================================================================
%% Internal Functions
%%==============================================================================
-
%% @doc Result: `(Command | CodeAction)[] | null'
-spec code_actions(uri(), range(), code_action_context()) -> [map()].
-code_actions(Uri, _Range, Context) ->
- #{ <<"diagnostics">> := Diagnostics } = Context,
- Actions0 = [ make_code_action(Uri, D) || D <- Diagnostics],
- Actions = lists:flatten(Actions0),
- Actions.
-
-%% @doc Note: if the start and end line of the range are the same, the line
-%% is simply added.
--spec replace_lines_action(uri(), binary(), binary(), binary(), range())
- -> map().
-replace_lines_action(Uri, Title, Kind, Lines, Range) ->
- #{ <<"start">> := #{ <<"character">> := _StartCol
- , <<"line">> := StartLine }
- , <<"end">> := #{ <<"character">> := _EndCol
- , <<"line">> := EndLine }
- } = Range,
- #{ title => Title
- , kind => Kind
- , command =>
- els_command:make_command( Title
- , <<"replace-lines">>
- , [#{ uri => Uri
- , lines => Lines
- , from => StartLine
- , to => EndLine }])
- }.
-
--spec make_code_action(uri(), els_diagnostics:diagnostic()) -> [map()].
-make_code_action(Uri, #{ <<"message">> := Message
- , <<"range">> := Range } = _Diagnostic) ->
- unused_variable_action(Uri, Range, Message).
-
-%%------------------------------------------------------------------------------
-
--spec unused_variable_action(uri(), range(), binary()) -> [map()].
-unused_variable_action(Uri, Range, Message) ->
- %% Processing messages like "variable 'Foo' is unused"
- case re:run(Message, "variable '(.*)' is unused"
- , [{capture, all_but_first, binary}]) of
- {match, [UnusedVariable]} ->
- make_unused_variable_action(Uri, Range, UnusedVariable);
- _ -> []
- end.
-
--spec make_unused_variable_action(uri(), range(), binary()) -> [map()].
-make_unused_variable_action(Uri, Range, UnusedVariable) ->
- #{ <<"start">> := #{ <<"character">> := _StartCol
- , <<"line">> := StartLine }
- , <<"end">> := _End
- } = Range,
- %% processing messages like "variable 'Foo' is unused"
- {ok, #{text := Bin}} = els_utils:lookup_document(Uri),
- Line = els_utils:to_list(els_text:line(Bin, StartLine)),
-
- {ok, Tokens, _} = erl_scan:string(Line, 1, [return, text]),
- UnusedString = els_utils:to_list(UnusedVariable),
- Replace =
- fun(Tok) ->
- case Tok of
- {var, [{text, UnusedString}, _], _} -> "_" ++ UnusedString;
- {var, [{text, VarName}, _], _} -> VarName;
- {_, [{text, Text }, _], _} -> Text;
- {_, [{text, Text }, _]} -> Text
- end
- end,
- UpdatedLine = lists:flatten(lists:map(Replace, Tokens)) ++ "\n",
- [ replace_lines_action( Uri
- , <<"Add '_' to '", UnusedVariable/binary, "'">>
- , ?CODE_ACTION_KIND_QUICKFIX
- , els_utils:to_binary(UpdatedLine)
- , Range)].
-
-%%------------------------------------------------------------------------------
+code_actions(Uri, Range, #{<<"diagnostics">> := Diagnostics}) ->
+ lists:usort(
+ lists:flatten([make_code_actions(Uri, D) || D <- Diagnostics]) ++
+ wrangler_handler:get_code_actions(Uri, Range) ++
+ els_code_actions:extract_function(Uri, Range) ++
+ els_code_actions:bump_variables(Uri, Range) ++
+ els_code_actions:browse_docs(Uri, Range)
+ ).
+
+-spec make_code_actions(uri(), map()) -> [map()].
+make_code_actions(
+ Uri,
+ #{<<"message">> := Message, <<"range">> := Range} = Diagnostic
+) ->
+ Data = maps:get(<<"data">>, Diagnostic, <<>>),
+ els_code_actions:browse_error(Diagnostic) ++
+ make_code_actions(
+ [
+ {"function (.*) is unused", fun els_code_actions:export_function/4},
+ {"variable '(.*)' is unused", fun els_code_actions:ignore_variable/4},
+ {"variable '(.*)' is unbound", fun els_code_actions:suggest_variable/4},
+ {"undefined macro '(.*)'", fun els_code_actions:add_include_lib_macro/4},
+ {"undefined macro '(.*)'", fun els_code_actions:define_macro/4},
+ {"undefined macro '(.*)'", fun els_code_actions:suggest_macro/4},
+ {"record (.*) undefined", fun els_code_actions:add_include_lib_record/4},
+ {"record (.*) undefined", fun els_code_actions:define_record/4},
+ {"record (.*) undefined", fun els_code_actions:suggest_record/4},
+ {"field (.*) undefined in record (.*)",
+ fun els_code_actions:suggest_record_field/4},
+ {"Module name '(.*)' does not match file name '(.*)'",
+ fun els_code_actions:fix_module_name/4},
+ {"Unused macro: (.*)", fun els_code_actions:remove_macro/4},
+ {"function (.*) undefined", fun els_code_actions:create_function/4},
+ {"function (.*) undefined", fun els_code_actions:suggest_function/4},
+ {"Cannot find definition for function (.*)",
+ fun els_code_actions:suggest_function/4},
+ {"Cannot find module (.*)", fun els_code_actions:suggest_module/4},
+ {"Unused file: (.*)", fun els_code_actions:remove_unused/4},
+ {"Atom typo\\? Did you mean: (.*)", fun els_code_actions:fix_atom_typo/4},
+ {"undefined callback function (.*) \\\(behaviour '(.*)'\\\)",
+ fun els_code_actions:undefined_callback/4}
+ ],
+ Uri,
+ Range,
+ Data,
+ Message
+ ).
+
+-spec make_code_actions([{string(), Fun}], uri(), range(), binary(), binary()) ->
+ [map()]
+when
+ Fun :: fun((uri(), range(), binary(), [binary()]) -> [map()]).
+make_code_actions([], _Uri, _Range, _Data, _Message) ->
+ [];
+make_code_actions([{RE, Fun} | Rest], Uri, Range, Data, Message) ->
+ Actions =
+ case re:run(Message, RE, [{capture, all_but_first, binary}]) of
+ {match, Matches} ->
+ Fun(Uri, Range, Data, Matches);
+ nomatch ->
+ []
+ end,
+ Actions ++ make_code_actions(Rest, Uri, Range, Data, Message).
diff --git a/apps/els_lsp/src/els_code_actions.erl b/apps/els_lsp/src/els_code_actions.erl
new file mode 100644
index 000000000..3d0ba952b
--- /dev/null
+++ b/apps/els_lsp/src/els_code_actions.erl
@@ -0,0 +1,787 @@
+-module(els_code_actions).
+-export([
+ extract_function/2,
+ create_function/4,
+ export_function/4,
+ fix_module_name/4,
+ ignore_variable/4,
+ remove_macro/4,
+ remove_unused/4,
+ suggest_variable/4,
+ fix_atom_typo/4,
+ undefined_callback/4,
+ define_macro/4,
+ define_record/4,
+ add_include_lib_macro/4,
+ add_include_lib_record/4,
+ suggest_macro/4,
+ suggest_record/4,
+ suggest_record_field/4,
+ suggest_function/4,
+ suggest_module/4,
+ bump_variables/2,
+ browse_error/1,
+ browse_docs/2
+]).
+
+-include("els_lsp.hrl").
+-spec create_function(uri(), range(), binary(), [binary()]) -> [map()].
+create_function(Uri, Range0, _Data, [UndefinedFun]) ->
+ {ok, #{text := Text} = Document} = els_utils:lookup_document(Uri),
+ Range = els_range:to_poi_range(Range0),
+ Indent = guess_indentation(string:lexemes(Text, "\n")),
+ IndentStr = lists:duplicate(Indent, 32),
+ FunPOIs = els_dt_document:pois(Document, [function]),
+ %% Figure out which function the error was found in, as we want to
+ %% create the function right after the current function.
+ %% (Where the wrapping_range ends)
+ case
+ [
+ R
+ || #{data := #{wrapping_range := R}} <- FunPOIs,
+ els_range:in(Range, R)
+ ]
+ of
+ [#{to := {Line, _}} | _] ->
+ [Name, ArityBin] = string:split(UndefinedFun, "/"),
+ Arity = binary_to_integer(ArityBin),
+ Args = format_args(Document, Arity, Range),
+ SpecAndFun = io_lib:format(
+ "~s(~s) ->\n~sok.\n\n",
+ [Name, Args, IndentStr]
+ ),
+ [
+ make_edit_action(
+ Uri,
+ <<"Create function ", UndefinedFun/binary>>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ iolist_to_binary(SpecAndFun),
+ els_protocol:range(#{
+ from => {Line + 1, 1},
+ to => {Line + 1, 1}
+ })
+ )
+ ];
+ _ ->
+ []
+ end.
+
+-spec export_function(uri(), range(), binary(), [binary()]) -> [map()].
+export_function(Uri, _Range, _Data, [UnusedFun]) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ case els_poi:sort(els_dt_document:pois(Document, [module, export])) of
+ [] ->
+ [];
+ POIs ->
+ #{range := #{to := {Line, _Col}}} = lists:last(POIs),
+ Pos = {Line + 1, 1},
+ [
+ make_edit_action(
+ Uri,
+ <<"Export ", UnusedFun/binary>>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <<"-export([", UnusedFun/binary, "]).\n">>,
+ els_protocol:range(#{from => Pos, to => Pos})
+ )
+ ]
+ end.
+
+-spec ignore_variable(uri(), range(), binary(), [binary()]) -> [map()].
+ignore_variable(Uri, Range, _Data, [UnusedVariable]) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_poi:sort(els_dt_document:pois(Document, [variable])),
+ case ensure_range(els_range:to_poi_range(Range), UnusedVariable, POIs) of
+ {ok, VarRange} ->
+ [
+ make_edit_action(
+ Uri,
+ <<"Add '_' to '", UnusedVariable/binary, "'">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <<"_", UnusedVariable/binary>>,
+ els_protocol:range(VarRange)
+ )
+ ];
+ error ->
+ []
+ end.
+
+-spec add_include_lib_macro(uri(), range(), binary(), [binary()]) -> [map()].
+add_include_lib_macro(Uri, Range, _Data, [Macro0]) ->
+ {Name, Id} =
+ case string:split(Macro0, "/") of
+ [MacroBin] ->
+ Name0 = binary_to_atom(MacroBin, utf8),
+ {Name0, Name0};
+ [MacroBin, ArityBin] ->
+ Name0 = binary_to_atom(MacroBin, utf8),
+ Arity = binary_to_integer(ArityBin),
+ {Name0, {Name0, Arity}}
+ end,
+ add_include_file(Uri, Range, 'define', Name, Id).
+
+-spec define_macro(uri(), range(), binary(), [binary()]) -> [map()].
+define_macro(Uri, Range, _Data, [Macro0]) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ NewText =
+ case string:split(Macro0, "/") of
+ [MacroBin] ->
+ <<"-define(", MacroBin/binary, ", undefined).\n">>;
+ [MacroBin, ArityBin] ->
+ Arity = binary_to_integer(ArityBin),
+ Args = string:join(lists:duplicate(Arity, "_"), ", "),
+ list_to_binary(
+ ["-define(", MacroBin, "(", Args, "), undefined).\n"]
+ )
+ end,
+ #{from := Pos} = els_range:to_poi_range(Range),
+ BeforeRange = #{from => {1, 1}, to => Pos},
+ POIs = els_dt_document:pois_in_range(
+ Document,
+ [module, include, include_lib, define],
+ BeforeRange
+ ),
+ case POIs of
+ [] ->
+ [];
+ _ ->
+ #{range := #{to := {Line, _}}} = lists:last(els_poi:sort(POIs)),
+ [
+ make_edit_action(
+ Uri,
+ <<"Define ", Macro0/binary>>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ NewText,
+ els_protocol:range(#{
+ to => {Line + 1, 1},
+ from => {Line + 1, 1}
+ })
+ )
+ ]
+ end.
+
+-spec define_record(uri(), range(), binary(), [binary()]) -> [map()].
+define_record(Uri, Range, _Data, [Record]) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ NewText = <<"-record(", Record/binary, ", {}).\n">>,
+ #{from := Pos} = els_range:to_poi_range(Range),
+ BeforeRange = #{from => {1, 1}, to => Pos},
+ POIs = els_dt_document:pois_in_range(
+ Document,
+ [module, include, include_lib, record],
+ BeforeRange
+ ),
+ case POIs of
+ [] ->
+ [];
+ _ ->
+ Line = end_line(lists:last(els_poi:sort(POIs))),
+ [
+ make_edit_action(
+ Uri,
+ <<"Define record ", Record/binary>>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ NewText,
+ els_protocol:range(#{
+ to => {Line + 1, 1},
+ from => {Line + 1, 1}
+ })
+ )
+ ]
+ end.
+
+-spec end_line(els_poi:poi()) -> non_neg_integer().
+end_line(#{data := #{value_range := #{to := {Line, _}}}}) ->
+ Line;
+end_line(#{range := #{to := {Line, _}}}) ->
+ Line.
+
+-spec add_include_lib_record(uri(), range(), _, [binary()]) -> [map()].
+add_include_lib_record(Uri, Range, _Data, [Record]) ->
+ Name = binary_to_atom(Record, utf8),
+ add_include_file(Uri, Range, 'record', Name, Name).
+
+-spec add_include_file(uri(), range(), els_poi:poi_kind(), atom(), els_poi:poi_id()) -> [map()].
+add_include_file(Uri, Range, Kind, Name, Id) ->
+ %% TODO: Add support for -include() also
+ CandidateUris =
+ els_dt_document:find_candidates_with_otp(Name, 'header'),
+ Uris = [
+ CandidateUri
+ || CandidateUri <- CandidateUris,
+ contains_poi(Kind, CandidateUri, Id)
+ ],
+ Paths = els_include_paths:include_libs(Uris),
+ {ok, Document} = els_utils:lookup_document(Uri),
+ #{from := Pos} = els_range:to_poi_range(Range),
+ BeforeRange = #{from => {1, 1}, to => Pos},
+ case
+ els_dt_document:pois_in_range(
+ Document,
+ [module, include, include_lib],
+ BeforeRange
+ )
+ of
+ [] ->
+ [];
+ POIs ->
+ #{range := #{to := {Line, _}}} = lists:last(els_poi:sort(POIs)),
+ [
+ make_edit_action(
+ Uri,
+ <<"Add -include_lib(\"", Path/binary, "\")">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <<"-include_lib(\"", Path/binary, "\").\n">>,
+ els_protocol:range(#{to => {Line + 1, 1}, from => {Line + 1, 1}})
+ )
+ || Path <- Paths
+ ]
+ end.
+
+-spec contains_poi(els_poi:poi_kind(), uri(), atom()) -> boolean().
+contains_poi(Kind, Uri, Macro) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_dt_document:pois(Document, [Kind]),
+ lists:any(fun(#{id := Id}) -> Id =:= Macro end, POIs).
+
+-spec suggest_variable(uri(), range(), binary(), [binary()]) -> [map()].
+suggest_variable(Uri, Range, _Data, [Var]) ->
+ %% Supply a quickfix to replace an unbound variable with the most similar
+ %% variable name in scope.
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_poi:sort(els_dt_document:pois(Document, [variable])),
+ case ensure_range(els_range:to_poi_range(Range), Var, POIs) of
+ {ok, VarRange} ->
+ ScopeRange = els_scope:variable_scope_range(VarRange, Document),
+ VarsInScope = [
+ atom_to_binary(Id, utf8)
+ || #{range := R, id := Id} <- POIs,
+ els_range:in(R, ScopeRange),
+ els_range:compare(R, VarRange)
+ ],
+ VariableDistances =
+ [{els_utils:jaro_distance(V, Var), V} || V <- VarsInScope, V =/= Var],
+ [
+ make_edit_action(
+ Uri,
+ <<"Did you mean '", V/binary, "'?">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ V,
+ els_protocol:range(VarRange)
+ )
+ || {Distance, V} <- lists:reverse(lists:usort(VariableDistances)),
+ Distance > 0.8
+ ];
+ error ->
+ []
+ end.
+
+-spec suggest_macro(uri(), range(), binary(), [binary()]) -> [map()].
+suggest_macro(Uri, Range, _Data, [Macro]) ->
+ %% Supply a quickfix to replace an unbound variable with the most similar
+ %% variable name in scope.
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs =
+ els_scope:local_and_included_pois(Document, [define]) ++
+ els_completion_provider:bif_pois(define),
+ {Name, MacrosInScope} =
+ case string:split(Macro, "/") of
+ [Name0] ->
+ {Name0, [atom_to_binary(Id) || #{id := Id} <- POIs, is_atom(Id)]};
+ [Name0, ArityBin] ->
+ Arity = binary_to_integer(ArityBin),
+ {Name0, [
+ atom_to_binary(Id)
+ || #{id := {Id, A}} <- POIs,
+ is_atom(Id),
+ A =:= Arity
+ ]}
+ end,
+ Distances =
+ [{els_utils:jaro_distance(M, Name), M} || M <- MacrosInScope, M =/= Macro],
+ [
+ make_edit_action(
+ Uri,
+ <<"Did you mean '", M/binary, "'?">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <<"?", M/binary>>,
+ Range
+ )
+ || {Distance, M} <- lists:reverse(lists:usort(Distances)),
+ Distance > 0.8
+ ].
+
+-spec suggest_record(uri(), range(), binary(), [binary()]) -> [map()].
+suggest_record(Uri, Range, _Data, [Record]) ->
+ %% Supply a quickfix to replace an unrecognized record with the most similar
+ %% record in scope.
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_scope:local_and_included_pois(Document, [record]),
+ RecordsInScope = [atom_to_binary(Id) || #{id := Id} <- POIs, is_atom(Id)],
+ Distances =
+ [{els_utils:jaro_distance(Rec, Record), Rec} || Rec <- RecordsInScope, Rec =/= Record],
+ [
+ make_edit_action(
+ Uri,
+ <<"Did you mean #", Rec/binary, "{}?">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <<"#", Rec/binary>>,
+ Range
+ )
+ || {Distance, Rec} <- lists:reverse(lists:usort(Distances)),
+ Distance > 0.8
+ ].
+
+-spec suggest_record_field(uri(), range(), binary(), [binary()]) -> [map()].
+suggest_record_field(Uri, Range, _Data, [Field, Record]) ->
+ %% Supply a quickfix to replace an unrecognized record field with the most
+ %% similar record field in Record.
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_scope:local_and_included_pois(Document, [record]),
+ RecordId = binary_to_atom(Record, utf8),
+ Fields = [
+ atom_to_binary(F)
+ || #{id := Id, data := #{field_list := Fs}} <- POIs,
+ F <- Fs,
+ Id =:= RecordId
+ ],
+ Distances =
+ [{els_utils:jaro_distance(F, Field), F} || F <- Fields, F =/= Field],
+ [
+ make_edit_action(
+ Uri,
+ <<"Did you mean #", Record/binary, ".", F/binary, "?">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <>,
+ Range
+ )
+ || {Distance, F} <- lists:reverse(lists:usort(Distances)),
+ Distance > 0.8
+ ].
+
+-spec suggest_function(uri(), range(), binary(), [binary()]) -> [map()].
+suggest_function(Uri, Range, _Data, [FunBin]) ->
+ [ModNameBin, _ArityBin] = string:split(FunBin, <<"/">>),
+ {{ok, Document}, NameBin} =
+ case string:split(ModNameBin, <<":">>) of
+ [ModBin, NameBin0] ->
+ Mod = binary_to_atom(ModBin, utf8),
+ {ok, ModUri} = els_utils:find_module(Mod),
+ {els_utils:lookup_document(ModUri), NameBin0};
+ [NameBin0] ->
+ {els_utils:lookup_document(Uri), NameBin0}
+ end,
+ POIs = els_dt_document:pois(Document, [function]),
+ Funs = [atom_to_binary(F) || #{id := {F, _A}} <- POIs],
+ Distances =
+ [{els_utils:jaro_distance(F, NameBin), F} || F <- Funs, F =/= NameBin],
+ [
+ make_edit_action(
+ Uri,
+ <<"Did you mean ", F/binary, "?">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ F,
+ Range
+ )
+ || {Distance, F} <- lists:reverse(lists:usort(Distances)),
+ Distance > 0.8
+ ].
+
+-spec suggest_module(uri(), range(), binary(), [binary()]) -> [map()].
+suggest_module(Uri, Range, _Data, [NameBin]) ->
+ {ok, Items} = els_dt_document_index:find_by_kind(module),
+ Mods = [atom_to_binary(M) || #{id := M} <- Items],
+ Distances =
+ [{els_utils:jaro_distance(M, NameBin), M} || M <- Mods, M =/= NameBin],
+ [
+ make_edit_action(
+ Uri,
+ <<"Did you mean ", M/binary, "?">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ M,
+ Range
+ )
+ || {Distance, M} <- lists:reverse(lists:usort(Distances)),
+ Distance > 0.8
+ ].
+
+-spec fix_module_name(uri(), range(), binary(), [binary()]) -> [map()].
+fix_module_name(Uri, Range0, _Data, [ModName, FileName]) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_poi:sort(els_dt_document:pois(Document, [module])),
+ case ensure_range(els_range:to_poi_range(Range0), ModName, POIs) of
+ {ok, Range} ->
+ [
+ make_edit_action(
+ Uri,
+ <<"Change to -module(", FileName/binary, ").">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ FileName,
+ els_protocol:range(Range)
+ )
+ ];
+ error ->
+ []
+ end.
+
+-spec remove_macro(uri(), range(), binary(), [binary()]) -> [map()].
+remove_macro(Uri, Range, _Data, [Macro]) ->
+ %% Supply a quickfix to remove the unused Macro
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_poi:sort(els_dt_document:pois(Document, [define])),
+ case ensure_range(els_range:to_poi_range(Range), Macro, POIs) of
+ {ok, MacroRange} ->
+ LineRange = els_range:line(MacroRange),
+ [
+ make_edit_action(
+ Uri,
+ <<"Remove unused macro ", Macro/binary, ".">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <<"">>,
+ els_protocol:range(LineRange)
+ )
+ ];
+ error ->
+ []
+ end.
+
+-spec remove_unused(uri(), range(), binary(), [binary()]) -> [map()].
+remove_unused(_Uri, _Range0, <<>>, [_Import]) ->
+ [];
+remove_unused(Uri, _Range0, Data, [Import]) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ case els_range:inclusion_range(Data, Document) of
+ {ok, UnusedRange} ->
+ LineRange = els_range:line(UnusedRange),
+ [
+ make_edit_action(
+ Uri,
+ <<"Remove unused -include_lib(", Import/binary, ").">>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ <<>>,
+ els_protocol:range(LineRange)
+ )
+ ];
+ error ->
+ []
+ end.
+
+-spec fix_atom_typo(uri(), range(), binary(), [binary()]) -> [map()].
+fix_atom_typo(Uri, Range, _Data, [Atom]) ->
+ [
+ make_edit_action(
+ Uri,
+ <<"Fix typo: ", Atom/binary>>,
+ ?CODE_ACTION_KIND_QUICKFIX,
+ Atom,
+ Range
+ )
+ ].
+
+-spec extract_function(uri(), range()) -> [map()].
+extract_function(Uri, Range) ->
+ {ok, [Document]} = els_dt_document:lookup(Uri),
+ PoiRange = els_range:to_poi_range(Range),
+ #{from := From = {Line, Column}, to := To} = PoiRange,
+ %% We only want to extract if selection is large enough
+ %% and cursor is inside a function
+ POIsInRange = els_dt_document:pois_in_range(Document, PoiRange),
+ #{text := Text} = Document,
+ MarkedText = els_text:range(Text, From, To),
+ case
+ (length(POIsInRange) > 1 orelse
+ els_text:is_keyword_expr(MarkedText)) andalso
+ large_enough_range(From, To) andalso
+ not contains_function_clause(Document, Line) andalso
+ els_dt_document:wrapping_functions(Document, Line, Column) /= []
+ of
+ true ->
+ [
+ #{
+ title => <<"Extract function">>,
+ kind => <<"refactor.extract">>,
+ command => make_extract_function_command(Range, Uri)
+ }
+ ];
+ false ->
+ []
+ end.
+
+-spec bump_variables(uri(), range()) -> [map()].
+bump_variables(Uri, Range) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ #{from := {Line, Column}} = els_range:to_poi_range(Range),
+ POIs = els_dt_document:get_element_at_pos(Document, Line, Column),
+ case [POI || #{kind := variable} = POI <- POIs] of
+ [] ->
+ [];
+ [#{id := Id, range := PoiRange} = _POI | _] ->
+ Name = atom_to_binary(Id),
+ case ends_with_digit(Name) of
+ false ->
+ [];
+ true ->
+ VarRange = els_protocol:range(PoiRange),
+ [
+ #{
+ title => <<"Bump variables: ", Name/binary>>,
+ kind => ?CODE_ACTION_KIND_QUICKFIX,
+ command => make_bump_variables_command(VarRange, Uri, Name)
+ }
+ ]
+ end
+ end.
+
+-spec ends_with_digit(binary()) -> boolean().
+ends_with_digit(Bin) ->
+ N = binary:last(Bin),
+ $0 =< N andalso N =< $9.
+
+-spec make_extract_function_command(range(), uri()) -> map().
+make_extract_function_command(Range, Uri) ->
+ els_command:make_command(
+ <<"Extract function">>,
+ <<"refactor.extract">>,
+ [#{uri => Uri, range => Range}]
+ ).
+
+-spec make_bump_variables_command(range(), uri(), binary()) -> map().
+make_bump_variables_command(Range, Uri, Name) ->
+ els_command:make_command(
+ <<"Bump variables">>,
+ <<"bump-variables">>,
+ [#{uri => Uri, range => Range, name => Name}]
+ ).
+
+-spec contains_function_clause(
+ els_dt_document:item(),
+ non_neg_integer()
+) -> boolean().
+contains_function_clause(Document, Line) ->
+ POIs = els_dt_document:get_element_at_pos(Document, Line, 1),
+ lists:any(
+ fun
+ (#{kind := 'function_clause'}) ->
+ true;
+ (_) ->
+ false
+ end,
+ POIs
+ ).
+
+-spec large_enough_range(pos(), pos()) -> boolean().
+large_enough_range({Line, FromC}, {Line, ToC}) when (ToC - FromC) < 2 ->
+ false;
+large_enough_range(_From, _To) ->
+ true.
+
+-spec undefined_callback(uri(), range(), binary(), [binary()]) -> [map()].
+undefined_callback(Uri, _Range, _Data, [_Function, Behaviour]) ->
+ Title = <<"Add missing callbacks for: ", Behaviour/binary>>,
+ [
+ #{
+ title => Title,
+ kind => ?CODE_ACTION_KIND_QUICKFIX,
+ command =>
+ els_command:make_command(
+ Title,
+ <<"add-behaviour-callbacks">>,
+ [
+ #{
+ uri => Uri,
+ behaviour => Behaviour
+ }
+ ]
+ )
+ }
+ ].
+
+-spec browse_docs(uri(), range()) -> [map()].
+browse_docs(Uri, Range) ->
+ #{from := {Line, Column}} = els_range:to_poi_range(Range),
+ {ok, Document} = els_utils:lookup_document(Uri),
+ POIs = els_dt_document:get_element_at_pos(Document, Line, Column),
+ lists:flatten([browse_docs(POI) || POI <- POIs]).
+
+-spec browse_docs(els_poi:poi()) -> [map()].
+browse_docs(#{id := {M, F, A}, kind := Kind}) when
+ Kind == application;
+ Kind == type_application
+->
+ case els_utils:find_module(M) of
+ {ok, ModUri} ->
+ case els_uri:app(ModUri) of
+ {ok, App} ->
+ DocType = doc_type(ModUri),
+ make_browse_docs_command(DocType, {M, F, A}, App, Kind);
+ error ->
+ []
+ end;
+ {error, not_found} ->
+ []
+ end;
+browse_docs(_) ->
+ [].
+
+-spec doc_type(uri()) -> otp | hex | other.
+doc_type(Uri) ->
+ Path = binary_to_list(els_uri:path(Uri)),
+ OtpPath = els_config:get(otp_path),
+ case lists:prefix(OtpPath, Path) of
+ true ->
+ otp;
+ false ->
+ case els_config:is_dep(Path) of
+ true ->
+ hex;
+ false ->
+ other
+ end
+ end.
+
+-spec make_browse_docs_command(atom(), mfa(), atom(), atom()) ->
+ [map()].
+make_browse_docs_command(other, _MFA, _App, _Kind) ->
+ [];
+make_browse_docs_command(DocType, {M, F, A}, App, Kind) ->
+ Title = make_browse_docs_title(DocType, {M, F, A}),
+ [
+ #{
+ title => Title,
+ kind => ?CODE_ACTION_KIND_BROWSE,
+ command =>
+ els_command:make_command(
+ Title,
+ <<"browse-docs">>,
+ [
+ #{
+ source => DocType,
+ module => M,
+ function => F,
+ arity => A,
+ app => App,
+ kind => els_dt_references:kind_to_category(Kind)
+ }
+ ]
+ )
+ }
+ ].
+
+-spec make_browse_docs_title(atom(), mfa()) -> binary().
+make_browse_docs_title(otp, {M, F, A}) ->
+ list_to_binary(io_lib:format("Browse: OTP docs: ~p:~p/~p", [M, F, A]));
+make_browse_docs_title(hex, {M, F, A}) ->
+ list_to_binary(io_lib:format("Browse: Hex docs: ~p:~p/~p", [M, F, A])).
+
+-spec browse_error(map()) -> [map()].
+browse_error(#{<<"source">> := <<"Compiler">>, <<"code">> := ErrorCode}) ->
+ Title = <<"Browse: Erlang Error Index: ", ErrorCode/binary>>,
+ [
+ #{
+ title => Title,
+ kind => ?CODE_ACTION_KIND_BROWSE,
+ command =>
+ els_command:make_command(
+ Title,
+ <<"browse-error">>,
+ [
+ #{
+ source => <<"Compiler">>,
+ code => ErrorCode
+ }
+ ]
+ )
+ }
+ ];
+browse_error(#{<<"source">> := <<"Elvis">>, <<"code">> := ErrorCode}) ->
+ Title = <<"Browse: Elvis rules: ", ErrorCode/binary>>,
+ [
+ #{
+ title => Title,
+ kind => ?CODE_ACTION_KIND_BROWSE,
+ command =>
+ els_command:make_command(
+ Title,
+ <<"browse-error">>,
+ [
+ #{
+ source => <<"Elvis">>,
+ code => ErrorCode
+ }
+ ]
+ )
+ }
+ ];
+browse_error(_Diagnostic) ->
+ [].
+
+-spec ensure_range(els_poi:poi_range(), binary(), [els_poi:poi()]) ->
+ {ok, els_poi:poi_range()} | error.
+ensure_range(#{from := {Line, _}}, SubjectId, POIs) ->
+ SubjectAtom = binary_to_atom(SubjectId, utf8),
+ Ranges = [
+ R
+ || #{range := R, id := Id} <- POIs,
+ els_range:in(R, #{from => {Line, 1}, to => {Line + 1, 1}}),
+ Id =:= SubjectAtom
+ ],
+ case Ranges of
+ [] ->
+ error;
+ [Range | _] ->
+ {ok, Range}
+ end.
+
+-spec make_edit_action(uri(), binary(), binary(), binary(), range()) ->
+ map().
+make_edit_action(Uri, Title, Kind, Text, Range) ->
+ #{
+ title => Title,
+ kind => Kind,
+ edit => edit(Uri, Text, Range)
+ }.
+
+-spec edit(uri(), binary(), range()) -> workspace_edit().
+edit(Uri, Text, Range) ->
+ #{changes => #{Uri => [#{newText => Text, range => Range}]}}.
+
+-spec format_args(
+ els_dt_document:item(),
+ non_neg_integer(),
+ els_poi:poi_range()
+) -> string().
+format_args(Document, Arity, Range) ->
+ %% Find the matching function application and extract
+ %% argument names from it.
+ AppPOIs = els_dt_document:pois(Document, [application]),
+ Matches = [
+ POI
+ || #{range := R} = POI <- AppPOIs,
+ els_range:in(R, Range)
+ ],
+ case Matches of
+ [#{data := #{args := Args0}} | _] ->
+ string:join([els_arg:name(A) || A <- Args0], ", ");
+ [] ->
+ string:join(lists:duplicate(Arity, "_"), ", ")
+ end.
+
+-spec guess_indentation([binary()]) -> pos_integer().
+guess_indentation([]) ->
+ 2;
+guess_indentation([A, B | Rest]) ->
+ ACount = count_leading_spaces(A, 0),
+ BCount = count_leading_spaces(B, 0),
+ case {ACount, BCount} of
+ {0, N} when N > 0 ->
+ N;
+ {_, _} ->
+ guess_indentation([B | Rest])
+ end.
+
+-spec count_leading_spaces(binary(), non_neg_integer()) -> non_neg_integer().
+count_leading_spaces(<<>>, _Acc) ->
+ 0;
+count_leading_spaces(<<" ", Rest/binary>>, Acc) ->
+ count_leading_spaces(Rest, 1 + Acc);
+count_leading_spaces(<<_:8, _/binary>>, Acc) ->
+ Acc.
diff --git a/apps/els_lsp/src/els_code_lens.erl b/apps/els_lsp/src/els_code_lens.erl
index 9468b6976..e786743a2 100644
--- a/apps/els_lsp/src/els_code_lens.erl
+++ b/apps/els_lsp/src/els_code_lens.erl
@@ -9,24 +9,26 @@
%%==============================================================================
-callback init(els_dt_document:item()) -> state().
--callback command(els_dt_document:item(), poi(), state()) ->
- els_command:command().
+-callback command(els_dt_document:item(), els_poi:poi(), state()) ->
+ els_command:command().
-callback is_default() -> boolean().
--callback pois(els_dt_document:item()) -> [poi()].
+-callback pois(els_dt_document:item()) -> [els_poi:poi()].
-callback precondition(els_dt_document:item()) -> boolean().
--optional_callbacks([ init/1
- , precondition/1
- ]).
+-optional_callbacks([
+ init/1,
+ precondition/1
+]).
%%==============================================================================
%% API
%%==============================================================================
--export([ available_lenses/0
- , default_lenses/0
- , enabled_lenses/0
- , lenses/2
- ]).
+-export([
+ available_lenses/0,
+ default_lenses/0,
+ enabled_lenses/0,
+ lenses/2
+]).
%%==============================================================================
%% Includes
@@ -39,16 +41,18 @@
%% Type Definitions
%%==============================================================================
--type lens() :: #{ range := range()
- , command => els_command:command()
- , data => any()
- }.
+-type lens() :: #{
+ range := range(),
+ command => els_command:command(),
+ data => any()
+}.
-type lens_id() :: binary().
-type state() :: any().
--export_type([ lens/0
- , lens_id/0
- , state/0
- ]).
+-export_type([
+ lens/0,
+ lens_id/0,
+ state/0
+]).
%%==============================================================================
%% API
@@ -56,58 +60,63 @@
-spec available_lenses() -> [lens_id()].
available_lenses() ->
- [ <<"ct-run-test">>
- , <<"server-info">>
- , <<"show-behaviour-usages">>
- , <<"suggest-spec">>
- , <<"function-references">>
- ].
+ [
+ <<"ct-run-test">>,
+ <<"server-info">>,
+ <<"show-behaviour-usages">>,
+ <<"suggest-spec">>,
+ <<"function-references">>
+ ].
-spec default_lenses() -> [lens_id()].
default_lenses() ->
- [Id || Id <- available_lenses(), (cb_module(Id)):is_default()].
+ [Id || Id <- available_lenses(), (cb_module(Id)):is_default()].
-spec enabled_lenses() -> [lens_id()].
enabled_lenses() ->
- Config = els_config:get(lenses),
- Default = default_lenses(),
- Enabled = maps:get("enabled", Config, []),
- Disabled = maps:get("disabled", Config, []),
- lists:usort((Default ++ valid(Enabled)) -- valid(Disabled)).
+ Config = els_config:get(lenses),
+ Default = default_lenses(),
+ Enabled = maps:get("enabled", Config, []),
+ Disabled = maps:get("disabled", Config, []),
+ lists:usort((Default ++ valid(Enabled)) -- valid(Disabled)).
-spec lenses(lens_id(), els_dt_document:item()) -> [lens()].
lenses(Id, Document) ->
- CbModule = cb_module(Id),
- case precondition(CbModule, Document) of
- true ->
- State = case erlang:function_exported(CbModule, init, 1) of
- true ->
- CbModule:init(Document);
- false ->
- 'state_not_initialized'
- end,
- [make_lens(CbModule, Document, POI, State) ||
- POI <- CbModule:pois(Document)];
- false ->
- []
- end.
+ CbModule = cb_module(Id),
+ case precondition(CbModule, Document) of
+ true ->
+ State =
+ case erlang:function_exported(CbModule, init, 1) of
+ true ->
+ CbModule:init(Document);
+ false ->
+ 'state_not_initialized'
+ end,
+ [
+ make_lens(CbModule, Document, POI, State)
+ || POI <- CbModule:pois(Document)
+ ];
+ false ->
+ []
+ end.
%%==============================================================================
%% Constructors
%%==============================================================================
--spec make_lens(atom(), els_dt_document:item(), poi(), state()) -> lens().
+-spec make_lens(atom(), els_dt_document:item(), els_poi:poi(), state()) -> lens().
make_lens(CbModule, Document, #{range := Range} = POI, State) ->
- #{ range => els_protocol:range(Range)
- , command => CbModule:command(Document, POI, State)
- , data => []
- }.
+ #{
+ range => els_protocol:range(Range),
+ command => CbModule:command(Document, POI, State),
+ data => []
+ }.
%% @doc Return the callback module for a given Code Lens Identifier
-spec cb_module(lens_id()) -> module().
cb_module(Id0) ->
- Id = re:replace(Id0, "-", "_", [global, {return, binary}]),
- binary_to_atom(<<"els_code_lens_", Id/binary>>, utf8).
+ Id = re:replace(Id0, "-", "_", [global, {return, binary}]),
+ binary_to_atom(<<"els_code_lens_", Id/binary>>, utf8).
%%==============================================================================
%% Internal Functions
@@ -115,32 +124,35 @@ cb_module(Id0) ->
-spec is_valid(lens_id()) -> boolean().
is_valid(Id) ->
- lists:member(Id, available_lenses()).
+ lists:member(Id, available_lenses()).
-spec valid([string()]) -> [lens_id()].
valid(Ids0) ->
- Ids = [els_utils:to_binary(Id) || Id <- Ids0],
- {Valid, Invalid} = lists:partition(fun is_valid/1, Ids),
- case Invalid of
- [] ->
- ok;
- _ ->
- Fmt = "Skipping invalid lenses in config file: ~p",
- Args = [Invalid],
- Msg = lists:flatten(io_lib:format(Fmt, Args)),
- ?LOG_WARNING(Msg),
- els_server:send_notification(<<"window/showMessage">>,
- #{ type => ?MESSAGE_TYPE_WARNING,
- message => els_utils:to_binary(Msg)
- })
- end,
- Valid.
+ Ids = [els_utils:to_binary(Id) || Id <- Ids0],
+ {Valid, Invalid} = lists:partition(fun is_valid/1, Ids),
+ case Invalid of
+ [] ->
+ ok;
+ _ ->
+ Fmt = "Skipping invalid lenses in config file: ~p",
+ Args = [Invalid],
+ Msg = lists:flatten(io_lib:format(Fmt, Args)),
+ ?LOG_WARNING(Msg),
+ els_server:send_notification(
+ <<"window/showMessage">>,
+ #{
+ type => ?MESSAGE_TYPE_WARNING,
+ message => els_utils:to_binary(Msg)
+ }
+ )
+ end,
+ Valid.
-spec precondition(atom(), els_dt_document:item()) -> boolean().
precondition(CbModule, Document) ->
- case erlang:function_exported(CbModule, precondition, 1) of
- true ->
- CbModule:precondition(Document);
- false ->
- true
- end.
+ case erlang:function_exported(CbModule, precondition, 1) of
+ true ->
+ CbModule:precondition(Document);
+ false ->
+ true
+ end.
diff --git a/apps/els_lsp/src/els_code_lens_ct_run_test.erl b/apps/els_lsp/src/els_code_lens_ct_run_test.erl
index 75b9a395e..ffa2e6078 100644
--- a/apps/els_lsp/src/els_code_lens_ct_run_test.erl
+++ b/apps/els_lsp/src/els_code_lens_ct_run_test.erl
@@ -5,51 +5,52 @@
-module(els_code_lens_ct_run_test).
-behaviour(els_code_lens).
--export([ command/3
- , is_default/0
- , pois/1
- , precondition/1
- ]).
-
--include("els_lsp.hrl").
-
--spec command(els_dt_document:item(), poi(), els_code_lens:state()) ->
- els_command:command().
+-export([
+ command/3,
+ is_default/0,
+ pois/1,
+ precondition/1
+]).
+
+-spec command(els_dt_document:item(), els_poi:poi(), els_code_lens:state()) ->
+ els_command:command().
command(#{uri := Uri} = _Document, POI, _State) ->
- #{id := {F, A}, range := #{from := {Line, _}}} = POI,
- Title = <<"Run test">>,
- CommandId = <<"ct-run-test">>,
- CommandArgs = [ #{ module => els_uri:module(Uri)
- , function => F
- , arity => A
- , uri => Uri
- , line => Line
- }
- ],
- els_command:make_command(Title, CommandId, CommandArgs).
+ #{id := {F, A}, range := #{from := {Line, _}}} = POI,
+ Title = <<"Run test">>,
+ CommandId = <<"ct-run-test">>,
+ CommandArgs = [
+ #{
+ module => els_uri:module(Uri),
+ function => F,
+ arity => A,
+ uri => Uri,
+ line => Line
+ }
+ ],
+ els_command:make_command(Title, CommandId, CommandArgs).
-spec is_default() -> boolean().
is_default() ->
- false.
+ false.
--spec pois(els_dt_document:item()) -> [poi()].
+-spec pois(els_dt_document:item()) -> [els_poi:poi()].
pois(Document) ->
- Functions = els_dt_document:pois(Document, [function]),
- [POI || #{id := {F, 1}} = POI <- Functions, not is_blacklisted(F)].
+ Functions = els_dt_document:pois(Document, [function]),
+ [POI || #{id := {F, 1}} = POI <- Functions, not is_blacklisted(F)].
-spec precondition(els_dt_document:item()) -> boolean().
precondition(Document) ->
- Includes = els_dt_document:pois(Document, [include_lib]),
- case [POI || #{id := "common_test/include/ct.hrl"} = POI <- Includes] of
- [] ->
- false;
- _ ->
- true
- end.
+ Includes = els_dt_document:pois(Document, [include_lib]),
+ case [POI || #{id := "common_test/include/ct.hrl"} = POI <- Includes] of
+ [] ->
+ false;
+ _ ->
+ true
+ end.
%%==============================================================================
%% Internal Functions
%%==============================================================================
-spec is_blacklisted(atom()) -> boolean().
is_blacklisted(Function) ->
- lists:member(Function, [init_per_suite, end_per_suite, group]).
+ lists:member(Function, [init_per_suite, end_per_suite, group]).
diff --git a/apps/els_lsp/src/els_code_lens_function_references.erl b/apps/els_lsp/src/els_code_lens_function_references.erl
index a2b6f01c9..e0696a556 100644
--- a/apps/els_lsp/src/els_code_lens_function_references.erl
+++ b/apps/els_lsp/src/els_code_lens_function_references.erl
@@ -1,34 +1,33 @@
-module(els_code_lens_function_references).
-behaviour(els_code_lens).
--export([ is_default/0
- , pois/1
- , command/3
- ]).
-
--include("els_lsp.hrl").
+-export([
+ is_default/0,
+ pois/1,
+ command/3
+]).
-spec is_default() -> boolean().
is_default() ->
- true.
+ true.
--spec pois(els_dt_document:item()) -> [poi()].
+-spec pois(els_dt_document:item()) -> [els_poi:poi()].
pois(Document) ->
- els_dt_document:pois(Document, [function]).
+ els_dt_document:pois(Document, [function]).
--spec command(els_dt_document:item(), poi(), els_code_lens:state()) ->
- els_command:command().
+-spec command(els_dt_document:item(), els_poi:poi(), els_code_lens:state()) ->
+ els_command:command().
command(Document, POI, _State) ->
- Title = title(Document, POI),
- CommandId = <<"function-references">>,
- CommandArgs = [],
- els_command:make_command(Title, CommandId, CommandArgs).
+ Title = title(Document, POI),
+ CommandId = <<"function-references">>,
+ CommandArgs = [],
+ els_command:make_command(Title, CommandId, CommandArgs).
--spec title(els_dt_document:item(), poi()) -> binary().
+-spec title(els_dt_document:item(), els_poi:poi()) -> binary().
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])).
+ #{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])).
diff --git a/apps/els_lsp/src/els_code_lens_provider.erl b/apps/els_lsp/src/els_code_lens_provider.erl
index 394a55458..a829c5ae8 100644
--- a/apps/els_lsp/src/els_code_lens_provider.erl
+++ b/apps/els_lsp/src/els_code_lens_provider.erl
@@ -1,79 +1,48 @@
-module(els_code_lens_provider).
-behaviour(els_provider).
--export([ handle_info/2
- , handle_request/2
- , init/0
- , is_enabled/0
- , options/0
- , cancel_request/2
- ]).
+-export([
+ options/0,
+ handle_request/1
+]).
-include("els_lsp.hrl").
-include_lib("kernel/include/logger.hrl").
--type state() :: #{in_progress => [progress_entry()]}.
--type progress_entry() :: {uri(), job()}.
--type job() :: pid().
-
--define(SERVER, ?MODULE).
-
%%==============================================================================
%% els_provider functions
%%==============================================================================
-
--spec is_enabled() -> boolean().
-is_enabled() -> true.
-
-spec options() -> map().
options() ->
- #{ resolveProvider => false }.
-
--spec init() -> state().
-init() ->
- #{ in_progress => [] }.
-
--spec handle_request(any(), state()) -> {job(), state()}.
-handle_request({document_codelens, Params}, State) ->
- #{in_progress := InProgress} = State,
- #{ <<"textDocument">> := #{ <<"uri">> := Uri}} = Params,
- ?LOG_DEBUG("Starting lenses job [uri=~p]", [Uri]),
- Job = run_lenses_job(Uri),
- {Job, State#{in_progress => [{Uri, Job}|InProgress]}}.
-
--spec handle_info(any(), state()) -> state().
-handle_info({result, Lenses, Job}, State) ->
- ?LOG_DEBUG("Received lenses result [job=~p]", [Job]),
- #{ in_progress := InProgress } = State,
- els_server:send_response(Job, Lenses),
- State#{ in_progress => lists:keydelete(Job, 2, InProgress) }.
+ #{resolveProvider => false}.
--spec cancel_request(job(), state()) -> state().
-cancel_request(Job, State) ->
- ?LOG_DEBUG("Cancelling lenses [job=~p]", [Job]),
- els_background_job:stop(Job),
- #{ in_progress := InProgress } = State,
- State#{ in_progress => lists:keydelete(Job, 2, InProgress) }.
+-spec handle_request(any()) -> {async, uri(), pid()}.
+handle_request({document_codelens, Params}) ->
+ #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params,
+ ?LOG_DEBUG("Starting lenses job [uri=~p]", [Uri]),
+ Job = run_lenses_job(Uri),
+ {async, Uri, Job}.
%%==============================================================================
%% Internal Functions
%%==============================================================================
-spec run_lenses_job(uri()) -> pid().
run_lenses_job(Uri) ->
- {ok, Document} = els_utils:lookup_document(Uri),
- Config = #{ task =>
- fun(Doc, _) ->
- lists:flatten(
- [els_code_lens:lenses(Id, Doc) ||
- Id <- els_code_lens:enabled_lenses()])
- end
- , entries => [Document]
- , title => <<"Lenses">>
- , on_complete =>
- fun(Lenses) ->
- ?SERVER ! {result, Lenses, self()},
- ok
- end
- },
- {ok, Pid} = els_background_job:new(Config),
- Pid.
+ {ok, Document} = els_utils:lookup_document(Uri),
+ Config = #{
+ task =>
+ fun(Doc, _) ->
+ lists:flatten(
+ [
+ els_code_lens:lenses(Id, Doc)
+ || Id <- els_code_lens:enabled_lenses()
+ ] ++
+ wrangler_handler:get_code_lenses(Doc)
+ )
+ end,
+ entries => [Document],
+ title => <<"Lenses">>,
+ on_complete => fun els_server:register_result/1
+ },
+ {ok, Pid} = els_background_job:new(Config),
+ Pid.
diff --git a/apps/els_lsp/src/els_code_lens_server_info.erl b/apps/els_lsp/src/els_code_lens_server_info.erl
index a500c865d..0d3cc9536 100644
--- a/apps/els_lsp/src/els_code_lens_server_info.erl
+++ b/apps/els_lsp/src/els_code_lens_server_info.erl
@@ -5,31 +5,30 @@
-module(els_code_lens_server_info).
-behaviour(els_code_lens).
--export([ command/3
- , is_default/0
- , pois/1
- ]).
+-export([
+ command/3,
+ is_default/0,
+ pois/1
+]).
--include("els_lsp.hrl").
-
--spec command(els_dt_document:item(), poi(), els_code_lens:state()) ->
- els_command:command().
+-spec command(els_dt_document:item(), els_poi:poi(), els_code_lens:state()) ->
+ els_command:command().
command(_Document, _POI, _State) ->
- Title = title(),
- CommandId = <<"server-info">>,
- CommandArgs = [],
- els_command:make_command(Title, CommandId, CommandArgs).
+ Title = title(),
+ CommandId = <<"server-info">>,
+ CommandArgs = [],
+ els_command:make_command(Title, CommandId, CommandArgs).
-spec is_default() -> boolean().
is_default() ->
- false.
+ false.
--spec pois(els_dt_document:item()) -> [poi()].
+-spec pois(els_dt_document:item()) -> [els_poi:poi()].
pois(_Document) ->
- %% Return a dummy POI on the first line
- [els_poi:new(#{from => {1, 1}, to => {2, 1}}, dummy, dummy)].
+ %% Return a dummy POI on the first line
+ [els_poi:new(#{from => {1, 1}, to => {2, 1}}, dummy, dummy)].
-spec title() -> binary().
title() ->
- Root = filename:basename(els_uri:path(els_config:get(root_uri))),
- <<"Erlang LS (in ", Root/binary, ") info">>.
+ Root = filename:basename(els_uri:path(els_config:get(root_uri))),
+ <<"Erlang LS (in ", Root/binary, ") info">>.
diff --git a/apps/els_lsp/src/els_code_lens_show_behaviour_usages.erl b/apps/els_lsp/src/els_code_lens_show_behaviour_usages.erl
index d76c06e7d..1e4635d89 100644
--- a/apps/els_lsp/src/els_code_lens_show_behaviour_usages.erl
+++ b/apps/els_lsp/src/els_code_lens_show_behaviour_usages.erl
@@ -5,40 +5,39 @@
-module(els_code_lens_show_behaviour_usages).
-behaviour(els_code_lens).
--export([ command/3
- , is_default/0
- , pois/1
- , precondition/1
- ]).
-
--include("els_lsp.hrl").
-
--spec command(els_dt_document:item(), poi(), els_code_lens:state()) ->
- els_command:command().
+-export([
+ command/3,
+ is_default/0,
+ pois/1,
+ precondition/1
+]).
+
+-spec command(els_dt_document:item(), els_poi:poi(), els_code_lens:state()) ->
+ els_command:command().
command(_Document, POI, _State) ->
- Title = title(POI),
- CommandId = <<"show-behaviour-usages">>,
- CommandArgs = [],
- els_command:make_command(Title, CommandId, CommandArgs).
+ Title = title(POI),
+ CommandId = <<"show-behaviour-usages">>,
+ CommandArgs = [],
+ els_command:make_command(Title, CommandId, CommandArgs).
-spec is_default() -> boolean().
is_default() ->
- true.
+ true.
-spec precondition(els_dt_document:item()) -> boolean().
precondition(Document) ->
- %% A behaviour is defined by the presence of one more callback
- %% attributes.
- Callbacks = els_dt_document:pois(Document, [callback]),
- length(Callbacks) > 0.
+ %% A behaviour is defined by the presence of one more callback
+ %% attributes.
+ Callbacks = els_dt_document:pois(Document, [callback]),
+ length(Callbacks) > 0.
--spec pois(els_dt_document:item()) -> [poi()].
+-spec pois(els_dt_document:item()) -> [els_poi:poi()].
pois(Document) ->
- els_dt_document:pois(Document, [module]).
+ els_dt_document:pois(Document, [module]).
--spec title(poi()) -> binary().
+-spec title(els_poi:poi()) -> binary().
title(#{id := Id} = _POI) ->
- {ok, Refs} = els_dt_references:find_by_id(behaviour, Id),
- Count = length(Refs),
- Msg = io_lib:format("Behaviour used in ~p place(s)", [Count]),
- els_utils:to_binary(Msg).
+ {ok, Refs} = els_dt_references:find_by_id(behaviour, Id),
+ Count = length(Refs),
+ Msg = io_lib:format("Behaviour used in ~p place(s)", [Count]),
+ els_utils:to_binary(Msg).
diff --git a/apps/els_lsp/src/els_code_lens_suggest_spec.erl b/apps/els_lsp/src/els_code_lens_suggest_spec.erl
index 483c71ee4..ccd43901f 100644
--- a/apps/els_lsp/src/els_code_lens_suggest_spec.erl
+++ b/apps/els_lsp/src/els_code_lens_suggest_spec.erl
@@ -4,16 +4,16 @@
-module(els_code_lens_suggest_spec).
-behaviour(els_code_lens).
--export([ init/1
- , command/3
- , is_default/0
- , pois/1
- ]).
+-export([
+ init/1,
+ command/3,
+ is_default/0,
+ pois/1
+]).
%%==============================================================================
%% Includes
%%==============================================================================
--include("els_lsp.hrl").
-include_lib("kernel/include/logger.hrl").
%%==============================================================================
@@ -26,51 +26,54 @@
%%==============================================================================
-spec init(els_dt_document:item()) -> state().
init(#{uri := Uri} = _Document) ->
- try els_typer:get_info(Uri) of
- Info ->
- Info
- catch C:E:S ->
- Fmt =
- "Cannot extract typer info.~n"
- "Class: ~p~n"
- "Exception: ~p~n"
- "Stacktrace: ~p~n",
- ?LOG_WARNING(Fmt, [C, E, S]),
- 'no_info'
- end.
+ try els_typer:get_info(Uri) of
+ Info ->
+ Info
+ catch
+ C:E:S ->
+ Fmt =
+ "Cannot extract typer info.~n"
+ "Class: ~p~n"
+ "Exception: ~p~n"
+ "Stacktrace: ~p~n",
+ ?LOG_WARNING(Fmt, [C, E, S]),
+ 'no_info'
+ end.
--spec command(els_dt_document:item(), poi(), state()) -> els_command:command().
+-spec command(els_dt_document:item(), els_poi:poi(), state()) -> els_command:command().
command(_Document, _POI, 'no_info') ->
- CommandId = <<"suggest-spec">>,
- Title = <<"Cannot extract specs (check logs for details)">>,
- els_command:make_command(Title, CommandId, []);
+ CommandId = <<"suggest-spec">>,
+ Title = <<"Cannot extract specs (check logs for details)">>,
+ els_command:make_command(Title, CommandId, []);
command(Document, #{range := #{from := {Line, _}}} = POI, Info) ->
- #{uri := Uri} = Document,
- CommandId = <<"suggest-spec">>,
- Spec = get_type_spec(POI, Info),
- Title = truncate_spec_title(Spec, spec_title_max_length()),
- CommandArgs = [ #{ uri => Uri
- , line => Line
- , spec => Spec
- }
- ],
- els_command:make_command(Title, CommandId, CommandArgs).
+ #{uri := Uri} = Document,
+ CommandId = <<"suggest-spec">>,
+ Spec = get_type_spec(POI, Info),
+ Title = truncate_spec_title(Spec, spec_title_max_length()),
+ CommandArgs = [
+ #{
+ uri => Uri,
+ line => Line,
+ spec => Spec
+ }
+ ],
+ els_command:make_command(Title, CommandId, CommandArgs).
-spec is_default() -> boolean().
is_default() ->
- true.
+ false.
--spec pois(els_dt_document:item()) -> [poi()].
+-spec pois(els_dt_document:item()) -> [els_poi:poi()].
pois(Document) ->
- Functions = els_dt_document:pois(Document, [function]),
- Specs = els_dt_document:pois(Document, [spec]),
- SpecsIds = [Id || #{id := Id} <- Specs],
- [POI || #{id := Id} = POI <- Functions, not lists:member(Id, SpecsIds)].
+ Functions = els_dt_document:pois(Document, [function]),
+ Specs = els_dt_document:pois(Document, [spec]),
+ SpecsIds = [Id || #{id := Id} <- Specs],
+ [POI || #{id := Id} = POI <- Functions, not lists:member(Id, SpecsIds)].
%%==============================================================================
%% Internal functions
%%==============================================================================
--spec get_type_spec(poi(), els_typer:info()) -> binary().
+-spec get_type_spec(els_poi:poi(), els_typer:info()) -> binary().
get_type_spec(POI, Info) ->
#{id := {Function, Arity}} = POI,
Spec = els_typer:get_type_spec(Function, Arity, Info),
@@ -78,17 +81,17 @@ get_type_spec(POI, Info) ->
-spec truncate_spec_title(binary(), integer()) -> binary().
truncate_spec_title(Spec, MaxLength) ->
- Length = string:length(Spec),
- case Length > MaxLength of
- true ->
- Title = unicode:characters_to_binary(
- string:slice(Spec, 0, MaxLength - 3)
- ),
- <>;
- false ->
- Spec
- end.
+ Length = string:length(Spec),
+ case Length > MaxLength of
+ true ->
+ Title = unicode:characters_to_binary(
+ string:slice(Spec, 0, MaxLength - 3)
+ ),
+ <>;
+ false ->
+ Spec
+ end.
-spec spec_title_max_length() -> integer().
spec_title_max_length() ->
- application:get_env(els_core, suggest_spec_title_max_length, 100).
+ application:get_env(els_core, suggest_spec_title_max_length, 100).
diff --git a/apps/els_lsp/src/els_code_navigation.erl b/apps/els_lsp/src/els_code_navigation.erl
index 988b8365f..3f1d8790c 100644
--- a/apps/els_lsp/src/els_code_navigation.erl
+++ b/apps/els_lsp/src/els_code_navigation.erl
@@ -8,208 +8,334 @@
%%==============================================================================
%% API
--export([ goto_definition/2 ]).
+-export([
+ goto_definition/2,
+ find_in_scope/2
+]).
%%==============================================================================
%% Includes
%%==============================================================================
-include("els_lsp.hrl").
--include_lib("kernel/include/logger.hrl").
+
+%%==============================================================================
+%% Type definitions
+%%==============================================================================
+-type goto_definition() :: [{uri(), els_poi:poi()}].
+-export_type([goto_definition/0]).
%%==============================================================================
%% API
%%==============================================================================
--spec goto_definition(uri(), poi()) ->
- {ok, uri(), poi()} | {error, any()}.
-goto_definition( Uri
- , Var = #{kind := variable, id := VarId, range := VarRange}
- ) ->
- %% This will naively try to find the definition of a variable by finding the
- %% first occurrence of the variable in the function clause.
- {ok, Document} = els_utils:lookup_document(Uri),
- FunPOIs = els_poi:sort(els_dt_document:pois(Document, [function_clause])),
- VarPOIs = els_poi:sort(els_dt_document:pois(Document, [variable])),
- %% Find the function clause we are in
- case [Range || #{range := Range} <- FunPOIs,
- els_range:compare(Range, VarRange)] of
- [] -> {error, not_in_function_clause};
- FunRanges ->
- FunRange = lists:last(FunRanges),
- %% Find the first occurance of the variable in the function clause
- [POI|_] = [P || P = #{range := Range, id := Id} <- VarPOIs,
- els_range:compare(Range, VarRange),
- els_range:compare(FunRange, Range),
- Id =:= VarId],
- case POI of
- Var -> {error, already_at_definition};
- POI -> {ok, Uri, POI}
- end
- end;
-goto_definition( _Uri
- , #{ kind := Kind, id := {M, F, A} }
- ) when Kind =:= application;
- Kind =:= implicit_fun;
- Kind =:= import_entry ->
- case els_utils:find_module(M) of
- {ok, Uri} -> find(Uri, function, {F, A});
- {error, Error} -> {error, Error}
- end;
-goto_definition( Uri
- , #{ kind := Kind, id := {F, A}} = POI
- ) when Kind =:= application;
- Kind =:= implicit_fun;
- Kind =:= export_entry ->
- %% try to find local function first
- %% fall back to bif search if unsuccesful
- case find(Uri, function, {F, A}) of
- {error, Error} ->
- case is_imported_bif(Uri, F, A) of
- true ->
- goto_definition(Uri, POI#{id := {erlang, F, A}});
- false ->
- {error, Error}
- end;
- Result ->
- Result
- end;
-goto_definition( _Uri
- , #{ kind := Kind, id := Module }
- ) when Kind =:= atom;
- Kind =:= behaviour;
- Kind =:= module ->
- case els_utils:find_module(Module) of
- {ok, Uri} -> find(Uri, module, Module);
- {error, Error} -> {error, Error}
- end;
-goto_definition(Uri
- , #{ kind := macro
- , id := {MacroName, _Arity} = Define } = POI
- ) ->
- case find(Uri, define, Define) of
- {error, not_found} ->
- goto_definition(Uri, POI#{id => MacroName});
- Else ->
- Else
- end;
-goto_definition(Uri, #{ kind := macro, id := Define }) ->
- find(Uri, define, Define);
-goto_definition(Uri, #{ kind := record_expr, id := Record }) ->
- find(Uri, record, Record);
-goto_definition(Uri, #{ kind := record_field, id := {Record, Field} }) ->
- find(Uri, record_def_field, {Record, Field});
-goto_definition(_Uri, #{ kind := Kind, id := Id }
- ) when Kind =:= include;
- Kind =:= include_lib ->
- case els_utils:find_header(els_utils:filename_to_atom(Id)) of
- {ok, Uri} -> {ok, Uri, beginning()};
- {error, Error} -> {error, Error}
- end;
-goto_definition(_Uri, #{ kind := type_application, id := {M, T, A} }) ->
- case els_utils:find_module(M) of
- {ok, Uri} -> find(Uri, type_definition, {T, A});
- {error, Error} -> {error, Error}
- end;
-goto_definition(Uri, #{ kind := Kind, id := {T, A} })
- when Kind =:= type_application; Kind =:= export_type_entry ->
- find(Uri, type_definition, {T, A});
-goto_definition(_Uri, #{ kind := parse_transform, id := Module }) ->
- case els_utils:find_module(Module) of
- {ok, Uri} -> find(Uri, module, Module);
- {error, Error} -> {error, Error}
- end;
+-spec goto_definition(uri(), els_poi:poi()) ->
+ {ok, goto_definition()} | {error, any()}.
+goto_definition(
+ Uri,
+ Var = #{kind := variable}
+) ->
+ %% This will naively try to find the definition of a variable by finding the
+ %% first occurrence of the variable in variable scope.
+ case find_in_scope(Uri, Var) of
+ [Var | _] -> {error, already_at_definition};
+ [POI | _] -> {ok, [{Uri, POI}]};
+ % Probably due to parse error
+ [] -> {error, nothing_in_scope}
+ end;
+goto_definition(
+ _Uri,
+ #{kind := Kind, id := {M, F, A}}
+) when
+ Kind =:= application;
+ Kind =:= implicit_fun;
+ Kind =:= import_entry
+->
+ case els_utils:find_module(M) of
+ {ok, Uri} -> defs_to_res(find(Uri, function, {F, A}));
+ {error, Error} -> {error, Error}
+ end;
+goto_definition(
+ Uri,
+ #{kind := Kind, id := {F, A}} = POI
+) when
+ Kind =:= application;
+ Kind =:= implicit_fun;
+ Kind =:= export_entry;
+ Kind =:= nifs_entry
+->
+ %% try to find local function first
+ %% fall back to bif search if unsuccessful
+ case find(Uri, function, {F, A}) of
+ [] ->
+ case is_imported_bif(Uri, F, A) of
+ true ->
+ goto_definition(Uri, POI#{id := {erlang, F, A}});
+ false ->
+ {error, not_found}
+ end;
+ Result ->
+ defs_to_res(Result)
+ end;
+goto_definition(
+ Uri,
+ #{kind := atom, id := Id}
+) ->
+ %% Two interesting cases for atoms: functions and modules.
+ %% We return all function defs with any arity combined with module defs.
+ DefsFun = find(Uri, function, {Id, any_arity}),
+ case els_utils:find_module(Id) of
+ {ok, ModUri} -> defs_to_res(DefsFun ++ find(ModUri, module, Id));
+ {error, _Error} -> defs_to_res(DefsFun)
+ end;
+goto_definition(
+ _Uri,
+ #{kind := Kind, id := Module}
+) when
+ Kind =:= behaviour;
+ Kind =:= module
+->
+ case els_utils:find_module(Module) of
+ {ok, Uri} -> defs_to_res(find(Uri, module, Module));
+ {error, Error} -> {error, Error}
+ end;
+goto_definition(
+ Uri,
+ #{
+ kind := macro,
+ id := {MacroName, _Arity} = Define
+ } = POI
+) ->
+ case find(Uri, define, Define) of
+ [] ->
+ goto_definition(Uri, POI#{id => MacroName});
+ Else ->
+ defs_to_res(Else)
+ end;
+goto_definition(Uri, #{kind := macro, id := Define}) ->
+ defs_to_res(find(Uri, define, Define));
+goto_definition(Uri, #{kind := record_expr, id := Record}) ->
+ defs_to_res(find(Uri, record, Record));
+goto_definition(Uri, #{kind := record_field, id := {Record, Field}}) ->
+ defs_to_res(find(Uri, record_def_field, {Record, Field}));
+goto_definition(_Uri, #{kind := Kind, id := Id}) when
+ Kind =:= include;
+ Kind =:= include_lib
+->
+ case els_utils:find_header(els_utils:filename_to_atom(Id)) of
+ {ok, Uri} -> {ok, [{Uri, beginning()}]};
+ {error, Error} -> {error, Error}
+ end;
+goto_definition(_Uri, #{kind := type_application, id := {M, T, A}}) ->
+ case els_utils:find_module(M) of
+ {ok, Uri} -> defs_to_res(find(Uri, type_definition, {T, A}));
+ {error, Error} -> {error, Error}
+ end;
+goto_definition(Uri, #{kind := Kind, id := {T, A}}) when
+ Kind =:= type_application; Kind =:= export_type_entry
+->
+ defs_to_res(find(Uri, type_definition, {T, A}));
+goto_definition(_Uri, #{kind := parse_transform, id := Module}) ->
+ case els_utils:find_module(Module) of
+ {ok, Uri} -> defs_to_res(find(Uri, module, Module));
+ {error, Error} -> {error, Error}
+ end;
+goto_definition(Uri, #{kind := callback, id := Id}) ->
+ defs_to_res(find(Uri, callback, Id));
goto_definition(_Filename, _) ->
- {error, not_found}.
+ {error, not_found}.
--spec is_imported_bif(uri(), atom(), non_neg_integer()) -> boolean().
+-spec is_imported_bif(uri(), atom(), non_neg_integer() | any_arity) -> boolean().
+is_imported_bif(_Uri, _F, any_arity) ->
+ false;
is_imported_bif(_Uri, F, A) ->
- OldBif = erl_internal:old_bif(F, A),
- Bif = erl_internal:bif(F, A),
- case {OldBif, Bif} of
- %% Cannot be shadowed, always imported
- {true, true} ->
- true;
- %% It's not a BIF at all
- {false, false} ->
- false;
- %% The hard case, just jump to the bif for now
- {_, _} ->
- true
- end.
-
--spec find(uri() | [uri()], poi_kind(), any()) ->
- {ok, uri(), poi()} | {error, not_found}.
+ OldBif = erl_internal:old_bif(F, A),
+ Bif = erl_internal:bif(F, A),
+ case {OldBif, Bif} of
+ %% Cannot be shadowed, always imported
+ {true, true} ->
+ true;
+ %% It's not a BIF at all
+ {false, false} ->
+ false;
+ %% The hard case, just jump to the bif for now
+ {_, _} ->
+ true
+ end.
+
+-spec defs_to_res([{uri(), els_poi:poi()}]) -> {ok, [{uri(), els_poi:poi()}]} | {error, not_found}.
+defs_to_res([]) -> {error, not_found};
+defs_to_res(Defs) -> {ok, Defs}.
+
+-spec find(uri() | [uri()], els_poi:poi_kind(), any()) ->
+ [{uri(), els_poi:poi()}].
find(UriOrUris, Kind, Data) ->
- find(UriOrUris, Kind, Data, sets:new()).
+ find(UriOrUris, Kind, Data, sets:new()).
--spec find(uri() | [uri()], poi_kind(), any(), sets:set(binary())) ->
- {ok, uri(), poi()} | {error, not_found}.
+-spec find(uri() | [uri()], els_poi:poi_kind(), any(), sets:set(binary())) ->
+ [{uri(), els_poi:poi()}].
find([], _Kind, _Data, _AlreadyVisited) ->
- {error, not_found};
-find([Uri|Uris0], Kind, Data, AlreadyVisited) ->
- case sets:is_element(Uri, AlreadyVisited) of
- true ->
- find(Uris0, Kind, Data, AlreadyVisited);
- false ->
- AlreadyVisited2 = sets:add_element(Uri, AlreadyVisited),
- case els_utils:lookup_document(Uri) of
- {ok, Document} ->
- find_in_document([Uri|Uris0], Document, Kind, Data, AlreadyVisited2);
- {error, _Error} ->
- find(Uris0, Kind, Data, AlreadyVisited2)
- end
- end;
+ [];
+find([Uri | Uris0], Kind, Data, AlreadyVisited) ->
+ case sets:is_element(Uri, AlreadyVisited) of
+ true ->
+ find(Uris0, Kind, Data, AlreadyVisited);
+ false ->
+ AlreadyVisited2 = sets:add_element(Uri, AlreadyVisited),
+ case els_utils:lookup_document(Uri) of
+ {ok, Document} ->
+ find_in_document([Uri | Uris0], Document, Kind, Data, AlreadyVisited2);
+ {error, _Error} ->
+ find(Uris0, Kind, Data, AlreadyVisited2)
+ end
+ end;
find(Uri, Kind, Data, AlreadyVisited) ->
- find([Uri], Kind, Data, AlreadyVisited).
-
--spec find_in_document(uri() | [uri()], els_dt_document:item(), poi_kind()
- , any(), sets:set(binary())) ->
- {ok, uri(), poi()} | {error, any()}.
-find_in_document([Uri|Uris0], Document, Kind, Data, AlreadyVisited) ->
- POIs = els_dt_document:pois(Document, [Kind]),
- case [POI || #{id := Id} = POI <- POIs, Id =:= Data] of
- [] ->
- case maybe_imported(Document, Kind, Data) of
- {ok, U, P} -> {ok, U, P};
- {error, not_found} ->
- find(lists:usort(include_uris(Document) ++ Uris0), Kind, Data,
- AlreadyVisited);
- {error, Other} ->
- ?LOG_INFO("find_in_document: [uri=~p] [error=~p]", [Uri, Other]),
- {error, not_found}
- end;
- Definitions ->
- {ok, Uri, hd(els_poi:sort(Definitions))}
- end.
+ find([Uri], Kind, Data, AlreadyVisited).
+
+-spec find_in_document(
+ uri() | [uri()],
+ els_dt_document:item(),
+ els_poi:poi_kind(),
+ any(),
+ sets:set(binary())
+) ->
+ [{uri(), els_poi:poi()}].
+find_in_document([Uri | Uris0], Document, Kind, Data, AlreadyVisited) ->
+ POIs = els_dt_document:pois(Document, [Kind]),
+ Defs = [POI || #{id := Id} = POI <- POIs, Id =:= Data],
+ {AllDefs, MultipleDefs} =
+ case Data of
+ {_, any_arity} when
+ Kind =:= function;
+ Kind =:= define;
+ Kind =:= type_definition
+ ->
+ %% Including defs with any arity
+ AnyArity = [
+ POI
+ || #{id := {F, _}} = POI <- POIs, Data =:= {F, any_arity}
+ ],
+ {AnyArity, true};
+ _ ->
+ {Defs, false}
+ end,
+ case AllDefs of
+ [] ->
+ case maybe_imported(Document, Kind, Data) of
+ [] ->
+ find(
+ lists:usort(include_uris(Document) ++ Uris0),
+ Kind,
+ Data,
+ AlreadyVisited
+ );
+ Else ->
+ Else
+ end;
+ Definitions ->
+ SortedDefs = els_poi:sort(Definitions),
+ case MultipleDefs of
+ true ->
+ %% This will be the case only when the user tries to
+ %% navigate to the definition of an atom or a
+ %% function/type/macro of wrong arity.
+ [{Uri, POI} || POI <- SortedDefs];
+ false ->
+ %% In the general case, we return only one def
+ [{Uri, hd(SortedDefs)}]
+ end
+ end.
-spec include_uris(els_dt_document:item()) -> [uri()].
include_uris(Document) ->
- POIs = els_dt_document:pois(Document, [include, include_lib]),
- lists:foldl(fun add_include_uri/2, [], POIs).
+ POIs = els_dt_document:pois(Document, [include, include_lib]),
+ lists:foldl(fun add_include_uri/2, [], POIs).
--spec add_include_uri(poi(), [uri()]) -> [uri()].
-add_include_uri(#{ id := Id }, Acc) ->
- case els_utils:find_header(els_utils:filename_to_atom(Id)) of
- {ok, Uri} -> [Uri | Acc];
- {error, _Error} -> Acc
- end.
+-spec add_include_uri(els_poi:poi(), [uri()]) -> [uri()].
+add_include_uri(#{id := Id}, Acc) ->
+ case els_utils:find_header(els_utils:filename_to_atom(Id)) of
+ {ok, Uri} -> [Uri | Acc];
+ {error, _Error} -> Acc
+ end.
-spec beginning() -> #{range => #{from => {1, 1}, to => {1, 1}}}.
beginning() ->
- #{range => #{from => {1, 1}, to => {1, 1}}}.
+ #{range => #{from => {1, 1}, to => {1, 1}}}.
%% @doc check for a match in any of the module imported functions.
--spec maybe_imported(els_dt_document:item(), poi_kind(), any()) ->
- {ok, uri(), poi()} | {error, any()}.
+-spec maybe_imported(els_dt_document:item(), els_poi:poi_kind(), any()) ->
+ [{uri(), els_poi:poi()}].
maybe_imported(Document, function, {F, A}) ->
- POIs = els_dt_document:pois(Document, [import_entry]),
- case [{M, F, A} || #{id := {M, FP, AP}} <- POIs, FP =:= F, AP =:= A] of
- [] -> {error, not_found};
- [{M, F, A}|_] ->
- case els_utils:find_module(M) of
- {ok, Uri0} -> find(Uri0, function, {F, A});
- {error, Error} -> {error, Error}
- end
- end;
+ POIs = els_dt_document:pois(Document, [import_entry]),
+ case [{M, F, A} || #{id := {M, FP, AP}} <- POIs, FP =:= F, AP =:= A] of
+ [] ->
+ [];
+ [{M, F, A} | _] ->
+ case els_utils:find_module(M) of
+ {ok, Uri0} -> find(Uri0, function, {F, A});
+ {error, not_found} -> []
+ end
+ end;
maybe_imported(_Document, _Kind, _Data) ->
- {error, not_found}.
+ [].
+
+-spec find_in_scope(uri(), els_poi:poi()) -> [els_poi:poi()].
+find_in_scope(
+ Uri,
+ #{kind := variable, id := VarId, range := VarRange}
+) ->
+ {ok, Document} = els_utils:lookup_document(Uri),
+ LcPOIs = els_poi:sort(els_dt_document:pois(Document, [list_comp])),
+ VarPOIs = els_poi:sort(els_dt_document:pois(Document, [variable])),
+ ScopeRange = els_scope:variable_scope_range(VarRange, Document),
+ MatchInScope = [
+ POI
+ || #{id := Id} = POI <- pois_in(VarPOIs, ScopeRange),
+ Id =:= VarId
+ ],
+ %% Handle special case if variable POI is inside list comprehension (LC)
+ MatchingLcPOIs = pois_contains(LcPOIs, VarRange),
+ case find_in_scope_list_comp(MatchingLcPOIs, MatchInScope) of
+ [] ->
+ MatchInScope -- find_in_scope_list_comp(LcPOIs, MatchInScope);
+ MatchInLc ->
+ MatchInLc
+ end.
+
+-spec find_in_scope_list_comp([els_poi:poi()], [els_poi:poi()]) ->
+ [els_poi:poi()].
+find_in_scope_list_comp([], _VarPOIs) ->
+ %% No match in LC, use regular scope
+ [];
+find_in_scope_list_comp([LcPOI | LcPOIs], VarPOIs) ->
+ #{data := #{pattern_ranges := PatRanges}, range := LcRange} = LcPOI,
+ VarsInLc = pois_in(VarPOIs, LcRange),
+ {PatVars, OtherVars} =
+ lists:partition(
+ fun(#{range := Range}) ->
+ lists:any(
+ fun(PatRange) ->
+ els_range:in(Range, PatRange)
+ end,
+ PatRanges
+ )
+ end,
+ VarsInLc
+ ),
+ case PatVars of
+ [] ->
+ %% Didn't find any patterned vars in this LC, try the next one
+ find_in_scope_list_comp(LcPOIs, VarPOIs);
+ _ ->
+ %% Put pattern vars first to make goto definition work
+ PatVars ++ OtherVars
+ end.
+
+-spec pois_in([els_poi:poi()], els_poi:poi_range()) ->
+ [els_poi:poi()].
+pois_in(POIs, Range) ->
+ [POI || #{range := R} = POI <- POIs, els_range:in(R, Range)].
+
+-spec pois_contains([els_poi:poi()], els_poi:poi_range()) ->
+ [els_poi:poi()].
+pois_contains(POIs, Range) ->
+ [POI || #{range := R} = POI <- POIs, els_range:in(Range, R)].
diff --git a/apps/els_lsp/src/els_code_reload.erl b/apps/els_lsp/src/els_code_reload.erl
new file mode 100644
index 000000000..a8b22a0a3
--- /dev/null
+++ b/apps/els_lsp/src/els_code_reload.erl
@@ -0,0 +1,96 @@
+-module(els_code_reload).
+
+-include("els_lsp.hrl").
+-include_lib("kernel/include/logger.hrl").
+
+-export([
+ maybe_compile_and_load/1
+]).
+
+-spec maybe_compile_and_load(uri()) -> ok.
+maybe_compile_and_load(Uri) ->
+ Ext = filename:extension(Uri),
+ case els_config:get(code_reload) of
+ #{"node" := NodeOrNodes} when Ext == <<".erl">> ->
+ Nodes = get_nodes(NodeOrNodes),
+ Module = els_uri:module(Uri),
+ [rpc_code_reload(Node, Module) || Node <- Nodes],
+ ok;
+ _ ->
+ ok
+ end.
+
+-spec rpc_code_reload(atom(), module()) -> ok.
+rpc_code_reload(Node, Module) ->
+ case rpc:call(Node, code, is_sticky, [Module]) of
+ true ->
+ ok;
+ _ ->
+ Options = options(Node, Module),
+ ?LOG_INFO(
+ "[code_reload] code_reload ~p on ~p with ~p",
+ [Module, Node, Options]
+ ),
+ handle_rpc_result(
+ rpc:call(Node, c, c, [Module, Options]), Module
+ )
+ end.
+
+-spec get_nodes([string()] | string()) -> [atom()].
+get_nodes(NodeOrNodes) ->
+ Type = els_config_runtime:get_name_type(),
+ case NodeOrNodes of
+ [Str | _] = Nodes when is_list(Str) ->
+ [els_utils:compose_node_name(Name, Type) || Name <- Nodes];
+ Name when is_list(Name) ->
+ [els_utils:compose_node_name(Name, Type)];
+ _ ->
+ []
+ end.
+
+-spec options(atom(), module()) -> [any()].
+options(Node, Module) ->
+ case rpc:call(Node, erlang, get_module_info, [Module]) of
+ Info when is_list(Info) ->
+ CompileInfo = proplists:get_value(compile, Info, []),
+ CompileOptions = proplists:get_value(
+ options, CompileInfo, []
+ ),
+ case lists:keyfind('TEST', 2, CompileOptions) of
+ false ->
+ %% Ensure TEST define is set, this is to
+ %% enable eunit diagnostics to run
+ [{d, 'TEST', true}];
+ _ ->
+ []
+ end;
+ _ ->
+ []
+ end.
+
+-spec handle_rpc_result(term() | {badrpc, term()}, atom()) -> ok.
+handle_rpc_result({ok, Module}, _) ->
+ Msg = io_lib:format("code_reload success for: ~s", [Module]),
+ els_server:send_notification(
+ <<"window/showMessage">>,
+ #{
+ type => ?MESSAGE_TYPE_INFO,
+ message => els_utils:to_binary(Msg)
+ }
+ );
+handle_rpc_result(Err, Module) ->
+ ?LOG_INFO(
+ "[code_reload] code_reload using c:c/1 crashed with: ~p",
+ [Err]
+ ),
+ Msg = io_lib:format(
+ "code_reload swap crashed for: ~s with: ~w",
+ [Module, Err]
+ ),
+ els_server:send_notification(
+ <<"window/showMessage">>,
+ #{
+ type => ?MESSAGE_TYPE_ERROR,
+ message => els_utils:to_binary(Msg)
+ }
+ ).
diff --git a/apps/els_lsp/src/els_command_ct_run_test.erl b/apps/els_lsp/src/els_command_ct_run_test.erl
index 7541f29bf..7a0fb39cb 100644
--- a/apps/els_lsp/src/els_command_ct_run_test.erl
+++ b/apps/els_lsp/src/els_command_ct_run_test.erl
@@ -1,6 +1,6 @@
-module(els_command_ct_run_test).
--export([ execute/1, task/2 ]).
+-export([execute/1, task/2]).
%%==============================================================================
%% Includes
@@ -9,50 +9,54 @@
-include_lib("kernel/include/logger.hrl").
-spec execute(map()) -> ok.
-execute(#{ <<"module">> := M
- , <<"function">> := F
- , <<"arity">> := A
- , <<"line">> := Line
- , <<"uri">> := Uri
- }) ->
- Msg = io_lib:format("Running Common Test [mfa=~s:~s/~p]", [M, F, A]),
- ?LOG_INFO(Msg, []),
- Title = unicode:characters_to_binary(Msg),
- Suite = els_uri:module(Uri),
- Case = binary_to_atom(F, utf8),
- Config = #{ task => fun ?MODULE:task/2
- , entries => [{Uri, Line, Suite, Case}]
- , title => Title
- , show_percentages => false
- },
- {ok, _Pid} = els_background_job:new(Config),
- ok.
+execute(#{
+ <<"module">> := M,
+ <<"function">> := F,
+ <<"arity">> := A,
+ <<"line">> := Line,
+ <<"uri">> := Uri
+}) ->
+ Msg = io_lib:format("Running Common Test [mfa=~s:~s/~p]", [M, F, A]),
+ ?LOG_INFO(Msg, []),
+ Title = unicode:characters_to_binary(Msg),
+ Suite = els_uri:module(Uri),
+ Case = binary_to_atom(F, utf8),
+ Config = #{
+ task => fun ?MODULE:task/2,
+ entries => [{Uri, Line, Suite, Case}],
+ title => Title,
+ show_percentages => false
+ },
+ {ok, _Pid} = els_background_job:new(Config),
+ ok.
-spec task({uri(), pos_integer(), atom(), atom()}, any()) -> ok.
task({Uri, Line, Suite, Case}, _State) ->
- case run_test(Suite, Case) of
- {ok, IO} ->
- ?LOG_INFO("CT Test passed", []),
- publish_result(Uri, Line, ?DIAGNOSTIC_INFO, IO);
- {Error, IO} ->
- Message = els_utils:to_binary(io_lib:format("~p", [Error])),
- ?LOG_INFO("CT Test failed [error=~p] [io=~p]", [Error, IO]),
- publish_result(Uri, Line, ?DIAGNOSTIC_ERROR, Message)
- end.
+ case run_test(Suite, Case) of
+ {ok, IO} ->
+ ?LOG_INFO("CT Test passed", []),
+ publish_result(Uri, Line, ?DIAGNOSTIC_INFO, IO);
+ {Error, IO} ->
+ Message = els_utils:to_binary(io_lib:format("~p", [Error])),
+ ?LOG_INFO("CT Test failed [error=~p] [io=~p]", [Error, IO]),
+ publish_result(Uri, Line, ?DIAGNOSTIC_ERROR, Message)
+ end.
--spec publish_result( uri()
- , pos_integer()
- , els_diagnostics:severity()
- , binary()) -> ok.
+-spec publish_result(
+ uri(),
+ pos_integer(),
+ els_diagnostics:severity(),
+ binary()
+) -> ok.
publish_result(Uri, Line, Severity, Message) ->
- Range = els_protocol:range(#{from => {Line, 1}, to => {Line + 1, 1}}),
- Source = <<"Common Test">>,
- D = els_diagnostics:make_diagnostic(Range, Message, Severity, Source),
- els_diagnostics_provider:publish(Uri, [D]),
- ok.
+ Range = els_protocol:range(#{from => {Line, 1}, to => {Line + 1, 1}}),
+ Source = <<"Common Test">>,
+ D = els_diagnostics:make_diagnostic(Range, Message, Severity, Source),
+ els_diagnostics_provider:publish(Uri, [D]),
+ ok.
-spec run_test(atom(), atom()) -> {ok, binary()} | {error, any()}.
run_test(Suite, Case) ->
- Module = els_config_ct_run_test:get_module(),
- Function = els_config_ct_run_test:get_function(),
- els_distribution_server:rpc_call(Module, Function, [Suite, Case], infinity).
+ Module = els_config_ct_run_test:get_module(),
+ Function = els_config_ct_run_test:get_function(),
+ els_distribution_server:rpc_call(Module, Function, [Suite, Case], infinity).
diff --git a/apps/els_lsp/src/els_compiler_diagnostics.erl b/apps/els_lsp/src/els_compiler_diagnostics.erl
index 3e4916188..9465d2da2 100644
--- a/apps/els_lsp/src/els_compiler_diagnostics.erl
+++ b/apps/els_lsp/src/els_compiler_diagnostics.erl
@@ -7,23 +7,26 @@
%% Exports
%%==============================================================================
-behaviour(els_diagnostics).
--export([ is_default/0
- , run/1
- , source/0
- , on_complete/2
- ]).
+-export([
+ is_default/0,
+ run/1,
+ source/0,
+ on_complete/2
+]).
%% identity function for our own diagnostics
--export([ format_error/1 ]).
+-export([format_error/1]).
--export([ inclusion_range/2
- , inclusion_range/3
- ]).
+-export([
+ inclusion_range/2,
+ inclusion_range/3
+]).
--export([ include_options/0
- , macro_options/0
- , telemetry/2
- ]).
+-export([
+ include_options/0,
+ macro_options/0,
+ telemetry/2
+]).
%%==============================================================================
%% Includes
@@ -34,10 +37,10 @@
%%==============================================================================
%% Type Definitions
%%==============================================================================
--type compiler_info() :: {erl_anno:anno() | 'none', module(), any()}.
--type compiler_msg() :: {file:filename(), [compiler_info()]}.
--type macro_config() :: #{string() => string()}.
--type macro_option() :: {'d', atom()} | {'d', atom(), any()}.
+-type compiler_info() :: {erl_anno:anno() | 'none', module(), any()}.
+-type compiler_msg() :: {file:filename(), [compiler_info()]}.
+-type macro_config() :: #{string() => string()}.
+-type macro_option() :: {'d', atom()} | {'d', atom(), any()}.
-type include_option() :: {'i', string()}.
%%==============================================================================
@@ -46,82 +49,111 @@
-spec is_default() -> boolean().
is_default() ->
- true.
+ true.
-spec run(uri()) -> [els_diagnostics:diagnostic()].
run(Uri) ->
- case filename:extension(Uri) of
- <<".erl">> ->
- compile(Uri);
- <<".hrl">> ->
- %% It does not make sense to 'compile' header files in isolation
- %% (e.g. using the compile:forms/1 function). That would in fact
- %% produce a big number of false positive errors and warnings,
- %% including 'record not used' or 'module attribute not
- %% specified'. An alternative could be to use a 'fake' module
- %% that simply includes the file, but that feels a bit too
- %% hackish. As a compromise, we decided to parse the include
- %% file, since that allows us to identify most of the common
- %% errors in header files.
- parse(Uri);
- <<".escript">> ->
- parse_escript(Uri);
- _Ext ->
- ?LOG_DEBUG("Skipping diagnostics due to extension [uri=~p]", [Uri]),
- []
- end.
+ case filename:extension(Uri) of
+ <<".erl">> ->
+ compile(Uri);
+ <<".hrl">> ->
+ %% It does not make sense to 'compile' header files in isolation
+ %% (e.g. using the compile:forms/1 function). That would in fact
+ %% produce a big number of false positive errors and warnings,
+ %% including 'record not used' or 'module attribute not
+ %% specified'. An alternative could be to use a 'fake' module
+ %% that simply includes the file, but that feels a bit too
+ %% hackish. As a compromise, we decided to parse the include
+ %% file, since that allows us to identify most of the common
+ %% errors in header files.
+ parse(Uri);
+ <<".escript">> ->
+ parse_escript(Uri);
+ _Ext ->
+ ?LOG_DEBUG("Skipping diagnostics due to extension [uri=~p]", [Uri]),
+ []
+ end.
-spec source() -> binary().
source() ->
- <<"Compiler">>.
+ <<"Compiler">>.
-spec on_complete(uri(), [els_diagnostics:diagnostic()]) -> ok.
on_complete(Uri, Diagnostics) ->
- ?MODULE:telemetry(Uri, Diagnostics),
- maybe_compile_and_load(Uri, Diagnostics).
+ ?MODULE:telemetry(Uri, Diagnostics).
%%==============================================================================
%% Internal Functions
%%==============================================================================
-spec compile(uri()) -> [els_diagnostics:diagnostic()].
compile(Uri) ->
- Dependencies = els_diagnostics_utils:dependencies(Uri),
- Path = els_utils:to_list(els_uri:path(Uri)),
- case compile_file(Path, Dependencies) of
- {{ok, _, WS}, Diagnostics} ->
- Diagnostics ++
- diagnostics(Path, WS, ?DIAGNOSTIC_WARNING);
- {{error, ES, WS}, Diagnostics} ->
- Diagnostics ++
- diagnostics(Path, WS, ?DIAGNOSTIC_WARNING) ++
- diagnostics(Path, ES, ?DIAGNOSTIC_ERROR)
- end.
+ Dependencies = els_diagnostics_utils:dependencies(Uri),
+ Path = els_utils:to_list(els_uri:path(Uri)),
+ case compile_file(Path, Dependencies) of
+ {{ok, _, WS}, Diagnostics} ->
+ Diagnostics ++
+ diagnostics(Path, WS, ?DIAGNOSTIC_WARNING);
+ {{error, ES, WS}, Diagnostics} ->
+ Diagnostics ++
+ diagnostics(Path, WS, ?DIAGNOSTIC_WARNING) ++
+ diagnostics(Path, ES, ?DIAGNOSTIC_ERROR)
+ end.
+
+-ifdef(OTP_RELEASE).
+-if(?OTP_RELEASE =< 23).
+%% Invalid spec in eep:open/1 will cause dialyzer to emit a warning
+%% in OTP 23 and earlier.
+-dialyzer([{nowarn_function, parse/1}]).
+-endif.
+-endif.
-spec parse(uri()) -> [els_diagnostics:diagnostic()].
parse(Uri) ->
- FileName = els_utils:to_list(els_uri:path(Uri)),
- Document = case els_dt_document:lookup(Uri) of
- {ok, [DocItem]} ->
- DocItem;
- _ ->
- undefined
- end,
- {ok, Epp} = epp:open([ {name, FileName}
- , {includes, els_config:get(include_paths)}
- ]),
- Res = [epp_diagnostic(Document, Anno, Module, Desc)
- || {error, {Anno, Module, Desc}} <- epp:parse_file(Epp)],
- epp:close(Epp),
- Res.
+ FileName = els_utils:to_list(els_uri:path(Uri)),
+ Document =
+ case els_dt_document:lookup(Uri) of
+ {ok, [DocItem]} ->
+ DocItem;
+ _ ->
+ undefined
+ end,
+ Options = [
+ {name, FileName},
+ {includes, els_config:get(include_paths)},
+ {macros, [
+ {'MODULE', dummy_module, redefine},
+ {'MODULE_STRING', "dummy_module", redefine}
+ ]}
+ ],
+ case epp:open(Options) of
+ {ok, Epp} ->
+ Res = [
+ epp_diagnostic(Document, Anno, Module, Desc)
+ || {error, {Anno, Module, Desc}} <- epp:parse_file(Epp)
+ ],
+ epp:close(Epp),
+ Res;
+ {error, Reason} ->
+ ?LOG_ERROR(
+ "Failed to open: ~s\n"
+ "Reason: ~p",
+ [FileName, Reason]
+ ),
+ []
+ end.
%% Possible cases to handle
%% ,{error,{19,erl_parse,["syntax error before: ","'-'"]}}
%% ,{error,{1,epp,{error,1,{undefined,'MODULE',none}}}}
%% ,{error,{3,epp,{error,"including nonexistent_macro.hrl is not allowed"}}}
%% ,{error,{3,epp,{include,file,"yaws.hrl"}}}
--spec epp_diagnostic(els_dt_document:item(),
- erl_anno:anno(), module(), any()) ->
- els_diagnostics:diagnostic().
+-spec epp_diagnostic(
+ els_dt_document:item(),
+ erl_anno:anno(),
+ module(),
+ any()
+) ->
+ els_diagnostics:diagnostic().
epp_diagnostic(Document, Anno, epp, {error, Anno, Reason}) ->
%% Workaround for https://bugs.erlang.org/browse/ERL-1310
epp_diagnostic(Document, Anno, epp, Reason);
@@ -130,14 +162,14 @@ epp_diagnostic(Document, Anno, Module, Desc) ->
-spec parse_escript(uri()) -> [els_diagnostics:diagnostic()].
parse_escript(Uri) ->
- FileName = els_utils:to_list(els_uri:path(Uri)),
- case els_escript:extract(FileName) of
- {ok, WS} ->
- diagnostics(FileName, WS, ?DIAGNOSTIC_WARNING);
- {error, ES, WS} ->
- diagnostics(FileName, WS, ?DIAGNOSTIC_WARNING) ++
- diagnostics(FileName, ES, ?DIAGNOSTIC_ERROR)
- end.
+ FileName = els_utils:to_list(els_uri:path(Uri)),
+ case els_escript:extract(FileName) of
+ {ok, WS} ->
+ diagnostics(FileName, WS, ?DIAGNOSTIC_WARNING);
+ {error, ES, WS} ->
+ diagnostics(FileName, WS, ?DIAGNOSTIC_WARNING) ++
+ diagnostics(FileName, ES, ?DIAGNOSTIC_ERROR)
+ end.
%% @doc Convert compiler messages into diagnostics
%%
@@ -148,64 +180,72 @@ parse_escript(Uri) ->
%% and they are presented to the user by highlighting the line where
%% the file inclusion happens.
-spec diagnostics(list(), [compiler_msg()], els_diagnostics:severity()) ->
- [els_diagnostics:diagnostic()].
+ [els_diagnostics:diagnostic()].
diagnostics(Path, List, Severity) ->
- Uri = els_uri:uri(els_utils:to_binary(Path)),
- case els_utils:lookup_document(Uri) of
- {ok, Document} ->
- lists:flatten([[ diagnostic( Path
- , MessagePath
- , range(Document, Anno)
- , Document
- , Module
- , Desc
- , Severity)
- || {Anno, Module, Desc} <- Info]
- || {MessagePath, Info} <- List]);
- {error, _Error} ->
- []
- end.
-
--spec diagnostic( string()
- , string()
- , poi_range()
- , els_dt_document:item()
- , module()
- , string()
- , integer()) -> els_diagnostics:diagnostic().
+ Uri = els_uri:uri(els_utils:to_binary(Path)),
+ case els_utils:lookup_document(Uri) of
+ {ok, Document} ->
+ lists:flatten([
+ [
+ diagnostic(
+ Path,
+ MessagePath,
+ range(Document, Anno),
+ Document,
+ Module,
+ Desc,
+ Severity
+ )
+ || {Anno, Module, Desc} <- Info
+ ]
+ || {MessagePath, Info} <- List
+ ]);
+ {error, _Error} ->
+ []
+ end.
+
+-spec diagnostic(
+ string(),
+ string(),
+ els_poi:poi_range(),
+ els_dt_document:item(),
+ module(),
+ string(),
+ integer()
+) -> els_diagnostics:diagnostic().
diagnostic(Path, Path, Range, _Document, Module, Desc, Severity) ->
- %% The compiler message is related to the same .erl file, so
- %% preserve the location information.
- diagnostic(Range, Module, Desc, Severity);
+ %% The compiler message is related to the same .erl file, so
+ %% preserve the location information.
+ diagnostic(Range, Module, Desc, Severity);
diagnostic(_Path, MessagePath, Range, Document, Module, Desc0, Severity) ->
- #{from := {Line, _}} = Range,
- InclusionRange = inclusion_range(MessagePath, Document),
- %% The compiler message is related to an included file. Replace the
- %% original location with the location of the file inclusion.
- %% And re-route the format_error call to this module as a no-op
- Desc1 = Module:format_error(Desc0),
- Desc = io_lib:format("Issue in included file (~p): ~s", [Line, Desc1]),
- diagnostic(InclusionRange, ?MODULE, Desc, Severity).
-
--spec diagnostic(poi_range(), module(), string(), integer()) ->
- els_diagnostics:diagnostic().
+ #{from := {Line, _}} = Range,
+ InclusionRange = inclusion_range(MessagePath, Document),
+ %% The compiler message is related to an included file. Replace the
+ %% original location with the location of the file inclusion.
+ %% And re-route the format_error call to this module as a no-op
+ Desc1 = Module:format_error(Desc0),
+ Desc = io_lib:format("Issue in included file (~p): ~s", [Line, Desc1]),
+ diagnostic(InclusionRange, ?MODULE, Desc, Severity).
+
+-spec diagnostic(els_poi:poi_range(), module(), string(), integer()) ->
+ els_diagnostics:diagnostic().
diagnostic(Range, Module, Desc, Severity) ->
- Message0 = lists:flatten(Module:format_error(Desc)),
- Message = els_utils:to_binary(Message0),
- Code = make_code(Module, Desc),
- #{ range => els_protocol:range(Range)
- , message => Message
- , severity => Severity
- , source => source()
- , code => Code
- }.
+ Message0 = lists:flatten(Module:format_error(Desc)),
+ Message = els_utils:to_binary(Message0),
+ Code = make_code(Module, Desc),
+ #{
+ range => els_protocol:range(Range),
+ message => Message,
+ severity => Severity,
+ source => source(),
+ code => Code
+ }.
%% @doc NOP function for the call to 'Module:format_error/1' in diagnostic/4
%% above.
-spec format_error(string()) -> [string()].
format_error(Str) ->
- Str.
-
+ Str.
%-----------------------------------------------------------------------
@@ -216,612 +256,639 @@ format_error(Str) ->
%% This file
make_code(els_compiler_diagnostics, _) ->
- <<"L0000">>;
-
+ <<"L0000">>;
%% compiler-8.0.2/src/compile.erl
make_code(compile, no_crypto) ->
- <<"C1000">>;
+ <<"C1000">>;
make_code(compile, bad_crypto_key) ->
- <<"C1001">>;
+ <<"C1001">>;
make_code(compile, no_crypto_key) ->
- <<"C1002">>;
+ <<"C1002">>;
make_code(compile, {open, _E}) ->
- <<"C1003">>;
+ <<"C1003">>;
make_code(compile, {epp, _E}) ->
- make_code(epp, compile);
+ make_code(epp, compile);
make_code(compile, write_error) ->
- <<"C1004">>;
+ <<"C1004">>;
make_code(compile, {write_error, _Error}) ->
- <<"C1005">>;
+ <<"C1005">>;
make_code(compile, {rename, _From, _To, _Error}) ->
- <<"C1006">>;
+ <<"C1006">>;
make_code(compile, {parse_transform, _M, _R}) ->
- <<"C1007">>;
+ <<"C1007">>;
make_code(compile, {undef_parse_transform, _M}) ->
- <<"C1008">>;
+ <<"C1008">>;
make_code(compile, {core_transform, _M, _R}) ->
- <<"C1009">>;
+ <<"C1009">>;
make_code(compile, {crash, _Pass, _Reason, _Stk}) ->
- <<"C1010">>;
+ <<"C1010">>;
make_code(compile, {bad_return, _Pass, _Reason}) ->
- <<"C1011">>;
-make_code(compile, {module_name, _Mod, _Filename}) ->
- <<"C1012">>;
+ <<"C1011">>;
+make_code(compile, {module_name, _Mod, _FileName}) ->
+ <<"C1012">>;
make_code(compile, _Other) ->
- <<"C1099">>;
-
+ <<"C1099">>;
%% syntax_tools-2.6/src/epp_dodger.erl
make_code(epp_dodger, macro_args) ->
- <<"D1100">>;
+ <<"D1100">>;
make_code(epp_dodger, {error, _Error}) ->
- <<"D1101">>;
+ <<"D1101">>;
make_code(epp_dodger, {warning, _Error}) ->
- <<"D1102">>;
+ <<"D1102">>;
make_code(epp_dodger, {unknown, _Reason}) ->
- <<"D1103">>;
+ <<"D1103">>;
make_code(epp_dodger, _Other) ->
- <<"D1199">>;
-
+ <<"D1199">>;
%% stdlib-3.15.2/src/erl_lint.erl
make_code(erl_lint, undefined_module) ->
- <<"L1201">>;
+ <<"L1201">>;
make_code(erl_lint, redefine_module) ->
- <<"L1202">>;
+ <<"L1202">>;
make_code(erl_lint, pmod_unsupported) ->
- <<"L1203">>;
+ <<"L1203">>;
make_code(erl_lint, non_latin1_module_unsupported) ->
- <<"L1204">>;
+ <<"L1204">>;
make_code(erl_lint, invalid_call) ->
- <<"L1205">>;
+ <<"L1205">>;
make_code(erl_lint, invalid_record) ->
- <<"L1206">>;
+ <<"L1206">>;
make_code(erl_lint, {attribute, _A}) ->
- <<"L1207">>;
+ <<"L1207">>;
make_code(erl_lint, {missing_qlc_hrl, _A}) ->
- <<"L1208">>;
+ <<"L1208">>;
make_code(erl_lint, {redefine_import, {{_F, _A}, _M}}) ->
- <<"L1209">>;
+ <<"L1209">>;
make_code(erl_lint, {bad_inline, {_F, _A}}) ->
- <<"L1210">>;
+ <<"L1210">>;
make_code(erl_lint, {invalid_deprecated, _D}) ->
- <<"L1211">>;
+ <<"L1211">>;
make_code(erl_lint, {bad_deprecated, {_F, _A}}) ->
- <<"L1212">>;
+ <<"L1212">>;
make_code(erl_lint, {invalid_removed, _D}) ->
- <<"L1213">>;
+ <<"L1213">>;
make_code(erl_lint, {bad_removed, {F, A}}) when F =:= '_'; A =:= '_' ->
- <<"L1214">>;
+ <<"L1214">>;
make_code(erl_lint, {bad_removed, {_F, _A}}) ->
- <<"L1215">>;
+ <<"L1215">>;
make_code(erl_lint, {bad_nowarn_unused_function, {_F, _A}}) ->
- <<"L1216">>;
+ <<"L1216">>;
make_code(erl_lint, {bad_nowarn_bif_clash, {_F, _A}}) ->
- <<"L1217">>;
+ <<"L1217">>;
make_code(erl_lint, disallowed_nowarn_bif_clash) ->
- <<"L1218">>;
+ <<"L1218">>;
make_code(erl_lint, {bad_on_load, _Term}) ->
- <<"L1219">>;
+ <<"L1219">>;
make_code(erl_lint, multiple_on_loads) ->
- <<"L1220">>;
+ <<"L1220">>;
make_code(erl_lint, {bad_on_load_arity, {_F, _A}}) ->
- <<"L1221">>;
+ <<"L1221">>;
make_code(erl_lint, {undefined_on_load, {_F, _A}}) ->
- <<"L1222">>;
+ <<"L1222">>;
make_code(erl_lint, nif_inline) ->
- <<"L1223">>;
+ <<"L1223">>;
make_code(erl_lint, export_all) ->
- <<"L1224">>;
+ <<"L1224">>;
make_code(erl_lint, {duplicated_export, {_F, _A}}) ->
- <<"L1225">>;
+ <<"L1225">>;
make_code(erl_lint, {unused_import, {{_F, _A}, _M}}) ->
- <<"L1226">>;
+ <<"L1226">>;
make_code(erl_lint, {undefined_function, {_F, _A}}) ->
- <<"L1227">>;
+ <<"L1227">>;
make_code(erl_lint, {redefine_function, {_F, _A}}) ->
- <<"L1228">>;
+ <<"L1228">>;
make_code(erl_lint, {define_import, {_F, _A}}) ->
- <<"L1229">>;
+ <<"L1229">>;
make_code(erl_lint, {unused_function, {_F, _A}}) ->
- <<"L1230">>;
+ <<"L1230">>;
make_code(erl_lint, {call_to_redefined_bif, {_F, _A}}) ->
- <<"L1231">>;
+ <<"L1231">>;
make_code(erl_lint, {call_to_redefined_old_bif, {_F, _A}}) ->
- <<"L1232">>;
+ <<"L1232">>;
make_code(erl_lint, {redefine_old_bif_import, {_F, _A}}) ->
- <<"L1233">>;
+ <<"L1233">>;
make_code(erl_lint, {redefine_bif_import, {_F, _A}}) ->
- <<"L1234">>;
+ <<"L1234">>;
make_code(erl_lint, {deprecated, _MFA, _String, _Rel}) ->
- <<"L1235">>;
+ <<"L1235">>;
make_code(erl_lint, {deprecated, _MFA, String}) when is_list(String) ->
- <<"L1236">>;
+ <<"L1236">>;
make_code(erl_lint, {deprecated_type, {_M1, _F1, _A1}, _String, _Rel}) ->
- <<"L1237">>;
-make_code(erl_lint, {deprecated_type, {_M1, _F1, _A1}, String})
- when is_list(String) ->
- <<"L1238">>;
+ <<"L1237">>;
+make_code(erl_lint, {deprecated_type, {_M1, _F1, _A1}, String}) when
+ is_list(String)
+->
+ <<"L1238">>;
make_code(erl_lint, {removed, _MFA, _ReplacementMFA, _Rel}) ->
- <<"L1239">>;
+ <<"L1239">>;
make_code(erl_lint, {removed, _MFA, String}) when is_list(String) ->
- <<"L1240">>;
+ <<"L1240">>;
make_code(erl_lint, {removed_type, _MNA, _String}) ->
- <<"L1241">>;
+ <<"L1241">>;
make_code(erl_lint, {obsolete_guard, {_F, _A}}) ->
- <<"L1242">>;
+ <<"L1242">>;
make_code(erl_lint, {obsolete_guard_overridden, _Test}) ->
- <<"L1243">>;
+ <<"L1243">>;
make_code(erl_lint, {too_many_arguments, _Arity}) ->
- <<"L1244">>;
+ <<"L1244">>;
make_code(erl_lint, illegal_pattern) ->
- <<"L1245">>;
+ <<"L1245">>;
make_code(erl_lint, illegal_map_key) ->
- <<"L1246">>;
+ <<"L1246">>;
make_code(erl_lint, illegal_bin_pattern) ->
- <<"L1247">>;
+ <<"L1247">>;
make_code(erl_lint, illegal_expr) ->
- <<"L1248">>;
+ <<"L1248">>;
make_code(erl_lint, {illegal_guard_local_call, {_F, _A}}) ->
- <<"L1249">>;
+ <<"L1249">>;
make_code(erl_lint, illegal_guard_expr) ->
- <<"L1250">>;
+ <<"L1250">>;
make_code(erl_lint, illegal_map_construction) ->
- <<"L1251">>;
+ <<"L1251">>;
make_code(erl_lint, {undefined_record, _T}) ->
- <<"L1252">>;
+ <<"L1252">>;
make_code(erl_lint, {redefine_record, _T}) ->
- <<"L1253">>;
+ <<"L1253">>;
make_code(erl_lint, {redefine_field, _T, _F}) ->
- <<"L1254">>;
+ <<"L1254">>;
make_code(erl_lint, bad_multi_field_init) ->
- <<"L1255">>;
+ <<"L1255">>;
make_code(erl_lint, {undefined_field, _T, _F}) ->
- <<"L1256">>;
+ <<"L1256">>;
make_code(erl_lint, illegal_record_info) ->
- <<"L1257">>;
+ <<"L1257">>;
make_code(erl_lint, {field_name_is_variable, _T, _F}) ->
- <<"L1258">>;
+ <<"L1258">>;
make_code(erl_lint, {wildcard_in_update, _T}) ->
- <<"L1259">>;
+ <<"L1259">>;
make_code(erl_lint, {unused_record, _T}) ->
- <<"L1260">>;
+ <<"L1260">>;
make_code(erl_lint, {untyped_record, _T}) ->
- <<"L1261">>;
+ <<"L1261">>;
make_code(erl_lint, {unbound_var, _V}) ->
- <<"L1262">>;
+ <<"L1262">>;
make_code(erl_lint, {unsafe_var, _V, {_What, _Where}}) ->
- <<"L1263">>;
+ <<"L1263">>;
make_code(erl_lint, {exported_var, _V, {_What, _Where}}) ->
- <<"L1264">>;
+ <<"L1264">>;
make_code(erl_lint, {match_underscore_var, _V}) ->
- <<"L1265">>;
+ <<"L1265">>;
make_code(erl_lint, {match_underscore_var_pat, _V}) ->
- <<"L1266">>;
+ <<"L1266">>;
make_code(erl_lint, {shadowed_var, _V, _In}) ->
- <<"L1267">>;
+ <<"L1267">>;
make_code(erl_lint, {unused_var, _V}) ->
- <<"L1268">>;
+ <<"L1268">>;
make_code(erl_lint, {variable_in_record_def, _V}) ->
- <<"L1269">>;
+ <<"L1269">>;
make_code(erl_lint, {stacktrace_guard, _V}) ->
- <<"L1270">>;
+ <<"L1270">>;
make_code(erl_lint, {stacktrace_bound, _V}) ->
- <<"L1271">>;
+ <<"L1271">>;
make_code(erl_lint, {undefined_bittype, _Type}) ->
- <<"L1272">>;
+ <<"L1272">>;
make_code(erl_lint, {bittype_mismatch, _Val1, _Val2, _What}) ->
- <<"L1273">>;
+ <<"L1273">>;
make_code(erl_lint, bittype_unit) ->
- <<"L1274">>;
+ <<"L1274">>;
make_code(erl_lint, illegal_bitsize) ->
- <<"L1275">>;
+ <<"L1275">>;
make_code(erl_lint, {illegal_bitsize_local_call, {_F, _A}}) ->
- <<"L1276">>;
+ <<"L1276">>;
make_code(erl_lint, non_integer_bitsize) ->
- <<"L1277">>;
+ <<"L1277">>;
make_code(erl_lint, unsized_binary_not_at_end) ->
- <<"L1278">>;
+ <<"L1278">>;
make_code(erl_lint, typed_literal_string) ->
- <<"L1279">>;
+ <<"L1279">>;
make_code(erl_lint, utf_bittype_size_or_unit) ->
- <<"L1280">>;
+ <<"L1280">>;
make_code(erl_lint, {bad_bitsize, _Type}) ->
- <<"L1281">>;
+ <<"L1281">>;
make_code(erl_lint, unsized_binary_in_bin_gen_pattern) ->
- <<"L1282">>;
-make_code(erl_lint, {conflicting_behaviours,
- {_Name, _Arity}, _B, _FirstL, _FirstB}) ->
- <<"L1283">>;
+ <<"L1282">>;
+make_code(erl_lint, {conflicting_behaviours, {_Name, _Arity}, _B, _FirstL, _FirstB}) ->
+ <<"L1283">>;
make_code(erl_lint, {undefined_behaviour_func, {_Func, _Arity}, _Behaviour}) ->
- <<"L1284">>;
+ <<"L1284">>;
make_code(erl_lint, {undefined_behaviour, _Behaviour}) ->
- <<"L1285">>;
+ <<"L1285">>;
make_code(erl_lint, {undefined_behaviour_callbacks, _Behaviour}) ->
- <<"L1286">>;
+ <<"L1286">>;
make_code(erl_lint, {ill_defined_behaviour_callbacks, _Behaviour}) ->
- <<"L1287">>;
+ <<"L1287">>;
make_code(erl_lint, {ill_defined_optional_callbacks, _Behaviour}) ->
- <<"L1288">>;
+ <<"L1288">>;
make_code(erl_lint, {behaviour_info, {_M, _F, _A}}) ->
- <<"L1289">>;
+ <<"L1289">>;
make_code(erl_lint, {redefine_optional_callback, {_F, _A}}) ->
- <<"L1290">>;
+ <<"L1290">>;
make_code(erl_lint, {undefined_callback, {_M, _F, _A}}) ->
- <<"L1291">>;
+ <<"L1291">>;
make_code(erl_lint, {singleton_typevar, _Name}) ->
- <<"L1292">>;
+ <<"L1292">>;
make_code(erl_lint, {bad_export_type, _ETs}) ->
- <<"L1293">>;
+ <<"L1293">>;
make_code(erl_lint, {duplicated_export_type, {_T, _A}}) ->
- <<"L1294">>;
+ <<"L1294">>;
make_code(erl_lint, {undefined_type, {_TypeName, _Arity}}) ->
- <<"L1295">>;
+ <<"L1295">>;
make_code(erl_lint, {unused_type, {_TypeName, _Arity}}) ->
- <<"L1296">>;
+ <<"L1296">>;
make_code(erl_lint, {new_builtin_type, {_TypeName, _Arity}}) ->
- <<"L1297">>;
+ <<"L1297">>;
make_code(erl_lint, {builtin_type, {_TypeName, _Arity}}) ->
- <<"L1298">>;
+ <<"L1298">>;
make_code(erl_lint, {renamed_type, _OldName, _NewName}) ->
- <<"L1299">>;
+ <<"L1299">>;
make_code(erl_lint, {redefine_type, {_TypeName, _Arity}}) ->
- <<"L1300">>;
+ <<"L1300">>;
make_code(erl_lint, {type_syntax, _Constr}) ->
- <<"L1301">>;
+ <<"L1301">>;
make_code(erl_lint, old_abstract_code) ->
- <<"L1302">>;
+ <<"L1302">>;
make_code(erl_lint, {redefine_spec, {_M, _F, _A}}) ->
- <<"L1303">>;
+ <<"L1303">>;
make_code(erl_lint, {redefine_spec, {_F, _A}}) ->
- <<"L1304">>;
+ <<"L1304">>;
make_code(erl_lint, {redefine_callback, {_F, _A}}) ->
- <<"L1305">>;
+ <<"L1305">>;
make_code(erl_lint, {bad_callback, {_M, _F, _A}}) ->
- <<"L1306">>;
+ <<"L1306">>;
make_code(erl_lint, {bad_module, {_M, _F, _A}}) ->
- <<"L1307">>;
+ <<"L1307">>;
make_code(erl_lint, {spec_fun_undefined, {_F, _A}}) ->
- <<"L1308">>;
+ <<"L1308">>;
make_code(erl_lint, {missing_spec, {_F, _A}}) ->
- <<"L1309">>;
+ <<"L1309">>;
make_code(erl_lint, spec_wrong_arity) ->
- <<"L1310">>;
+ <<"L1310">>;
make_code(erl_lint, callback_wrong_arity) ->
- <<"L1311">>;
-make_code(erl_lint, {deprecated_builtin_type, {_Name, _Arity},
- _Replacement, _Rel}) ->
- <<"L1312">>;
+ <<"L1311">>;
+make_code(erl_lint, {deprecated_builtin_type, {_Name, _Arity}, _Replacement, _Rel}) ->
+ <<"L1312">>;
make_code(erl_lint, {not_exported_opaque, {_TypeName, _Arity}}) ->
- <<"L1313">>;
+ <<"L1313">>;
make_code(erl_lint, {underspecified_opaque, {_TypeName, _Arity}}) ->
- <<"L1314">>;
+ <<"L1314">>;
make_code(erl_lint, {bad_dialyzer_attribute, _Term}) ->
- <<"L1315">>;
+ <<"L1315">>;
make_code(erl_lint, {bad_dialyzer_option, _Term}) ->
- <<"L1316">>;
+ <<"L1316">>;
make_code(erl_lint, {format_error, {_Fmt, _Args}}) ->
- <<"L1317">>;
+ <<"L1317">>;
+make_code(erl_lint, {undefined_nif, {_F, _A}}) ->
+ <<"L1318">>;
+make_code(erl_link, no_load_nif) ->
+ <<"L1319">>;
make_code(erl_lint, _Other) ->
- <<"L1399">>;
-
+ <<"L1399">>;
%% stdlib-3.15.2/src/erl_scan.erl
make_code(erl_scan, {string, _Quote, _Head}) ->
- <<"S1400">>;
+ <<"S1400">>;
make_code(erl_scan, {illegal, _Type}) ->
- <<"S1401">>;
+ <<"S1401">>;
make_code(erl_scan, char) ->
- <<"S1402">>;
+ <<"S1402">>;
make_code(erl_scan, {base, _Base}) ->
- <<"S1403">>;
-make_code(erl_scan, _Other) ->
- <<"S1499">>;
-
+ <<"S1403">>;
+make_code(erl_scan, _Other) ->
+ <<"S1499">>;
%% stdlib-3.15.2/src/epp.erl
make_code(epp, cannot_parse) ->
- <<"E1500">>;
+ <<"E1500">>;
make_code(epp, {bad, _W}) ->
- <<"E1501">>;
+ <<"E1501">>;
make_code(epp, {duplicated_argument, _Arg}) ->
- <<"E1502">>;
+ <<"E1502">>;
make_code(epp, missing_parenthesis) ->
- <<"E1503">>;
+ <<"E1503">>;
make_code(epp, missing_comma) ->
- <<"E1504">>;
+ <<"E1504">>;
make_code(epp, premature_end) ->
- <<"E1505">>;
+ <<"E1505">>;
make_code(epp, {call, _What}) ->
- <<"E1506">>;
+ <<"E1506">>;
make_code(epp, {undefined, _M, none}) ->
- <<"E1507">>;
+ <<"E1507">>;
make_code(epp, {undefined, _M, _A}) ->
- <<"E1508">>;
+ <<"E1508">>;
make_code(epp, {depth, _What}) ->
- <<"E1509">>;
+ <<"E1509">>;
make_code(epp, {mismatch, _M}) ->
- <<"E1510">>;
+ <<"E1510">>;
make_code(epp, {arg_error, _M}) ->
- <<"E1511">>;
+ <<"E1511">>;
make_code(epp, {redefine, _M}) ->
- <<"E1512">>;
+ <<"E1512">>;
make_code(epp, {redefine_predef, _M}) ->
- <<"E1513">>;
+ <<"E1513">>;
make_code(epp, {circular, _M, none}) ->
- <<"E1514">>;
+ <<"E1514">>;
make_code(epp, {circular, _M, _A}) ->
- <<"E1515">>;
+ <<"E1515">>;
make_code(epp, {include, _W, _F}) ->
- <<"E1516">>;
+ <<"E1516">>;
make_code(epp, {illegal, _How, _What}) ->
- <<"E1517">>;
+ <<"E1517">>;
make_code(epp, {illegal_function, _Macro}) ->
- <<"E1518">>;
+ <<"E1518">>;
make_code(epp, {illegal_function_usage, _Macro}) ->
- <<"E1519">>;
+ <<"E1519">>;
make_code(epp, elif_after_else) ->
- <<"E1520">>;
+ <<"E1520">>;
make_code(epp, {'NYI', _What}) ->
- <<"E1521">>;
+ <<"E1521">>;
make_code(epp, {error, _Term}) ->
- <<"E1522">>;
+ <<"E1522">>;
make_code(epp, {warning, _Term}) ->
- <<"E1523">>;
+ <<"E1523">>;
+make_code(epp, {moduledoc, invalid, _}) ->
+ <<"E1524">>;
+make_code(epp, {moduledoc, file, _}) ->
+ <<"E1525">>;
+make_code(epp, {doc, invalid, _}) ->
+ <<"E1526">>;
+make_code(epp, {doc, file, _}) ->
+ <<"E1527">>;
make_code(epp, _E) ->
- <<"E1599">>;
-
+ <<"E1599">>;
%% stdlib-3.15.2/src/qlc.erl
make_code(qlc, not_a_query_list_comprehension) ->
- <<"Q1600">>;
+ <<"Q1600">>;
make_code(qlc, {used_generator_variable, _V}) ->
- <<"Q1601">>;
+ <<"Q1601">>;
make_code(qlc, binary_generator) ->
- <<"Q1602">>;
+ <<"Q1602">>;
make_code(qlc, too_complex_join) ->
- <<"Q1603">>;
+ <<"Q1603">>;
make_code(qlc, too_many_joins) ->
- <<"Q1604">>;
+ <<"Q1604">>;
make_code(qlc, nomatch_pattern) ->
- <<"Q1605">>;
+ <<"Q1605">>;
make_code(qlc, nomatch_filter) ->
- <<"Q1606">>;
+ <<"Q1606">>;
make_code(qlc, {Location, _Mod, _Reason}) when is_integer(Location) ->
- <<"Q1607">>;
+ <<"Q1607">>;
make_code(qlc, {bad_object, _FileName}) ->
- <<"Q1608">>;
+ <<"Q1608">>;
make_code(qlc, bad_object) ->
- <<"Q1609">>;
+ <<"Q1609">>;
make_code(qlc, {file_error, _FileName, _Reason}) ->
- <<"Q1610">>;
+ <<"Q1610">>;
make_code(qlc, {premature_eof, _FileName}) ->
- <<"Q1611">>;
+ <<"Q1611">>;
make_code(qlc, {tmpdir_usage, _Why}) ->
- <<"Q1612">>;
+ <<"Q1612">>;
make_code(qlc, {error, _Module, _Reason}) ->
- <<"Q1613">>;
+ <<"Q1613">>;
make_code(qlc, _E) ->
- <<"Q1699">>;
-
+ <<"Q1699">>;
%% stdlib-3.15.2/src/erl_parse.yrl
-make_code(erl_parse, "head mismatch") ->
- <<"P1700">>;
+make_code(erl_parse, "head mismatch" ++ _) ->
+ <<"P1700">>;
make_code(erl_parse, "bad type variable") ->
- <<"P1701">>;
+ <<"P1701">>;
make_code(erl_parse, "bad attribute") ->
- <<"P1702">>;
+ <<"P1702">>;
make_code(erl_parse, "unsupported constraint" ++ _) ->
- <<"P1703">>;
+ <<"P1703">>;
make_code(erl_parse, "bad binary type") ->
- <<"P1704">>;
+ <<"P1704">>;
make_code(erl_parse, "bad variable list") ->
- <<"P1705">>;
+ <<"P1705">>;
make_code(erl_parse, "bad function arity") ->
- <<"P1706">>;
+ <<"P1706">>;
make_code(erl_parse, "bad function name") ->
- <<"P1707">>;
+ <<"P1707">>;
make_code(erl_parse, "bad Name/Arity") ->
- <<"P1708">>;
+ <<"P1708">>;
make_code(erl_parse, "bad record declaration") ->
- <<"P1709">>;
+ <<"P1709">>;
make_code(erl_parse, "bad record field") ->
- <<"P1710">>;
+ <<"P1710">>;
make_code(erl_parse, ["syntax error before: ", _]) ->
- <<"P1711">>;
+ <<"P1711">>;
%% Matching 'io_lib:format("bad ~tw declaration", [S])).', must come last
make_code(erl_parse, "bad " ++ _Str) ->
- <<"P1798">>;
+ <<"P1798">>;
make_code(erl_parse, _Other) ->
- <<"P1799">>;
-
+ <<"P1799">>;
make_code(Module, _Reason) ->
- unicode:characters_to_binary(io_lib:format("~p", [Module])).
+ unicode:characters_to_binary(io_lib:format("~p", [Module])).
%-----------------------------------------------------------------------
--spec range(els_dt_document:item() | undefined,
- erl_anno:anno() | none) -> poi_range().
+-spec range(
+ els_dt_document:item() | undefined,
+ erl_anno:anno() | none
+) -> els_poi:poi_range().
range(Document, Anno) ->
- els_diagnostics_utils:range(Document, Anno).
+ els_diagnostics_utils:range(Document, Anno).
%% @doc Find the inclusion range for a header file.
%%
%% Given the path of e .hrl path, find its inclusion range within
%% a given document.
--spec inclusion_range(string(), els_dt_document:item()) -> poi_range().
+-spec inclusion_range(string(), els_dt_document:item()) -> els_poi:poi_range().
inclusion_range(IncludePath, Document) ->
- case
- inclusion_range(IncludePath, Document, include) ++
- inclusion_range(IncludePath, Document, include_lib) ++
- inclusion_range(IncludePath, Document, behaviour) ++
- inclusion_range(IncludePath, Document, parse_transform)
- of
- [Range|_] -> Range;
- _ -> range(undefined, none)
- end.
-
--spec inclusion_range( string()
- , els_dt_document:item()
- , include | include_lib | behaviour | parse_transform)
- -> [poi_range()].
+ case
+ inclusion_range(IncludePath, Document, include) ++
+ inclusion_range(IncludePath, Document, include_lib) ++
+ inclusion_range(IncludePath, Document, behaviour) ++
+ inclusion_range(IncludePath, Document, parse_transform)
+ of
+ [Range | _] -> Range;
+ _ -> range(undefined, none)
+ end.
+
+-spec inclusion_range(
+ string(),
+ els_dt_document:item(),
+ include | include_lib | behaviour | parse_transform
+) ->
+ [els_poi:poi_range()].
inclusion_range(IncludePath, Document, include) ->
- POIs = els_dt_document:pois(Document, [include]),
- IncludeId = els_utils:include_id(IncludePath),
- [Range || #{id := Id, range := Range} <- POIs, Id =:= IncludeId];
+ POIs = els_dt_document:pois(Document, [include]),
+ IncludeId = els_utils:include_id(IncludePath),
+ [Range || #{id := Id, range := Range} <- POIs, Id =:= IncludeId];
inclusion_range(IncludePath, Document, include_lib) ->
- POIs = els_dt_document:pois(Document, [include_lib]),
- IncludeId = els_utils:include_lib_id(IncludePath),
- [Range || #{id := Id, range := Range} <- POIs, Id =:= IncludeId];
+ POIs = els_dt_document:pois(Document, [include_lib]),
+ IncludeId = els_utils:include_lib_id(absolute_path(IncludePath)),
+ [Range || #{id := Id, range := Range} <- POIs, Id =:= IncludeId];
inclusion_range(IncludePath, Document, behaviour) ->
- POIs = els_dt_document:pois(Document, [behaviour]),
- BehaviourId = els_uri:module(els_uri:uri(els_utils:to_binary(IncludePath))),
- [Range || #{id := Id, range := Range} <- POIs, Id =:= BehaviourId];
+ POIs = els_dt_document:pois(Document, [behaviour]),
+ BehaviourId = els_uri:module(els_uri:uri(els_utils:to_binary(absolute_path(IncludePath)))),
+ [Range || #{id := Id, range := Range} <- POIs, Id =:= BehaviourId];
inclusion_range(IncludePath, Document, parse_transform) ->
- POIs = els_dt_document:pois(Document, [parse_transform]),
- ParseTransformId
- = els_uri:module(els_uri:uri(els_utils:to_binary(IncludePath))),
- [Range || #{id := Id, range := Range} <- POIs, Id =:= ParseTransformId].
+ POIs = els_dt_document:pois(Document, [parse_transform]),
+ ParseTransformId =
+ els_uri:module(els_uri:uri(els_utils:to_binary(absolute_path(IncludePath)))),
+ [Range || #{id := Id, range := Range} <- POIs, Id =:= ParseTransformId].
+
+-spec absolute_path(string()) -> string().
+absolute_path(Path) ->
+ {ok, Cwd} = file:get_cwd(),
+ filename:join(Cwd, Path).
-spec macro_options() -> [macro_option()].
macro_options() ->
- Macros = els_config:get(macros),
- [macro_option(M) || M <- Macros].
+ Macros = els_config:get(macros),
+ [macro_option(M) || M <- Macros].
-spec macro_option(macro_config()) -> macro_option().
macro_option(#{"name" := Name, "value" := Value}) ->
- {'d', list_to_atom(Name), els_utils:macro_string_to_term(Value)};
+ {'d', list_to_atom(Name), els_utils:macro_string_to_term(Value)};
macro_option(#{"name" := Name}) ->
- {'d', list_to_atom(Name), true}.
+ {'d', list_to_atom(Name), true}.
-spec include_options() -> [include_option()].
include_options() ->
- Paths = els_config:get(include_paths),
- [ {i, Path} || Path <- Paths ].
+ Paths = els_config:get(include_paths),
+ [{i, Path} || Path <- Paths].
-spec diagnostics_options() -> [any()].
diagnostics_options() ->
- [basic_validation|diagnostics_options_bare()].
+ [basic_validation | diagnostics_options_bare()].
-spec diagnostics_options_load_code() -> [any()].
diagnostics_options_load_code() ->
- [binary|diagnostics_options_bare()].
+ [binary | diagnostics_options_bare()].
-spec diagnostics_options_bare() -> [any()].
diagnostics_options_bare() ->
- lists:append([ macro_options()
- , include_options()
- , [ return_warnings
- , return_errors
- ]]).
+ lists:append([
+ macro_options(),
+ include_options(),
+ [
+ return_warnings,
+ return_errors
+ ]
+ ]).
-spec compile_file(string(), [atom()]) ->
- {{ok | error, [compiler_msg()], [compiler_msg()]}
- , [els_diagnostics:diagnostic()]}.
+ {{ok | error, [compiler_msg()], [compiler_msg()]}, [els_diagnostics:diagnostic()]}.
compile_file(Path, Dependencies) ->
- %% Load dependencies required for the compilation
- Olds = [load_dependency(Dependency, Path)
- || Dependency <- Dependencies
- , not code:is_sticky(Dependency) ],
- Res = compile:file(Path, diagnostics_options()),
- %% Restore things after compilation
- [code:load_binary(Dependency, Filename, Binary)
- || {{Dependency, Binary, Filename}, _} <- Olds],
- Diagnostics = lists:flatten([ Diags || {_, Diags} <- Olds ]),
- {Res, Diagnostics}.
+ %% Load dependencies required for the compilation
+ Olds = [
+ load_dependency(Dependency, Path)
+ || Dependency <- Dependencies,
+ not code:is_sticky(Dependency)
+ ],
+ Res = compile:file(Path, diagnostics_options()),
+ %% Restore things after compilation
+ [
+ code:load_binary(Dependency, FileName, Binary)
+ || {{Dependency, Binary, FileName}, _} <- Olds
+ ],
+ Diagnostics = lists:flatten([Diags || {_, Diags} <- Olds]),
+ {Res, Diagnostics ++ module_name_check(Path)}.
+
+%% The module_name error is only emitted by the Erlang compiler during
+%% the "save binary" phase. This phase does not occur when code
+%% generation is disabled (e.g. by using the basic_validation or
+%% strong_validation arguments when invoking the compile:file/2
+%% function), which is exactly the case for the Erlang LS compiler
+%% diagnostics. Therefore, let's replicate the check explicitly.
+%% See issue #1152.
+-spec module_name_check(string()) -> [els_diagnostics:diagnostic()].
+module_name_check(Path) ->
+ Basename = filename:basename(Path, ".erl"),
+ Uri = els_uri:uri(els_utils:to_binary(Path)),
+ case els_dt_document:lookup(Uri) of
+ {ok, [Document]} ->
+ case els_dt_document:pois(Document, [module]) of
+ [#{id := Module, range := Range}] ->
+ case atom_to_list(Module) =:= Basename of
+ true ->
+ [];
+ false ->
+ Message =
+ io_lib:format(
+ "Module name '~s' does not match file name '~ts'",
+ [Module, Basename]
+ ),
+ Diagnostic = els_diagnostics:make_diagnostic(
+ els_protocol:range(Range),
+ els_utils:to_binary(Message),
+ ?DIAGNOSTIC_ERROR,
+ <<"Compiler (via Erlang LS)">>
+ ),
+ [Diagnostic]
+ end;
+ _ ->
+ []
+ end;
+ _ ->
+ []
+ end.
%% @doc Load a dependency, return the old version of the code (if any),
%% so it can be restored.
-spec load_dependency(atom(), string()) ->
- {{atom(), binary(), file:filename()}, [els_diagnostics:diagnostic()]}
- | error.
+ {{atom(), binary(), file:filename()} | error, [els_diagnostics:diagnostic()]}.
load_dependency(Module, IncludingPath) ->
- Old = code:get_object_code(Module),
- Diagnostics =
- case els_utils:find_module(Module) of
- {ok, Uri} ->
- Path = els_utils:to_list(els_uri:path(Uri)),
- Opts = compile_options(Module),
- case compile:file(Path, diagnostics_options_load_code() ++ Opts) of
- {ok, [], []} ->
- [];
- {ok, Module, Binary} ->
- code:load_binary(Module, atom_to_list(Module), Binary),
- [];
- {ok, Module, Binary, WS} ->
- code:load_binary(Module, atom_to_list(Module), Binary),
- diagnostics(IncludingPath, WS, ?DIAGNOSTIC_WARNING);
- {error, ES, WS} ->
- diagnostics(IncludingPath, WS, ?DIAGNOSTIC_WARNING) ++
- diagnostics(IncludingPath, ES, ?DIAGNOSTIC_ERROR)
- end;
- {error, Error} ->
- ?LOG_WARNING( "Error finding dependency [module=~p] [error=~p]"
- , [Module, Error]),
- []
- end,
- {Old, Diagnostics}.
-
--spec maybe_compile_and_load(uri(), [els_diagnostics:diagnostic()]) -> ok.
-maybe_compile_and_load(Uri, [] = _CDiagnostics) ->
- case els_config:get(code_reload) of
- #{"node" := NodeStr} ->
- Node = els_utils:compose_node_name(NodeStr,
- els_config_runtime:get_name_type()),
- Module = els_uri:module(Uri),
- case rpc:call(Node, code, is_sticky, [Module]) of
- true -> ok;
- _ -> handle_rpc_result(rpc:call(Node, c, c, [Module]), Module)
- end;
- disabled ->
- ok
- end;
-maybe_compile_and_load(_Uri, _CDiagnostics) ->
- ok.
-
--spec handle_rpc_result(term() | {badrpc, term()}, atom()) -> ok.
-handle_rpc_result({ok, Module}, _) ->
- Msg = io_lib:format("code_reload success for: ~s", [Module]),
- els_server:send_notification(<<"window/showMessage">>,
- #{ type => ?MESSAGE_TYPE_INFO,
- message => els_utils:to_binary(Msg)
- });
-handle_rpc_result(Err, Module) ->
- ?LOG_INFO("[code_reload] code_reload using c:c/1 crashed with: ~p",
- [Err]),
- Msg = io_lib:format("code_reload swap crashed for: ~s with: ~w",
- [Module, Err]),
- els_server:send_notification(<<"window/showMessage">>,
- #{ type => ?MESSAGE_TYPE_ERROR,
- message => els_utils:to_binary(Msg)
- }).
+ Old = code:get_object_code(Module),
+ Diagnostics =
+ case els_utils:find_module(Module) of
+ {ok, Uri} ->
+ Path = els_utils:to_list(els_uri:path(Uri)),
+ Opts = compile_options(Module),
+ case compile:file(Path, diagnostics_options_load_code() ++ Opts) of
+ {ok, [], []} ->
+ [];
+ {ok, Module, Binary} ->
+ code:load_binary(Module, atom_to_list(Module), Binary),
+ [];
+ {ok, Module, Binary, WS} ->
+ code:load_binary(Module, atom_to_list(Module), Binary),
+ diagnostics(IncludingPath, WS, ?DIAGNOSTIC_WARNING);
+ {error, ES, WS} ->
+ diagnostics(IncludingPath, WS, ?DIAGNOSTIC_WARNING) ++
+ diagnostics(IncludingPath, ES, ?DIAGNOSTIC_ERROR)
+ end;
+ {error, Error} ->
+ ?LOG_WARNING(
+ "Error finding dependency [module=~p] [error=~p]",
+ [Module, Error]
+ ),
+ []
+ end,
+ {Old, Diagnostics}.
%% @doc Return the compile options from the compile_info chunk
-spec compile_options(atom()) -> [any()].
compile_options(Module) ->
- case code:which(Module) of
- non_existing ->
- ?LOG_DEBUG("Could not find compile options. [module=~p]", [Module]),
- [];
- Beam ->
- case beam_lib:chunks(Beam, [compile_info]) of
- {ok, {_, Chunks}} ->
- Info = proplists:get_value(compile_info, Chunks),
- proplists:get_value(options, Info, []);
- Error ->
- ?LOG_DEBUG( "Error extracting compile_info. [module=~p] [error=~p]"
- , [Module, Error]),
- []
- end
- end.
+ case code:which(Module) of
+ non_existing ->
+ ?LOG_DEBUG("Could not find compile options. [module=~p]", [Module]),
+ [];
+ Beam ->
+ case beam_lib:chunks(Beam, [compile_info]) of
+ {ok, {_, Chunks}} ->
+ Info = proplists:get_value(compile_info, Chunks),
+ proplists:get_value(options, Info, []);
+ Error ->
+ ?LOG_DEBUG(
+ "Error extracting compile_info. [module=~p] [error=~p]",
+ [Module, Error]
+ ),
+ []
+ end
+ end.
%% @doc Send a telemetry/event LSP message, for logging in the client
-spec telemetry(uri(), [els_diagnostics:diagnostic()]) -> ok.
telemetry(Uri, Diagnostics) ->
- case els_config:get(compiler_telemetry_enabled) of
- true ->
- Codes = [Code || #{ code := Code } <- Diagnostics ],
- Method = <<"telemetry/event">>,
- Params = #{ uri => Uri
- , diagnostics => Codes
- , type => <<"erlang-diagnostic-codes">>
- },
- els_server:send_notification(Method, Params);
- _ ->
- ok
- end.
+ case els_config:get(compiler_telemetry_enabled) of
+ true ->
+ Codes = [Code || #{code := Code} <- Diagnostics],
+ Method = <<"telemetry/event">>,
+ Params = #{
+ uri => Uri,
+ diagnostics => Codes,
+ type => <<"erlang-diagnostic-codes">>
+ },
+ els_server:send_notification(Method, Params);
+ _ ->
+ ok
+ end.
diff --git a/apps/els_lsp/src/els_completion_provider.erl b/apps/els_lsp/src/els_completion_provider.erl
index 30c40ae12..bce853e09 100644
--- a/apps/els_lsp/src/els_completion_provider.erl
+++ b/apps/els_lsp/src/els_completion_provider.erl
@@ -5,408 +5,902 @@
-include("els_lsp.hrl").
-include_lib("kernel/include/logger.hrl").
--export([ handle_request/2
- , is_enabled/0
- , trigger_characters/0
- ]).
+-export([
+ handle_request/1,
+ trigger_characters/0,
+ bif_pois/1
+]).
%% Exported to ease testing.
--export([ bifs/2
- , keywords/0
- ]).
-
--type options() :: #{ trigger := binary()
- , document := els_dt_document:item()
- , line := line()
- , column := column()
- }.
+-export([
+ bifs/2,
+ keywords/2
+]).
+
+-type options() :: #{
+ trigger := binary(),
+ document := els_dt_document:item(),
+ line := line(),
+ column := column()
+}.
-type items() :: [item()].
-type item() :: completion_item().
--type state() :: any().
+
+-type item_format() :: arity_only | args | no_args.
+-type tokens() :: [any()].
+-type poi_kind_or_any() :: els_poi:poi_kind() | any.
%%==============================================================================
%% els_provider functions
%%==============================================================================
-
--spec is_enabled() -> boolean().
-is_enabled() ->
- true.
-
-spec trigger_characters() -> [binary()].
trigger_characters() ->
- [<<":">>, <<"#">>, <<"?">>, <<".">>, <<"-">>, <<"\"">>].
-
--spec handle_request(els_provider:request(), state()) -> {any(), state()}.
-handle_request({completion, Params}, State) ->
- #{ <<"position">> := #{ <<"line">> := Line
- , <<"character">> := Character
- }
- , <<"textDocument">> := #{<<"uri">> := Uri}
- } = Params,
- ok = els_index_buffer:flush(Uri),
- {ok, #{text := Text} = Document} = els_utils:lookup_document(Uri),
- Context = maps:get( <<"context">>
- , Params
- , #{ <<"triggerKind">> => ?COMPLETION_TRIGGER_KIND_INVOKED }
- ),
- TriggerKind = maps:get(<<"triggerKind">>, Context),
- TriggerCharacter = maps:get(<<"triggerCharacter">>, Context, <<>>),
- %% We subtract 1 to strip the character that triggered the
- %% completion from the string.
- Length = case Character > 0 of true -> 1; false -> 0 end,
- Prefix = case TriggerKind of
- ?COMPLETION_TRIGGER_KIND_CHARACTER ->
- els_text:line(Text, Line, Character - Length);
- ?COMPLETION_TRIGGER_KIND_INVOKED ->
- els_text:line(Text, Line, Character);
- ?COMPLETION_TRIGGER_KIND_FOR_INCOMPLETE_COMPLETIONS ->
- els_text:line(Text, Line, Character)
- end,
- Opts = #{ trigger => TriggerCharacter
- , document => Document
- , line => Line + 1
- , column => Character
- },
- Completions = find_completions(Prefix, TriggerKind, Opts),
- {Completions, State};
-handle_request({resolve, CompletionItem}, State) ->
- {resolve(CompletionItem), State}.
+ [
+ <<":">>,
+ <<"#">>,
+ <<"?">>,
+ <<".">>,
+ <<"-">>,
+ <<"\"">>,
+ <<"{">>,
+ <<"/">>,
+ <<" ">>
+ ].
+
+-spec handle_request(els_provider:provider_request()) ->
+ {response, any()} | {async, uri(), pid()}.
+handle_request({completion, Params}) ->
+ #{
+ <<"position">> := #{
+ <<"line">> := Line,
+ <<"character">> := Character
+ },
+ <<"textDocument">> := #{<<"uri">> := Uri}
+ } = Params,
+ Context = maps:get(
+ <<"context">>,
+ Params,
+ #{<<"triggerKind">> => ?COMPLETION_TRIGGER_KIND_INVOKED}
+ ),
+ TriggerKind = maps:get(<<"triggerKind">>, Context),
+ TriggerCharacter = maps:get(<<"triggerCharacter">>, Context, <<>>),
+ Job = run_completion_job(Uri, Line, Character, TriggerKind, TriggerCharacter),
+ {async, Uri, Job};
+handle_request({resolve, CompletionItem}) ->
+ {response, resolve(CompletionItem)}.
%%==============================================================================
%% Internal functions
%%==============================================================================
+-spec run_completion_job(
+ uri(),
+ line(),
+ column(),
+ completion_trigger_kind(),
+ binary()
+) -> pid().
+run_completion_job(Uri, Line, Character, TriggerKind, TriggerCharacter) ->
+ {ok, #{text := Text} = Document} = els_utils:lookup_document(Uri),
+ %% We subtract 1 to strip the character that triggered the
+ %% completion from the string.
+ Length =
+ case Character > 0 of
+ true -> 1;
+ false -> 0
+ end,
+ Prefix =
+ case TriggerKind of
+ ?COMPLETION_TRIGGER_KIND_CHARACTER ->
+ els_text:line(Text, Line, Character - Length);
+ ?COMPLETION_TRIGGER_KIND_INVOKED ->
+ els_text:line(Text, Line, Character);
+ ?COMPLETION_TRIGGER_KIND_FOR_INCOMPLETE_COMPLETIONS ->
+ els_text:line(Text, Line, Character)
+ end,
+ ?LOG_INFO("Find completions for ~s", [Prefix]),
+ Opts = #{
+ trigger => TriggerCharacter,
+ document => Document,
+ line => Line + 1,
+ column => Character
+ },
+ Config = #{
+ task => fun find_completions/2,
+ entries => [{Prefix, TriggerKind, Opts}],
+ title => <<"Completion">>,
+ on_complete => fun els_server:register_result/1
+ },
+ {ok, Pid} = els_background_job:new(Config),
+ Pid.
+
+-spec find_completions({binary(), completion_trigger_kind(), options()}, any()) -> items().
+find_completions({Prefix, TriggerKind, Opts}, _) ->
+ Result = find_completions(Prefix, TriggerKind, Opts),
+ ?LOG_INFO("Found completions: ~p", [length(Result)]),
+ Result.
+
-spec resolve(map()) -> map().
-resolve(#{ <<"kind">> := ?COMPLETION_ITEM_KIND_FUNCTION
- , <<"data">> := #{ <<"module">> := Module
- , <<"function">> := Function
- , <<"arity">> := Arity
- }
- } = CompletionItem) ->
- Entries = els_docs:function_docs ( 'remote'
- , binary_to_atom(Module, utf8)
- , binary_to_atom(Function, utf8)
- , Arity),
- CompletionItem#{documentation => els_markup_content:new(Entries)};
-resolve(#{ <<"kind">> := ?COMPLETION_ITEM_KIND_TYPE_PARAM
- , <<"data">> := #{ <<"module">> := Module
- , <<"type">> := Type
- , <<"arity">> := Arity
- }
- } = CompletionItem) ->
- Entries = els_docs:type_docs('remote'
- , binary_to_atom(Module, utf8)
- , binary_to_atom(Type, utf8)
- , Arity),
- CompletionItem#{ documentation => els_markup_content:new(Entries) };
+resolve(
+ #{
+ <<"kind">> := ?COMPLETION_ITEM_KIND_FUNCTION,
+ <<"data">> := #{
+ <<"module">> := Module,
+ <<"function">> := Function,
+ <<"arity">> := Arity
+ }
+ } = CompletionItem
+) ->
+ Entries = els_docs:function_docs(
+ 'remote',
+ binary_to_atom(Module, utf8),
+ binary_to_atom(Function, utf8),
+ Arity
+ ),
+ CompletionItem#{documentation => els_markup_content:new(Entries)};
+resolve(
+ #{
+ <<"kind">> := ?COMPLETION_ITEM_KIND_TYPE_PARAM,
+ <<"data">> := #{
+ <<"module">> := Module,
+ <<"type">> := Type,
+ <<"arity">> := Arity
+ }
+ } = CompletionItem
+) ->
+ Entries = els_docs:type_docs(
+ 'remote',
+ binary_to_atom(Module, utf8),
+ binary_to_atom(Type, utf8),
+ Arity
+ ),
+ CompletionItem#{documentation => els_markup_content:new(Entries)};
resolve(CompletionItem) ->
- CompletionItem.
-
--spec find_completions(binary(), integer(), options()) -> items().
-find_completions( Prefix
- , ?COMPLETION_TRIGGER_KIND_CHARACTER
- , #{ trigger := <<":">>
- , document := Document
- , line := Line
- , column := Column
- }
- ) ->
- case lists:reverse(els_text:tokens(Prefix)) of
- [{atom, _, Module}, {'fun', _}| _] ->
- exported_definitions(Module, 'function', true);
- [{atom, _, Module}|_] ->
- {ExportFormat, TypeOrFun} = completion_context(Document, Line, Column),
- exported_definitions(Module, TypeOrFun, ExportFormat);
- _ ->
- []
- end;
-find_completions( _Prefix
- , ?COMPLETION_TRIGGER_KIND_CHARACTER
- , #{trigger := <<"?">>, document := Document}
- ) ->
- definitions(Document, define);
-find_completions( _Prefix
- , ?COMPLETION_TRIGGER_KIND_CHARACTER
- , #{trigger := <<"-">>, document := _Document, column := 1}
- ) ->
- attributes();
-find_completions( _Prefix
- , ?COMPLETION_TRIGGER_KIND_CHARACTER
- , #{trigger := <<"#">>, document := Document}
- ) ->
- definitions(Document, record);
-find_completions( <<"-include_lib(">>
- , ?COMPLETION_TRIGGER_KIND_CHARACTER
- , #{trigger := <<"\"">>}
- ) ->
- [item_kind_file(Path) || Path <- paths_include_lib()];
-find_completions( <<"-include(">>
- , ?COMPLETION_TRIGGER_KIND_CHARACTER
- , #{trigger := <<"\"">>, document := Document}
- ) ->
- [item_kind_file(Path) || Path <- paths_include(Document)];
-find_completions( Prefix
- , ?COMPLETION_TRIGGER_KIND_CHARACTER
- , #{trigger := <<".">>, document := Document}
- ) ->
- case lists:reverse(els_text:tokens(Prefix)) of
- [{atom, _, RecordName}, {'#', _} | _] ->
- record_fields(Document, RecordName);
- _ ->
- []
+ CompletionItem.
+
+-spec find_completions(binary(), completion_trigger_kind(), options()) -> items().
+find_completions(
+ Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{
+ trigger := <<":">>,
+ document := Document,
+ line := Line,
+ column := Column
+ }
+) ->
+ case lists:reverse(els_text:tokens(Prefix)) of
+ [{atom, _, Module}, {'fun', _} | _] ->
+ exported_definitions(Module, 'function', arity_only);
+ [{atom, _, Module} | _] = Tokens ->
+ {ItemFormat, TypeOrFun} =
+ completion_context(Document, Line, Column, Tokens),
+ exported_definitions(Module, TypeOrFun, ItemFormat);
+ [{var, _, 'MODULE'}, {'?', _}, {'fun', _} | _] ->
+ Module = els_uri:module(els_dt_document:uri(Document)),
+ exported_definitions(Module, 'function', arity_only);
+ [{var, _, 'MODULE'}, {'?', _} | _] = Tokens ->
+ Module = els_uri:module(els_dt_document:uri(Document)),
+ {ItemFormat, TypeOrFun} =
+ completion_context(Document, Line, Column, Tokens),
+ exported_definitions(Module, TypeOrFun, ItemFormat);
+ _ ->
+ []
end;
-find_completions( Prefix
- , ?COMPLETION_TRIGGER_KIND_INVOKED
- , #{ document := Document
- , line := Line
- , column := Column
- }
- ) ->
- case lists:reverse(els_text:tokens(Prefix)) of
- %% Check for "[...] fun atom:"
- [{':', _}, {atom, _, Module}, {'fun', _} | _] ->
- exported_definitions(Module, function, _ExportFormat = true);
- %% Check for "[...] fun atom:atom"
- [{atom, _, _}, {':', _}, {atom, _, Module}, {'fun', _} | _] ->
- exported_definitions(Module, function, _ExportFormat = true);
- %% Check for "[...] atom:"
- [{':', _}, {atom, _, Module} | _] ->
- {ExportFormat, TypeOrFun} = completion_context(Document, Line, Column),
- exported_definitions(Module, TypeOrFun, ExportFormat);
- %% Check for "[...] atom:atom"
- [{atom, _, _}, {':', _}, {atom, _, Module} | _] ->
- {ExportFormat, TypeOrFun} = completion_context(Document, Line, Column),
- exported_definitions(Module, TypeOrFun, ExportFormat);
- %% Check for "[...] ?"
- [{'?', _} | _] ->
- definitions(Document, define);
- %% Check for "[...] ?anything"
- [_, {'?', _} | _] ->
- definitions(Document, define);
- %% Check for "[...] #anything."
- [{'.', _}, {atom, _, RecordName}, {'#', _} | _] ->
- record_fields(Document, RecordName);
- %% Check for "[...] #anything.something"
- [_, {'.', _}, {atom, _, RecordName}, {'#', _} | _] ->
- record_fields(Document, RecordName);
- %% Check for "[...] #"
- [{'#', _} | _] ->
- definitions(Document, record);
- %% Check for "[...] #anything"
- [_, {'#', _} | _] ->
- definitions(Document, record);
- %% Check for "[...] Variable"
- [{var, _, _} | _] ->
- variables(Document);
- %% Check for "-anything"
- [{atom, _, _}, {'-', _}] ->
- attributes();
- %% Check for "-export(["
- [{'[', _}, {'(', _}, {atom, _, export}, {'-', _}] ->
- unexported_definitions(Document, function);
- %% Check for "-export_type(["
- [{'[', _}, {'(', _}, {atom, _, export_type}, {'-', _}] ->
- unexported_definitions(Document, type_definition);
- %% Check for "-behaviour(anything"
- [{atom, _, _}, {'(', _}, {atom, _, Attribute}, {'-', _}]
- when Attribute =:= behaviour; Attribute =:= behavior ->
- [item_kind_module(Module) || Module <- behaviour_modules()];
- %% Check for "-behaviour("
- [{'(', _}, {atom, _, Attribute}, {'-', _}]
- when Attribute =:= behaviour; Attribute =:= behavior ->
- [item_kind_module(Module) || Module <- behaviour_modules()];
- %% Check for "[...] fun atom"
- [{atom, _, _}, {'fun', _} | _] ->
- bifs(function, ExportFormat = true)
- ++ definitions(Document, function, ExportFormat = true);
- %% Check for "[...] atom"
- [{atom, _, Name} | _] ->
- NameBinary = atom_to_binary(Name, utf8),
- {ExportFormat, POIKind} = completion_context(Document, Line, Column),
- case ExportFormat of
+find_completions(
+ _Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"?">>, document := Document}
+) ->
+ bifs(define, _ItemFormat = args) ++ definitions(Document, define);
+find_completions(
+ _Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"-">>, document := Document, column := 1, line := Line}
+) ->
+ attributes(Document, Line);
+find_completions(
+ Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"/">>}
+) ->
+ Tokens = lists:reverse(els_text:tokens(Prefix)),
+ case in_binary_heuristic(Tokens) of
true ->
- %% Only complete unexported definitions when in export
- unexported_definitions(Document, POIKind);
+ binary_type_specifier();
false ->
- keywords()
- ++ bifs(POIKind, ExportFormat)
- ++ atoms(Document, NameBinary)
- ++ all_record_fields(Document, NameBinary)
- ++ modules(NameBinary)
- ++ definitions(Document, POIKind, ExportFormat)
- ++ els_snippets_server:snippets()
- end;
- Tokens ->
- ?LOG_DEBUG("No completion found. [prefix=~p] [tokens=~p]",
- [Prefix, Tokens]),
- []
- end;
+ []
+ end;
+find_completions(
+ Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"-">>}
+) ->
+ binary_type_specifiers(Prefix);
+find_completions(
+ _Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"#">>, document := Document}
+) ->
+ definitions(Document, record);
+find_completions(
+ Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"{">>, document := Document}
+) ->
+ case lists:reverse(els_text:tokens(string:trim(Prefix))) of
+ [{atom, _, Name} | _] ->
+ record_fields_with_var(Document, Name);
+ _ ->
+ []
+ end;
+find_completions(
+ Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<" ">>} = Opts
+) ->
+ case lists:reverse(els_text:tokens(string:trim(Prefix))) of
+ [{',', _} | _] = Tokens ->
+ complete_record_field(Opts, Tokens);
+ _ ->
+ []
+ end;
+find_completions(
+ <<"-include_lib(">>,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"\"">>}
+) ->
+ [item_kind_file(Path) || Path <- els_include_paths:include_libs()];
+find_completions(
+ <<"-include(">>,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<"\"">>, document := Document}
+) ->
+ [item_kind_file(Path) || Path <- els_include_paths:includes(Document)];
+find_completions(
+ Prefix,
+ ?COMPLETION_TRIGGER_KIND_CHARACTER,
+ #{trigger := <<".">>, document := Document}
+) ->
+ case lists:reverse(els_text:tokens(Prefix)) of
+ [{atom, _, RecordName}, {'#', _} | _] ->
+ record_fields(Document, RecordName);
+ _ ->
+ []
+ end;
+find_completions(
+ Prefix,
+ TriggerKind,
+ #{
+ document := Document,
+ line := Line,
+ column := Column
+ } = Opts
+) when
+ TriggerKind =:= ?COMPLETION_TRIGGER_KIND_INVOKED;
+ TriggerKind =:= ?COMPLETION_TRIGGER_KIND_FOR_INCOMPLETE_COMPLETIONS
+->
+ case lists:reverse(els_text:tokens(Prefix)) of
+ %% Check for "[...] fun atom:"
+ [{':', _}, {atom, _, Module}, {'fun', _} | _] ->
+ exported_definitions(Module, function, arity_only);
+ %% Check for "[...] fun atom:atom"
+ [{atom, _, _}, {':', _}, {atom, _, Module}, {'fun', _} | _] ->
+ exported_definitions(Module, function, arity_only);
+ %% Check for "[...] atom:"
+ [{':', _}, {atom, _, Module} | _] = Tokens ->
+ {ItemFormat, POIKind} =
+ completion_context(Document, Line, Column, Tokens),
+ exported_definitions(Module, POIKind, ItemFormat);
+ %% Check for "[...] atom:atom"
+ [{atom, _, _}, {':', _}, {atom, _, Module} | _] = Tokens ->
+ {ItemFormat, POIKind} =
+ completion_context(Document, Line, Column, Tokens),
+ exported_definitions(Module, POIKind, ItemFormat);
+ %% Check for "[...] ?"
+ [{'?', _} | _] ->
+ bifs(define, _ItemFormat = args) ++ definitions(Document, define);
+ %% Check for "[...] ?anything"
+ [_, {'?', _} | _] ->
+ bifs(define, _ItemFormat = args) ++ definitions(Document, define);
+ %% Check for "[...] #anything."
+ [{'.', _}, {atom, _, RecordName}, {'#', _} | _] ->
+ record_fields(Document, RecordName);
+ %% Check for "[...] #anything.something"
+ [_, {'.', _}, {atom, _, RecordName}, {'#', _} | _] ->
+ record_fields(Document, RecordName);
+ %% Check for "[...] #"
+ [{'#', _} | _] ->
+ definitions(Document, record);
+ %% Check for "#{"
+ [{'{', _}, {'#', _} | _] ->
+ [map_comprehension_completion_item(Document, Line, Column)];
+ %% Check for "[...] #anything"
+ [_, {'#', _} | _] ->
+ definitions(Document, record);
+ %% Check for "[...] #anything{"
+ [{'{', _}, {atom, _, RecordName}, {'#', _} | _] ->
+ record_fields_with_var(Document, RecordName);
+ %% Check for "[...] Variable"
+ [{var, _, _} | _] ->
+ variables(Document);
+ %% Check for "-anything"
+ [{atom, _, _}, {'-', _}] ->
+ attributes(Document, Line);
+ %% Check for "[...] -"
+ [{'-', _} | _] ->
+ binary_type_specifiers(Prefix);
+ %% Check for "[...] -"
+ [{'/', _} | _] ->
+ Tokens = lists:reverse(els_text:tokens(Prefix)),
+ case in_binary_heuristic(Tokens) of
+ true ->
+ binary_type_specifier();
+ false ->
+ []
+ end;
+ %% Check for "-export(["
+ [{'[', _}, {'(', _}, {atom, _, export}, {'-', _}] ->
+ unexported_definitions(Document, function);
+ %% Check for "-nifs(["
+ [{'[', _}, {'(', _}, {atom, _, nifs}, {'-', _}] ->
+ definitions(Document, function, arity_only, false);
+ %% Check for "-export_type(["
+ [{'[', _}, {'(', _}, {atom, _, export_type}, {'-', _}] ->
+ unexported_definitions(Document, type_definition);
+ %% Check for "-feature("
+ [{'(', _}, {atom, _, feature}, {'-', _}] ->
+ features();
+ %% Check for "?FEATURE_ENABLED("
+ [{'(', _}, {var, _, 'FEATURE_ENABLED'}, {'?', _} | _] ->
+ features();
+ %% Check for "?FEATURE_AVAILABLE("
+ [{'(', _}, {var, _, 'FEATURE_AVAILABLE'}, {'?', _} | _] ->
+ features();
+ %% Check for "-behaviour(anything"
+ [{atom, _, Begin}, {'(', _}, {atom, _, Attribute}, {'-', _}] when
+ Attribute =:= behaviour; Attribute =:= behavior
+ ->
+ [
+ item_kind_module(Module)
+ || Module <- behaviour_modules(atom_to_list(Begin))
+ ];
+ %% Check for "-behaviour("
+ [{'(', _}, {atom, _, Attribute}, {'-', _}] when
+ Attribute =:= behaviour; Attribute =:= behavior
+ ->
+ [item_kind_module(Module) || Module <- behaviour_modules("")];
+ %% Check for "["
+ [{'[', _} | _] ->
+ [list_comprehension_completion_item(Document, Line, Column)];
+ %% Check for "[...] fun atom"
+ [{atom, _, _}, {'fun', _} | _] ->
+ bifs(function, ItemFormat = arity_only) ++
+ definitions(Document, function, ItemFormat = arity_only);
+ %% Check for "| atom"
+ [{atom, _, Name}, {'|', _} | _] = Tokens ->
+ {ItemFormat, _POIKind} =
+ completion_context(Document, Line, Column, Tokens),
+ complete_type_definition(Document, Name, ItemFormat);
+ %% Check for "::"
+ [{'::', _} | _] = Tokens ->
+ {ItemFormat, _POIKind} =
+ completion_context(Document, Line, Column, Tokens),
+ complete_type_definition(Document, '', ItemFormat);
+ %% Check for ":: atom"
+ [{atom, _, Name}, {'::', _} | _] = Tokens ->
+ {ItemFormat, _POIKind} =
+ completion_context(Document, Line, Column, Tokens),
+ complete_type_definition(Document, Name, ItemFormat);
+ %% Check for "[...] atom"
+ [{atom, _, Name} | _] = Tokens ->
+ complete_atom(Name, Tokens, Opts);
+ %% Treat keywords as atom completion
+ [{Name, _} | _] = Tokens ->
+ case lists:member(Name, keywords()) of
+ true ->
+ complete_atom(Name, Tokens, Opts);
+ false ->
+ []
+ end;
+ Tokens ->
+ ?LOG_DEBUG(
+ "No completion found. [prefix=~p] [tokens=~p]",
+ [Prefix, Tokens]
+ ),
+ []
+ end;
find_completions(_Prefix, _TriggerKind, _Opts) ->
- [].
+ [].
+
+-spec list_comprehension_completion_item(els_dt_document:item(), line(), column()) ->
+ completion_item().
+list_comprehension_completion_item(#{text := Text}, Line, Column) ->
+ Suffix =
+ try els_text:get_char(Text, Line, Column + 1) of
+ {ok, $]} ->
+ %% Don't include ']' if next character is a ']'
+ %% I.e if cursor is at []
+ %% ^
+ <<"">>;
+ _ ->
+ <<"]">>
+ catch
+ _:_:_ ->
+ <<"]">>
+ end,
+ InsertText =
+ case snippet_support() of
+ true ->
+ <<"${3:Expr} || ${2:Elem} <- ${1:List}", Suffix/binary>>;
+ false ->
+ <<"Expr || Elem <- List", Suffix/binary>>
+ end,
+ #{
+ label => <<"[Expr || Elem <- List]">>,
+ kind => ?COMPLETION_ITEM_KIND_KEYWORD,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET,
+ insertText => InsertText
+ }.
+
+-spec map_comprehension_completion_item(els_dt_document:item(), line(), column()) ->
+ completion_item().
+map_comprehension_completion_item(#{text := Text}, Line, Column) ->
+ Suffix =
+ try els_text:get_char(Text, Line, Column + 1) of
+ {ok, $}} ->
+ %% Don't include '}' if next character is a '}'
+ %% I.e if cursor is at #{}
+ %% ^
+ <<"">>;
+ _ ->
+ <<"}">>
+ catch
+ _:_:_ ->
+ <<"}">>
+ end,
+ InsertText =
+ case snippet_support() of
+ true ->
+ <<"${4:K} => ${5:V} || ${2:K} => ${3:V} <- ${1:Map}", Suffix/binary>>;
+ false ->
+ <<"K => V || K := V <- Map", Suffix/binary>>
+ end,
+ #{
+ label => <<"#{K => V || K := V <- Map}">>,
+ kind => ?COMPLETION_ITEM_KIND_KEYWORD,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET,
+ insertText => InsertText
+ }.
+
+-spec complete_atom(atom(), [any()], map()) -> [completion_item()].
+complete_atom(Name, Tokens, Opts) ->
+ #{document := Document, line := Line, column := Column} = Opts,
+ NameBinary = atom_to_binary(Name, utf8),
+ {ItemFormat, POIKind} = completion_context(Document, Line, Column, Tokens),
+ case ItemFormat of
+ arity_only ->
+ #{text := Text} = Document,
+ case
+ is_in(Document, Line, Column, [nifs]) orelse
+ is_in_heuristic(Text, <<"nifs">>, Line - 1)
+ of
+ true ->
+ definitions(Document, POIKind, ItemFormat, false);
+ _ ->
+ %% Only complete unexported definitions when in
+ %% export
+ unexported_definitions(Document, POIKind)
+ end;
+ _ ->
+ case complete_record_field(Opts, Tokens) of
+ [] ->
+ keywords(POIKind, ItemFormat) ++
+ bifs(POIKind, ItemFormat) ++
+ atoms(Document, NameBinary) ++
+ all_record_fields(Document, NameBinary) ++
+ modules(NameBinary) ++
+ definitions(Document, POIKind, ItemFormat) ++
+ snippets(POIKind, ItemFormat);
+ RecordFields ->
+ RecordFields
+ end
+ end.
+
+-spec binary_type_specifiers(binary()) -> [completion_item()].
+binary_type_specifiers(Prefix) ->
+ %% in_binary_heuristic will only consider current line
+ %% TODO: make it work for multi-line binaries too.
+ Tokens = lists:reverse(els_text:tokens(Prefix)),
+ case
+ in_binary_heuristic(Tokens) andalso
+ in_binary_type_specifier(Tokens, [])
+ of
+ {true, TypeListTokens} ->
+ HasType = lists:any(
+ fun(T) ->
+ lists:member(T, binary_types())
+ end,
+ TypeListTokens
+ ),
+ HasEndianess = lists:any(
+ fun(T) ->
+ lists:member(T, binary_endianness())
+ end,
+ TypeListTokens
+ ),
+ HasSignedness = lists:any(
+ fun(T) ->
+ lists:member(T, binary_signedness())
+ end,
+ TypeListTokens
+ ),
+ HasUnit = lists:member(unit, TypeListTokens),
+ [binary_type_specifier(unit) || not HasUnit] ++
+ [binary_type_specifier(Label) || Label <- binary_types(), not HasType] ++
+ [binary_type_specifier(Label) || Label <- binary_endianness(), not HasEndianess] ++
+ [binary_type_specifier(Label) || Label <- binary_signedness(), not HasSignedness];
+ false ->
+ []
+ end.
+
+-spec in_binary_heuristic([any()]) -> boolean().
+in_binary_heuristic([{'>>', _} | _]) ->
+ false;
+in_binary_heuristic([{'<<', _} | _]) ->
+ true;
+in_binary_heuristic([_ | T]) ->
+ in_binary_heuristic(T);
+in_binary_heuristic([]) ->
+ false.
+
+-spec in_binary_type_specifier([any()], [atom()]) -> {true, [atom()]} | false.
+in_binary_type_specifier([{integer, _, _}, {':', _}, {atom, _, unit} | T], Spec) ->
+ in_binary_type_specifier(T, [unit | Spec]);
+in_binary_type_specifier([{atom, _, Atom} | T], Spec) ->
+ case lists:member(Atom, binary_type_specifiers()) of
+ true ->
+ in_binary_type_specifier(T, [Atom | Spec]);
+ false ->
+ false
+ end;
+in_binary_type_specifier([{'-', _} | T], Spec) ->
+ in_binary_type_specifier(T, Spec);
+in_binary_type_specifier([{'/', _} | _], Spec) ->
+ {true, Spec};
+in_binary_type_specifier([], _Spec) ->
+ false.
+
+-spec binary_type_specifiers() -> [atom()].
+binary_type_specifiers() ->
+ binary_types() ++ binary_signedness() ++ binary_endianness() ++ [unit].
+
+-spec binary_signedness() -> [atom()].
+binary_signedness() ->
+ [signed, unsigned].
+
+-spec binary_types() -> [atom()].
+binary_types() ->
+ [integer, float, binary, bytes, bitstring, bits, utf8, utf16, utf32].
+
+-spec binary_endianness() -> [atom()].
+binary_endianness() ->
+ [big, little, native].
+
+-spec binary_type_specifier() -> [completion_item()].
+binary_type_specifier() ->
+ Labels = binary_type_specifiers(),
+ [binary_type_specifier(Label) || Label <- Labels].
+
+-spec binary_type_specifier(atom()) -> completion_item().
+binary_type_specifier(unit) ->
+ case snippet_support() of
+ true ->
+ #{
+ label => <<"unit:N">>,
+ kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET,
+ insertText => <<"unit:${1:N}">>
+ };
+ false ->
+ #{
+ label => <<"unit:">>,
+ kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
+ }
+ end;
+binary_type_specifier(Label) ->
+ #{
+ label => atom_to_binary(Label),
+ kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
+ }.
+
+-spec complete_record_field(map(), list()) -> items().
+complete_record_field(_Opts, [{atom, _, _}, {'=', _} | _]) ->
+ [];
+complete_record_field(
+ #{document := Document, line := Line, column := Col},
+ _Tokens
+) ->
+ complete_record_field(Document, {Line, Col}, <<"key=val}.">>).
+
+-spec complete_record_field(map(), pos(), binary()) -> items().
+complete_record_field(#{text := Text0} = Document, Pos, Suffix) ->
+ Prefix0 = els_text:range(Text0, {1, 1}, Pos),
+ POIs = els_dt_document:pois(Document, [function, spec, define, callback, record]),
+ %% Look for record start between current position and end of last
+ %% relevant top level expression
+ Prefix =
+ case els_scope:pois_before(POIs, #{from => Pos, to => Pos}) of
+ [#{range := #{to := {Line, _}}} | _] ->
+ {_, Prefix1} = els_text:split_at_line(Prefix0, Line),
+ Prefix1;
+ _ ->
+ %% Found no POI before, consider all the text
+ Prefix0
+ end,
+ case parse_record(els_text:strip_comments(Prefix), Suffix) of
+ {ok, Id} ->
+ record_fields_with_var(Document, Id);
+ error ->
+ []
+ end.
+
+-spec parse_record(binary(), binary()) -> {ok, els_poi:poi_id()} | error.
+parse_record(Text, Suffix) ->
+ case string:split(Text, <<"#">>, trailing) of
+ [_] ->
+ error;
+ [Left, Right] ->
+ Str = <<"#", Right/binary, Suffix/binary>>,
+ case els_parser:parse(Str) of
+ {ok, [#{kind := record_expr, id := Id} | _]} ->
+ {ok, Id};
+ _ ->
+ parse_record(Left, Str)
+ end
+ end.
+
+-spec snippets(poi_kind_or_any(), item_format()) -> items().
+snippets(type_definition, _ItemFormat) ->
+ [];
+snippets(_POIKind, args) ->
+ els_snippets_server:snippets();
+snippets(_POIKind, _ItemFormat) ->
+ [].
+
+-spec poikind_from_tokens(tokens()) -> poi_kind_or_any().
+poikind_from_tokens(Tokens) ->
+ case Tokens of
+ [{'::', _} | _] ->
+ type_definition;
+ [{atom, _, _}, {'::', _} | _] ->
+ type_definition;
+ [{atom, _, _}, {'|', _} | _] ->
+ type_definition;
+ [{atom, _, _}, {'=', _} | _] ->
+ function;
+ _ ->
+ any
+ end.
+
+-spec complete_type_definition(els_dt_document:item(), atom(), item_format()) -> items().
+complete_type_definition(Document, Name, ItemFormat) ->
+ NameBinary = atom_to_binary(Name, utf8),
+ definitions(Document, type_definition, ItemFormat) ++
+ bifs(type_definition, ItemFormat) ++
+ modules(NameBinary) ++
+ atoms(Document, NameBinary).
%%=============================================================================
%% Attributes
%%=============================================================================
--spec attributes() -> items().
-attributes() ->
- [ snippet(attribute_behaviour)
- , snippet(attribute_callback)
- , snippet(attribute_compile)
- , snippet(attribute_define)
- , snippet(attribute_dialyzer)
- , snippet(attribute_export)
- , snippet(attribute_export_type)
- , snippet(attribute_if)
- , snippet(attribute_ifdef)
- , snippet(attribute_ifndef)
- , snippet(attribute_import)
- , snippet(attribute_include)
- , snippet(attribute_include_lib)
- , snippet(attribute_on_load)
- , snippet(attribute_opaque)
- , snippet(attribute_record)
- , snippet(attribute_type)
- , snippet(attribute_vsn)
- ].
+-spec attributes(els_dt_document:item(), line()) -> items().
+attributes(Document, Line) ->
+ [
+ snippet(attribute_behaviour),
+ snippet(attribute_callback),
+ snippet(attribute_compile),
+ snippet(attribute_define),
+ snippet(attribute_dialyzer),
+ snippet(attribute_export),
+ snippet(attribute_export_type),
+ snippet(attribute_feature),
+ snippet(attribute_if),
+ snippet(attribute_ifdef),
+ snippet(attribute_ifndef),
+ snippet(attribute_import),
+ snippet(attribute_include),
+ snippet(attribute_include_lib),
+ snippet(attribute_on_load),
+ snippet(attribute_nifs),
+ snippet(attribute_opaque),
+ snippet(attribute_record),
+ snippet(attribute_type),
+ snippet(attribute_vsn),
+ attribute_module(Document)
+ ] ++ docs_attributes() ++ attribute_spec(Document, Line).
+
+-spec attribute_module(els_dt_document:item()) -> item().
+attribute_module(#{id := Id}) ->
+ IdBin = atom_to_binary(Id, utf8),
+ snippet(
+ <<"-module(", IdBin/binary, ").">>,
+ <<"module(", IdBin/binary, ").">>
+ ).
+
+-spec docs_attributes() -> items().
+-if(?OTP_RELEASE >= 27).
+docs_attributes() ->
+ [
+ snippet(attribute_moduledoc_map),
+ snippet(attribute_doc_map),
+ snippet(attribute_moduledoc_file),
+ snippet(attribute_doc_file),
+ snippet(attribute_moduledoc_text),
+ snippet(attribute_doc_text),
+ snippet(attribute_moduledoc_false),
+ snippet(attribute_doc_false)
+ ].
+-else.
+docs_attributes() ->
+ [].
+-endif.
+
+-spec attribute_spec(Document :: els_dt_document:item(), line()) -> items().
+attribute_spec(#{text := Text}, Line) ->
+ POIs = els_incomplete_parser:parse_after(Text, Line),
+ case [P || #{kind := function} = P <- POIs] of
+ [] ->
+ [];
+ FunPOIs ->
+ [#{id := {Id, Arity}} | _] = els_poi:sort(FunPOIs),
+ Args = [els_arg:new(I, "_") || I <- lists:seq(1, Arity)],
+ SnippetSupport = snippet_support(),
+ FunBin = format_function(Id, Args, SnippetSupport, spec),
+ RetBin =
+ case SnippetSupport of
+ false ->
+ <<" -> _.">>;
+ true ->
+ N = integer_to_binary(Arity + 1),
+ <<" -> ${", N/binary, ":_}.">>
+ end,
+ [snippet(<<"-spec">>, <<"spec ", FunBin/binary, RetBin/binary>>)]
+ end.
%%=============================================================================
%% Include paths
%%=============================================================================
--spec paths_include(els_dt_document:item()) -> [binary()].
-paths_include(#{uri := Uri}) ->
- case match_in_path(els_uri:path(Uri), els_config:get(apps_paths)) of
- [] ->
- [];
- [Path|_] ->
- AppPath = filename:join(lists:droplast(filename:split(Path))),
- {ok, Headers} = els_dt_document_index:find_by_kind(header),
- lists:flatmap(
- fun(#{uri := HeaderUri}) ->
- case string:prefix(els_uri:path(HeaderUri), AppPath) of
- nomatch ->
- [];
- IncludePath ->
- [relative_include_path(IncludePath)]
- end
- end, Headers)
- end.
-
--spec paths_include_lib() -> [binary()].
-paths_include_lib() ->
- Paths = els_config:get(otp_paths)
- ++ els_config:get(deps_paths)
- ++ els_config:get(apps_paths)
- ++ els_config:get(include_paths),
- {ok, Headers} = els_dt_document_index:find_by_kind(header),
- lists:flatmap(
- fun(#{uri := Uri}) ->
- HeaderPath = els_uri:path(Uri),
- case match_in_path(HeaderPath, Paths) of
- [] ->
- [];
- [Path|_] ->
- <<"/", PathSuffix/binary>> = string:prefix(HeaderPath, Path),
- PathBin = unicode:characters_to_binary(Path),
- case lists:reverse(filename:split(PathBin)) of
- [<<"include">>, App | _] ->
- [filename:join([ strip_app_version(App)
- , <<"include">>
- , PathSuffix])];
- _ ->
- []
- end
- end
- end, Headers).
-
--spec match_in_path(binary(), [binary()]) -> [binary()].
-match_in_path(DocumentPath, Paths) ->
- [P || P <- Paths, string:prefix(DocumentPath, P) =/= nomatch].
-
--spec relative_include_path(binary()) -> binary().
-relative_include_path(Path) ->
- case filename:split(Path) of
- [_App, <<"include">> | Rest] -> filename:join(Rest);
- [_App, <<"src">> | Rest] -> filename:join(Rest);
- [_App, SubDir | Rest] -> filename:join([<<"..">>, SubDir|Rest])
- end.
-
--spec strip_app_version(binary()) -> binary().
-strip_app_version(App0) ->
- %% Transform "foo-1.0" into "foo"
- case string:lexemes(App0, "-") of
- [] -> App0;
- [_] -> App0;
- Lexemes ->
- Vsn = lists:last(Lexemes),
- case re:run(Vsn, "^[0-9.]+$", [global, {capture, none}]) of
- match -> list_to_binary(lists:join("-", lists:droplast(Lexemes)));
- nomatch -> App0
- end
- end.
-
-spec item_kind_file(binary()) -> item().
item_kind_file(Path) ->
- #{ label => Path
- , kind => ?COMPLETION_ITEM_KIND_FILE
- , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
- }.
+ #{
+ label => Path,
+ kind => ?COMPLETION_ITEM_KIND_FILE,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
+ }.
%%==============================================================================
%% Snippets
%%==============================================================================
-spec snippet(atom()) -> item().
snippet(attribute_behaviour) ->
- snippet(<<"-behaviour().">>, <<"behaviour(${1:Behaviour}).">>);
+ snippet(<<"-behaviour().">>, <<"behaviour(${1:Behaviour}).">>);
snippet(attribute_export) ->
- snippet(<<"-export().">>, <<"export([${1:}]).">>);
+ snippet(<<"-export().">>, <<"export([${1:}]).">>);
snippet(attribute_vsn) ->
- snippet(<<"-vsn(Version).">>, <<"vsn(${1:Version}).">>);
+ snippet(<<"-vsn(Version).">>, <<"vsn(${1:Version}).">>);
snippet(attribute_callback) ->
- snippet(<<"-callback name(Args) -> return().">>,
- <<"callback ${1:name}(${2:Args}) -> ${3:return()}.">>);
+ snippet(
+ <<"-callback name(Args) -> return().">>,
+ <<"callback ${1:name}(${2:Args}) -> ${3:return()}.">>
+ );
snippet(attribute_on_load) ->
- snippet(<<"-on_load().">>,
- <<"on_load(${1:Function}).">>);
+ snippet(
+ <<"-on_load().">>,
+ <<"on_load(${1:Function}).">>
+ );
+snippet(attribute_nifs) ->
+ snippet(
+ <<"-nifs().">>,
+ <<"nifs([${1:}]).">>
+ );
snippet(attribute_export_type) ->
- snippet(<<"-export_type().">>, <<"export_type([${1:}]).">>);
+ snippet(<<"-export_type().">>, <<"export_type([${1:}]).">>);
+snippet(attribute_feature) ->
+ snippet(<<"-feature().">>, <<"feature(${1:Feature}, ${2:enable}).">>);
snippet(attribute_include) ->
- snippet(<<"-include().">>, <<"include(${1:}).">>);
+ snippet(<<"-include().">>, <<"include(${1:}).">>);
snippet(attribute_include_lib) ->
- snippet(<<"-include_lib().">>, <<"include_lib(${1:}).">>);
+ snippet(<<"-include_lib().">>, <<"include_lib(${1:}).">>);
snippet(attribute_type) ->
- snippet(<<"-type name() :: definition.">>,
- <<"type ${1:name}() :: ${2:definition}.">>);
+ snippet(
+ <<"-type name() :: definition.">>,
+ <<"type ${1:name}() :: ${2:definition}.">>
+ );
snippet(attribute_opaque) ->
- snippet(<<"-opaque name() :: definition.">>,
- <<"opaque ${1:name}() :: ${2:definition}.">>);
+ snippet(
+ <<"-opaque name() :: definition.">>,
+ <<"opaque ${1:name}() :: ${2:definition}.">>
+ );
snippet(attribute_ifdef) ->
- snippet(<<"-ifdef().">>, <<"ifdef(${1:VAR}).\n${2:}\n-endif.">>);
+ snippet(<<"-ifdef().">>, <<"ifdef(${1:VAR}).\n${2:}\n-endif.">>);
snippet(attribute_ifndef) ->
- snippet(<<"-ifndef().">>, <<"ifndef(${1:VAR}).\n${2:}\n-endif.">>);
+ snippet(<<"-ifndef().">>, <<"ifndef(${1:VAR}).\n${2:}\n-endif.">>);
snippet(attribute_if) ->
- snippet(<<"-if().">>, <<"if(${1:Pred}).\n${2:}\n-endif.">>);
+ snippet(<<"-if().">>, <<"if(${1:Pred}).\n${2:}\n-endif.">>);
snippet(attribute_define) ->
- snippet(<<"-define().">>, <<"define(${1:MACRO}, ${2:Value}).">>);
+ snippet(<<"-define().">>, <<"define(${1:MACRO}, ${2:Value}).">>);
snippet(attribute_record) ->
- snippet(<<"-record().">>,
- <<"record(${1:name}, {${2:field} = ${3:Value} :: ${4:Type}()}).">>);
+ snippet(
+ <<"-record().">>,
+ <<"record(${1:name}, {${2:field} = ${3:Value} :: ${4:Type}()}).">>
+ );
snippet(attribute_import) ->
- snippet(<<"-import().">>,
- <<"import(${1:Module}, [${2:}]).">>);
+ snippet(
+ <<"-import().">>,
+ <<"import(${1:Module}, [${2:}]).">>
+ );
snippet(attribute_dialyzer) ->
- snippet(<<"-dialyzer().">>,
- <<"dialyzer(${1:}).">>);
+ snippet(
+ <<"-dialyzer().">>,
+ <<"dialyzer(${1:}).">>
+ );
snippet(attribute_compile) ->
- snippet(<<"-compile().">>,
- <<"compile(${1:}).">>).
+ snippet(
+ <<"-compile().">>,
+ <<"compile(${1:}).">>
+ );
+snippet(attribute_moduledoc_text) ->
+ snippet(
+ <<"-moduledoc \"\"\"Text\"\"\".">>,
+ <<"moduledoc \"\"\"\n${1:Text}\n\"\"\".">>
+ );
+snippet(attribute_doc_text) ->
+ snippet(
+ <<"-doc \"\"\"Text\"\"\".">>,
+ <<"doc \"\"\"\n${1:Text}\n\"\"\".">>
+ );
+snippet(attribute_moduledoc_false) ->
+ snippet(
+ <<"-moduledoc false.">>,
+ <<"moduledoc false.">>
+ );
+snippet(attribute_doc_false) ->
+ snippet(
+ <<"-doc false.">>,
+ <<"doc false.">>
+ );
+snippet(attribute_moduledoc_map) ->
+ snippet(
+ <<"-moduledoc #{}.">>,
+ <<"moduledoc #{${1:}}.">>
+ );
+snippet(attribute_doc_map) ->
+ snippet(
+ <<"-doc #{}.">>,
+ <<"doc #{${1:}}.">>
+ );
+snippet(attribute_moduledoc_file) ->
+ snippet(
+ <<"-moduledoc File.">>,
+ <<"moduledoc {file,\"${1:File}\"}.">>
+ );
+snippet(attribute_doc_file) ->
+ snippet(
+ <<"-doc File.">>,
+ <<"doc {file,\"${1:File}\"}.">>
+ ).
-spec snippet(binary(), binary()) -> item().
snippet(Label, InsertText) ->
- #{ label => Label
- , kind => ?COMPLETION_ITEM_KIND_SNIPPET
- , insertText => InsertText
- , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET
- }.
+ #{
+ label => Label,
+ kind => ?COMPLETION_ITEM_KIND_SNIPPET,
+ insertText => InsertText,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET
+ }.
%%==============================================================================
%% Atoms
@@ -414,17 +908,18 @@ snippet(Label, InsertText) ->
-spec atoms(els_dt_document:item(), binary()) -> [map()].
atoms(Document, Prefix) ->
- POIs = els_scope:local_and_included_pois(Document, atom),
- Atoms = [Id || #{id := Id} <- POIs],
- Unique = lists:usort(Atoms),
- filter_by_prefix(Prefix, Unique, fun atom_to_label/1, fun item_kind_atom/1).
+ POIs = els_scope:local_and_included_pois(Document, atom),
+ Atoms = [Id || #{id := Id} <- POIs],
+ Unique = lists:usort(Atoms),
+ filter_by_prefix(Prefix, Unique, fun atom_to_label/1, fun item_kind_atom/1).
-spec item_kind_atom(binary()) -> map().
item_kind_atom(Atom) ->
- #{ label => Atom
- , kind => ?COMPLETION_ITEM_KIND_CONSTANT
- , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
- }.
+ #{
+ label => Atom,
+ kind => ?COMPLETION_ITEM_KIND_CONSTANT,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
+ }.
%%==============================================================================
%% Modules
@@ -432,119 +927,200 @@ item_kind_atom(Atom) ->
-spec modules(binary()) -> [map()].
modules(Prefix) ->
- {ok, Items} = els_dt_document_index:find_by_kind(module),
- Modules = [Id || #{id := Id} <- Items],
- filter_by_prefix(Prefix, Modules,
- fun atom_to_label/1, fun item_kind_module/1).
+ {ok, Items} = els_dt_document_index:find_by_kind(module),
+ Modules = [Id || #{id := Id} <- Items],
+ filter_by_prefix(
+ Prefix,
+ Modules,
+ fun atom_to_label/1,
+ fun item_kind_module/1
+ ).
-spec item_kind_module(atom()) -> item().
item_kind_module(Module) ->
- #{ label => Module
- , kind => ?COMPLETION_ITEM_KIND_MODULE
- , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
- }.
-
--spec behaviour_modules() -> [atom()].
-behaviour_modules() ->
- {ok, Modules} = els_dt_document_index:find_by_kind(module),
- OtpBehaviours = [ gen_event
- , gen_server
- , gen_statem
- , supervisor
- ],
- Behaviours = [Id || #{id := Id, uri := Uri} <- Modules, is_behaviour(Uri)],
- OtpBehaviours ++ Behaviours.
+ #{
+ label => Module,
+ kind => ?COMPLETION_ITEM_KIND_MODULE,
+ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT
+ }.
+
+-spec behaviour_modules(list()) -> [atom()].
+behaviour_modules(Begin) ->
+ Candidates = els_dt_document:find_candidates_with_otp(callback, 'module'),
+ Behaviours = [
+ els_uri:module(Uri)
+ || Uri <- Candidates,
+ lists:prefix(Begin, atom_to_list(els_uri:module(Uri))),
+ is_behaviour(Uri)
+ ],
+ Behaviours.
-spec is_behaviour(uri()) -> boolean().
is_behaviour(Uri) ->
- case els_dt_document:lookup(Uri) of
- {ok, [Document]} ->
- [] =/= els_dt_document:pois(Document, [callback]);
- _ ->
- false
- end.
+ case els_dt_document:lookup(Uri) of
+ {ok, [Document]} ->
+ [] =/= els_dt_document:pois(Document, [callback]);
+ _ ->
+ false
+ end.
%%==============================================================================
%% Functions, Types, Macros and Records
%%==============================================================================
--spec unexported_definitions(els_dt_document:item(), poi_kind()) -> items().
+-spec unexported_definitions(els_dt_document:item(), els_poi:poi_kind()) -> items().
+unexported_definitions(Document, any) ->
+ unexported_definitions(Document, function) ++
+ unexported_definitions(Document, type_definition);
unexported_definitions(Document, POIKind) ->
- AllDefs = definitions(Document, POIKind, true, false),
- ExportedDefs = definitions(Document, POIKind, true, true),
- AllDefs -- ExportedDefs.
+ AllDefs = definitions(Document, POIKind, arity_only, false),
+ ExportedDefs = definitions(Document, POIKind, arity_only, true),
+ AllDefs -- ExportedDefs.
--spec definitions(els_dt_document:item(), poi_kind()) -> [map()].
+-spec definitions(els_dt_document:item(), els_poi:poi_kind()) -> [map()].
definitions(Document, POIKind) ->
- definitions(Document, POIKind, _ExportFormat = false, _ExportedOnly = false).
-
--spec definitions(els_dt_document:item(), poi_kind(), boolean()) -> [map()].
-definitions(Document, POIKind, ExportFormat) ->
- definitions(Document, POIKind, ExportFormat, _ExportedOnly = false).
-
--spec definitions(els_dt_document:item(), poi_kind(), boolean(), boolean()) ->
- [map()].
-definitions(Document, POIKind, ExportFormat, ExportedOnly) ->
- POIs = els_scope:local_and_included_pois(Document, POIKind),
- #{uri := Uri} = Document,
- %% Find exported entries when there is an export_entry kind available
- FAs = case export_entry_kind(POIKind) of
- {error, no_export_entry_kind} -> [];
- ExportKind ->
- Exports = els_scope:local_and_included_pois(Document, ExportKind),
- [FA || #{id := FA} <- Exports]
+ definitions(Document, POIKind, _ItemFormat = args, _ExportedOnly = false).
+
+-spec definitions(els_dt_document:item(), poi_kind_or_any(), item_format()) -> [map()].
+definitions(Document, any, ItemFormat) ->
+ definitions(Document, function, ItemFormat) ++
+ definitions(Document, type_definition, ItemFormat);
+definitions(Document, POIKind, ItemFormat) ->
+ definitions(Document, POIKind, ItemFormat, _ExportedOnly = false).
+
+-spec definitions(els_dt_document:item(), els_poi:poi_kind(), item_format(), boolean()) ->
+ [map()].
+definitions(Document, POIKind, ItemFormat, ExportedOnly) ->
+ POIs = els_scope:local_and_included_pois(Document, POIKind),
+ #{uri := Uri} = Document,
+ %% Find exported entries when there is an export_entry kind available
+ FAs =
+ case export_entry_kind(POIKind) of
+ {error, no_export_entry_kind} ->
+ [];
+ ExportKind ->
+ Exports = els_scope:local_and_included_pois(Document, ExportKind),
+ [FA || #{id := FA} <- Exports]
+ end,
+ Items = resolve_definitions(Uri, POIs, FAs, ExportedOnly, ItemFormat),
+ lists:usort(Items).
+
+-spec completion_context(els_dt_document:item(), line(), column(), tokens()) ->
+ {item_format(), els_poi:poi_kind() | any}.
+completion_context(#{text := Text} = Document, Line, Column, Tokens) ->
+ ItemFormat =
+ case is_in_mfa_list_attr(Document, Line, Column) of
+ true ->
+ arity_only;
+ false ->
+ case els_text:get_char(Text, Line, Column + 1) of
+ {ok, $(} ->
+ %% Don't inlude args if next character is a '('
+ no_args;
+ _ ->
+ args
+ end
end,
- Items = resolve_definitions(Uri, POIs, FAs, ExportedOnly, ExportFormat),
- lists:usort(Items).
-
--spec completion_context(els_dt_document:item(), line(), column()) ->
- {boolean(), poi_kind()}.
-completion_context(Document, Line, Column) ->
- ExportFormat = is_in(Document, Line, Column, [export, export_type]),
- POIKind = case is_in(Document, Line, Column, [spec, export_type]) of
- true -> type_definition;
- false -> function
- end,
- {ExportFormat, POIKind}.
-
--spec resolve_definitions(uri(), [poi()], [{atom(), arity()}],
- boolean(), boolean()) ->
- [map()].
-resolve_definitions(Uri, Functions, ExportsFA, ExportedOnly, ArityOnly) ->
- [ resolve_definition(Uri, POI, ArityOnly)
- || #{id := FA} = POI <- Functions,
- not ExportedOnly orelse lists:member(FA, ExportsFA)
- ].
-
--spec resolve_definition(uri(), poi(), boolean()) -> map().
-resolve_definition(Uri, #{kind := 'function', id := {F, A}} = POI, ArityOnly) ->
- Data = #{ <<"module">> => els_uri:module(Uri)
- , <<"function">> => F
- , <<"arity">> => A
- },
- completion_item(POI, Data, ArityOnly);
-resolve_definition(Uri, #{kind := 'type_definition', id := {T, A}} = POI,
- ArityOnly) ->
- Data = #{ <<"module">> => els_uri:module(Uri)
- , <<"type">> => T
- , <<"arity">> => A
- },
- completion_item(POI, Data, ArityOnly);
-resolve_definition(_Uri, POI, ArityOnly) ->
- completion_item(POI, ArityOnly).
-
--spec exported_definitions(module(), poi_kind(), boolean()) -> [map()].
-exported_definitions(Module, POIKind, ExportFormat) ->
- case els_utils:find_module(Module) of
- {ok, Uri} ->
- case els_utils:lookup_document(Uri) of
- {ok, Document} ->
- definitions(Document, POIKind, ExportFormat, true);
- {error, _} ->
- []
- end;
- {error, _Error} ->
- []
- end.
+ POIKind =
+ case
+ is_in(
+ Document,
+ Line,
+ Column,
+ [spec, export_type, type_definition]
+ )
+ of
+ true ->
+ type_definition;
+ false ->
+ case is_in(Document, Line, Column, [export, nifs, function]) of
+ true ->
+ function;
+ false ->
+ poikind_from_tokens(Tokens)
+ end
+ end,
+ {ItemFormat, POIKind}.
+
+-spec is_in_mfa_list_attr(els_dt_document:item(), line(), column()) -> boolean().
+is_in_mfa_list_attr(#{text := Text} = Document, Line, Column) ->
+ %% Sometimes is_in will be confused because e.g. -export() failed to be parsed.
+ %% In such case we can use a heuristic to determine if we are inside
+ %% an export.
+ is_in(Document, Line, Column, [export, export_type, nifs]) orelse
+ is_in_mfa_list_attr_heuristic(Text, Line - 1).
+
+-spec is_in_mfa_list_attr_heuristic(binary(), line()) -> boolean().
+is_in_mfa_list_attr_heuristic(Text, Line) ->
+ is_in_heuristic(Text, <<"export">>, Line) orelse
+ is_in_heuristic(Text, <<"nifs">>, Line).
+
+-spec is_in_heuristic(binary(), binary(), line()) -> boolean().
+is_in_heuristic(Text, Attr, Line) ->
+ Len = byte_size(Attr),
+ case els_text:line(Text, Line) of
+ <<"-", Attr:Len/binary, _/binary>> ->
+ %% In Attr
+ true;
+ <<" ", _/binary>> when Line > 1 ->
+ %% Indented line, continue to search previous line
+ is_in_heuristic(Text, Attr, Line - 1);
+ _ ->
+ false
+ end.
+
+-spec resolve_definitions(
+ uri(),
+ [els_poi:poi()],
+ [{atom(), arity()}],
+ boolean(),
+ item_format()
+) ->
+ [map()].
+resolve_definitions(Uri, Functions, ExportsFA, ExportedOnly, ItemFormat) ->
+ [
+ resolve_definition(Uri, POI, ItemFormat)
+ || #{id := FA} = POI <- Functions,
+ not ExportedOnly orelse lists:member(FA, ExportsFA)
+ ].
+
+-spec resolve_definition(uri(), els_poi:poi(), item_format()) -> map().
+resolve_definition(Uri, #{kind := 'function', id := {F, A}} = POI, ItemFormat) ->
+ Data = #{
+ <<"module">> => els_uri:module(Uri),
+ <<"function">> => F,
+ <<"arity">> => A
+ },
+ completion_item(POI, Data, ItemFormat, Uri);
+resolve_definition(
+ Uri,
+ #{kind := 'type_definition', id := {T, A}} = POI,
+ ItemFormat
+) ->
+ Data = #{
+ <<"module">> => els_uri:module(Uri),
+ <<"type">> => T,
+ <<"arity">> => A
+ },
+ completion_item(POI, Data, ItemFormat, Uri);
+resolve_definition(Uri, POI, ItemFormat) ->
+ completion_item(POI, #{}, ItemFormat, Uri).
+
+-spec exported_definitions(module(), els_poi:poi_kind(), item_format()) -> [map()].
+exported_definitions(Module, any, ItemFormat) ->
+ exported_definitions(Module, function, ItemFormat) ++
+ exported_definitions(Module, type_definition, ItemFormat);
+exported_definitions(Module, POIKind, ItemFormat) ->
+ case els_utils:find_module(Module) of
+ {ok, Uri} ->
+ case els_utils:lookup_document(Uri) of
+ {ok, Document} ->
+ definitions(Document, POIKind, ItemFormat, true);
+ {error, _} ->
+ []
+ end;
+ {error, _Error} ->
+ []
+ end.
%%==============================================================================
%% Variables
@@ -552,13 +1128,15 @@ exported_definitions(Module, POIKind, ExportFormat) ->
-spec variables(els_dt_document:item()) -> [map()].
variables(Document) ->
- POIs = els_dt_document:pois(Document, [variable]),
- Vars = [ #{ label => atom_to_binary(Name, utf8)
- , kind => ?COMPLETION_ITEM_KIND_VARIABLE
- }
- || #{id := Name} <- POIs
- ],
- lists:usort(Vars).
+ POIs = els_dt_document:pois(Document, [variable]),
+ Vars = [
+ #{
+ label => atom_to_binary(Name, utf8),
+ kind => ?COMPLETION_ITEM_KIND_VARIABLE
+ }
+ || #{id := Name} <- POIs
+ ],
+ lists:usort(Vars).
%%==============================================================================
%% Record Fields
@@ -566,95 +1144,353 @@ variables(Document) ->
-spec all_record_fields(els_dt_document:item(), binary()) -> [map()].
all_record_fields(Document, Prefix) ->
- POIs = els_scope:local_and_included_pois(Document, [ record_def_field
- , record_field]),
- Fields = [Id || #{id := {_Record, Id}} <- POIs],
- Unique = lists:usort(Fields),
- filter_by_prefix(Prefix, Unique, fun atom_to_label/1, fun item_kind_field/1).
+ POIs = els_scope:local_and_included_pois(Document, [
+ record_def_field,
+ record_field
+ ]),
+ Fields = [Id || #{id := {_Record, Id}} <- POIs],
+ Unique = lists:usort(Fields),
+ filter_by_prefix(Prefix, Unique, fun atom_to_label/1, fun item_kind_field/1).
-spec record_fields(els_dt_document:item(), atom()) -> [map()].
record_fields(Document, RecordName) ->
- case find_record_definition(Document, RecordName) of
- [] -> [];
- POIs ->
- [#{data := #{field_list := Fields}} | _] = els_poi:sort(POIs),
- [ item_kind_field(atom_to_label(Name))
- || Name <- Fields
- ]
- end.
-
--spec find_record_definition(els_dt_document:item(), atom()) -> [poi()].
+ case find_record_definition(Document, RecordName) of
+ [] ->
+ [];
+ POIs ->
+ [#{data := #{field_list := Fields}} | _] = els_poi:sort(POIs),
+ [
+ item_kind_field(atom_to_label(Name))
+ || Name <- Fields
+ ]
+ end.
+
+-spec record_fields_with_var(els_dt_document:item(), atom()) -> [map()].
+record_fields_with_var(Document, RecordName) ->
+ case find_record_definition(Document, RecordName) of
+ [] ->
+ [];
+ POIs ->
+ [#{data := #{field_list := Fields}} | _] = els_poi:sort(POIs),
+ SnippetSupport = snippet_support(),
+ Format =
+ case SnippetSupport of
+ true -> ?INSERT_TEXT_FORMAT_SNIPPET;
+ false -> ?INSERT_TEXT_FORMAT_PLAIN_TEXT
+ end,
+ [
+ #{
+ label => atom_to_label(Name),
+ kind => ?COMPLETION_ITEM_KIND_FIELD,
+ insertText => format_record_field_with_var(Name, SnippetSupport),
+ insertTextFormat => Format
+ }
+ || Name <- Fields
+ ]
+ end.
+
+-spec format_record_field_with_var(atom(), SnippetSupport :: boolean()) -> binary().
+format_record_field_with_var(Name, true) ->
+ Label = atom_to_label(Name),
+ Var = els_utils:camel_case(Label),
+ <