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 ![erlang_ls](images/erlang-ls-logo-small.png?raw=true "Erlang LS") @@ -5,13 +14,19 @@ ![Build](https://github.com/erlang-ls/erlang_ls/workflows/Build/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/erlang-ls/erlang_ls/badge.svg?branch=main)](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) + ), + <<Title/binary, "...">>; + 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), + <<Label/binary, " = ${1:", Var/binary, "}">>; +format_record_field_with_var(Name, false) -> + atom_to_label(Name). + +-spec find_record_definition(els_dt_document:item(), atom()) -> [els_poi:poi()]. find_record_definition(Document, RecordName) -> - POIs = els_scope:local_and_included_pois(Document, record), - [X || X = #{id := Name} <- POIs, Name =:= RecordName]. + POIs = els_scope:local_and_included_pois(Document, record), + [X || X = #{id := Name} <- POIs, Name =:= RecordName]. -spec item_kind_field(binary()) -> map(). item_kind_field(Name) -> - #{ label => Name - , kind => ?COMPLETION_ITEM_KIND_FIELD - }. + #{ + label => Name, + kind => ?COMPLETION_ITEM_KIND_FIELD + }. %%============================================================================== %% Keywords %%============================================================================== - --spec keywords() -> [map()]. +-spec keywords(poi_kind_or_any(), item_format()) -> [map()]. +keywords(type_definition, _ItemFormat) -> + []; +keywords(_POIKind, arity_only) -> + []; +keywords(_POIKind, _ItemFormat) -> + Keywords = keywords(), + [ + keyword_completion_item(K, snippet_support()) + || K <- Keywords + ]. + +-spec keywords() -> [atom()]. keywords() -> - Keywords = [ 'after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl' - , 'bsr', 'bxor', 'case', 'catch', 'cond', 'div', 'end', 'fun' - , 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', 'rem' - , 'try', 'when', 'xor'], - [ #{ label => atom_to_binary(K, utf8) - , kind => ?COMPLETION_ITEM_KIND_KEYWORD - } || K <- Keywords ]. + [ + 'after', + 'and', + 'andalso', + 'band', + 'begin', + 'bnot', + 'bor', + 'bsl', + 'bsr', + 'bxor', + 'case', + 'catch', + 'cond', + 'div', + 'end', + 'else', + 'fun', + 'if', + 'let', + 'maybe', + 'not', + 'of', + 'or', + 'orelse', + 'receive', + 'rem', + 'try', + 'when', + 'xor' + ]. + +-spec keyword_completion_item(_, _) -> _. +keyword_completion_item('case', true) -> + #{ + label => <<"case">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "case ${1:Exprs} of\n" + " ${2:Pattern} ->\n" + " ${3:Body}\n" + "end" + >> + }; +keyword_completion_item('try', true) -> + #{ + label => <<"try">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "try ${1:Exprs}\n" + "catch\n" + " ${2:Class}:${3:ExceptionPattern}:${4:Stacktrace} ->\n" + " ${5:ExceptionBody}\n" + "end" + >> + }; +keyword_completion_item('catch', true) -> + #{ + label => <<"catch">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "catch\n" + " ${1:Class}:${2:ExceptionPattern}:${3:Stacktrace} ->\n" + " ${4:ExceptionBody}\n" + "end" + >> + }; +keyword_completion_item('begin', true) -> + #{ + label => <<"begin">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "begin\n" + " ${1:Body}\n" + "end" + >> + }; +keyword_completion_item('maybe', true) -> + #{ + label => <<"maybe">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "maybe\n" + " ${1:Body}\n" + "end" + >> + }; +keyword_completion_item('after', true) -> + #{ + label => <<"after">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "after\n" + " ${1:Duration} ->\n" + " ${2:Body}" + >> + }; +keyword_completion_item('else', true) -> + #{ + label => <<"else">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "else\n" + " ${1:Pattern} ->\n" + " ${2:Body}" + >> + }; +keyword_completion_item('of', true) -> + #{ + label => <<"of">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "of\n" + " ${1:Pattern} ->\n" + " ${2:Body}" + >> + }; +keyword_completion_item('receive', true) -> + #{ + label => <<"receive">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + insertText => + << + "receive\n" + " ${1:Pattern} ->\n" + " ${2:Body}\n" + "end" + >> + }; +keyword_completion_item(K, _SnippetSupport) -> + #{ + label => atom_to_binary(K, utf8), + kind => ?COMPLETION_ITEM_KIND_KEYWORD + }. %%============================================================================== %% Built-in functions %%============================================================================== --spec bifs(poi_kind(), boolean()) -> [map()]. -bifs(function, ExportFormat) -> - Range = #{from => {0, 0}, to => {0, 0}}, - Exports = erlang:module_info(exports), - BIFs = [ #{ kind => function - , id => X - , range => Range - , data => #{args => generate_arguments("Arg", A)} - } - || {F, A} = X <- Exports, erl_internal:bif(F, A) - ], - [completion_item(X, ExportFormat) || X <- BIFs]; -bifs(type_definition, true = _ExportFormat) -> - %% We don't want to include the built-in types when we are in - %% a -export_types(). context. - []; -bifs(type_definition, false = ExportFormat) -> - Types = [ {'any', 0}, {'arity', 0}, {'atom', 0}, {'binary', 0} - , {'bitstring', 0}, {'boolean', 0}, {'byte', 0}, {'char', 0} - , {'float', 0}, {'fun', 0}, {'fun', 1}, {'function', 0} - , {'identifier', 0}, {'integer', 0}, {'iodata', 0}, {'iolist', 0} - , {'list', 0}, {'list', 1}, {'map', 0}, {'maybe_improper_list', 0} - , {'maybe_improper_list', 2}, {'mfa', 0}, {'module', 0} - , {'neg_integer', 0}, {'nil', 0}, {'no_return', 0}, {'node', 0} - , {'nonempty_improper_list', 2}, {'nonempty_list', 1} - , {'non_neg_integer', 0}, {'none', 0}, {'nonempty_list', 0} - , {'nonempty_string', 0}, {'number', 0}, {'pid', 0}, {'port', 0} - , {'pos_integer', 0}, {'reference', 0}, {'string', 0}, {'term', 0} - , {'timeout', 0} - ], - Range = #{from => {0, 0}, to => {0, 0}}, - POIs = [ #{ kind => type_definition - , id => X - , range => Range - , data => #{args => generate_arguments("Type", A)} - } - || {_, A} = X <- Types - ], - [completion_item(X, ExportFormat) || X <- POIs]. - --spec generate_arguments(string(), integer()) -> [{integer(), string()}]. +-spec bifs(poi_kind_or_any(), item_format()) -> [map()]. +bifs(type_definition, arity_only) -> + %% We don't want to include the built-in types when we are in + %% a -export_types(). context. + []; +bifs(Kind, ItemFormat) -> + [completion_item(X, ItemFormat) || X <- bif_pois(Kind)]. + +-spec bif_pois(poi_kind_or_any()) -> [map()]. +bif_pois(any) -> + bif_pois(function) ++ bif_pois(type_definition); +bif_pois(function) -> + Range = #{from => {0, 0}, to => {0, 0}}, + Exports = erlang:module_info(exports), + [ + #{ + kind => function, + id => X, + range => Range, + data => #{args => generate_arguments("Arg", A)} + } + || {F, A} = X <- Exports, erl_internal:bif(F, A) + ]; +bif_pois(type_definition) -> + Types = [ + {'any', 0}, + {'arity', 0}, + {'atom', 0}, + {'binary', 0}, + {'bitstring', 0}, + {'boolean', 0}, + {'byte', 0}, + {'char', 0}, + {'float', 0}, + {'fun', 0}, + {'fun', 1}, + {'function', 0}, + {'identifier', 0}, + {'integer', 0}, + {'iodata', 0}, + {'iolist', 0}, + {'list', 0}, + {'list', 1}, + {'map', 0}, + {'maybe_improper_list', 0}, + {'maybe_improper_list', 2}, + {'mfa', 0}, + {'module', 0}, + {'neg_integer', 0}, + {'nil', 0}, + {'no_return', 0}, + {'node', 0}, + {'nonempty_binary', 0}, + {'nonempty_bitstring', 0}, + {'nonempty_improper_list', 2}, + {'nonempty_list', 1}, + {'non_neg_integer', 0}, + {'none', 0}, + {'nonempty_list', 0}, + {'nonempty_string', 0}, + {'number', 0}, + {'pid', 0}, + {'port', 0}, + {'pos_integer', 0}, + {'reference', 0}, + {'string', 0}, + {'term', 0}, + {'timeout', 0} + ], + Range = #{from => {0, 0}, to => {0, 0}}, + [ + #{ + kind => type_definition, + id => X, + range => Range, + data => #{args => generate_arguments("Type", A)} + } + || {_, A} = X <- Types + ]; +bif_pois(define) -> + Macros = [ + {'MODULE', none}, + {'MODULE_STRING', none}, + {'FILE', none}, + {'LINE', none}, + {'MACHINE', none}, + {'FUNCTION_NAME', none}, + {'FUNCTION_ARITY', none}, + {'OTP_RELEASE', none}, + {{'FEATURE_AVAILABLE', 1}, [#{index => 1, name => "Feature"}]}, + {{'FEATURE_ENABLED', 1}, [#{index => 1, name => "Feature"}]} + ], + Range = #{from => {0, 0}, to => {0, 0}}, + [ + #{ + kind => define, + id => Id, + range => Range, + data => #{args => Args} + } + || {Id, Args} <- Macros + ]. + +-spec generate_arguments(string(), integer()) -> els_arg:args(). generate_arguments(Prefix, Arity) -> - [{N, Prefix ++ integer_to_list(N)} || N <- lists:seq(1, Arity)]. + [ + els_arg:new(N, Prefix ++ integer_to_list(N)) + || N <- lists:seq(1, Arity) + ]. %%============================================================================== %% Filter by prefix @@ -663,111 +1499,248 @@ generate_arguments(Prefix, Arity) -> %% TODO: Implement as select -spec filter_by_prefix(binary(), [binary()], function(), function()) -> [map()]. filter_by_prefix(Prefix, List, ToBinary, ItemFun) -> - FilterMapFun = fun(X) -> - Str = ToBinary(X), - case string:prefix(Str, Prefix) of - nomatch -> false; - _ -> {true, ItemFun(Str)} - end - end, - lists:filtermap(FilterMapFun, List). + FilterMapFun = fun(X) -> + Str = ToBinary(X), + case string:prefix(Str, Prefix) of + nomatch -> false; + _ -> {true, ItemFun(Str)} + end + end, + lists:filtermap(FilterMapFun, List). %%============================================================================== %% Helper functions %%============================================================================== --spec completion_item(poi(), boolean()) -> map(). -completion_item(POI, ExportFormat) -> - completion_item(POI, #{}, ExportFormat). - --spec completion_item(poi(), map(), ExportFormat :: boolean()) -> map(). -completion_item(#{kind := Kind, id := {F, A}, data := POIData}, Data, false) - when Kind =:= function; - Kind =:= type_definition -> - ArgsNames = maps:get(args, POIData), - Label = io_lib:format("~p/~p", [F, A]), - #{ label => els_utils:to_binary(Label) - , kind => completion_item_kind(Kind) - , insertText => snippet_function(F, ArgsNames) - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => Data - }; -completion_item(#{kind := Kind, id := {F, A}}, Data, true) - when Kind =:= function; - Kind =:= type_definition -> - Label = io_lib:format("~p/~p", [F, A]), - #{ label => els_utils:to_binary(Label) - , kind => completion_item_kind(Kind) - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => Data - }; -completion_item(#{kind := Kind = record, id := Name}, Data, _) -> - #{ label => atom_to_label(Name) - , kind => completion_item_kind(Kind) - , data => Data - }; -completion_item(#{kind := Kind = define, id := Name, data := Info}, Data, _) -> - #{args := ArgNames} = Info, - #{ label => macro_label(Name) - , kind => completion_item_kind(Kind) - , insertText => snippet_macro(Name, ArgNames) - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => Data - }. +-spec completion_item(els_poi:poi(), item_format()) -> map(). +completion_item(POI, ItemFormat) -> + completion_item(POI, #{}, ItemFormat, undefined). + +-spec completion_item(els_poi:poi(), map(), item_format(), uri() | undefined) -> map(). +completion_item(#{kind := Kind, id := {F, A}} = POI, Data, args, Uri) when + Kind =:= function; + Kind =:= type_definition +-> + Args = args(POI, Uri), + Label = io_lib:format("~p/~p", [F, A]), + SnippetSupport = snippet_support(), + Format = + case SnippetSupport of + true -> ?INSERT_TEXT_FORMAT_SNIPPET; + false -> ?INSERT_TEXT_FORMAT_PLAIN_TEXT + end, + #{ + label => els_utils:to_binary(Label), + kind => completion_item_kind(Kind), + insertText => format_function(F, Args, SnippetSupport, Kind), + insertTextFormat => Format, + data => Data + }; +completion_item(#{kind := Kind, id := {F, A}}, Data, no_args, _Uri) when + Kind =:= function; + Kind =:= type_definition +-> + Label = io_lib:format("~p/~p", [F, A]), + #{ + label => els_utils:to_binary(Label), + kind => completion_item_kind(Kind), + insertText => atom_to_label(F), + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => Data + }; +completion_item(#{kind := Kind, id := {F, A}}, Data, arity_only, _Uri) when + Kind =:= function; + Kind =:= type_definition +-> + Label = io_lib:format("~p/~p", [F, A]), + #{ + label => els_utils:to_binary(Label), + kind => completion_item_kind(Kind), + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => Data + }; +completion_item(#{kind := Kind = record, id := Name}, Data, _, _Uri) -> + #{ + label => atom_to_label(Name), + kind => completion_item_kind(Kind), + data => Data + }; +completion_item(#{kind := Kind = define, id := Name, data := Info}, Data, _, _Uri) -> + #{args := ArgNames} = Info, + SnippetSupport = snippet_support(), + Format = + case SnippetSupport of + true -> ?INSERT_TEXT_FORMAT_SNIPPET; + false -> ?INSERT_TEXT_FORMAT_PLAIN_TEXT + end, + #{ + label => macro_label(Name), + kind => completion_item_kind(Kind), + insertText => format_macro(Name, ArgNames, SnippetSupport), + insertTextFormat => Format, + data => Data + }. + +-spec args(els_poi:poi(), uri()) -> els_arg:args(). +args(#{kind := type_definition, data := POIData}, _Uri) -> + maps:get(args, POIData); +args(#{kind := _Kind, data := POIData}, _Uri = undefined) -> + maps:get(args, POIData); +args(#{kind := function} = POI, Uri) -> + els_arg:get_args(Uri, POI). + +-spec features() -> items(). +features() -> + %% Hardcoded for now. Could use erl_features:all() in the future. + Features = [maybe_expr], + [ + #{ + label => atom_to_binary(Feature, utf8), + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + insertText => atom_to_binary(Feature, utf8), + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{} + } + || Feature <- Features + ]. -spec macro_label(atom() | {atom(), non_neg_integer()}) -> binary(). macro_label({Name, Arity}) -> - els_utils:to_binary(io_lib:format("~ts/~p", [Name, Arity])); + els_utils:to_binary( + io_lib:format( + "~ts/~p", + [macro_to_label(Name), Arity] + ) + ); macro_label(Name) -> - atom_to_binary(Name, utf8). - --spec snippet_function(atom(), [{integer(), string()}]) -> binary(). -snippet_function(Name, Args) -> - snippet_args(atom_to_label(Name), Args). - --spec snippet_macro( atom() | {atom(), non_neg_integer()} - , [{integer(), string()}]) -> binary(). -snippet_macro({Name0, _Arity}, Args) -> - Name = atom_to_binary(Name0, utf8), - snippet_args(Name, Args); -snippet_macro(Name, none) -> - atom_to_binary(Name, utf8). - --spec snippet_args(binary(), [{integer(), string()}]) -> binary(). -snippet_args(Name, Args0) -> - Args = [ ["${", integer_to_list(N), ":", A, "}"] - || {N, A} <- Args0 - ], - Snippet = [Name, "(", string:join(Args, ", "), ")"], - els_utils:to_binary(Snippet). - --spec is_in(els_dt_document:item(), line(), column(), [poi_kind()]) -> - boolean(). + macro_to_label(Name). + +-spec macro_to_label(atom()) -> binary(). +macro_to_label(Name) -> + %% Trick to ensure we can handle macros like ?'FOO BAR'. + Bin = atom_to_binary(Name, utf8), + LowerBin = string:lowercase(Bin), + LowerAtom = binary_to_atom(LowerBin, utf8), + case atom_to_label(LowerAtom) == LowerBin of + true -> + Bin; + false -> + atom_to_label(Name) + end. + +-spec format_function(atom(), els_arg:args(), boolean(), els_poi:poi_kind()) -> binary(). +format_function(Name, Args, SnippetSupport, Kind) -> + format_args(atom_to_label(Name), Args, SnippetSupport, Kind). + +-spec format_macro( + atom() | {atom(), non_neg_integer()}, + els_arg:args(), + boolean() +) -> binary(). +format_macro({Name0, _Arity}, Args, SnippetSupport) -> + Name = macro_to_label(Name0), + format_args(Name, Args, SnippetSupport, define); +format_macro(Name, none, _SnippetSupport) -> + macro_to_label(Name). + +-spec format_args( + binary(), + els_arg:args(), + boolean(), + els_poi:poi_kind() +) -> binary(). +format_args(Name, Args0, SnippetSupport, Kind) -> + Args = + case SnippetSupport of + false -> + []; + true -> + ArgList = [format_arg(Arg, Kind) || Arg <- Args0], + ["(", string:join(ArgList, ", "), ")"] + end, + els_utils:to_binary([Name | Args]). + +-spec format_arg(els_arg:arg(), els_poi:poi_kind()) -> iolist(). +format_arg(Arg, Kind) -> + [ + "${", + els_arg:index(Arg), + ":", + els_arg:name(prefix(Kind), Arg), + "}" + ]. + +-spec prefix(els_poi:poi_kind()) -> string(). +prefix(type_definition) -> + "Type"; +prefix(_) -> + "Arg". + +-spec snippet_support() -> boolean(). +snippet_support() -> + case els_config:get(capabilities) of + #{ + <<"textDocument">> := + #{ + <<"completion">> := + #{ + <<"completionItem">> := + #{<<"snippetSupport">> := SnippetSupport} + } + } + } -> + SnippetSupport; + _ -> + false + end. + +-spec is_in(els_dt_document:item(), line(), column(), [els_poi:poi_kind()]) -> + boolean(). is_in(Document, Line, Column, POIKinds) -> - POIs = els_dt_document:get_element_at_pos(Document, Line, Column), - IsKind = fun(#{kind := Kind}) -> lists:member(Kind, POIKinds) end, - lists:any(IsKind, POIs). + POIs = match_all_pos(els_dt_document:pois(Document), {Line, Column}), + IsKind = fun(#{kind := Kind}) -> lists:member(Kind, POIKinds) end, + lists:any(IsKind, POIs). + +-spec match_all_pos([els_poi:poi()], pos()) -> [els_poi:poi()]. +match_all_pos(POIs, Pos) -> + lists:usort( + [ + POI + || #{range := #{from := From, to := To}} = POI <- POIs, + (From =< Pos) andalso (Pos =< To) + ] ++ + [ + POI + || #{ + data := #{ + wrapping_range := + #{from := From, to := To} + } + } = POI <- POIs, + (From =< Pos) andalso (Pos =< To) + ] + ). %% @doc Maps a POI kind to its completion item kind --spec completion_item_kind(poi_kind()) -> completion_item_kind(). +-spec completion_item_kind(els_poi:poi_kind()) -> completion_item_kind(). completion_item_kind(define) -> - ?COMPLETION_ITEM_KIND_CONSTANT; + ?COMPLETION_ITEM_KIND_CONSTANT; completion_item_kind(record) -> - ?COMPLETION_ITEM_KIND_STRUCT; + ?COMPLETION_ITEM_KIND_STRUCT; completion_item_kind(type_definition) -> - ?COMPLETION_ITEM_KIND_TYPE_PARAM; + ?COMPLETION_ITEM_KIND_TYPE_PARAM; completion_item_kind(function) -> - ?COMPLETION_ITEM_KIND_FUNCTION. + ?COMPLETION_ITEM_KIND_FUNCTION. %% @doc Maps a POI kind to its export entry POI kind --spec export_entry_kind(poi_kind()) -> - poi_kind() | {error, no_export_entry_kind}. +-spec export_entry_kind(els_poi:poi_kind()) -> + els_poi:poi_kind() | {error, no_export_entry_kind}. export_entry_kind(type_definition) -> export_type_entry; export_entry_kind(function) -> export_entry; export_entry_kind(_) -> {error, no_export_entry_kind}. -spec atom_to_label(atom()) -> binary(). atom_to_label(Atom) when is_atom(Atom) -> - unicode:characters_to_binary(io_lib:write(Atom)). + unicode:characters_to_binary(io_lib:write(Atom)). %%============================================================================== %% Tests @@ -775,13 +1748,40 @@ atom_to_label(Atom) when is_atom(Atom) -> -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -strip_app_version_test() -> - ?assertEqual(<<"foo">>, strip_app_version(<<"foo">>)), - ?assertEqual(<<"foo">>, strip_app_version(<<"foo-1.2.3">>)), - ?assertEqual(<<"">>, strip_app_version(<<"">>)), - ?assertEqual(<<"foo-bar">>, strip_app_version(<<"foo-bar">>)), - ?assertEqual(<<"foo-bar">>, strip_app_version(<<"foo-bar-1.2.3">>)), - ?assertEqual(<<"foo-bar-baz">>, strip_app_version(<<"foo-bar-baz">>)), - ?assertEqual(<<"foo-bar-baz">>, strip_app_version(<<"foo-bar-baz-1.2.3">>)). +parse_record_test() -> + ?assertEqual( + {ok, foo}, + parse_record(<<"#foo">>, <<"{}.">>) + ), + ?assertEqual( + {ok, foo}, + parse_record(<<"#foo{x = y">>, <<"}.">>) + ), + ?assertEqual( + {ok, foo}, + parse_record(<<"#foo{x = #bar{}">>, <<"}.">>) + ), + ?assertEqual( + {ok, foo}, + parse_record(<<"#foo{x = #bar{y = #baz{}}">>, <<"}.">>) + ). + +is_exported_heuristic_test_() -> + Text = << + "-module(test).\n" + "-export([foo/0\n" + " bar/0\n" + " baz/0\n" + " ]).\n" + "-define(FOO, foo).\n" + >>, + [ + ?_assertEqual(false, is_in_mfa_list_attr_heuristic(Text, 0)), + ?_assertEqual(true, is_in_mfa_list_attr_heuristic(Text, 1)), + ?_assertEqual(true, is_in_mfa_list_attr_heuristic(Text, 2)), + ?_assertEqual(true, is_in_mfa_list_attr_heuristic(Text, 3)), + ?_assertEqual(true, is_in_mfa_list_attr_heuristic(Text, 4)), + ?_assertEqual(false, is_in_mfa_list_attr_heuristic(Text, 5)) + ]. -endif. diff --git a/apps/els_lsp/src/els_crossref_diagnostics.erl b/apps/els_lsp/src/els_crossref_diagnostics.erl index 6a8ac0ca9..5e6b03f50 100644 --- a/apps/els_lsp/src/els_crossref_diagnostics.erl +++ b/apps/els_lsp/src/els_crossref_diagnostics.erl @@ -12,90 +12,219 @@ %%============================================================================== %% Exports %%============================================================================== --export([ is_default/0 - , run/1 - , source/0 - ]). +-export([ + is_default/0, + run/1, + source/0 +]). %%============================================================================== %% Includes %%============================================================================== -include("els_lsp.hrl"). -include_lib("kernel/include/logger.hrl"). - %%============================================================================== %% Callback Functions %%============================================================================== -spec is_default() -> boolean(). is_default() -> - false. + false. -spec run(uri()) -> [els_diagnostics:diagnostic()]. run(Uri) -> - case els_utils:lookup_document(Uri) of - {error, _Error} -> - []; - {ok, Document} -> - POIs = els_dt_document:pois(Document, [ application - , implicit_fun - , import_entry - , export_entry - ]), - [make_diagnostic(POI) || POI <- POIs, not has_definition(POI, Document)] - end. + EnabledDiagnostics = els_diagnostics:enabled_diagnostics(), + CompilerEnabled = lists:member(<<"compiler">>, EnabledDiagnostics), + Start = erlang:monotonic_time(millisecond), + Res = + case els_utils:lookup_document(Uri) of + {error, _Error} -> + []; + {ok, Document} -> + POIs = els_dt_document:pois(Document, kinds()), + Opts = #{ + compiler_enabled => CompilerEnabled + }, + {Diags, _Cache} = + lists:mapfoldl( + fun(#{id := Id} = POI, Cache) -> + case find_in_cache(Id, Cache) of + {ok, HasDef} -> + {[make_diagnostic(HasDef, POI)], Cache}; + error -> + HasDef = has_definition(POI, Document, Opts), + { + make_diagnostic(HasDef, POI), + update_cache(HasDef, Id, Cache) + } + end + end, + #{}, + POIs + ), + lists:flatten(Diags) + end, + End = erlang:monotonic_time(millisecond), + Duration = End - Start, + ?LOG_DEBUG("Crossref done for ~p [duration: ~p ms]", [els_uri:module(Uri), Duration]), + Res. -spec source() -> binary(). source() -> - <<"CrossRef">>. + <<"CrossRef">>. %%============================================================================== %% Internal Functions %%============================================================================== --spec make_diagnostic(poi()) -> els_diagnostics:diagnostic(). -make_diagnostic(#{range := Range, id := Id}) -> - Function = case Id of - {F, A} -> lists:flatten(io_lib:format("~p/~p", [F, A])); - {M, F, A} -> lists:flatten(io_lib:format("~p:~p/~p", [M, F, A])) - end, - Message = els_utils:to_binary( - io_lib:format( "Cannot find definition for function ~s" - , [Function])), - Severity = ?DIAGNOSTIC_ERROR, - els_diagnostics:make_diagnostic( els_protocol:range(Range) - , Message - , Severity - , source()). - --spec has_definition(poi(), els_dt_document:item()) -> boolean(). -has_definition(#{ kind := application - , id := {module_info, 0} }, _) -> true; -has_definition(#{ kind := application - , id := {module_info, 1} }, _) -> true; -has_definition(#{ kind := application - , id := {Module, module_info, Arity} - }, _) when Arity =:= 0; Arity =:= 1 -> - {ok, []} =/= els_dt_document_index:lookup(Module); -has_definition(#{ kind := application - , id := {record_info, 2} }, _) -> true; -has_definition(#{ kind := application - , id := {behaviour_info, 1} }, _) -> true; -has_definition(#{ kind := application - , id := {lager, Level, Arity} }, _) -> - lager_definition(Level, Arity); -has_definition(POI, #{uri := Uri}) -> - case els_code_navigation:goto_definition(Uri, POI) of - {ok, _Uri, _POI} -> - true; - {error, _Error} -> - false - end. +-spec update_cache(true | {missing, function | module}, els_poi:poi_id(), map()) -> map(). +update_cache({missing, module}, {M, _F, _A}, Cache) -> + %% Cache missing module to avoid repeated lookups + Cache#{M => missing}; +update_cache(HasDef, Id, Cache) -> + Cache#{Id => HasDef}. + +-spec find_in_cache(els_poi:poi_id(), map()) -> _. +find_in_cache({M, _F, _A}, Cache) when is_map_key(M, Cache) -> + {ok, {missing, module}}; +find_in_cache(Id, Cache) -> + maps:find(Id, Cache). + +-spec kinds() -> [els_poi:poi_kind()]. +kinds() -> + [ + application, + implicit_fun, + import_entry, + export_entry, + nifs_entry + ]. + +-spec make_diagnostic(_, els_poi:poi()) -> [els_diagnostics:diagnostic()]. +make_diagnostic({missing, Kind}, #{id := Id} = POI) -> + Message = error_msg(Kind, Id), + Severity = ?DIAGNOSTIC_ERROR, + [ + els_diagnostics:make_diagnostic( + els_protocol:range(range(Kind, POI)), + Message, + Severity, + source() + ) + ]; +make_diagnostic(true, _) -> + []. + +-spec range(module | function, els_poi:poi()) -> els_poi:poi_range(). +range(module, #{data := #{mod_range := Range}}) -> + Range; +range(function, #{data := #{name_range := Range}}) -> + Range; +range(_, #{range := Range}) -> + Range. + +-spec error_msg(module | function, els_poi:poi_id()) -> binary(). +error_msg(module, {M, _F, _A}) -> + els_utils:to_binary(io_lib:format("Cannot find module ~p", [M])); +error_msg(function, Id) -> + els_utils:to_binary(io_lib:format("Cannot find definition for function ~s", [id_str(Id)])). + +-spec id_str(els_poi:poi_id()) -> string(). +id_str(Id) -> + case Id of + {F, A} -> lists:flatten(io_lib:format("~p/~p", [F, A])); + {M, F, A} -> lists:flatten(io_lib:format("~p:~p/~p", [M, F, A])) + end. + +-spec has_definition(els_poi:poi(), els_dt_document:item(), _) -> + true | {missing, function | module}. +has_definition(#{data := #{imported := true}}, _Document, _Opts) -> + %% Call to a bif + true; +has_definition(#{id := {module_info, 0}}, _, _) -> + true; +has_definition(#{id := {module_info, 1}}, _, _) -> + true; +has_definition(#{data := #{mod_is_variable := true}}, _, _) -> + true; +has_definition(#{data := #{fun_is_variable := true}}, _, _) -> + true; +has_definition(#{id := {Module, module_info, Arity}}, _, _) when Arity =:= 0; Arity =:= 1 -> + case els_dt_document_index:lookup(Module) of + {ok, []} -> + {missing, module}; + {ok, _} -> + true + end; +has_definition(#{id := {record_info, 2}}, _, _) -> + true; +has_definition(#{id := {behaviour_info, 1}}, _, _) -> + true; +has_definition(#{id := {lager, Level, Arity}}, _, _) -> + lager_definition(Level, Arity); +has_definition(#{id := {lists, append, 1}}, _, _) -> + %% lists:append/1 isn't indexed for some reason + true; +has_definition( + #{id := {F, A}} = POI, + Document, + #{ + %% Compiler already checks local function calls + compiler_enabled := false + } +) -> + Uri = els_dt_document:uri(Document), + MFA = {els_uri:module(Uri), F, A}, + case function_lookup(MFA) of + true -> + true; + false -> + case els_code_navigation:goto_definition(Uri, POI) of + {ok, _Defs} -> + true; + {error, _Error} -> + {missing, function} + end + end; +has_definition(#{id := {M, _F, _A} = MFA} = POI, _Document, _Opts) -> + case function_lookup(MFA) of + true -> + true; + false -> + case els_utils:find_module(M) of + {ok, Uri} -> + case els_code_navigation:goto_definition(Uri, POI) of + {ok, _Defs} -> + true; + {error, _Error} -> + {missing, function} + end; + {error, _} -> + {missing, module} + end + end; +has_definition(_POI, #{uri := _Uri}, _Opts) -> + true. + +-spec function_lookup(mfa()) -> boolean(). +function_lookup(MFA) -> + case els_db:lookup(els_dt_functions:name(), MFA) of + {ok, []} -> + false; + {ok, _} -> + true + end. -spec lager_definition(atom(), integer()) -> boolean(). lager_definition(Level, Arity) when Arity =:= 1 orelse Arity =:= 2 -> - lists:member(Level, lager_levels()); -lager_definition(_, _) -> false. + case lists:member(Level, lager_levels()) of + true -> + true; + false -> + {missing, function} + end; +lager_definition(_, _) -> + {missing, function}. -spec lager_levels() -> [atom()]. lager_levels() -> - [debug, info, notice, warning, error, critical, alert, emergency]. + [debug, debug_unsafe, info, notice, warning, error, critical, alert, emergency]. diff --git a/apps/els_lsp/src/els_db.erl b/apps/els_lsp/src/els_db.erl index 322202dba..a0fca420c 100644 --- a/apps/els_lsp/src/els_db.erl +++ b/apps/els_lsp/src/els_db.erl @@ -1,16 +1,25 @@ -module(els_db). %% API --export([ clear_table/1 - , clear_tables/0 - , delete/2 - , delete_object/2 - , lookup/2 - , match/2 - , match_delete/2 - , tables/0 - , write/2 - ]). +-export([ + clear_table/1, + clear_tables/0, + delete/2, + delete_object/2, + lookup/2, + match/2, + match_delete/2, + select_delete/2, + tables/0, + write/2, + conditional_write/4 +]). + +%%============================================================================== +%% Type Definitions +%%============================================================================== +-type condition() :: fun((tuple()) -> boolean()). +-export_type([condition/0]). %%============================================================================== %% Exported functions @@ -18,41 +27,53 @@ -spec tables() -> [atom()]. tables() -> - [ els_dt_document - , els_dt_document_index - , els_dt_references - , els_dt_signatures - ]. + [ + els_dt_document, + els_dt_document_index, + els_dt_references, + els_dt_signatures, + els_dt_functions, + els_docs_memo + ]. -spec delete(atom(), any()) -> ok. delete(Table, Key) -> - els_db_server:delete(Table, Key). + els_db_server:delete(Table, Key). -spec delete_object(atom(), any()) -> ok. delete_object(Table, Object) -> - els_db_server:delete_object(Table, Object). + els_db_server:delete_object(Table, Object). -spec lookup(atom(), any()) -> {ok, [tuple()]}. lookup(Table, Key) -> - {ok, ets:lookup(Table, Key)}. + {ok, ets:lookup(Table, Key)}. -spec match(atom(), tuple()) -> {ok, [tuple()]}. match(Table, Pattern) when is_tuple(Pattern) -> - {ok, ets:match_object(Table, Pattern)}. + {ok, ets:match_object(Table, Pattern)}. -spec match_delete(atom(), tuple()) -> ok. match_delete(Table, Pattern) when is_tuple(Pattern) -> - els_db_server:match_delete(Table, Pattern). + els_db_server:match_delete(Table, Pattern). + +-spec select_delete(atom(), any()) -> ok. +select_delete(Table, MS) -> + els_db_server:select_delete(Table, MS). -spec write(atom(), tuple()) -> ok. write(Table, Object) when is_tuple(Object) -> - els_db_server:write(Table, Object). + els_db_server:write(Table, Object). + +-spec conditional_write(atom(), any(), tuple(), condition()) -> + ok | {error, any()}. +conditional_write(Table, Key, Object, Condition) when is_tuple(Object) -> + els_db_server:conditional_write(Table, Key, Object, Condition). -spec clear_table(atom()) -> ok. clear_table(Table) -> - els_db_server:clear_table(Table). + els_db_server:clear_table(Table). -spec clear_tables() -> ok. clear_tables() -> - [ok = clear_table(T) || T <- tables()], - ok. + [ok = clear_table(T) || T <- tables()], + ok. diff --git a/apps/els_lsp/src/els_db_server.erl b/apps/els_lsp/src/els_db_server.erl index ff89b13f6..f08e5fa99 100644 --- a/apps/els_lsp/src/els_db_server.erl +++ b/apps/els_lsp/src/els_db_server.erl @@ -2,29 +2,37 @@ %%% @doc The db gen_server. %%% @end %%%============================================================================= - -module(els_db_server). %%============================================================================== %% API %%============================================================================== --export([ start_link/0 - , clear_table/1 - , delete/2 - , delete_object/2 - , match_delete/2 - , write/2 - ]). +-export([ + start_link/0, + clear_table/1, + delete/2, + delete_object/2, + match_delete/2, + select_delete/2, + write/2, + conditional_write/4 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include_lib("kernel/include/logger.hrl"). %%============================================================================== %% 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 +]). -type state() :: #{}. %%============================================================================== @@ -37,58 +45,89 @@ %%============================================================================== -spec start_link() -> {ok, pid()}. start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, unused, []). + gen_server:start_link({local, ?SERVER}, ?MODULE, unused, []). -spec clear_table(atom()) -> ok. clear_table(Table) -> - gen_server:call(?SERVER, {clear_table, Table}). + gen_server:call(?SERVER, {clear_table, Table}). -spec delete(atom(), any()) -> ok. delete(Table, Key) -> - gen_server:call(?SERVER, {delete, Table, Key}). + gen_server:call(?SERVER, {delete, Table, Key}). -spec delete_object(atom(), any()) -> ok. delete_object(Table, Key) -> - gen_server:call(?SERVER, {delete_object, Table, Key}). + gen_server:call(?SERVER, {delete_object, Table, Key}). -spec match_delete(atom(), tuple()) -> ok. match_delete(Table, Pattern) -> - gen_server:call(?SERVER, {match_delete, Table, Pattern}). + gen_server:call(?SERVER, {match_delete, Table, Pattern}). + +-spec select_delete(atom(), any()) -> ok. +select_delete(Table, MS) -> + gen_server:call(?SERVER, {select_delete, Table, MS}). -spec write(atom(), tuple()) -> ok. write(Table, Object) -> - gen_server:call(?SERVER, {write, Table, Object}). + gen_server:call(?SERVER, {write, Table, Object}). + +-spec conditional_write(atom(), any(), tuple(), els_db:condition()) -> + ok | {error, any()}. +conditional_write(Table, Key, Object, Condition) -> + gen_server:call(?SERVER, {conditional_write, Table, Key, Object, Condition}). %%============================================================================== %% Callbacks for the gen_server behaviour %%============================================================================== -spec init(unused) -> {ok, state()}. init(unused) -> - [ok = els_db_table:init(Table) || Table <- els_db:tables()], - {ok, #{}}. + [ok = els_db_table:init(Table) || Table <- els_db:tables()], + {ok, #{}}. -spec handle_call(any(), {pid(), any()}, state()) -> - {reply, any(), state()} | {noreply, state()}. + {reply, any(), state()} | {noreply, state()}. handle_call({clear_table, Table}, _From, State) -> - true = ets:delete_all_objects(Table), - {reply, ok, State}; + true = ets:delete_all_objects(Table), + {reply, ok, State}; handle_call({delete, Table, Key}, _From, State) -> - true = ets:delete(Table, Key), - {reply, ok, State}; + true = ets:delete(Table, Key), + {reply, ok, State}; handle_call({delete_object, Table, Key}, _From, State) -> - true = ets:delete_object(Table, Key), - {reply, ok, State}; + true = ets:delete_object(Table, Key), + {reply, ok, State}; handle_call({match_delete, Table, Pattern}, _From, State) -> - true = ets:match_delete(Table, Pattern), - {reply, ok, State}; + true = ets:match_delete(Table, Pattern), + {reply, ok, State}; +handle_call({select_delete, Table, MS}, _From, State) -> + ets:select_delete(Table, MS), + {reply, ok, State}; handle_call({write, Table, Object}, _From, State) -> - true = ets:insert(Table, Object), - {reply, ok, State}. + true = ets:insert(Table, Object), + {reply, ok, State}; +handle_call({conditional_write, Table, Key, Object, Condition}, _From, State) -> + case ets:lookup(Table, Key) of + [Entry] -> + case Condition(Entry) of + true -> + true = ets:insert(Table, Object), + {reply, ok, State}; + false -> + ?LOG_DEBUG( + "Skip insertion due to invalid condition " + "[table=~p] [key=~p]", + [Table, Key] + ), + {reply, {error, condition_not_satisfied}, State} + end; + [] -> + true = ets:insert(Table, Object), + {reply, ok, State} + end. -spec handle_cast(any(), state()) -> {noreply, state()}. handle_cast(_Request, State) -> - {noreply, State}. + {noreply, State}. -spec handle_info(any(), state()) -> {noreply, state()}. handle_info(_Request, State) -> - {noreply, State}. + {noreply, State}. diff --git a/apps/els_lsp/src/els_db_table.erl b/apps/els_lsp/src/els_db_table.erl index 3b6cc328c..2c6afb486 100644 --- a/apps/els_lsp/src/els_db_table.erl +++ b/apps/els_lsp/src/els_db_table.erl @@ -15,11 +15,12 @@ %% Exports %%============================================================================== --export([ default_opts/0 - , init/1 - , name/1 - , opts/1 - ]). +-export([ + default_opts/0, + init/1, + name/1, + opts/1 +]). %%============================================================================== %% Includes @@ -30,7 +31,7 @@ %% Type Definitions %%============================================================================== -type table() :: atom(). --export_type([ table/0 ]). +-export_type([table/0]). %%============================================================================== %% API @@ -38,19 +39,19 @@ -spec default_opts() -> [any()]. default_opts() -> - [public, named_table, {keypos, 2}, {read_concurrency, true}]. + [public, named_table, {keypos, 2}, {read_concurrency, true}]. -spec init(table()) -> ok. init(Table) -> - TableName = name(Table), - ?LOG_INFO("Creating table [name=~p]", [TableName]), - ets:new(TableName, opts(Table)), - ok. + TableName = name(Table), + ?LOG_INFO("Creating table [name=~p]", [TableName]), + ets:new(TableName, opts(Table)), + ok. -spec name(table()) -> atom(). name(Table) -> - Table:name(). + Table:name(). -spec opts(table()) -> proplists:proplist(). opts(Table) -> - default_opts() ++ Table:opts(). + default_opts() ++ Table:opts(). diff --git a/apps/els_lsp/src/els_definition_provider.erl b/apps/els_lsp/src/els_definition_provider.erl index 937cf3758..1a21c226f 100644 --- a/apps/els_lsp/src/els_definition_provider.erl +++ b/apps/els_lsp/src/els_definition_provider.erl @@ -2,46 +2,294 @@ -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 handle_request(any()) -> {response, any()} | {async, uri(), pid()}. +handle_request({definition, Params}) -> + #{ + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + {ok, Document} = els_utils:lookup_document(Uri), + POIs = els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1), + case goto_definition(Uri, POIs) of + null -> + #{text := Text} = Document, + IncompletePOIs = match_incomplete(Text, {Line, Character}), + case goto_definition(Uri, IncompletePOIs) of + null -> + FuzzyPOIs = make_fuzzy(POIs), + case goto_definition(Uri, FuzzyPOIs) of + null -> + els_references_provider:handle_request({references, Params}); + GoTo -> + {response, GoTo} + end; + GoTo -> + {response, GoTo} + end; + GoTo -> + {response, GoTo} + end. --spec is_enabled() -> boolean(). -is_enabled() -> - true. - --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({definition, Params}, State) -> - #{ <<"position">> := #{ <<"line">> := Line - , <<"character">> := Character - } - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - {ok, Document} = els_utils:lookup_document(Uri), - POIs = els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1), - case goto_definition(Uri, POIs) of - null -> - els_references_provider:handle_request({references, Params}, State); - GoTo -> - {GoTo, State} - end. - --spec goto_definition(uri(), [poi()]) -> map() | null. +-spec make_fuzzy([els_poi:poi()]) -> [els_poi:poi()]. +make_fuzzy(POIs) -> + lists:flatmap( + fun + (#{kind := application, id := {M, F, _A}} = POI) -> + [ + POI#{id => {M, F, any_arity}, kind => application}, + POI#{id => {M, F, any_arity}, kind => type_application} + ]; + (#{kind := type_application, id := {M, F, _A}} = POI) -> + [ + POI#{id => {M, F, any_arity}, kind => type_application}, + POI#{id => {M, F, any_arity}, kind => application} + ]; + (#{kind := application, id := {F, _A}} = POI) -> + [ + POI#{id => {F, any_arity}, kind => application}, + POI#{id => {F, any_arity}, kind => type_application}, + POI#{id => {F, any_arity}, kind => macro}, + POI#{id => F, kind => macro} + ]; + (#{kind := type_application, id := {F, _A}} = POI) -> + [ + POI#{id => {F, any_arity}, kind => type_application}, + POI#{id => {F, any_arity}, kind => application}, + POI#{id => {F, any_arity}, kind => macro}, + POI#{id => F, kind => macro} + ]; + (#{kind := macro, id := {M, _A}} = POI) -> + [ + POI#{id => M}, + POI#{id => {M, any_arity}} + ]; + (#{kind := macro, id := M} = POI) -> + [ + POI#{id => {M, any_arity}} + ]; + (#{kind := atom, id := Id} = POI) -> + [ + POI#{id => {Id, any_arity}, kind => application}, + POI#{id => {Id, any_arity}, kind => type_application}, + POI#{id => Id, kind => macro} + ]; + (_POI) -> + [] + end, + POIs + ). + +-spec goto_definition(uri(), [els_poi:poi()]) -> [map()] | null. goto_definition(_Uri, []) -> - null; -goto_definition(Uri, [POI|Rest]) -> - case els_code_navigation:goto_definition(Uri, POI) of - {ok, DefUri, #{range := Range}} -> - #{uri => DefUri, range => els_protocol:range(Range)}; - _ -> - goto_definition(Uri, Rest) - end. + null; +goto_definition(Uri, [#{id := FunId, kind := function} = POI | Rest]) -> + {ok, Document} = els_utils:lookup_document(Uri), + BehaviourPOIs = els_dt_document:pois(Document, [behaviour]), + case BehaviourPOIs of + [] -> + %% cursor is not over a function - continue + case els_code_navigation:goto_definition(Uri, POI) of + {ok, Definitions} -> + goto_definitions_to_goto(Definitions); + _ -> + goto_definition(Uri, Rest) + end; + Behaviours -> + case does_implement_behaviour(FunId, Behaviours) of + false -> + %% no matching callback for this behaviour so proceed + goto_definition(Uri, Rest); + {true, BehaviourModuleUri, MatchingCallback} -> + {ok, Definitions} = els_code_navigation:goto_definition( + BehaviourModuleUri, + MatchingCallback + ), + goto_definitions_to_goto(Definitions) + end + end; +goto_definition(Uri, [POI | Rest]) -> + case els_code_navigation:goto_definition(Uri, POI) of + {ok, Definitions} -> + goto_definitions_to_goto(Definitions); + _ -> + goto_definition(Uri, Rest) + end. + +-spec match_incomplete(binary(), pos()) -> [els_poi:poi()]. +match_incomplete(Text, {Line, Col} = Pos) -> + %% Try parsing subsets of text to find a matching POI at Pos + case match_after(Text, Pos) ++ match_line(Text, Pos) of + [] -> + %% Still found nothing, let's analyze the tokens to kludge a POI + LineText = els_text:line(Text, Line), + Tokens = els_text:tokens(LineText, {Line, 1}), + kludge_match(Tokens, {Line, Col + 1}); + POIs -> + POIs + end. + +-spec kludge_match([any()], pos()) -> [els_poi:poi()]. +kludge_match([], _Pos) -> + []; +kludge_match( + [ + {atom, {FromL, FromC}, Module}, + {':', _}, + {atom, _, Function}, + {'(', {ToL, ToC}} + | _ + ], + {_, C} +) when + FromC =< C, C < ToC +-> + %% Match mod:fun( + Range = #{from => {FromL, FromC}, to => {ToL, ToC}}, + POI = els_poi:new(Range, application, {Module, Function, any_arity}), + [POI]; +kludge_match([{atom, {FromL, FromC}, Function}, {'(', {ToL, ToC}} | _], {_, C}) when + FromC =< C, C < ToC +-> + %% Match fun( + Range = #{from => {FromL, FromC}, to => {ToL, ToC}}, + POI = els_poi:new(Range, application, {Function, any_arity}), + [POI]; +kludge_match([{'#', _}, {atom, {FromL, FromC}, Record} | T], {_, C} = Pos) when + FromC =< C +-> + %% Match #record + ToC = FromC + length(atom_to_list(Record)), + case C =< ToC of + true -> + Range = #{from => {FromL, FromC}, to => {FromL, ToC}}, + POI = els_poi:new(Range, record_expr, Record), + [POI]; + false -> + kludge_match(T, Pos) + end; +kludge_match([{'?', _}, {VarOrAtom, {FromL, FromC}, Macro} | T], {_, C} = Pos) when + FromC =< C, (VarOrAtom == var orelse VarOrAtom == atom) +-> + %% Match ?MACRO + ToC = FromC + length(atom_to_list(Macro)), + case C =< ToC of + true -> + %% Match fun( + Range = #{from => {FromL, FromC}, to => {FromL, ToC}}, + POI = els_poi:new(Range, macro, Macro), + [POI]; + false -> + kludge_match(T, Pos) + end; +kludge_match([{atom, {FromL, FromC}, Atom} | T], {_, C} = Pos) when + FromC =< C +-> + %% Match atom + ToC = FromC + length(atom_to_list(Atom)), + case C =< ToC of + true -> + Range = #{from => {FromL, FromC}, to => {FromL, ToC}}, + POI = els_poi:new(Range, atom, Atom), + [POI]; + false -> + kludge_match(T, Pos) + end; +kludge_match([_ | T], Pos) -> + %% TODO: Add more kludges here + kludge_match(T, Pos). + +-spec match_after(binary(), pos()) -> [els_poi:poi()]. +match_after(Text, {Line, Character}) -> + %% Try to parse current line and the lines after it + POIs = els_incomplete_parser:parse_after(Text, Line), + MatchingPOIs = match_pois(POIs, {1, Character + 1}), + fix_line_offsets(MatchingPOIs, Line). + +-spec match_line(binary(), pos()) -> [els_poi:poi()]. +match_line(Text, {Line, Character}) -> + %% Try to parse only current line + POIs = els_incomplete_parser:parse_line(Text, Line), + MatchingPOIs = match_pois(POIs, {1, Character + 1}), + fix_line_offsets(MatchingPOIs, Line). + +-spec match_pois([els_poi:poi()], pos()) -> [els_poi:poi()]. +match_pois(POIs, Pos) -> + els_poi:sort(els_poi:match_pos(POIs, Pos)). + +-spec fix_line_offsets([els_poi:poi()], integer()) -> [els_poi:poi()]. +fix_line_offsets(POIs, Offset) -> + [fix_line_offset(POI, Offset) || POI <- POIs]. + +-spec fix_line_offset(els_poi:poi(), integer()) -> els_poi:poi(). +fix_line_offset( + #{ + range := #{ + from := {FromL, FromC}, + to := {ToL, ToC} + } + } = POI, + Offset +) -> + %% TODO: Fix other ranges too + POI#{ + range => #{ + from => {FromL + Offset, FromC}, + to => {ToL + Offset, ToC} + } + }. + +-spec goto_definitions_to_goto(Definitions) -> Result when + Definitions :: els_code_navigation:goto_definition(), + Result :: [map()]. +goto_definitions_to_goto(Definitions) -> + lists:map( + fun({DefUri, DefPOI}) -> + #{range := Range} = DefPOI, + #{uri => DefUri, range => els_protocol:range(Range)} + end, + Definitions + ). + +-spec does_implement_behaviour(any(), list()) -> {true, uri(), els_poi:poi()} | false. +does_implement_behaviour(_, []) -> + false; +does_implement_behaviour(FunId, [#{id := ModuleId, kind := behaviour} | Rest]) -> + {ok, BehaviourModuleUri} = els_utils:find_module(ModuleId), + {ok, BehaviourModuleDocument} = els_utils:lookup_document(BehaviourModuleUri), + DefinedCallbacks = els_dt_document:pois( + BehaviourModuleDocument, + [callback] + ), + MaybeMatchingCallback = lists:filter( + fun(#{id := CallbackId}) -> + CallbackId =:= FunId + end, + DefinedCallbacks + ), + case MaybeMatchingCallback of + [] -> does_implement_behaviour(FunId, Rest); + [H | _] -> {true, BehaviourModuleUri, H} + end. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +fix_line_offset_test() -> + In = #{range => #{from => {1, 16}, to => {1, 32}}}, + ?assertMatch( + #{range := #{from := {66, 16}, to := {66, 32}}}, + fix_line_offset(In, 65) + ). + +-endif. diff --git a/apps/els_lsp/src/els_diagnostics.erl b/apps/els_lsp/src/els_diagnostics.erl index 9c9b4b303..96cb58989 100644 --- a/apps/els_lsp/src/els_diagnostics.erl +++ b/apps/els_lsp/src/els_diagnostics.erl @@ -12,44 +12,51 @@ %%============================================================================== %% Types %%============================================================================== --type diagnostic() :: #{ range := range() - , severity => severity() - , code => number() | binary() - , source => binary() - , message := binary() - , relatedInformation => [related_info()] - }. +-type diagnostic() :: #{ + range := range(), + severity => severity(), + code => number() | binary(), + source => binary(), + message := binary(), + relatedInformation => [related_info()], + data => binary() +}. -type diagnostic_id() :: binary(). --type related_info() :: #{ location := location() - , message := binary() - }. --type severity() :: ?DIAGNOSTIC_ERROR - | ?DIAGNOSTIC_WARNING - | ?DIAGNOSTIC_INFO - | ?DIAGNOSTIC_HINT. --export_type([ diagnostic/0 - , diagnostic_id/0 - , severity/0 - ]). +-type related_info() :: #{ + location := location(), + message := binary() +}. +-type severity() :: + ?DIAGNOSTIC_ERROR + | ?DIAGNOSTIC_WARNING + | ?DIAGNOSTIC_INFO + | ?DIAGNOSTIC_HINT. +-export_type([ + diagnostic/0, + diagnostic_id/0, + severity/0 +]). %%============================================================================== %% Callback Functions Definitions %%============================================================================== --callback is_default() -> boolean(). --callback run(uri()) -> [diagnostic()]. --callback source() -> binary(). +-callback is_default() -> boolean(). +-callback run(uri()) -> [diagnostic()]. +-callback source() -> binary(). -callback on_complete(uri(), [diagnostic()]) -> ok. --optional_callbacks([ on_complete/2 ]). +-optional_callbacks([on_complete/2]). %%============================================================================== %% API %%============================================================================== --export([ available_diagnostics/0 - , default_diagnostics/0 - , enabled_diagnostics/0 - , make_diagnostic/4 - , run_diagnostics/1 - ]). +-export([ + available_diagnostics/0, + default_diagnostics/0, + enabled_diagnostics/0, + make_diagnostic/4, + make_diagnostic/5, + run_diagnostics/1 +]). %%============================================================================== %% API @@ -57,92 +64,165 @@ -spec available_diagnostics() -> [diagnostic_id()]. available_diagnostics() -> - [ <<"bound_var_in_pattern">> - , <<"compiler">> - , <<"crossref">> - , <<"dialyzer">> - , <<"gradualizer">> - , <<"elvis">> - , <<"unused_includes">> - , <<"unused_macros">> - , <<"unused_record_fields">> - ]. + [ + <<"atom_typo">>, + <<"bound_var_in_pattern">>, + <<"compiler">>, + <<"crossref">>, + <<"dialyzer">>, + <<"edoc">>, + <<"gradualizer">>, + <<"elvis">>, + <<"unused_includes">>, + <<"unused_macros">>, + <<"unused_record_fields">>, + <<"refactorerl">>, + <<"eqwalizer">>, + <<"eunit">> + ]. -spec default_diagnostics() -> [diagnostic_id()]. default_diagnostics() -> - [Id || Id <- available_diagnostics(), (cb_module(Id)):is_default()]. + [Id || Id <- available_diagnostics(), (cb_module(Id)):is_default()]. -spec enabled_diagnostics() -> [diagnostic_id()]. enabled_diagnostics() -> - Config = els_config:get(diagnostics), - Default = default_diagnostics(), - Enabled = maps:get("enabled", Config, []), - Disabled = maps:get("disabled", Config, []), - lists:usort((Default ++ valid(Enabled)) -- valid(Disabled)). + Config = els_config:get(diagnostics), + Default = default_diagnostics(), + Enabled = maps:get("enabled", Config, []), + Disabled = maps:get("disabled", Config, []), + lists:usort((Default ++ valid(Enabled)) -- valid(Disabled)). --spec make_diagnostic(range(), binary(), severity(), binary()) -> diagnostic(). +-spec make_diagnostic(range(), binary(), severity(), binary()) -> + diagnostic(). make_diagnostic(Range, Message, Severity, Source) -> - #{ range => Range - , message => Message - , severity => Severity - , source => Source - }. + #{ + range => Range, + message => Message, + severity => Severity, + source => Source + }. + +-spec make_diagnostic(range(), binary(), severity(), binary(), binary()) -> + diagnostic(). +make_diagnostic(Range, Message, Severity, Source, Data) -> + #{ + range => Range, + message => Message, + severity => Severity, + source => Source, + data => Data + }. -spec run_diagnostics(uri()) -> [pid()]. run_diagnostics(Uri) -> - [run_diagnostic(Uri, Id) || Id <- enabled_diagnostics()]. + case is_initial_indexing_done() of + true -> + ok = wait_for_indexing_job(Uri), + [run_diagnostic(Uri, Id) || Id <- enabled_diagnostics()]; + false -> + ?LOG_INFO( + "Initial indexing is not done, skip running diagnostics for ~p", + [els_uri:module(Uri)] + ), + [] + end. %%============================================================================== %% Internal Functions %%============================================================================== +-spec is_initial_indexing_done() -> boolean(). +is_initial_indexing_done() -> + %% Keep in sync with els_indexing + Jobs = [<<"Applications">>, <<"OTP">>, <<"Dependencies">>], + JobTitles = els_background_job:list_titles(), + lists:all( + fun(Job) -> + not lists:member( + <<"Indexing ", Job/binary>>, + JobTitles + ) + end, + Jobs + ). + +-spec wait_for_indexing_job(uri()) -> ok. +wait_for_indexing_job(Uri) -> + %% Add delay to allowing indexing job to start + timer:sleep(10), + JobTitles = els_background_job:list_titles(), + case lists:member(<<"Indexing ", Uri/binary>>, JobTitles) of + false -> + %% No indexing job is running, we're ready! + ok; + true -> + %% Indexing job is still running, retry until it finishes + wait_for_indexing_job(Uri) + end. -spec run_diagnostic(uri(), diagnostic_id()) -> pid(). run_diagnostic(Uri, Id) -> - CbModule = cb_module(Id), - Source = CbModule:source(), - Module = atom_to_binary(els_uri:module(Uri), utf8), - Title = <<Source/binary, " (", Module/binary, ")">>, - Config = #{ task => fun(U, _) -> CbModule:run(U) end - , entries => [Uri] - , title => Title - , on_complete => - fun(Diagnostics) -> - case erlang:function_exported(CbModule, on_complete, 2) of - true -> + CbModule = cb_module(Id), + Source = CbModule:source(), + Module = atom_to_binary(els_uri:module(Uri), utf8), + Title = <<Source/binary, " (", Module/binary, ")">>, + Start = erlang:monotonic_time(millisecond), + Config = #{ + task => fun(U, _) -> CbModule:run(U) end, + entries => [Uri], + title => Title, + on_complete => + fun(Diagnostics) -> + case erlang:function_exported(CbModule, on_complete, 2) of + true -> CbModule:on_complete(Uri, Diagnostics); - false -> + false -> ok - end, - els_diagnostics_provider:notify(Diagnostics, self()) - end - }, - {ok, Pid} = els_background_job:new(Config), - Pid. + end, + els_diagnostics_provider:notify(Diagnostics, self()), + End = erlang:monotonic_time(millisecond), + Duration = End - Start, + Event = #{ + uri => Uri, + source => Source, + duration_ms => Duration, + number_of_diagnostics => length(Diagnostics), + type => <<"diagnostics">> + }, + ?LOG_DEBUG("Diagnostics completed. [event=~p]", [Event]), + els_telemetry:send_notification(Event) + end + }, + {ok, Pid} = els_background_job:new(Config), + Pid. %% @doc Return the callback module for a given Diagnostic Identifier -spec cb_module(diagnostic_id()) -> module(). cb_module(Id) -> - binary_to_existing_atom(<<"els_", Id/binary, "_diagnostics">>, utf8). + binary_to_existing_atom(<<"els_", Id/binary, "_diagnostics">>, utf8). -spec is_valid(diagnostic_id()) -> boolean(). is_valid(Id) -> - lists:member(Id, available_diagnostics()). + lists:member(Id, available_diagnostics()). -spec valid([string()]) -> [diagnostic_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 diagnostics 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 diagnostics 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. diff --git a/apps/els_lsp/src/els_diagnostics_provider.erl b/apps/els_lsp/src/els_diagnostics_provider.erl index 240d9556c..81c9505d8 100644 --- a/apps/els_lsp/src/els_diagnostics_provider.erl +++ b/apps/els_lsp/src/els_diagnostics_provider.erl @@ -2,17 +2,15 @@ -behaviour(els_provider). --export([ handle_info/2 - , handle_request/2 - , init/0 - , is_enabled/0 - , options/0 - ]). - --export([ notify/2 - , publish/2 - ]). +-export([ + options/0, + handle_request/1 +]). +-export([ + notify/2, + publish/2 +]). %%============================================================================== %% Includes @@ -20,97 +18,32 @@ -include("els_lsp.hrl"). -include_lib("kernel/include/logger.hrl"). --type state() :: #{in_progress => [progress_entry()]}. --type progress_entry() :: #{ uri := uri() - , pending := [job()] - , diagnostics := [els_diagnostics:diagnostic()] - }. --type job() :: pid(). - --define(SERVER, ?MODULE). - %%============================================================================== %% els_provider functions %%============================================================================== - --spec is_enabled() -> boolean(). -is_enabled() -> true. - -spec options() -> map(). options() -> - #{}. - --spec init() -> state(). -init() -> - #{ in_progress => [] }. + #{}. -%% LSP 3.15 introduce versioning for diagnostics. Until all clients -%% support it, we need to keep track of old diagnostics and re-publish -%% them every time we get a new chunk. --spec handle_info(any(), state()) -> state(). -handle_info({diagnostics, Diagnostics, Job}, State) -> - ?LOG_DEBUG("Received diagnostics [job=~p]", [Job]), - #{ in_progress := InProgress } = State, - { #{ pending := Jobs - , diagnostics := OldDiagnostics - , uri := Uri - } - , Rest - } = find_entry(Job, InProgress), - NewDiagnostics = Diagnostics ++ OldDiagnostics, - ?MODULE:publish(Uri, NewDiagnostics), - case lists:delete(Job, Jobs) of - [] -> - State#{in_progress => Rest}; - Remaining -> - State#{in_progress => [#{ pending => Remaining - , diagnostics => NewDiagnostics - , uri => Uri - }|Rest]} - end; -handle_info(_Request, State) -> - State. - --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({run_diagnostics, Params}, State) -> - #{in_progress := InProgress} = State, - #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, - ?LOG_DEBUG("Starting diagnostics jobs [uri=~p]", [Uri]), - Jobs = els_diagnostics:run_diagnostics(Uri), - Entry = #{uri => Uri, pending => Jobs, diagnostics => []}, - {noresponse, State#{in_progress => [Entry|InProgress]}}. +-spec handle_request(any()) -> {diagnostics, uri(), [pid()]}. +handle_request({run_diagnostics, Params}) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + ?LOG_DEBUG("Starting diagnostics jobs [uri=~p]", [Uri]), + Jobs = els_diagnostics:run_diagnostics(Uri), + {diagnostics, Uri, Jobs}. %%============================================================================== %% API %%============================================================================== -spec notify([els_diagnostics:diagnostic()], pid()) -> ok. notify(Diagnostics, Job) -> - ?SERVER ! {diagnostics, Diagnostics, Job}, - ok. + els_server:register_diagonstics(Diagnostics, Job). -spec publish(uri(), [els_diagnostics:diagnostic()]) -> ok. publish(Uri, Diagnostics) -> - Method = <<"textDocument/publishDiagnostics">>, - Params = #{ uri => Uri - , diagnostics => Diagnostics - }, - els_server:send_notification(Method, Params). - -%%============================================================================== -%% Internal Functions -%%============================================================================== - --spec find_entry(job(), [progress_entry()]) -> - {progress_entry(), [progress_entry()]}. -find_entry(Job, InProgress) -> - find_entry(Job, InProgress, []). - --spec find_entry(job(), [progress_entry()], [progress_entry()]) -> - {progress_entry(), [progress_entry()]}. -find_entry(Job, [#{pending := Pending} = Entry|Rest], Acc) -> - case lists:member(Job, Pending) of - true -> - {Entry, Rest ++ Acc}; - false -> - find_entry(Job, Rest, [Entry|Acc]) - end. + Method = <<"textDocument/publishDiagnostics">>, + Params = #{ + uri => Uri, + diagnostics => Diagnostics + }, + els_server:send_notification(Method, Params). diff --git a/apps/els_lsp/src/els_diagnostics_utils.erl b/apps/els_lsp/src/els_diagnostics_utils.erl index 01d442249..c1b49744d 100644 --- a/apps/els_lsp/src/els_diagnostics_utils.erl +++ b/apps/els_lsp/src/els_diagnostics_utils.erl @@ -6,12 +6,13 @@ %%============================================================================== %% Exports %%============================================================================== --export([ dependencies/1 - , included_uris/1 - , included_documents/1 - , range/2 - , traverse_include_graph/3 - ]). +-export([ + dependencies/1, + included_uris/1, + included_documents/1, + range/2, + traverse_include_graph/3 +]). %%============================================================================== %% Includes %%============================================================================== @@ -20,61 +21,64 @@ -spec dependencies(uri()) -> [atom()]. dependencies(Uri) -> - dependencies([Uri], [], sets:new()). + lists:reverse(dependencies([Uri], [], sets:new())). -spec included_uris(els_dt_document:item()) -> [uri()]. included_uris(Document) -> - POIs = els_dt_document:pois(Document, [include, include_lib]), - included_uris([Id || #{id := Id} <- POIs], []). + POIs = els_dt_document:pois(Document, [include, include_lib]), + included_uris([Id || #{id := Id} <- POIs], []). -spec included_documents(els_dt_document:item()) -> [els_dt_document:item()]. included_documents(Document) -> - lists:filtermap(fun find_included_document/1, included_uris(Document)). + lists:filtermap(fun find_included_document/1, included_uris(Document)). -spec find_included_document(uri()) -> {true, els_dt_document:item()} | false. find_included_document(Uri) -> - case els_utils:lookup_document(Uri) of - {ok, IncludeDocument} -> - {true, IncludeDocument}; - {error, _} -> - ?LOG_WARNING("Failed included document lookup [uri=~p]", [Uri]), - false - end. - --spec range(els_dt_document:item() | undefined, - erl_anno:anno() | none) -> poi_range(). + case els_utils:lookup_document(Uri) of + {ok, IncludeDocument} -> + {true, IncludeDocument}; + {error, _} -> + ?LOG_WARNING("Failed included document lookup [uri=~p]", [Uri]), + false + end. + +-spec range( + els_dt_document:item() | undefined, + erl_anno:anno() | none +) -> els_poi:poi_range(). range(Document, none) -> - range(Document, erl_anno:new(1)); + range(Document, erl_anno:new(1)); range(Document, Anno) -> - true = erl_anno:is_anno(Anno), - Line = erl_anno:line(Anno), - case erl_anno:column(Anno) of - Col when Document =:= undefined; Col =:= undefined -> - #{from => {Line, 1}, to => {Line + 1, 1}}; - Col -> - POIs0 = els_dt_document:get_element_at_pos(Document, Line, Col), - - %% Exclude folding range since line is more exact anyway - POIs = [POI || #{kind := Kind} = POI <- POIs0, Kind =/= folding_range], - - %% * If we find no pois that we just return the original line - %% * If we find a poi that start on the line and col as the anno - %% we are looking for we that that one. - %% * We take the "first" poi if we find some, but none come from - %% the correct line and number. - - case lists:search( - fun(#{ range := #{ from := {FromLine, FromCol} } }) -> - FromLine =:= Line andalso FromCol =:= Col - end, POIs) of - {value, #{ range := Range } } -> - Range; - false when POIs =:= [] -> - #{ from => {Line, 1}, to => {Line + 1, 1} }; - false -> - maps:get(range, hd(POIs)) - end - end. + true = erl_anno:is_anno(Anno), + Line = erl_anno:line(Anno), + case erl_anno:column(Anno) of + Col when Document =:= undefined; Col =:= undefined -> + #{from => {Line, 1}, to => {Line + 1, 1}}; + Col -> + POIs = els_dt_document:get_element_at_pos(Document, Line, Col), + + %% * If we find no pois that we just return the original line + %% * If we find a poi that start on the line and col as the anno + %% we are looking for we that that one. + %% * We take the "first" poi if we find some, but none come from + %% the correct line and number. + + case + lists:search( + fun(#{range := #{from := {FromLine, FromCol}}}) -> + FromLine =:= Line andalso FromCol =:= Col + end, + POIs + ) + of + {value, #{range := Range}} -> + Range; + false when POIs =:= [] -> + #{from => {Line, 1}, to => {Line + 1, 1}}; + false -> + maps:get(range, hd(POIs)) + end + end. -spec traverse_include_graph(AccFun, AccT, From) -> AccT when AccFun :: fun((Included, Includer, AccT) -> AccT), @@ -83,8 +87,9 @@ range(Document, Anno) -> Includer :: els_dt_document:item(). traverse_include_graph(AccFun, Acc, From) -> Graph = els_fungraph:new( - fun els_dt_document:uri/1, - fun included_documents/1), + fun els_dt_document:uri/1, + fun included_documents/1 + ), els_fungraph:traverse(AccFun, Acc, From, Graph). %%============================================================================== @@ -92,73 +97,129 @@ traverse_include_graph(AccFun, Acc, From) -> %%============================================================================== -spec dependencies([uri()], [atom()], sets:set(binary())) -> [atom()]. dependencies([], Acc, _AlreadyProcessed) -> - Acc; -dependencies([Uri|Uris], Acc, AlreadyProcessed) -> - case els_utils:lookup_document(Uri) of - {ok, Document} -> - Behaviours = els_dt_document:pois(Document, [behaviour]), - ParseTransforms = els_dt_document:pois(Document, [parse_transform]), - IncludedUris = included_uris(Document), - FilteredIncludedUris = exclude_already_processed( IncludedUris - , AlreadyProcessed - ), - PTUris = lists:usort( - lists:flatten( - [pt_deps(Id) || #{id := Id} <- ParseTransforms])), - FilteredPTUris = exclude_already_processed( PTUris - , AlreadyProcessed - ), - dependencies( Uris ++ FilteredIncludedUris ++ FilteredPTUris - , Acc ++ [Id || #{id := Id} <- Behaviours ++ ParseTransforms] - ++ [els_uri:module(FPTUri) || FPTUri <- FilteredPTUris] - , sets:add_element(Uri, AlreadyProcessed)); - {error, _Error} -> - [] - end. + Acc; +dependencies([Uri | Uris], Acc, AlreadyProcessed) -> + case els_utils:lookup_document(Uri) of + {ok, Document} -> + Behaviours = els_dt_document:pois(Document, [behaviour]), + ParseTransforms = els_dt_document:pois(Document, [parse_transform]), + IncludedUris = included_uris(Document), + FilteredIncludedUris = exclude_already_processed( + IncludedUris, + AlreadyProcessed + ), + BeUris = lists:usort( + lists:flatten( + [be_deps(Id) || #{id := Id} <- Behaviours] + ) + ), + FilteredBeUris = exclude_already_processed( + BeUris, + AlreadyProcessed + ), + PTUris = lists:usort( + lists:flatten( + [pt_deps(Id) || #{id := Id} <- ParseTransforms] + ) + ), + FilteredPTUris = exclude_already_processed( + PTUris, + AlreadyProcessed + ), + dependencies( + Uris ++ FilteredIncludedUris ++ FilteredPTUris ++ FilteredBeUris, + Acc ++ [Id || #{id := Id} <- Behaviours ++ ParseTransforms] ++ + [els_uri:module(FPTUri) || FPTUri <- FilteredPTUris] ++ + [els_uri:module(BeUri) || BeUri <- FilteredBeUris], + sets:add_element(Uri, AlreadyProcessed) + ); + {error, _Error} -> + [] + end. -spec exclude_already_processed([uri()], sets:set()) -> [uri()]. exclude_already_processed(Uris, AlreadyProcessed) -> - [Uri || Uri <- Uris, not sets:is_element(Uri, AlreadyProcessed)]. + [Uri || Uri <- Uris, not sets:is_element(Uri, AlreadyProcessed)]. -spec pt_deps(atom()) -> [uri()]. pt_deps(Module) -> - case els_utils:find_module(Module) of - {ok, Uri} -> - case els_utils:lookup_document(Uri) of - {ok, Document} -> - Applications = els_dt_document:pois(Document, [ application - , implicit_fun - ]), - applications_to_uris(Applications); - {error, _Error} -> - [] - end; - {error, Error} -> - ?LOG_INFO("Find module failed [module=~p] [error=~p]", [Module, Error]), - [] - end. - --spec applications_to_uris([poi()]) -> [uri()]. + case els_utils:find_module(Module) of + {ok, Uri} -> + case els_utils:lookup_document(Uri) of + {ok, Document} -> + Applications = els_dt_document:pois(Document, [ + application, + implicit_fun + ]), + applications_to_uris(Applications); + {error, _Error} -> + [] + end; + {error, Error} -> + ?LOG_INFO("Find module failed [module=~p] [error=~p]", [Module, Error]), + [] + end. + +-spec be_deps(atom()) -> [uri()]. +be_deps(Module) -> + case els_utils:find_module(Module) of + {ok, Uri} -> + case els_utils:lookup_document(Uri) of + {ok, Document} -> + Behaviours = els_dt_document:pois( + Document, + [ + behaviour + ] + ), + behaviours_to_uris(Behaviours); + {error, _Error} -> + [] + end; + {error, Error} -> + ?LOG_INFO("Find module failed [module=~p] [error=~p]", [Module, Error]), + [] + end. + +-spec applications_to_uris([els_poi:poi()]) -> [uri()]. applications_to_uris(Applications) -> - Modules = [M || #{id := {M, _F, _A}} <- Applications], - Fun = fun(M, Acc) -> - case els_utils:find_module(M) of - {ok, Uri} -> - [Uri|Acc]; - {error, Error} -> - ?LOG_INFO( "Could not find module [module=~p] [error=~p]" - , [M, Error] - ), + Modules = [M || #{id := {M, _F, _A}} <- Applications], + Fun = fun(M, Acc) -> + case els_utils:find_module(M) of + {ok, Uri} -> + [Uri | Acc]; + {error, Error} -> + ?LOG_INFO( + "Could not find module [module=~p] [error=~p]", + [M, Error] + ), Acc - end - end, - lists:foldl(Fun, [], Modules). + end + end, + lists:foldl(Fun, [], Modules). + +-spec behaviours_to_uris([els_poi:poi()]) -> [uri()]. +behaviours_to_uris(Behaviours) -> + Modules = [M || #{id := M} <- Behaviours], + Fun = fun(M, Acc) -> + case els_utils:find_module(M) of + {ok, Uri} -> + [Uri | Acc]; + {error, Error} -> + ?LOG_INFO( + "Could not find module [module=~p] [error=~p]", + [M, Error] + ), + Acc + end + end, + lists:foldl(Fun, [], Modules). -spec included_uris([atom()], [uri()]) -> [uri()]. included_uris([], Acc) -> - lists:usort(Acc); -included_uris([Id|Ids], Acc) -> - case els_utils:find_header(els_utils:filename_to_atom(Id)) of - {ok, Uri} -> included_uris(Ids, [Uri | Acc]); - {error, _Error} -> included_uris(Ids, Acc) - end. + lists:usort(Acc); +included_uris([Id | Ids], Acc) -> + case els_utils:find_header(els_utils:filename_to_atom(Id)) of + {ok, Uri} -> included_uris(Ids, [Uri | Acc]); + {error, _Error} -> included_uris(Ids, Acc) + end. diff --git a/apps/els_lsp/src/els_dialyzer_diagnostics.erl b/apps/els_lsp/src/els_dialyzer_diagnostics.erl index 47c3c9d90..2a82fe1ff 100644 --- a/apps/els_lsp/src/els_dialyzer_diagnostics.erl +++ b/apps/els_lsp/src/els_dialyzer_diagnostics.erl @@ -11,10 +11,11 @@ %%============================================================================== %% Exports %%============================================================================== --export([ is_default/0 - , run/1 - , source/0 - ]). +-export([ + is_default/0, + run/1, + source/0 +]). %%============================================================================== %% Includes @@ -34,63 +35,72 @@ -spec is_default() -> boolean(). is_default() -> - true. + true. -spec run(uri()) -> [els_diagnostics:diagnostic()]. run(Uri) -> - Path = els_uri:path(Uri), - case els_config:get(plt_path) of - undefined -> []; - DialyzerPltPath -> - {ok, Document} = els_utils:lookup_document(Uri), - Deps = [dep_path(X) || X <- els_diagnostics_utils:dependencies(Uri)], - Files = [els_utils:to_list(Path) | Deps], - WS = try dialyzer:run([ {files, Files} - , {from, src_code} - , {include_dirs, els_config:get(include_paths)} - , {plts, [DialyzerPltPath]} - , {defines, defines()} - ]) - catch Type:Error -> - ?LOG_ERROR( "Error while running dialyzer [type=~p] [error=~p]" - , [Type, Error] - ), - [] - end, - [diagnostic(Document, W) || W <- WS] - end. + Path = els_uri:path(Uri), + case els_config:get(plt_path) of + undefined -> + []; + DialyzerPltPath -> + {ok, Document} = els_utils:lookup_document(Uri), + Deps = [dep_path(X) || X <- els_diagnostics_utils:dependencies(Uri)], + Files = [els_utils:to_list(Path) | Deps], + WS = + try + dialyzer:run([ + {files, Files}, + {from, src_code}, + {include_dirs, els_config:get(include_paths)}, + {plts, [DialyzerPltPath]}, + {defines, defines()} + ]) + catch + Type:Error -> + ?LOG_ERROR( + "Error while running dialyzer [type=~p] [error=~p]", + [Type, Error] + ), + [] + end, + [diagnostic(Document, W) || W <- WS] + end. -spec source() -> binary(). source() -> - <<"Dialyzer">>. + <<"Dialyzer">>. %%============================================================================== %% Internal Functions %%============================================================================== --spec diagnostic(els_dt_document:item(), - {any(), {any(), erl_anno:anno()}, any()}) -> - els_diagnostics:diagnostic(). +-spec diagnostic( + els_dt_document:item(), + {any(), {any(), erl_anno:anno()}, any()} +) -> + els_diagnostics:diagnostic(). diagnostic(Document, {_, {_, Anno}, _} = Warning) -> - Range = els_diagnostics_utils:range(Document, Anno), - Message = lists:flatten(dialyzer:format_warning(Warning)), - #{ range => els_protocol:range(Range) - , message => els_utils:to_binary(Message) - , severity => ?DIAGNOSTIC_WARNING - , source => source() - }. + Range = els_diagnostics_utils:range(Document, Anno), + Message = lists:flatten(dialyzer:format_warning(Warning)), + #{ + range => els_protocol:range(Range), + message => els_utils:to_binary(Message), + severity => ?DIAGNOSTIC_WARNING, + source => source() + }. -spec dep_path(module()) -> string(). dep_path(Module) -> - {ok, Uri} = els_utils:find_module(Module), - els_utils:to_list(els_uri:path(Uri)). + {ok, Uri} = els_utils:find_module(Module), + els_utils:to_list(els_uri:path(Uri)). -spec defines() -> [macro_option()]. defines() -> - Macros = els_config:get(macros), - [define(M) || M <- Macros]. + Macros = els_config:get(macros), + [define(M) || M <- Macros]. -spec define(macro_config()) -> macro_option(). define(#{"name" := Name, "value" := Value}) -> - {list_to_atom(Name), els_utils:macro_string_to_term(Value)}; + {list_to_atom(Name), els_utils:macro_string_to_term(Value)}; define(#{"name" := Name}) -> - {list_to_atom(Name), true}. + {list_to_atom(Name), true}. diff --git a/apps/els_lsp/src/els_docs.erl b/apps/els_lsp/src/els_docs.erl index ddea8f2e2..83451e829 100644 --- a/apps/els_lsp/src/els_docs.erl +++ b/apps/els_lsp/src/els_docs.erl @@ -6,10 +6,11 @@ %%============================================================================== %% Exports %%============================================================================== --export([ docs/2 - , function_docs/4 - , type_docs/4 - ]). +-export([ + docs/2, + function_docs/4, + type_docs/4 +]). %%============================================================================== %% Includes @@ -17,13 +18,10 @@ -include("els_lsp.hrl"). -include_lib("kernel/include/logger.hrl"). --ifdef(OTP_RELEASE). --if(?OTP_RELEASE >= 23). -include_lib("kernel/include/eep48.hrl"). -export([eep48_docs/4]). +-export([eep59_docs/4]). -type docs_v1() :: #docs_v1{}. --endif. --endif. %%============================================================================== %% Macro Definitions @@ -36,70 +34,82 @@ -type application_type() :: 'local' | 'remote'. %%============================================================================== -%% Dialyer Ignores (due to upstream bug, see ERL-1262 +%% Dialyzer Ignores (due to upstream bug, see ERL-1262 %%============================================================================== -dialyzer({nowarn_function, function_docs/4}). %%============================================================================== %% API %%============================================================================== --spec docs(uri(), poi()) -> [els_markup_content:doc_entry()]. -docs(_Uri, #{kind := Kind, id := {M, F, A}}) - when Kind =:= application; - Kind =:= implicit_fun -> - function_docs('remote', M, F, A); -docs(Uri, #{kind := Kind, id := {F, A}}) - when Kind =:= application; - Kind =:= implicit_fun; - Kind =:= export_entry -> - M = els_uri:module(Uri), - function_docs('local', M, F, A); +-spec docs(uri(), els_poi:poi()) -> [els_markup_content:doc_entry()]. +docs(_Uri, #{kind := Kind, id := {M, F, A}}) when + Kind =:= application; + Kind =:= implicit_fun +-> + function_docs('remote', M, F, A); +docs(Uri, #{kind := Kind, id := {F, A}}) when + Kind =:= application; + Kind =:= implicit_fun; + Kind =:= export_entry; + Kind =:= nifs_entry; + Kind =:= spec +-> + M = els_uri:module(Uri), + function_docs('local', M, F, A); +docs(Uri, #{kind := function_clause, id := {F, A, _Index}}) -> + M = els_uri:module(Uri), + function_docs('local', M, F, A); docs(Uri, #{kind := macro, id := Name} = POI) -> - case els_code_navigation:goto_definition(Uri, POI) of - {ok, DefUri, #{data := #{args := Args, value_range := ValueRange}}} - when is_list(Args); is_atom(Name) -> - NameStr = macro_signature(Name, Args), - - ValueText = get_valuetext(DefUri, ValueRange), - - Line = lists:flatten(["?", NameStr, " = ", ValueText]), - [{code_line, Line}]; - _ -> - [] - end; + case els_code_navigation:goto_definition(Uri, POI) of + {ok, [{DefUri, #{data := #{args := Args, value_range := ValueRange}}}]} when + is_list(Args); is_atom(Name) + -> + NameStr = macro_signature(Name, Args), + + ValueText = get_valuetext(DefUri, ValueRange), + + Line = lists:flatten(["?", NameStr, " = ", ValueText]), + [{code_line, Line}]; + _ -> + [] + end; docs(Uri, #{kind := record_expr} = POI) -> - case els_code_navigation:goto_definition(Uri, POI) of - {ok, DefUri, #{data := #{value_range := ValueRange}}} -> - ValueText = get_valuetext(DefUri, ValueRange), - - [{code_line, ValueText}]; - _ -> - [] - end; -docs(_M, #{kind := type_application, id := {M, F, A}}) -> - type_docs('remote', M, F, A); -docs(Uri, #{kind := type_application, id := {F, A}}) -> - type_docs('local', els_uri:module(Uri), F, A); + case els_code_navigation:goto_definition(Uri, POI) of + {ok, [{DefUri, #{data := #{value_range := ValueRange}}}]} -> + ValueText = get_valuetext(DefUri, ValueRange), + + [{code_line, ValueText}]; + _ -> + [] + end; +docs(_M, #{kind := Kind, id := {M, F, A}}) when + Kind =:= type_application; + Kind =:= type_definition +-> + type_docs('remote', M, F, A); +docs(Uri, #{kind := Kind, id := {F, A}}) when + Kind =:= type_application; + Kind =:= type_definition +-> + type_docs('local', els_uri:module(Uri), F, A); docs(_M, _POI) -> - []. + []. %%============================================================================== %% Internal Functions %%============================================================================== -spec function_docs(application_type(), atom(), atom(), non_neg_integer()) -> - [els_markup_content:doc_entry()]. + [els_markup_content:doc_entry()]. function_docs(Type, M, F, A) -> - case eep48_docs(function, M, F, A) of - {ok, Docs} -> - [{text, Docs}]; - {error, not_available} -> - %% We cannot fetch the EEP-48 style docs, so instead we create - %% something similar using the tools we have. + case edoc_parse_enabled() of + true -> + function_docs(Type, M, F, A, els_config:get(docs_memo)); + false -> Sig = {h2, signature(Type, M, F, A)}, - L = [ function_clauses(M, F, A) - , specs(M, F, A) - , edoc(M, F, A) - ], + L = [ + function_clauses(M, F, A), + specs(M, F, A) + ], case lists:append(L) of [] -> [Sig]; @@ -108,10 +118,70 @@ function_docs(Type, M, F, A) -> end end. +-spec function_docs(application_type(), atom(), atom(), non_neg_integer(), boolean()) -> + [els_markup_content:doc_entry()]. +function_docs(Type, M, F, A, true = _DocsMemo) -> + MFACT = {M, F, A, Type, function}, + case els_docs_memo:lookup(MFACT) of + {ok, [#{entries := Entries}]} -> + Entries; + {ok, []} -> + Entries = function_docs(Type, M, F, A, false), + ok = els_docs_memo:insert(#{mfact => MFACT, entries => Entries}), + Entries + end; +function_docs(Type, M, F, A, false = _DocsMemo) -> + %% call via ?MODULE to enable mocking in tests + case ?MODULE:eep59_docs(function, M, F, A) of + {ok, Docs} -> + [{text, Docs}]; + {error, not_available} -> + case ?MODULE:eep48_docs(function, M, F, A) of + {ok, Docs} -> + [{text, Docs}]; + {error, not_available} -> + %% We cannot fetch the EEP-48 style docs, so instead we create + %% something similar using the tools we have. + Sig = {h2, signature(Type, M, F, A)}, + L = [ + function_clauses(M, F, A), + specs(M, F, A), + edoc(M, F, A) + ], + case lists:append(L) of + [] -> + [Sig]; + Docs -> + [Sig, {text, "---"} | Docs] + end + end + end. + -spec type_docs(application_type(), atom(), atom(), non_neg_integer()) -> - [els_markup_content:doc_entry()]. -type_docs(_Type, M, F, A) -> - case eep48_docs(type, M, F, A) of + [els_markup_content:doc_entry()]. +type_docs(Type, M, F, A) -> + case edoc_parse_enabled() of + true -> + type_docs(Type, M, F, A, els_config:get(docs_memo)); + false -> + type(M, F, A) + end. + +-spec type_docs(application_type(), atom(), atom(), non_neg_integer(), boolean()) -> + [els_markup_content:doc_entry()]. +type_docs(Type, M, F, A, true = _DocsMemo) -> + MFACT = {M, F, A, Type, type}, + case els_docs_memo:lookup(MFACT) of + {ok, [#{entries := Entries}]} -> + Entries; + {ok, []} -> + Entries = type_docs(Type, M, F, A, false), + ok = els_docs_memo:insert(#{mfact => MFACT, entries => Entries}), + Entries + end; +type_docs(_Type, M, F, A, false = _DocsMemo) -> + %% call via ?MODULE to enable mocking in tests + case ?MODULE:eep48_docs(type, M, F, A) of {ok, Docs} -> [{text, Docs}]; {error, not_available} -> @@ -120,16 +190,15 @@ type_docs(_Type, M, F, A) -> -spec get_valuetext(uri(), map()) -> list(). get_valuetext(DefUri, #{from := From, to := To}) -> - {ok, #{text := Text}} = els_utils:lookup_document(DefUri), - els_utils:to_list(els_text:range(Text, From, To)). - + {ok, #{text := Text}} = els_utils:lookup_document(DefUri), + els_utils:to_list(els_text:range(Text, From, To)). -spec signature(application_type(), atom(), atom(), non_neg_integer()) -> - string(). + string(). signature('local', _M, F, A) -> - io_lib:format("~p/~p", [F, A]); + io_lib:format("~p/~p", [F, A]); signature('remote', M, F, A) -> - io_lib:format("~p:~p/~p", [M, F, A]). + io_lib:format("~p:~p/~p", [M, F, A]). %% @doc Fetch EEP-48 style Docs %% @@ -140,48 +209,143 @@ signature('remote', M, F, A) -> %% If it is not available it tries to create the EEP-48 style docs %% using edoc. -ifdef(NATIVE_FORMAT). +-define(MARKDOWN_FORMAT, <<"text/markdown">>). + -spec eep48_docs(function | type, atom(), atom(), non_neg_integer()) -> - {ok, string()} | {error, not_available}. + {ok, string()} | {error, not_available}. eep48_docs(Type, M, F, A) -> - Render = case Type of - function -> - render; - type -> - render_type - end, - GL = setup_group_leader_proxy(), - try get_doc_chunk(M) of - {ok, #docs_v1{ format = ?NATIVE_FORMAT - , module_doc = MDoc - } = DocChunk} when MDoc =/= hidden -> - - flush_group_leader_proxy(GL), - - case els_eep48_docs:Render(M, F, A, DocChunk) of - {error, _R0} -> - case els_eep48_docs:Render(M, F, DocChunk) of - {error, _R1} -> - {error, not_available}; - Docs -> - {ok, els_utils:to_list(Docs)} - end; - Docs -> - {ok, els_utils:to_list(Docs)} - end; - _R1 -> - ?LOG_DEBUG(#{ error => _R1 }), - {error, not_available} - catch C:E:ST -> - %% code:get_doc/1 fails for escriptized modules, so fall back - %% reading docs from source. See #751 for details - IO = flush_group_leader_proxy(GL), - ?LOG_DEBUG(#{ slogan => "Error fetching docs, falling back to src.", - module => M, - error => {C, E}, - st => ST, - io => IO }), - {error, not_available} - end. + GL = setup_group_leader_proxy(), + try get_doc_chunk(M) of + {ok, + #docs_v1{ + format = Format, + module_doc = MDoc + } = DocChunk} when + MDoc =/= hidden, + (Format == ?MARKDOWN_FORMAT orelse + Format == ?NATIVE_FORMAT) + -> + flush_group_leader_proxy(GL), + render_doc(Type, M, F, A, DocChunk); + _R1 -> + ?LOG_DEBUG(#{error => _R1}), + {error, not_available} + catch + C:E:ST -> + %% code:get_doc/1 fails for escriptized modules, so fall back + %% reading docs from source. See #751 for details + IO = flush_group_leader_proxy(GL), + ?LOG_DEBUG(#{ + slogan => "Error fetching docs, falling back to src.", + module => M, + error => {C, E}, + st => ST, + io => IO + }), + {error, not_available} + end. + +-spec eep59_docs(function | type, atom(), atom(), non_neg_integer()) -> + {ok, string()} | {error, not_available}. +eep59_docs(Type, M, F, A) -> + try get_doc(M) of + {ok, + #docs_v1{ + format = Format, + module_doc = MDoc + } = DocChunk} when + MDoc =/= hidden, + (Format == ?MARKDOWN_FORMAT orelse + Format == ?NATIVE_FORMAT) + -> + render_doc(Type, M, F, A, DocChunk); + _R1 -> + ?LOG_DEBUG(#{error => _R1}), + {error, not_available} + catch + C:E:ST -> + %% code:get_doc/1 fails for escriptized modules, so fall back + %% reading docs from source. See #751 for details + ?LOG_DEBUG(#{ + slogan => "Error fetching docs, falling back to src.", + module => M, + error => {C, E}, + st => ST + }), + {error, not_available} + end. + +-spec get_doc(module()) -> {ok, docs_v1()} | {error, not_available}. +get_doc(Module) when is_atom(Module) -> + %% This will error if module isn't loaded + try code:get_doc(Module) of + {ok, DocChunk} -> + {ok, DocChunk}; + {error, _} -> + %% If the module isn't loaded, we try + %% to find the doc chunks from any .beam files + %% matching the module name. + Beams = find_beams(Module), + get_doc(Beams, Module) + catch + C:E:ST -> + %% code:get_doc/1 fails for escriptized modules, so fall back + %% reading docs from source. See #751 for details + ?LOG_INFO(#{ + slogan => "Error fetching docs, falling back to src.", + module => Module, + error => {C, E}, + st => ST + }), + {error, not_available} + end. + +-spec get_doc([file:filename()], module()) -> + {ok, docs_v1()} | {error, not_available}. +get_doc([], _Module) -> + {error, not_available}; +get_doc([Beam | T], Module) -> + case beam_lib:chunks(Beam, ["Docs"]) of + {ok, {Module, [{"Docs", Bin}]}} -> + {ok, binary_to_term(Bin)}; + _ -> + get_doc(T, Module) + end. + +-spec find_beams(module()) -> [file:filename()]. +find_beams(Module) -> + %% Look for matching .beam files under the project root + RootUri = els_config:get(root_uri), + Root = binary_to_list(els_uri:path(RootUri)), + Beams0 = filelib:wildcard( + filename:join([Root, "**", atom_to_list(Module) ++ ".beam"]) + ), + %% Sort the beams, to ensure we try the newest beam first + TimeBeams = [{filelib:last_modified(Beam), Beam} || Beam <- Beams0], + {_, Beams} = lists:unzip(lists:reverse(lists:sort(TimeBeams))), + Beams. + +-spec render_doc(function | type, module(), atom(), arity(), docs_v1()) -> + {ok, string()} | {error, not_available}. +render_doc(Type, M, F, A, DocChunk) -> + Render = + case Type of + function -> + render; + type -> + render_type + end, + case els_eep48_docs:Render(M, F, A, DocChunk) of + {error, _R0} -> + case els_eep48_docs:Render(M, F, DocChunk) of + {error, _R1} -> + {error, not_available}; + Docs -> + {ok, els_utils:to_list(Docs)} + end; + Docs -> + {ok, els_utils:to_list(Docs)} + end. %% This function first tries to read the doc chunk from the .beam file %% and if that fails it attempts to find the .chunk file. @@ -195,12 +359,18 @@ get_doc_chunk(M) -> %% Erlang/OTP there will be two "lists" modules, and we want to %% fetch the docs from any version that has docs built. - case lists:foldl( - fun(Uri, undefined) -> - get_doc_chunk(M, Uri); - (_Uri, Chunk) -> - Chunk - end, undefined, Uris) of + case + lists:foldl( + fun + (Uri, undefined) -> + get_doc_chunk(M, Uri); + (_Uri, Chunk) -> + Chunk + end, + undefined, + Uris + ) + of undefined -> get_edoc_chunk(M, hd(Uris)); Chunk -> @@ -209,11 +379,20 @@ get_doc_chunk(M) -> -spec get_doc_chunk(module(), uri()) -> docs_v1() | undefined. get_doc_chunk(M, Uri) -> - SrcDir = filename:dirname(els_utils:to_list(els_uri:path(Uri))), - BeamFile = filename:join([SrcDir, "..", "ebin", - lists:concat([M, ".beam"])]), - ChunkFile = filename:join([SrcDir, "..", "doc", "chunks", - lists:concat([M, ".chunk"])]), + SrcDir = filename:dirname(els_utils:to_list(els_uri:path(Uri))), + BeamFile = filename:join([ + SrcDir, + "..", + "ebin", + lists:concat([M, ".beam"]) + ]), + ChunkFile = filename:join([ + SrcDir, + "..", + "doc", + "chunks", + lists:concat([M, ".chunk"]) + ]), case beam_lib:chunks(BeamFile, ["Docs"]) of {ok, {_Mod, [{"Docs", Bin}]}} -> binary_to_term(Bin); @@ -229,62 +408,131 @@ get_doc_chunk(M, Uri) -> -spec get_edoc_chunk(M :: module(), Uri :: uri()) -> {ok, term()} | error. get_edoc_chunk(M, Uri) -> %% edoc in Erlang/OTP 24 and later can create doc chunks for edoc - case {code:ensure_loaded(edoc_doclet_chunks), - code:ensure_loaded(edoc_layout_chunks)} of + case {code:ensure_loaded(edoc_doclet_chunks), code:ensure_loaded(edoc_layout_chunks)} of {{module, _}, {module, _}} -> - Path = els_uri:path(Uri), - Dir = erlang_ls:cache_root(), - ok = edoc:run([els_utils:to_list(Path)], - [{doclet, edoc_doclet_chunks}, - {layout, edoc_layout_chunks}, - {dir, Dir} | edoc_options()]), - Chunk = filename:join([Dir, "chunks", atom_to_list(M) ++ ".chunk"]), - {ok, Bin} = file:read_file(Chunk), - {ok, binary_to_term(Bin)}; + case edoc_run(Uri) of + ok -> + {ok, Bin} = file:read_file(chunk_file_path(M)), + {ok, binary_to_term(Bin)}; + error -> + error + end; E -> ?LOG_DEBUG("[edoc_chunk] load error", [E]), error end. + +-spec chunk_file_path(module()) -> file:filename_all(). +chunk_file_path(M) -> + Dir = erlang_ls:cache_root(), + filename:join([Dir, "chunks", atom_to_list(M) ++ ".chunk"]). + +-spec is_chunk_file_up_to_date(binary(), module()) -> boolean(). +is_chunk_file_up_to_date(Path, Module) -> + ChunkPath = chunk_file_path(Module), + filelib:is_file(ChunkPath) andalso + filelib:last_modified(ChunkPath) > filelib:last_modified(Path). + +-spec edoc_run(uri()) -> ok | error. +edoc_run(Uri) -> + Ref = make_ref(), + Module = els_uri:module(Uri), + Path = els_uri:path(Uri), + Opts = [ + {doclet, edoc_doclet_chunks}, + {layout, edoc_layout_chunks}, + {dir, erlang_ls:cache_root()} + | edoc_options() + ], + Parent = self(), + case is_chunk_file_up_to_date(Path, Module) of + true -> + ?LOG_DEBUG("Chunk file is up to date!"), + ok; + false -> + %% Run job to generate chunk file + %% This can be slow, run it in a spawned process so + %% we can timeout + spawn_link( + fun() -> + Name = list_to_atom(lists:concat(['docs_', Module])), + try + %% Use register to ensure we only run one of these + %% processes at the same time. + true = register(Name, self()), + ?LOG_DEBUG("Generating doc chunks for ~s.", [Module]), + Res = edoc:run([els_utils:to_list(Path)], Opts), + ?LOG_DEBUG("Done generating doc chunks for ~s.", [Module]), + Parent ! {Ref, Res} + catch + _:Err:ST -> + ?LOG_INFO( + "Generating do chunks for ~s failed: ~p\n~p", + [Module, Err, ST] + ), + %% Respond to parent with error + Parent ! {Ref, error} + end + end + ), + receive + {Ref, Res} -> + Res + after 1000 -> + %% This took too long, return and let job continue + %% running in background in order to let it generate + %% a chunk file + error + end + end. + -else. --dialyzer({no_match, function_docs/4}). --dialyzer({no_match, type_docs/4}). +-dialyzer({no_match, function_docs/5}). +-dialyzer({no_match, type_docs/5}). -spec eep48_docs(function | type, atom(), atom(), non_neg_integer()) -> - {error, not_available}. + {error, not_available}. eep48_docs(_Type, _M, _F, _A) -> {error, not_available}. -endif. --spec edoc_options() -> [{'includes' | 'macros', [any()]} | - {'preprocess', 'true'}]. +-spec edoc_options() -> + [ + {'includes' | 'macros', [any()]} + | {'preprocess', 'true'} + ]. edoc_options() -> - [{preprocess, true}, - {macros, - [{N, V} || {'d', N, V} <- els_compiler_diagnostics:macro_options()]}, - {includes, - [I || {i, I} <- els_compiler_diagnostics:include_options()]}]. + [ + {preprocess, true}, + {macros, [{N, V} || {'d', N, V} <- els_compiler_diagnostics:macro_options()]}, + {includes, [I || {i, I} <- els_compiler_diagnostics:include_options()]} + ]. -spec specs(atom(), atom(), non_neg_integer()) -> - [els_markup_content:doc_entry()]. + [els_markup_content:doc_entry()]. specs(M, F, A) -> - case els_dt_signatures:lookup({M, F, A}) of - {ok, [#{spec := Spec}]} -> - [ {code_line, els_utils:to_list(Spec)} ]; - {ok, []} -> - [] - end. + case els_dt_signatures:lookup({M, F, A}) of + {ok, [#{spec := Spec}]} -> + [{code_line, els_utils:to_list(Spec)}]; + {ok, []} -> + [] + end. -spec type(module(), atom(), arity()) -> - [els_markup_content:doc_entry()]. + [els_markup_content:doc_entry()]. type(M, T, A) -> case els_utils:find_module(M) of {ok, Uri} -> {ok, Document} = els_utils:lookup_document(Uri), ExportedTypes = els_dt_document:pois(Document, [type_definition]), - case lists:search( - fun(#{ id := Id }) -> - Id =:= {T, A} - end, ExportedTypes) of - {value, #{ range := Range }} -> + case + lists:search( + fun(#{id := Id}) -> + Id =:= {T, A} + end, + ExportedTypes + ) + of + {value, #{range := Range}} -> [{code_line, get_valuetext(Uri, Range)}]; false -> [] @@ -294,127 +542,161 @@ type(M, T, A) -> end. -spec function_clauses(atom(), atom(), non_neg_integer()) -> - [els_markup_content:doc_entry()]. + [els_markup_content:doc_entry()]. function_clauses(_Module, _Function, 0) -> - []; + []; function_clauses(Module, Function, Arity) -> - case els_utils:find_module(Module) of - {ok, Uri} -> - {ok, Doc} = els_utils:lookup_document(Uri), - ClausesPOIs = els_dt_document:pois(Doc, [function_clause]), - Lines = [{code_block_line, atom_to_list(F) ++ els_utils:to_list(Data)} - || #{id := {F, A, _}, data := Data} <- ClausesPOIs, - F =:= Function, A =:= Arity], - lists:append([ [{code_block_begin, "erlang"}] - , truncate_lines(Lines) - , [{code_block_end, "erlang"}] - ]); - {error, _Reason} -> - [] - end. + case els_utils:find_module(Module) of + {ok, Uri} -> + {ok, Doc} = els_utils:lookup_document(Uri), + ClausesPOIs = els_dt_document:pois(Doc, [function_clause]), + Lines = [ + {code_block_line, atom_to_list(F) ++ els_utils:to_list(Data)} + || #{id := {F, A, _}, data := Data} <- ClausesPOIs, + F =:= Function, + A =:= Arity + ], + lists:append([ + [{code_block_begin, "erlang"}], + truncate_lines(Lines), + [{code_block_end, "erlang"}] + ]); + {error, _Reason} -> + [] + end. -spec truncate_lines([els_markup_content:doc_entry()]) -> - [els_markup_content:doc_entry()]. + [els_markup_content:doc_entry()]. truncate_lines(Lines) when length(Lines) =< ?MAX_CLAUSES -> - Lines; + Lines; truncate_lines(Lines0) -> - Lines = lists:sublist(Lines0, ?MAX_CLAUSES), - lists:append(Lines, [{code_block_line, "[...]"}]). + Lines = lists:sublist(Lines0, ?MAX_CLAUSES), + lists:append(Lines, [{code_block_line, "[...]"}]). -spec edoc(atom(), atom(), non_neg_integer()) -> - [els_markup_content:doc_entry()]. + [els_markup_content:doc_entry()]. edoc(M, F, A) -> - case els_utils:find_module(M) of - {ok, Uri} -> - GL = setup_group_leader_proxy(), - try - Path = els_uri:path(Uri), - {M, EDoc} = edoc:get_doc( - els_utils:to_list(Path) - , [{private, true} - , edoc_options()] ), - Internal = xmerl:export_simple([EDoc], docsh_edoc_xmerl), - %% TODO: Something is weird with the docsh specs. - %% For now, let's avoid the Dialyzer warnings. - Docs = erlang:apply(docsh_docs_v1, from_internal, [Internal]), - Res = erlang:apply(docsh_docs_v1, lookup, [ Docs - , {M, F, A} - , [doc, spec]]), - flush_group_leader_proxy(GL), - - {ok, [{{function, F, A}, _Anno, - _Signature, Desc, _Metadata}|_]} = Res, - format_edoc(Desc) - catch C:E:ST -> - IO = flush_group_leader_proxy(GL), - ?LOG_DEBUG("[hover] Error fetching edoc [error=~p]", - [{M, F, A, C, E, ST, IO}]), - case IO of - timeout -> - []; - IO -> - [{text, IO}] - end - end; - _ -> - [] - end. + case els_utils:find_module(M) of + {ok, Uri} -> + GL = setup_group_leader_proxy(), + try + Path = els_uri:path(Uri), + {M, EDoc} = edoc:get_doc( + els_utils:to_list(Path), + [ + {private, true}, + edoc_options() + ] + ), + Internal = xmerl:export_simple([EDoc], docsh_edoc_xmerl), + %% TODO: Something is weird with the docsh specs. + %% For now, let's avoid the Dialyzer warnings. + Docs = erlang:apply(docsh_docs_v1, from_internal, [Internal]), + Res = erlang:apply(docsh_docs_v1, lookup, [ + Docs, + {M, F, A}, + [doc, spec] + ]), + flush_group_leader_proxy(GL), + + case Res of + {ok, [{{function, F, A}, _Anno, _Signature, Desc, _Metadata} | _]} -> + format_edoc(Desc); + {not_found, _} -> + [] + end + catch + C:E:ST -> + IO = flush_group_leader_proxy(GL), + ?LOG_DEBUG( + "[hover] Error fetching edoc [error=~p]", + [{M, F, A, C, E, ST, IO}] + ), + case IO of + timeout -> + []; + noproc -> + []; + IO -> + [{text, IO}] + end + end; + _ -> + [] + end. -spec format_edoc(none | map()) -> [els_markup_content:doc_entry()]. format_edoc(none) -> - []; + []; format_edoc(Desc) when is_map(Desc) -> - Lang = <<"en">>, - Doc = maps:get(Lang, Desc, <<>>), - FormattedDoc = els_utils:to_list(docsh_edoc:format_edoc(Doc, #{})), - [{text, FormattedDoc}]. + Lang = <<"en">>, + Doc = maps:get(Lang, Desc, <<>>), + FormattedDoc = els_utils:to_list(docsh_edoc:format_edoc(Doc, #{})), + [{text, FormattedDoc}]. --spec macro_signature(poi_id(), [{integer(), string()}]) -> unicode:charlist(). +-spec macro_signature(els_poi:poi_id(), els_arg:args()) -> unicode:charlist(). macro_signature({Name, _Arity}, Args) -> - [atom_to_list(Name), "(", lists:join(", ", [A || {_N, A} <- Args]), ")"]; + [atom_to_list(Name), "(", lists:join(", ", [els_arg:name(A) || A <- Args]), ")"]; macro_signature(Name, none) -> - atom_to_list(Name). + atom_to_list(Name). -spec setup_group_leader_proxy() -> pid(). setup_group_leader_proxy() -> OrigGL = group_leader(), group_leader( - spawn_link( - fun() -> + spawn_link( + fun() -> spawn_group_proxy([]) - end), - self()), + end + ), + self() + ), OrigGL. -spec flush_group_leader_proxy(pid()) -> [term()] | term(). flush_group_leader_proxy(OrigGL) -> GL = group_leader(), - Ref = monitor(process, GL), - group_leader(OrigGL, self()), - GL ! {get, Ref, self()}, - receive - {Ref, Msg} -> - demonitor(Ref, [flush]), - Msg; - {'DOWN', process, Ref, Reason} -> - Reason + case GL of + OrigGL -> + % This is the effect of setting a monitor on nonexisting process. + noproc; + _ -> + Ref = monitor(process, GL), + group_leader(OrigGL, self()), + GL ! {get, Ref, self()}, + receive + {Ref, Msg} -> + demonitor(Ref, [flush]), + Msg; + {'DOWN', process, Ref, Reason} -> + Reason + after 5000 -> + demonitor(Ref, [flush]), + timeout + end end. -spec spawn_group_proxy([any()]) -> ok. spawn_group_proxy(Acc) -> receive {get, Ref, Pid} -> - Pid ! {Ref, lists:reverse(Acc)}, ok; + Pid ! {Ref, lists:reverse(Acc)}, + ok; {io_request, From, ReplyAs, {put_chars, unicode, Chars}} -> From ! {io_reply, ReplyAs, ok}, - spawn_group_proxy([catch unicode:characters_to_binary(Chars)|Acc]); + spawn_group_proxy([catch unicode:characters_to_binary(Chars) | Acc]); {io_request, From, ReplyAs, {put_chars, unicode, M, F, As}} -> From ! {io_reply, ReplyAs, ok}, spawn_group_proxy( - [catch unicode:characters_to_binary(apply(M, F, As))|Acc]); + [catch unicode:characters_to_binary(apply(M, F, As)) | Acc] + ); {io_request, From, ReplyAs, _Request} = M -> From ! {io_reply, ReplyAs, ok}, - spawn_group_proxy([M|Acc]); + spawn_group_proxy([M | Acc]); M -> - spawn_group_proxy([M|Acc]) + spawn_group_proxy([M | Acc]) end. + +-spec edoc_parse_enabled() -> boolean(). +edoc_parse_enabled() -> + true == els_config:get(edoc_parse_enabled). diff --git a/apps/els_lsp/src/els_docs_memo.erl b/apps/els_lsp/src/els_docs_memo.erl new file mode 100644 index 000000000..af3a32dea --- /dev/null +++ b/apps/els_lsp/src/els_docs_memo.erl @@ -0,0 +1,105 @@ +%%============================================================================== +%% The 'docs' table +%%============================================================================== + +-module(els_docs_memo). + +%%============================================================================== +%% Behaviour els_db_table +%%============================================================================== + +-behaviour(els_db_table). +-export([ + name/0, + opts/0 +]). + +%%============================================================================== +%% API +%%============================================================================== + +-export([ + insert/1, + lookup/1, + delete_by_uri/1 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include("els_lsp.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +%%============================================================================== +%% Item Definition +%%============================================================================== + +-record(els_docs_memo, { + mfact :: mfact() | '_' | {atom(), '_', '_', call_type() | '_', function | type | '_'}, + entries :: [els_markup_content:doc_entry()] | '_' +}). +-type call_type() :: 'local' | 'remote'. +-type mfact() :: {module(), atom(), arity(), call_type(), function | type}. +-type els_docs_memo() :: #els_docs_memo{}. +-type item() :: #{ + mfact := mfact(), + entries := [els_markup_content:doc_entry()] +}. +-export_type([item/0]). + +%%============================================================================== +%% Callbacks for the els_db_table Behaviour +%%============================================================================== + +-spec name() -> atom(). +name() -> ?MODULE. + +-spec opts() -> proplists:proplist(). +opts() -> + [ordered_set]. + +%%============================================================================== +%% API +%%============================================================================== + +-spec from_item(item()) -> els_docs_memo(). +from_item(#{ + mfact := MFACT, + entries := Entries +}) -> + #els_docs_memo{ + mfact = MFACT, + entries = Entries + }. + +-spec to_item(els_docs_memo()) -> item(). +to_item(#els_docs_memo{ + mfact = MFACT, + entries = Entries +}) -> + #{ + mfact => MFACT, + entries => Entries + }. + +-spec insert(item()) -> ok | {error, any()}. +insert(Map) when is_map(Map) -> + Record = from_item(Map), + els_db:write(name(), Record). + +-spec lookup(mfact()) -> {ok, [item()]}. +lookup({M, _F, _A, _C, _T} = MFACT) -> + {ok, _Uris} = els_utils:find_modules(M), + {ok, Items} = els_db:lookup(name(), MFACT), + {ok, [to_item(Item) || Item <- Items]}. + +-spec delete_by_uri(uri()) -> ok. +delete_by_uri(Uri) -> + case filename:extension(Uri) of + <<".erl">> -> + Module = els_uri:module(Uri), + Pattern = #els_docs_memo{mfact = {Module, '_', '_', '_', '_'}, _ = '_'}, + ok = els_db:match_delete(name(), Pattern); + _ -> + ok + end. diff --git a/apps/els_lsp/src/els_document_highlight_provider.erl b/apps/els_lsp/src/els_document_highlight_provider.erl index ce94f82a2..089e06e26 100644 --- a/apps/els_lsp/src/els_document_highlight_provider.erl +++ b/apps/els_lsp/src/els_document_highlight_provider.erl @@ -2,119 +2,142 @@ -behaviour(els_provider). --export([ handle_request/2 - , is_enabled/0 - ]). +-export([ + handle_request/1 +]). %%============================================================================== %% Includes %%============================================================================== -include("els_lsp.hrl"). -%%============================================================================== -%% Types -%%============================================================================== --type state() :: any(). - %%============================================================================== %% els_provider functions %%============================================================================== +-spec handle_request(any()) -> {response, any()}. +handle_request({document_highlight, Params}) -> + #{ + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + {ok, Document} = els_utils:lookup_document(Uri), + Highlights = + case valid_highlight_pois(Document, Line, Character) of + [POI | _] -> find_highlights(Document, POI); + [] -> null + end, + case {Highlights, wrangler_handler:get_highlights(Uri, Line, Character)} of + {H, null} -> {response, H}; + {_, H} -> {response, H} + %% overwrites them for more transparent Wrangler forms. + end. --spec is_enabled() -> boolean(). -is_enabled() -> - true. - --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({document_highlight, Params}, State) -> - #{ <<"position">> := #{ <<"line">> := Line - , <<"character">> := Character - } - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - {ok, Document} = els_utils:lookup_document(Uri), - case - els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1) - of - [POI | _] -> {find_highlights(Document, POI), State}; - [] -> {null, State} - end. +-spec valid_highlight_pois(els_dt_document:item(), integer(), integer()) -> + [els_poi:poi()]. +valid_highlight_pois(Document, Line, Character) -> + POIs = els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1), + [ + P + || #{kind := Kind} = P <- POIs, + Kind /= keyword_expr, + Kind /= module, + Kind /= function_clause + ]. %%============================================================================== %% Internal functions %%============================================================================== --spec find_highlights(els_dt_document:item(), poi()) -> any(). -find_highlights(Document, #{ id := Id, kind := atom }) -> - AtomHighlights = do_find_highlights(Document, Id, [ atom ]), - FieldPOIs = els_dt_document:pois(Document, [ record_def_field - , record_field]), - FieldHighlights = [document_highlight(R) || - #{id := I, range := R} <- FieldPOIs, - element(2, I) =:= Id - ], - normalize_result(AtomHighlights ++ FieldHighlights); -find_highlights(Document, #{ id := Id, kind := Kind }) -> - POIs = els_dt_document:pois(Document, find_similar_kinds(Kind)), - Highlights = [document_highlight(R) || - #{id := I, kind := K, range := R} <- POIs, - I =:= Id, - K =/= 'folding_range' - ], - normalize_result(Highlights). +-spec find_highlights(els_dt_document:item(), els_poi:poi()) -> any(). +find_highlights(Document, #{id := Id, kind := atom}) -> + AtomHighlights = do_find_highlights(Document, Id, [atom]), + FieldPOIs = els_dt_document:pois(Document, [ + record_def_field, + record_field + ]), + FieldHighlights = [ + document_highlight(R) + || #{id := I, range := R} <- FieldPOIs, + element(2, I) =:= Id + ], + normalize_result(AtomHighlights ++ FieldHighlights); +find_highlights(Document, #{id := Id, kind := Kind}) -> + POIs = els_dt_document:pois(Document, find_similar_kinds(Kind)), + Highlights = [ + document_highlight(R) + || #{id := I, range := R} <- POIs, + I =:= Id + ], + normalize_result(Highlights). --spec do_find_highlights(els_dt_document:item() , poi_id() , [poi_kind()]) - -> any(). +-spec do_find_highlights(els_dt_document:item(), els_poi:poi_id(), [els_poi:poi_kind()]) -> + any(). do_find_highlights(Document, Id, Kinds) -> - POIs = els_dt_document:pois(Document, Kinds), - _Highlights = [document_highlight(R) || - #{id := I, range := R} <- POIs, - I =:= Id - ]. + POIs = els_dt_document:pois(Document, Kinds), + _Highlights = [ + document_highlight(R) + || #{id := I, range := R} <- POIs, + I =:= Id + ]. --spec document_highlight(poi_range()) -> map(). +-spec document_highlight(els_poi:poi_range()) -> map(). document_highlight(Range) -> - #{ range => els_protocol:range(Range) - , kind => ?DOCUMENT_HIGHLIGHT_KIND_TEXT - }. + #{ + range => els_protocol:range(Range), + kind => ?DOCUMENT_HIGHLIGHT_KIND_TEXT + }. -spec normalize_result([map()]) -> [map()] | null. normalize_result([]) -> - null; + null; normalize_result(L) when is_list(L) -> - L. + L. --spec find_similar_kinds(poi_kind()) -> [poi_kind()]. +-spec find_similar_kinds(els_poi:poi_kind()) -> [els_poi:poi_kind()]. find_similar_kinds(Kind) -> - find_similar_kinds(Kind, kind_groups()). + find_similar_kinds(Kind, kind_groups()). --spec find_similar_kinds(poi_kind(), [[poi_kind()]]) -> [poi_kind()]. +-spec find_similar_kinds(els_poi:poi_kind(), [[els_poi:poi_kind()]]) -> [els_poi:poi_kind()]. find_similar_kinds(Kind, []) -> - [Kind]; + [Kind]; find_similar_kinds(Kind, [Group | Groups]) -> - case lists:member(Kind, Group) of - true -> - Group; - false -> - find_similar_kinds(Kind, Groups) - end. + case lists:member(Kind, Group) of + true -> + Group; + false -> + find_similar_kinds(Kind, Groups) + end. %% Each group represents a list of POI kinds which represent the same or similar %% objects (usually the definition and the usages of an object). Each POI kind %% in one group must have the same id format. --spec kind_groups() -> [[poi_kind()]]. +-spec kind_groups() -> [[els_poi:poi_kind()]]. kind_groups() -> - [ %% function - [ application - , implicit_fun - , function - , export_entry] - %% record - , [ record - , record_expr] - %% record_field - , [ record_def_field - , record_field] - %% macro - , [ define - , macro] - ]. + %% function + [ + [ + application, + implicit_fun, + function, + export_entry, + nifs_entry + ], + %% record + [ + record, + record_expr + ], + %% record_field + [ + record_def_field, + record_field + ], + %% macro + [ + define, + macro + ] + ]. diff --git a/apps/els_lsp/src/els_document_symbol_provider.erl b/apps/els_lsp/src/els_document_symbol_provider.erl index f3bfafb9e..db4160834 100644 --- a/apps/els_lsp/src/els_document_symbol_provider.erl +++ b/apps/els_lsp/src/els_document_symbol_provider.erl @@ -2,45 +2,34 @@ -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_symbol, Params}, State) -> - #{ <<"textDocument">> := #{ <<"uri">> := Uri}} = Params, - Functions = functions(Uri), - case Functions of - [] -> {null, State}; - _ -> {Functions, State} - end. +-spec handle_request(any()) -> {response, any()}. +handle_request({document_symbol, Params}) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + Symbols = symbols(Uri), + case Symbols of + [] -> {response, null}; + _ -> {response, Symbols} + end. %%============================================================================== %% Internal Functions %%============================================================================== --spec functions(uri()) -> [map()]. -functions(Uri) -> - {ok, Document} = els_utils:lookup_document(Uri), - POIs = els_dt_document:pois(Document, [function]), - lists:reverse([ #{ name => function_name(F, A) - , kind => ?SYMBOLKIND_FUNCTION - , location => #{ uri => Uri - , range => els_protocol:range(Range) - } - } || #{id := {F, A}, range := Range} <- POIs ]). - --spec function_name(atom(), non_neg_integer()) -> binary(). -function_name(F, A) -> - els_utils:to_binary(io_lib:format("~p/~p", [F, A])). +-spec symbols(uri()) -> [map()]. +symbols(Uri) -> + {ok, Document} = els_utils:lookup_document(Uri), + POIs = els_dt_document:pois(Document, [ + function, + define, + record, + type_definition + ]), + lists:reverse([els_poi:to_symbol(Uri, POI) || POI <- POIs]). diff --git a/apps/els_lsp/src/els_dt_document.erl b/apps/els_lsp/src/els_dt_document.erl index e6bf5ebf6..39c2ea29c 100644 --- a/apps/els_lsp/src/els_dt_document.erl +++ b/apps/els_lsp/src/els_dt_document.erl @@ -9,64 +9,86 @@ %%============================================================================== -behaviour(els_db_table). --export([ name/0 - , opts/0 - ]). +-export([ + name/0, + opts/0 +]). %%============================================================================== %% API %%============================================================================== --export([ insert/1 - , lookup/1 - ]). +-export([ + insert/1, + versioned_insert/1, + lookup/1, + delete/1 +]). --export([ new/2 - , pois/1 - , pois/2 - , get_element_at_pos/3 - , uri/1 - , functions_at_pos/3 - , applications_at_pos/3 - , wrapping_functions/2 - , wrapping_functions/3 - ]). +-export([ + new/3, + new/4, + pois/1, + pois/2, + pois_in_range/2, + pois_in_range/3, + get_element_at_pos/3, + uri/1, + functions_at_pos/3, + applications_at_pos/3, + wrapping_functions/2, + wrapping_functions/3, + find_candidates/1, + find_candidates/2, + find_candidates_with_otp/2, + get_words/1 +]). %%============================================================================== %% Includes %%============================================================================== -include("els_lsp.hrl"). +-include_lib("kernel/include/logger.hrl"). %%============================================================================== %% Type Definitions %%============================================================================== --type id() :: atom(). +-type id() :: atom(). -type kind() :: module | header | other. +-type source() :: otp | app | dep. +-type version() :: null | integer(). +-export_type([source/0]). %%============================================================================== %% Item Definition %%============================================================================== - --record(els_dt_document, { uri :: uri() | '_' - , id :: id() | '_' - , kind :: kind() | '_' - , text :: binary() | '_' - , md5 :: binary() | '_' - , pois :: [poi()] | '_' - }). +-record(els_dt_document, { + uri :: uri() | '_' | '$1', + id :: id() | '_', + kind :: kind() | '_', + text :: binary() | '_', + pois :: [els_poi:poi()] | '_' | ondemand, + source :: source() | '_' | '$2', + words :: sets:set() | '_' | '$3', + version :: version() | '_' +}). -type els_dt_document() :: #els_dt_document{}. --type item() :: #{ uri := uri() - , id := id() - , kind := kind() - , text := binary() - , md5 => binary() - , pois => [poi()] - }. --export_type([ id/0 - , item/0 - , kind/0 - ]). +-type item() :: #{ + uri := uri(), + id := id(), + kind := kind(), + text := binary(), + pois => [els_poi:poi()] | ondemand, + source => source(), + words => sets:set(), + version => version() +}. +-export_type([ + id/0, + item/0, + kind/0 +]). %%============================================================================== %% Callbacks for the els_db_table Behaviour @@ -77,122 +99,276 @@ name() -> ?MODULE. -spec opts() -> proplists:proplist(). opts() -> - [set, compressed]. + [set, compressed]. %%============================================================================== %% API %%============================================================================== -spec from_item(item()) -> els_dt_document(). -from_item(#{ uri := Uri - , id := Id - , kind := Kind - , text := Text - , md5 := MD5 - , pois := POIs - }) -> - #els_dt_document{ uri = Uri - , id = Id - , kind = Kind - , text = Text - , md5 = MD5 - , pois = POIs - }. +from_item(#{ + uri := Uri, + id := Id, + kind := Kind, + text := Text, + pois := POIs, + source := Source, + words := Words, + version := Version +}) -> + #els_dt_document{ + uri = Uri, + id = Id, + kind = Kind, + text = Text, + pois = POIs, + source = Source, + words = Words, + version = Version + }. -spec to_item(els_dt_document()) -> item(). -to_item(#els_dt_document{ uri = Uri - , id = Id - , kind = Kind - , text = Text - , md5 = MD5 - , pois = POIs - }) -> - #{ uri => Uri - , id => Id - , kind => Kind - , text => Text - , md5 => MD5 - , pois => POIs - }. +to_item(#els_dt_document{ + uri = Uri, + id = Id, + kind = Kind, + text = Text, + pois = POIs, + source = Source, + words = Words, + version = Version +}) -> + #{ + uri => Uri, + id => Id, + kind => Kind, + text => Text, + pois => POIs, + source => Source, + words => Words, + version => Version + }. -spec insert(item()) -> ok | {error, any()}. insert(Map) when is_map(Map) -> - Record = from_item(Map), - els_db:write(name(), Record). + Record = from_item(Map), + els_db:write(name(), Record). + +-spec versioned_insert(item()) -> ok | {error, any()}. +versioned_insert(#{uri := Uri, version := Version} = Map) -> + Record = from_item(Map), + Condition = fun(#els_dt_document{version = CurrentVersion}) -> + CurrentVersion =:= null orelse Version >= CurrentVersion + end, + els_db:conditional_write(name(), Uri, Record, Condition). -spec lookup(uri()) -> {ok, [item()]}. lookup(Uri) -> - {ok, Items} = els_db:lookup(name(), Uri), - {ok, [to_item(Item) || Item <- Items]}. - --spec new(uri(), binary()) -> item(). -new(Uri, Text) -> - Extension = filename:extension(Uri), - Id = binary_to_atom(filename:basename(Uri, Extension), utf8), - case Extension of - <<".erl">> -> - new(Uri, Text, Id, module); - <<".hrl">> -> - new(Uri, Text, Id, header); - _ -> - new(Uri, Text, Id, other) - end. - --spec new(uri(), binary(), atom(), kind()) -> item(). -new(Uri, Text, Id, Kind) -> - {ok, POIs} = els_parser:parse(Text), - MD5 = erlang:md5(Text), - #{ uri => Uri - , id => Id - , kind => Kind - , text => Text - , md5 => MD5 - , pois => POIs - }. + {ok, Items} = els_db:lookup(name(), Uri), + {ok, [to_item(Item) || Item <- Items]}. + +-spec delete(uri()) -> ok. +delete(Uri) -> + els_db:delete(name(), Uri). + +-spec new(uri(), binary(), source()) -> item(). +new(Uri, Text, Source) -> + new(Uri, Text, Source, _Version = null). + +-spec new(uri(), binary(), source(), version()) -> item(). +new(Uri, Text, Source, Version) -> + Extension = filename:extension(Uri), + Id = binary_to_atom(filename:basename(Uri, Extension), utf8), + case Extension of + <<".erl">> -> + new(Uri, Text, Id, module, Source, Version); + <<".hrl">> -> + new(Uri, Text, Id, header, Source, Version); + _ -> + new(Uri, Text, Id, other, Source, Version) + end. + +-spec new(uri(), binary(), atom(), kind(), source(), version()) -> item(). +new(Uri, Text, Id, Kind, Source, Version) -> + #{ + uri => Uri, + id => Id, + kind => Kind, + text => Text, + pois => ondemand, + source => Source, + words => get_words(Text), + version => Version + }. %% @doc Returns the list of POIs for the current document --spec pois(item()) -> [poi()]. -pois(#{ pois := POIs }) -> - POIs. +-spec pois(item()) -> [els_poi:poi()]. +pois(#{uri := Uri, pois := ondemand}) -> + #{pois := POIs} = els_indexing:ensure_deeply_indexed(Uri), + POIs; +pois(#{pois := POIs}) -> + POIs. %% @doc Returns the list of POIs of the given types for the current %% document --spec pois(item(), [poi_kind()]) -> [poi()]. +-spec pois(item(), [els_poi:poi_kind()]) -> [els_poi:poi()]. pois(Item, Kinds) -> - [POI || #{kind := K} = POI <- pois(Item), lists:member(K, Kinds)]. + [POI || #{kind := K} = POI <- pois(Item), lists:member(K, Kinds)]. + +%% @doc Returns the list of POIs of the given types in the given range +%% for the current document +-spec pois_in_range(item(), els_poi:poi_range()) -> [els_poi:poi()]. +pois_in_range(Item, Range) -> + [ + POI + || #{range := R} = POI <- pois(Item), + els_range:in(R, Range) + ]. + +%% @doc Returns the list of POIs of the given types in the given range +%% for the current document +-spec pois_in_range( + item(), + [els_poi:poi_kind()], + els_poi:poi_range() +) -> [els_poi:poi()]. +pois_in_range(Item, Kinds, Range) -> + [ + POI + || #{kind := K, range := R} = POI <- pois(Item), + lists:member(K, Kinds), + els_range:in(R, Range) + ]. -spec get_element_at_pos(item(), non_neg_integer(), non_neg_integer()) -> - [poi()]. + [els_poi:poi()]. get_element_at_pos(Item, Line, Column) -> - POIs = pois(Item), - MatchedPOIs = els_poi:match_pos(POIs, {Line, Column}), - els_poi:sort(MatchedPOIs). + POIs = pois(Item), + MatchedPOIs = els_poi:match_pos(POIs, {Line, Column}), + els_poi:sort(MatchedPOIs). %% @doc Returns the URI of the current document -spec uri(item()) -> uri(). -uri(#{ uri := Uri }) -> - Uri. +uri(#{uri := Uri}) -> + Uri. --spec functions_at_pos(item(), non_neg_integer(), non_neg_integer()) -> [poi()]. +-spec functions_at_pos(item(), non_neg_integer(), non_neg_integer()) -> [els_poi:poi()]. functions_at_pos(Item, Line, Column) -> - POIs = get_element_at_pos(Item, Line, Column), - [POI || #{kind := 'function'} = POI <- POIs]. + POIs = get_element_at_pos(Item, Line, Column), + [POI || #{kind := 'function'} = POI <- POIs]. -spec applications_at_pos(item(), non_neg_integer(), non_neg_integer()) -> - [poi()]. + [els_poi:poi()]. applications_at_pos(Item, Line, Column) -> - POIs = get_element_at_pos(Item, Line, Column), - [POI || #{kind := 'application'} = POI <- POIs]. + POIs = get_element_at_pos(Item, Line, Column), + [POI || #{kind := 'application'} = POI <- POIs]. -spec wrapping_functions(item(), non_neg_integer(), non_neg_integer()) -> - [poi()]. + [els_poi:poi()]. wrapping_functions(Document, Line, Column) -> - Range = #{from => {Line, Column}, to => {Line, Column}}, - Functions = pois(Document, ['function']), - [F || #{data := #{ wrapping_range := WR}} = F <- Functions, - els_range:in(Range, WR)]. + Range = #{from => {Line, Column}, to => {Line, Column}}, + Functions = pois(Document, ['function']), + [ + F + || #{data := #{wrapping_range := WR}} = F <- Functions, + els_range:in(Range, WR) + ]. --spec wrapping_functions(item(), range()) -> [poi()]. +-spec wrapping_functions(item(), range()) -> [els_poi:poi()]. wrapping_functions(Document, Range) -> - #{start := #{character := Character, line := Line}} = Range, - wrapping_functions(Document, Line, Character). + #{start := #{character := Character, line := Line}} = Range, + wrapping_functions(Document, Line, Character). + +-spec find_candidates(atom() | string()) -> [uri()]. +find_candidates(Pattern) -> + find_candidates(Pattern, '_'). + +-spec find_candidates(atom() | string(), module | header | '_') -> [uri()]. +find_candidates(Pattern, Kind) -> + %% ets:fun2ms(fun(#els_dt_document{source = Source, uri = Uri, words = Words}) + %% when Source =/= otp -> {Uri, Words} end). + MS = [ + { + #els_dt_document{ + uri = '$1', + id = '_', + kind = Kind, + text = '_', + pois = '_', + source = '$2', + words = '$3', + version = '_' + }, + [{'=/=', '$2', otp}], + [{{'$1', '$3'}}] + } + ], + All = ets:select(name(), MS), + Fun = fun({Uri, Words}) -> + case sets:is_element(Pattern, Words) of + true -> {true, Uri}; + false -> false + end + end, + lists:filtermap(Fun, All). + +-spec find_candidates_with_otp(atom() | string(), module | header | '_') -> [uri()]. +find_candidates_with_otp(Pattern, Kind) -> + %% ets:fun2ms(fun(#els_dt_document{source = Source, uri = Uri, words = Words}) + %% when Source =/= otp -> {Uri, Words} end). + MS = [ + { + #els_dt_document{ + uri = '$1', + id = '_', + kind = Kind, + text = '_', + pois = '_', + source = '_', + words = '$3', + version = '_' + }, + [], + [{{'$1', '$3'}}] + } + ], + All = ets:select(name(), MS), + Fun = fun({Uri, Words}) -> + case sets:is_element(Pattern, Words) of + true -> {true, Uri}; + false -> false + end + end, + lists:filtermap(Fun, All). + +-spec get_words(binary()) -> sets:set(). +get_words(Text) -> + case erl_scan:string(els_utils:to_list(Text)) of + {ok, Tokens, _EndLocation} -> + tokens_to_words(Tokens, sets:new([{version, 2}])); + {error, ErrorInfo, ErrorLocation} -> + ?LOG_WARNING("Errors while get_words [info=~p] [location=~p]", [ + ErrorInfo, ErrorLocation + ]), + sets:new([{version, 2}]) + end. + +-spec tokens_to_words([erl_scan:token()], sets:set()) -> sets:set(). +tokens_to_words([{atom, _Location, Atom} | Tokens], Words) -> + tokens_to_words(Tokens, sets:add_element(Atom, Words)); +tokens_to_words([{string, _Location, String} | Tokens], Words) -> + case filename:extension(String) of + ".hrl" -> + Id = filename:rootname(filename:basename(String)), + tokens_to_words(Tokens, sets:add_element(Id, Words)); + _ -> + tokens_to_words(Tokens, Words) + end; +tokens_to_words([{'?', _}, {var, _, Macro} | Tokens], Words) -> + tokens_to_words(Tokens, sets:add_element(Macro, Words)); +tokens_to_words([{'-', _}, {atom, _, define}, {'(', _}, {var, _, Macro} | Tokens], Words) -> + tokens_to_words(Tokens, sets:add_element(Macro, Words)); +tokens_to_words([_ | Tokens], Words) -> + tokens_to_words(Tokens, Words); +tokens_to_words([], Words) -> + Words. diff --git a/apps/els_lsp/src/els_dt_document_index.erl b/apps/els_lsp/src/els_dt_document_index.erl index dcb70cd74..790b4f359 100644 --- a/apps/els_lsp/src/els_dt_document_index.erl +++ b/apps/els_lsp/src/els_dt_document_index.erl @@ -9,9 +9,10 @@ %%============================================================================== -behaviour(els_db_table). --export([ name/0 - , opts/0 - ]). +-export([ + name/0, + opts/0 +]). %%============================================================================== %% API @@ -19,10 +20,12 @@ -export([new/3]). --export([ find_by_kind/1 - , insert/1 - , lookup/1 - ]). +-export([ + find_by_kind/1, + insert/1, + lookup/1, + delete_by_uri/1 +]). %%============================================================================== %% Includes @@ -33,17 +36,19 @@ %% Item Definition %%============================================================================== --record(els_dt_document_index, { id :: els_dt_document:id() | '_' - , uri :: uri() | '_' - , kind :: els_dt_document:kind() | '_' - }). +-record(els_dt_document_index, { + id :: els_dt_document:id() | '_', + uri :: uri() | '_', + kind :: els_dt_document:kind() | '_' +}). -type els_dt_document_index() :: #els_dt_document_index{}. --type item() :: #{ id := els_dt_document:id() - , uri := uri() - , kind := els_dt_document:kind() - }. --export_type([ item/0 ]). +-type item() :: #{ + id := els_dt_document:id(), + uri := uri(), + kind := els_dt_document:kind() +}. +-export_type([item/0]). %%============================================================================== %% Callbacks for the els_db_table Behaviour @@ -54,7 +59,7 @@ name() -> ?MODULE. -spec opts() -> proplists:proplist(). opts() -> - [bag]. + [bag]. %%============================================================================== %% API @@ -62,31 +67,37 @@ opts() -> -spec from_item(item()) -> els_dt_document_index(). from_item(#{id := Id, uri := Uri, kind := Kind}) -> - #els_dt_document_index{id = Id, uri = Uri, kind = Kind}. + #els_dt_document_index{id = Id, uri = Uri, kind = Kind}. -spec to_item(els_dt_document_index()) -> item(). to_item(#els_dt_document_index{id = Id, uri = Uri, kind = Kind}) -> - #{id => Id, uri => Uri, kind => Kind}. + #{id => Id, uri => Uri, kind => Kind}. -spec insert(item()) -> ok | {error, any()}. insert(Map) when is_map(Map) -> - Record = from_item(Map), - els_db:write(name(), Record). + Record = from_item(Map), + els_db:write(name(), Record). -spec lookup(atom()) -> {ok, [item()]}. lookup(Id) -> - {ok, Items} = els_db:lookup(name(), Id), - {ok, [to_item(Item) || Item <- Items]}. + {ok, Items} = els_db:lookup(name(), Id), + {ok, [to_item(Item) || Item <- Items]}. + +-spec delete_by_uri(uri()) -> ok | {error, any()}. +delete_by_uri(Uri) -> + Pattern = #els_dt_document_index{uri = Uri, _ = '_'}, + ok = els_db:match_delete(name(), Pattern). -spec find_by_kind(els_dt_document:kind()) -> {ok, [item()]}. find_by_kind(Kind) -> - Pattern = #els_dt_document_index{kind = Kind, _ = '_'}, - {ok, Items} = els_db:match(name(), Pattern), - {ok, [to_item(Item) || Item <- Items]}. + Pattern = #els_dt_document_index{kind = Kind, _ = '_'}, + {ok, Items} = els_db:match(name(), Pattern), + {ok, [to_item(Item) || Item <- Items]}. -spec new(atom(), uri(), els_dt_document:kind()) -> item(). new(Id, Uri, Kind) -> - #{ id => Id - , uri => Uri - , kind => Kind - }. + #{ + id => Id, + uri => Uri, + kind => Kind + }. diff --git a/apps/els_lsp/src/els_dt_functions.erl b/apps/els_lsp/src/els_dt_functions.erl new file mode 100644 index 000000000..8b7b955e7 --- /dev/null +++ b/apps/els_lsp/src/els_dt_functions.erl @@ -0,0 +1,134 @@ +%%============================================================================== +%% The 'functions' table +%%============================================================================== +-module(els_dt_functions). + +%%============================================================================== +%% Behaviour els_db_table +%%============================================================================== + +-behaviour(els_db_table). +-export([ + name/0, + opts/0 +]). + +%%============================================================================== +%% API +%%============================================================================== + +-export([ + insert/1, + versioned_insert/1, + lookup/1, + delete_by_uri/1, + versioned_delete_by_uri/2 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include("els_lsp.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +%%============================================================================== +%% Item Definition +%%============================================================================== + +-record(els_dt_functions, { + mfa :: mfa() | '_' | {atom(), '_', '_'}, + version :: version() | '_' +}). +-type els_dt_functions() :: #els_dt_functions{}. +-type version() :: null | integer(). +-type item() :: #{ + mfa := mfa(), + version := version() +}. +-export_type([item/0]). + +%%============================================================================== +%% Callbacks for the els_db_table Behaviour +%%============================================================================== + +-spec name() -> atom(). +name() -> ?MODULE. + +-spec opts() -> proplists:proplist(). +opts() -> + [set]. + +%%============================================================================== +%% API +%%============================================================================== + +-spec from_item(item()) -> els_dt_functions(). +from_item(#{ + mfa := MFA, + version := Version +}) -> + #els_dt_functions{ + mfa = MFA, + version = Version + }. + +-spec to_item(els_dt_functions()) -> item(). +to_item(#els_dt_functions{ + mfa = MFA, + version = Version +}) -> + #{ + mfa => MFA, + version => Version + }. + +-spec insert(item()) -> ok | {error, any()}. +insert(Map) when is_map(Map) -> + Record = from_item(Map), + els_db:write(name(), Record). + +-spec versioned_insert(item()) -> ok | {error, any()}. +versioned_insert(#{mfa := MFA, version := Version} = Map) -> + Record = from_item(Map), + Condition = fun(#els_dt_functions{version = CurrentVersion}) -> + CurrentVersion =:= null orelse Version >= CurrentVersion + end, + els_db:conditional_write(name(), MFA, Record, Condition). + +-spec lookup(mfa()) -> {ok, [item()]}. +lookup({M, _F, _A} = MFA) -> + {ok, _Uris} = els_utils:find_modules(M), + {ok, Items} = els_db:lookup(name(), MFA), + {ok, [to_item(Item) || Item <- Items]}. + +-spec delete_by_uri(uri()) -> ok. +delete_by_uri(Uri) -> + case filename:extension(Uri) of + <<".erl">> -> + Module = els_uri:module(Uri), + Pattern = #els_dt_functions{mfa = {Module, '_', '_'}, _ = '_'}, + ok = els_db:match_delete(name(), Pattern); + _ -> + ok + end. + +-spec versioned_delete_by_uri(uri(), version()) -> ok. +versioned_delete_by_uri(Uri, Version) -> + case filename:extension(Uri) of + <<".erl">> -> + Module = els_uri:module(Uri), + MS = ets:fun2ms( + fun + (#els_dt_functions{mfa = {M, _, _}, version = CurrentVersion}) when + M =:= Module, + CurrentVersion =:= null orelse CurrentVersion < Version + -> + true; + (_) -> + false + end + ), + ok = els_db:select_delete(name(), MS); + _ -> + ok + end. diff --git a/apps/els_lsp/src/els_dt_references.erl b/apps/els_lsp/src/els_dt_references.erl index 0926f1790..17dbf3000 100644 --- a/apps/els_lsp/src/els_dt_references.erl +++ b/apps/els_lsp/src/els_dt_references.erl @@ -9,41 +9,61 @@ %%============================================================================== -behaviour(els_db_table). --export([ name/0 - , opts/0 - ]). +-export([ + name/0, + opts/0 +]). %%============================================================================== %% API %%============================================================================== --export([ delete_by_uri/1 - , find_all/0 - , find_by/1 - , find_by_id/2 - , insert/2 - ]). +-export([ + delete_by_uri/1, + versioned_delete_by_uri/2, + find_by/1, + find_by_id/2, + insert/2, + versioned_insert/2, + kind_to_category/1 +]). %%============================================================================== %% Includes %%============================================================================== -include("els_lsp.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). %%============================================================================== %% Item Definition %%============================================================================== --record(els_dt_references, { id :: any() | '_' - , uri :: uri() | '_' - , range :: poi_range() | '_' - }). +-record(els_dt_references, { + id :: any() | '_', + uri :: uri() | '_', + range :: els_poi:poi_range() | '_', + version :: version() | '_' +}). -type els_dt_references() :: #els_dt_references{}. - --type item() :: #{ id := any() - , uri := uri() - , range := poi_range() - }. --export_type([ item/0 ]). +-type version() :: null | integer(). +-type item() :: #{ + id := any(), + uri := uri(), + range := els_poi:poi_range(), + version := version() +}. +-export_type([item/0]). + +-type poi_category() :: + function + | type + | macro + | record + | include + | include_lib + | behaviour + | atom. +-export_type([poi_category/0]). %%============================================================================== %% Callbacks for the els_db_table Behaviour @@ -54,73 +74,124 @@ name() -> ?MODULE. -spec opts() -> proplists:proplist(). opts() -> - [bag]. + [bag]. %%============================================================================== %% API %%============================================================================== --spec from_item(poi_kind(), item()) -> els_dt_references(). -from_item(Kind, #{ id := Id, uri := Uri, range := Range}) -> - InternalId = {kind_to_category(Kind), Id}, - #els_dt_references{ id = InternalId, uri = Uri, range = Range}. +-spec from_item(els_poi:poi_kind(), item()) -> els_dt_references(). +from_item(Kind, #{ + id := Id, + uri := Uri, + range := Range, + version := Version +}) -> + InternalId = {kind_to_category(Kind), Id}, + #els_dt_references{ + id = InternalId, + uri = Uri, + range = Range, + version = Version + }. -spec to_item(els_dt_references()) -> item(). -to_item(#els_dt_references{ id = {_Category, Id}, uri = Uri, range = Range }) -> - #{ id => Id - , uri => Uri - , range => Range - }. +to_item(#els_dt_references{ + id = {_Category, Id}, + uri = Uri, + range = Range, + version = Version +}) -> + #{ + id => Id, + uri => Uri, + range => Range, + version => Version + }. -spec delete_by_uri(uri()) -> ok | {error, any()}. delete_by_uri(Uri) -> - Pattern = #els_dt_references{uri = Uri, _ = '_'}, - ok = els_db:match_delete(name(), Pattern). - --spec insert(poi_kind(), item()) -> ok | {error, any()}. + Pattern = #els_dt_references{uri = Uri, _ = '_'}, + ok = els_db:match_delete(name(), Pattern). + +-spec versioned_delete_by_uri(uri(), version()) -> ok. +versioned_delete_by_uri(Uri, Version) -> + MS = ets:fun2ms(fun + (#els_dt_references{uri = U, version = CurrentVersion}) when + U =:= Uri, + CurrentVersion =:= null orelse + CurrentVersion =< Version + -> + true; + (_) -> + false + end), + ok = els_db:select_delete(name(), MS). + +-spec insert(els_poi:poi_kind(), item()) -> ok | {error, any()}. insert(Kind, Map) when is_map(Map) -> - Record = from_item(Kind, Map), - els_db:write(name(), Record). + Record = from_item(Kind, Map), + els_db:write(name(), Record). -%% @doc Find all --spec find_all() -> {ok, [item()]} | {error, any()}. -find_all() -> - Pattern = #els_dt_references{_ = '_'}, - find_by(Pattern). +-spec versioned_insert(els_poi:poi_kind(), item()) -> ok | {error, any()}. +versioned_insert(Kind, #{id := Id, version := Version} = Map) -> + Record = from_item(Kind, Map), + Condition = fun(#els_dt_references{version = CurrentVersion}) -> + CurrentVersion =:= null orelse Version >= CurrentVersion + end, + els_db:conditional_write(name(), Id, Record, Condition). %% @doc Find by id --spec find_by_id(poi_kind(), any()) -> {ok, [item()]} | {error, any()}. +-spec find_by_id(els_poi:poi_kind(), any()) -> {ok, [item()]} | {error, any()}. find_by_id(Kind, Id) -> - InternalId = {kind_to_category(Kind), Id}, - Pattern = #els_dt_references{id = InternalId, _ = '_'}, - find_by(Pattern). + InternalId = {kind_to_category(Kind), Id}, + Pattern = #els_dt_references{id = InternalId, _ = '_'}, + find_by(Pattern). -spec find_by(tuple()) -> {ok, [item()]}. -find_by(Pattern) -> - {ok, Items} = els_db:match(name(), Pattern), - {ok, [to_item(Item) || Item <- Items]}. - --spec kind_to_category(poi_kind()) -> function | type | macro | record. -kind_to_category(Kind) when Kind =:= application; - Kind =:= export_entry; - Kind =:= function; - Kind =:= function_clause; - Kind =:= import_entry; - Kind =:= implicit_fun -> - function; -kind_to_category(Kind) when Kind =:= export_type_entry; - Kind =:= type_application; - Kind =:= type_definition -> - type; -kind_to_category(Kind) when Kind =:= macro; - Kind =:= define -> - macro; -kind_to_category(Kind) when Kind =:= record_expr; - Kind =:= record -> - record; +find_by(#els_dt_references{id = Id} = Pattern) -> + Uris = els_text_search:find_candidate_uris(Id), + [els_indexing:ensure_deeply_indexed(Uri) || Uri <- Uris], + {ok, Items} = els_db:match(name(), Pattern), + {ok, [to_item(Item) || Item <- Items]}. + +-spec kind_to_category(els_poi:poi_kind()) -> poi_category(). +kind_to_category(Kind) when + Kind =:= application; + Kind =:= export_entry; + Kind =:= function; + Kind =:= function_clause; + Kind =:= import_entry; + Kind =:= implicit_fun; + Kind =:= nifs_entry +-> + function; +kind_to_category(Kind) when + Kind =:= export_type_entry; + Kind =:= type_application; + Kind =:= type_definition +-> + type; +kind_to_category(Kind) when + Kind =:= macro; + Kind =:= define +-> + macro; +kind_to_category(Kind) when + Kind =:= record_expr; + Kind =:= record +-> + record; +kind_to_category(Kind) when + Kind =:= record_field; + Kind =:= record_def_field +-> + record_field; kind_to_category(Kind) when Kind =:= include -> - include; + include; kind_to_category(Kind) when Kind =:= include_lib -> - include_lib; + include_lib; kind_to_category(Kind) when Kind =:= behaviour -> - behaviour. + behaviour; +kind_to_category(Kind) when Kind =:= atom -> + atom. diff --git a/apps/els_lsp/src/els_dt_signatures.erl b/apps/els_lsp/src/els_dt_signatures.erl index 9d7cab853..c23edd546 100644 --- a/apps/els_lsp/src/els_dt_signatures.erl +++ b/apps/els_lsp/src/els_dt_signatures.erl @@ -9,36 +9,48 @@ %%============================================================================== -behaviour(els_db_table). --export([ name/0 - , opts/0 - ]). +-export([ + name/0, + opts/0 +]). %%============================================================================== %% API %%============================================================================== --export([ insert/1 - , lookup/1 - ]). +-export([ + insert/1, + versioned_insert/1, + lookup/1, + delete_by_uri/1, + versioned_delete_by_uri/2 +]). %%============================================================================== %% Includes %%============================================================================== -include("els_lsp.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). %%============================================================================== %% Item Definition %%============================================================================== --record(els_dt_signatures, { mfa :: mfa() | '_' - , spec :: binary() - }). +-record(els_dt_signatures, { + mfa :: mfa() | '_' | {atom(), '_', '_'}, + spec :: binary() | '_', + version :: version() | '_', + args :: els_arg:args() | '_' +}). -type els_dt_signatures() :: #els_dt_signatures{}. - --type item() :: #{ mfa := mfa() - , spec := binary() - }. --export_type([ item/0 ]). +-type version() :: null | integer(). +-type item() :: #{ + mfa := mfa(), + spec := binary(), + version := version(), + args := els_arg:args() +}. +-export_type([item/0]). %%============================================================================== %% Callbacks for the els_db_table Behaviour @@ -49,28 +61,87 @@ name() -> ?MODULE. -spec opts() -> proplists:proplist(). opts() -> - [set]. + [set]. %%============================================================================== %% API %%============================================================================== -spec from_item(item()) -> els_dt_signatures(). -from_item(#{ mfa := MFA, spec := Spec }) -> - #els_dt_signatures{ mfa = MFA, spec = Spec }. +from_item(#{ + mfa := MFA, + spec := Spec, + version := Version, + args := Args +}) -> + #els_dt_signatures{ + mfa = MFA, + spec = Spec, + version = Version, + args = Args + }. -spec to_item(els_dt_signatures()) -> item(). -to_item(#els_dt_signatures{ mfa = MFA, spec = Spec }) -> - #{ mfa => MFA - , spec => Spec - }. +to_item(#els_dt_signatures{ + mfa = MFA, + spec = Spec, + version = Version, + args = Args +}) -> + #{ + mfa => MFA, + spec => Spec, + version => Version, + args => Args + }. -spec insert(item()) -> ok | {error, any()}. insert(Map) when is_map(Map) -> - Record = from_item(Map), - els_db:write(name(), Record). + Record = from_item(Map), + els_db:write(name(), Record). + +-spec versioned_insert(item()) -> ok | {error, any()}. +versioned_insert(#{mfa := MFA, version := Version} = Map) -> + Record = from_item(Map), + Condition = fun(#els_dt_signatures{version = CurrentVersion}) -> + CurrentVersion =:= null orelse Version >= CurrentVersion + end, + els_db:conditional_write(name(), MFA, Record, Condition). -spec lookup(mfa()) -> {ok, [item()]}. -lookup(MFA) -> - {ok, Items} = els_db:lookup(name(), MFA), - {ok, [to_item(Item) || Item <- Items]}. +lookup({M, _F, _A} = MFA) -> + {ok, _Uris} = els_utils:find_modules(M), + {ok, Items} = els_db:lookup(name(), MFA), + {ok, [to_item(Item) || Item <- Items]}. + +-spec delete_by_uri(uri()) -> ok. +delete_by_uri(Uri) -> + case filename:extension(Uri) of + <<".erl">> -> + Module = els_uri:module(Uri), + Pattern = #els_dt_signatures{mfa = {Module, '_', '_'}, _ = '_'}, + ok = els_db:match_delete(name(), Pattern); + _ -> + ok + end. + +-spec versioned_delete_by_uri(uri(), version()) -> ok. +versioned_delete_by_uri(Uri, Version) -> + case filename:extension(Uri) of + <<".erl">> -> + Module = els_uri:module(Uri), + MS = ets:fun2ms( + fun + (#els_dt_signatures{mfa = {M, _, _}, version = CurrentVersion}) when + M =:= Module, + CurrentVersion =:= null orelse CurrentVersion < Version + -> + true; + (_) -> + false + end + ), + ok = els_db:select_delete(name(), MS); + _ -> + ok + end. diff --git a/apps/els_lsp/src/els_edoc_diagnostics.erl b/apps/els_lsp/src/els_edoc_diagnostics.erl new file mode 100644 index 000000000..8b8bad6a0 --- /dev/null +++ b/apps/els_lsp/src/els_edoc_diagnostics.erl @@ -0,0 +1,110 @@ +%%============================================================================== +%% Edoc diagnostics +%%============================================================================== +-module(els_edoc_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. + +%% The edoc application currently does not offer an API to +%% programmatically return a list of warnings and errors. Instead, +%% it simply outputs the warnings and errors to the standard +%% output. +%% We created an issue for the OTP team to address this: +%% https://github.com/erlang-ls/erlang_ls/issues/384 +%% Meanwhile, hackity-hack! +%% Let's override the reporting module for edoc (edoc_report) +%% and (ab)use the process dictionary to collect the list of +%% warnings and errors. +-spec run(uri()) -> [els_diagnostics:diagnostic()]. +run(Uri) -> + case filename:extension(Uri) of + <<".erl">> -> + do_run(Uri); + _ -> + [] + end. + +-spec do_run(uri()) -> [els_diagnostics:diagnostic()]. +do_run(Uri) -> + Paths = [els_utils:to_list(els_uri:path(Uri))], + Fun = fun(Dir) -> + Options = edoc_options(Dir), + put(edoc_diagnostics, []), + try + edoc:run(Paths, Options) + catch + _:_:_ -> + ok + end, + [ + make_diagnostic(L, Format, Args, Severity) + || {L, _Where, Format, Args, Severity} <- + get(edoc_diagnostics), + L =/= 0 + ] + end, + tempdir:mktmp(Fun). + +-spec source() -> binary(). +source() -> + <<"Edoc">>. + +%%============================================================================== +%% Internal Functions +%%============================================================================== +-spec edoc_options(string()) -> proplists:proplist(). +edoc_options(Dir) -> + Macros = [{N, V} || {'d', N, V} <- els_compiler_diagnostics:macro_options()], + Includes = [I || {i, I} <- els_compiler_diagnostics:include_options()], + [ + {preprocess, true}, + {macros, Macros}, + {includes, Includes}, + {dir, Dir} + ]. + +-spec make_diagnostic(pos_integer(), string(), [any()], warning | error) -> + els_diagnostics:diagnostic(). +make_diagnostic(Line, Format, Args, Severity0) -> + Severity = severity(Severity0), + Message = els_utils:to_binary(io_lib:format(Format, Args)), + els_diagnostics:make_diagnostic( + els_protocol:range(#{ + from => {Line, 1}, + to => {Line + 1, 1} + }), + Message, + Severity, + source() + ). + +-spec severity(warning | error) -> els_diagnostics:severity(). +severity(warning) -> + ?DIAGNOSTIC_WARNING; +severity(error) -> + ?DIAGNOSTIC_ERROR. diff --git a/apps/els_lsp/src/els_eep48_docs.erl b/apps/els_lsp/src/els_eep48_docs.erl index 21b8661a6..a2b8c4884 100644 --- a/apps/els_lsp/src/els_eep48_docs.erl +++ b/apps/els_lsp/src/els_eep48_docs.erl @@ -36,43 +36,86 @@ -export_type([chunk_elements/0, chunk_element_attr/0]). --record(config, { docs :: docs_v1() }). - --define(ALL_ELEMENTS,[a,p,'div',br,h1,h2,h3,h4,h5,h6,hr, - i,b,em,strong,pre,code,ul,ol,li,dl,dt,dd]). +-record(config, {docs :: docs_v1()}). + +-define(ALL_ELEMENTS, [ + a, + p, + 'div', + br, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + i, + b, + em, + strong, + pre, + code, + ul, + ol, + li, + dl, + dt, + dd +]). %% inline elements are: --define(INLINE,[i,b,em,strong,code,a]). --define(IS_INLINE(ELEM),(((ELEM) =:= a) orelse ((ELEM) =:= code) - orelse ((ELEM) =:= i) orelse ((ELEM) =:= em) - orelse ((ELEM) =:= b) orelse ((ELEM) =:= strong))). +-define(IS_INLINE(ELEM), + (((ELEM) =:= a) orelse ((ELEM) =:= code) orelse + ((ELEM) =:= i) orelse ((ELEM) =:= em) orelse + ((ELEM) =:= b) orelse ((ELEM) =:= strong)) +). %% non-inline elements are: --define(BLOCK,[p,'div',pre,br,ul,ol,li,dl,dt,dd,h1,h2,h3,h4,h5,h6,hr]). --define(IS_BLOCK(ELEM),not ?IS_INLINE(ELEM)). --define(IS_PRE(ELEM),(((ELEM) =:= pre))). +-define(BLOCK, [p, 'div', pre, br, ul, ol, li, dl, dt, dd, h1, h2, h3, h4, h5, h6, hr]). +-define(IS_BLOCK(ELEM), not ?IS_INLINE(ELEM)). +-define(IS_PRE(ELEM), ((ELEM) =:= pre)). %% If you update the below types, make sure to update the documentation in %% erl_docgen/doc/src/doc_storage.xml as well!!! --type docs_v1() :: #docs_v1{ docs :: [chunk_entry()], format :: binary(), module_doc :: chunk_elements() }. --type chunk_entry() :: {{Type :: function | type | callback, Name :: atom(), Arity :: non_neg_integer()}, - Anno :: erl_anno:anno(), - Sigs :: [binary()], - Docs :: none | hidden | #{ binary() => chunk_elements() }, - Meta :: #{ signature := term() }}. --type config() :: #{ }. +-type docs_v1() :: #docs_v1{ + docs :: [chunk_entry()], format :: binary(), module_doc :: chunk_elements() +}. +-type chunk_entry() :: { + {Type :: function | type | callback, Name :: atom(), Arity :: non_neg_integer()}, + Anno :: erl_anno:anno(), + Sigs :: [binary()], + Docs :: none | hidden | #{binary() => chunk_elements()}, + Meta :: #{signature := term()} +}. +-type config() :: #{}. -type chunk_elements() :: [chunk_element()]. --type chunk_element() :: {chunk_element_type(),chunk_element_attrs(), - chunk_elements()} | binary(). +-type chunk_element() :: + {chunk_element_type(), chunk_element_attrs(), chunk_elements()} + | binary(). -type chunk_element_attrs() :: [chunk_element_attr()]. --type chunk_element_attr() :: {atom(),unicode:chardata()}. +-type chunk_element_attr() :: {atom(), unicode:chardata()}. -type chunk_element_type() :: chunk_element_inline_type() | chunk_element_block_type(). -type chunk_element_inline_type() :: a | code | em | strong | i | b. --type chunk_element_block_type() :: p | 'div' | br | pre | ul | - ol | li | dl | dt | dd | - h1 | h2 | h3 | h4 | h5 | h6. +-type chunk_element_block_type() :: + p + | 'div' + | br + | pre + | ul + | ol + | li + | dl + | dt + | dd + | h1 + | h2 + | h3 + | h4 + | h5 + | h6. -spec normalize(Docs) -> NormalizedDocs when - Docs :: chunk_elements(), - NormalizedDocs :: chunk_elements(). + Docs :: chunk_elements(), + NormalizedDocs :: chunk_elements(). normalize(Docs) -> shell_docs:normalize(Docs). @@ -80,242 +123,346 @@ normalize(Docs) -> %% API function for dealing with the function documentation %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec render(Module, Function, Docs) -> Res when - Module :: module(), - Function :: atom(), - Docs :: docs_v1(), - Res :: unicode:chardata() | {error,function_missing}. -render(_Module, Function, #docs_v1{ } = D) -> + Module :: module(), + Function :: atom(), + Docs :: docs_v1(), + Res :: unicode:chardata() | {error, function_missing}. +render(_Module, Function, #docs_v1{} = D) -> render(_Module, Function, D, #{}). --spec render(Module, Function, Docs, Config) -> Res when - Module :: module(), - Function :: atom(), - Docs :: docs_v1(), - Config :: config(), - Res :: unicode:chardata() | {error,function_missing}; - - (Module, Function, Arity, Docs) -> Res when - Module :: module(), - Function :: atom(), - Arity :: arity(), - Docs :: docs_v1(), - Res :: unicode:chardata() | {error,function_missing}. -render(Module, Function, #docs_v1{ docs = Docs } = D, Config) - when is_atom(Module), is_atom(Function), is_map(Config) -> +-spec render + (Module, Function, Docs, Config) -> Res when + Module :: module(), + Function :: atom(), + Docs :: docs_v1(), + Config :: config(), + Res :: unicode:chardata() | {error, function_missing}; + (Module, Function, Arity, Docs) -> Res when + Module :: module(), + Function :: atom(), + Arity :: arity(), + Docs :: docs_v1(), + Res :: unicode:chardata() | {error, function_missing}. +render(Module, Function, #docs_v1{docs = Docs} = D, Config) when + is_atom(Module), is_atom(Function), is_map(Config) +-> render_function( - lists:filter(fun({{function, F, _},_Anno,_Sig,Doc,_Meta}) when Doc =/= none -> - F =:= Function; - (_) -> - false - end, Docs), D, Config); -render(_Module, Function, Arity, #docs_v1{ } = D) -> + lists:filter( + fun + ({{function, F, _}, _Anno, _Sig, Doc, _Meta}) when Doc =/= none -> + F =:= Function; + (_) -> + false + end, + Docs + ), + D, + Module, + Config + ); +render(_Module, Function, Arity, #docs_v1{} = D) -> render(_Module, Function, Arity, D, #{}). -spec render(Module, Function, Arity, Docs, Config) -> Res when - Module :: module(), - Function :: atom(), - Arity :: arity(), - Docs :: docs_v1(), - Config :: config(), - Res :: unicode:chardata() | {error,function_missing}. -render(Module, Function, Arity, #docs_v1{ docs = Docs } = D, Config) - when is_atom(Module), is_atom(Function), is_integer(Arity), is_map(Config) -> + Module :: module(), + Function :: atom(), + Arity :: arity(), + Docs :: docs_v1(), + Config :: config(), + Res :: unicode:chardata() | {error, function_missing}. +render(Module, Function, Arity, #docs_v1{docs = Docs} = D, Config) when + is_atom(Module), is_atom(Function), is_integer(Arity), is_map(Config) +-> render_function( - lists:filter(fun({{function, F, A},_Anno,_Sig,Doc,_Meta}) when Doc =/= none-> - F =:= Function andalso A =:= Arity; - (_) -> - false - end, Docs), D, Config). + lists:filter( + fun + ({{function, F, A}, _Anno, _Sig, Doc, _Meta}) when Doc =/= none -> + F =:= Function andalso A =:= Arity; + (_) -> + false + end, + Docs + ), + D, + Module, + Config + ). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% API function for dealing with the type documentation %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec render_type(Module, Type, Docs) -> Res when - Module :: module(), Type :: atom(), - Docs :: docs_v1(), - Res :: unicode:chardata() | {error, type_missing}. + Module :: module(), + Type :: atom(), + Docs :: docs_v1(), + Res :: unicode:chardata() | {error, type_missing}. render_type(Module, Type, D = #docs_v1{}) -> render_type(Module, Type, D, #{}). --spec render_type(Module, Type, Docs, Config) -> Res when - Module :: module(), Type :: atom(), - Docs :: docs_v1(), - Config :: config(), - Res :: unicode:chardata() | {error, type_missing}; - (Module, Type, Arity, Docs) -> Res when - Module :: module(), Type :: atom(), Arity :: arity(), - Docs :: docs_v1(), - Res :: unicode:chardata() | {error, type_missing}. -render_type(_Module, Type, #docs_v1{ docs = Docs } = D, Config) -> +-spec render_type + (Module, Type, Docs, Config) -> Res when + Module :: module(), + Type :: atom(), + Docs :: docs_v1(), + Config :: config(), + Res :: unicode:chardata() | {error, type_missing}; + (Module, Type, Arity, Docs) -> Res when + Module :: module(), + Type :: atom(), + Arity :: arity(), + Docs :: docs_v1(), + Res :: unicode:chardata() | {error, type_missing}. +render_type(Module, Type, #docs_v1{docs = Docs} = D, Config) -> render_typecb_docs( - lists:filter(fun({{type, T, _},_Anno,_Sig,_Doc,_Meta}) -> - T =:= Type; - (_) -> - false - end, Docs), D, Config); -render_type(_Module, Type, Arity, #docs_v1{ } = D) -> + lists:filter( + fun + ({{type, T, _}, _Anno, _Sig, _Doc, _Meta}) -> + T =:= Type; + (_) -> + false + end, + Docs + ), + D, + Module, + Config + ); +render_type(_Module, Type, Arity, #docs_v1{} = D) -> render_type(_Module, Type, Arity, D, #{}). -spec render_type(Module, Type, Arity, Docs, Config) -> Res when - Module :: module(), Type :: atom(), Arity :: arity(), - Docs :: docs_v1(), - Config :: config(), - Res :: unicode:chardata() | {error, type_missing}. -render_type(_Module, Type, Arity, #docs_v1{ docs = Docs } = D, Config) -> + Module :: module(), + Type :: atom(), + Arity :: arity(), + Docs :: docs_v1(), + Config :: config(), + Res :: unicode:chardata() | {error, type_missing}. +render_type(Module, Type, Arity, #docs_v1{docs = Docs} = D, Config) -> render_typecb_docs( - lists:filter(fun({{type, T, A},_Anno,_Sig,_Doc,_Meta}) -> - T =:= Type andalso A =:= Arity; - (_) -> - false - end, Docs), D, Config). + lists:filter( + fun + ({{type, T, A}, _Anno, _Sig, _Doc, _Meta}) -> + T =:= Type andalso A =:= Arity; + (_) -> + false + end, + Docs + ), + D, + Module, + Config + ). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% API function for dealing with the callback documentation %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec render_callback(Module, Callback, Docs) -> Res when - Module :: module(), Callback :: atom(), - Docs :: docs_v1(), - Res :: unicode:chardata() | {error, callback_missing}. -render_callback(_Module, Callback, #docs_v1{ } = D) -> + Module :: module(), + Callback :: atom(), + Docs :: docs_v1(), + Res :: unicode:chardata() | {error, callback_missing}. +render_callback(_Module, Callback, #docs_v1{} = D) -> render_callback(_Module, Callback, D, #{}). --spec render_callback(Module, Callback, Docs, Config) -> Res when - Module :: module(), Callback :: atom(), - Docs :: docs_v1(), - Config :: config(), - Res :: unicode:chardata() | {error, callback_missing}; - (Module, Callback, Arity, Docs) -> Res when - Module :: module(), Callback :: atom(), Arity :: arity(), - Docs :: docs_v1(), - Res :: unicode:chardata() | {error, callback_missing}. -render_callback(_Module, Callback, Arity, #docs_v1{ } = D) -> +-spec render_callback + (Module, Callback, Docs, Config) -> Res when + Module :: module(), + Callback :: atom(), + Docs :: docs_v1(), + Config :: config(), + Res :: unicode:chardata() | {error, callback_missing}; + (Module, Callback, Arity, Docs) -> Res when + Module :: module(), + Callback :: atom(), + Arity :: arity(), + Docs :: docs_v1(), + Res :: unicode:chardata() | {error, callback_missing}. +render_callback(_Module, Callback, Arity, #docs_v1{} = D) -> render_callback(_Module, Callback, Arity, D, #{}); -render_callback(_Module, Callback, #docs_v1{ docs = Docs } = D, Config) -> +render_callback(Module, Callback, #docs_v1{docs = Docs} = D, Config) -> render_typecb_docs( - lists:filter(fun({{callback, T, _},_Anno,_Sig,_Doc,_Meta}) -> - T =:= Callback; - (_) -> - false - end, Docs), D, Config). + lists:filter( + fun + ({{callback, T, _}, _Anno, _Sig, _Doc, _Meta}) -> + T =:= Callback; + (_) -> + false + end, + Docs + ), + D, + Module, + Config + ). -spec render_callback(Module, Callback, Arity, Docs, Config) -> Res when - Module :: module(), Callback :: atom(), Arity :: arity(), - Docs :: docs_v1(), - Config :: config(), - Res :: unicode:chardata() | {error, callback_missing}. -render_callback(_Module, Callback, Arity, #docs_v1{ docs = Docs } = D, Config) -> + Module :: module(), + Callback :: atom(), + Arity :: arity(), + Docs :: docs_v1(), + Config :: config(), + Res :: unicode:chardata() | {error, callback_missing}. +render_callback(Module, Callback, Arity, #docs_v1{docs = Docs} = D, Config) -> render_typecb_docs( - lists:filter(fun({{callback, T, A},_Anno,_Sig,_Doc,_Meta}) -> - T =:= Callback andalso A =:= Arity; - (_) -> - false - end, Docs), D, Config). + lists:filter( + fun + ({{callback, T, A}, _Anno, _Sig, _Doc, _Meta}) -> + T =:= Callback andalso A =:= Arity; + (_) -> + false + end, + Docs + ), + D, + Module, + Config + ). %% Get the docs in the correct locale if it exists. -spec get_local_doc(_, 'hidden' | 'none' | map(), #docs_v1{}) -> chunk_elements(). get_local_doc(MissingMod, Docs, D) when is_atom(MissingMod) -> get_local_doc(atom_to_binary(MissingMod), Docs, D); -get_local_doc({F,A}, Docs, D) -> - get_local_doc(unicode:characters_to_binary( - io_lib:format("~tp/~p",[F,A])), Docs, D); -get_local_doc({_Type,F,A}, Docs, D) -> - get_local_doc({F,A}, Docs, D); -get_local_doc(_Missing, #{ <<"en">> := Docs }, D) -> +get_local_doc({F, A}, Docs, D) -> + get_local_doc( + unicode:characters_to_binary( + io_lib:format("~tp/~p", [F, A]) + ), + Docs, + D + ); +get_local_doc({_Type, F, A}, Docs, D) -> + get_local_doc({F, A}, Docs, D); +get_local_doc(_Missing, #{<<"en">> := Docs}, D) -> %% English if it exists normalize_format(Docs, D); get_local_doc(_Missing, ModuleDoc, D) when map_size(ModuleDoc) > 0 -> %% Otherwise take first alternative found normalize_format(maps:get(hd(maps:keys(ModuleDoc)), ModuleDoc), D); get_local_doc(Missing, hidden, _D) -> - [{p,[],[<<"The documentation for ">>,Missing, - <<" is hidden. This probably means that it is internal " - "and not to be used by other applications.">>]}]; + [ + {p, [], [ + <<"The documentation for ">>, + Missing, + << + " is hidden. This probably means that it is internal " + "and not to be used by other applications." + >> + ]} + ]; get_local_doc(_Missing, None, _D) when None =:= none; None =:= #{} -> []. -spec normalize_format(chunk_elements(), #docs_v1{}) -> chunk_elements(). -normalize_format(Docs, #docs_v1{ format = ?NATIVE_FORMAT }) -> +normalize_format(Docs, #docs_v1{format = ?NATIVE_FORMAT}) -> normalize(Docs); -normalize_format(Docs, #docs_v1{ format = <<"text/", _/binary>> }) when is_binary(Docs) -> +normalize_format(Docs, #docs_v1{format = <<"text/", _/binary>>}) when is_binary(Docs) -> [{pre, [], [Docs]}]. %%% Functions for rendering reference documentation --spec render_function([chunk_entry()], #docs_v1{}, map()) -> unicode:chardata() | {'error', 'function_missing'}. -render_function([], _D, _Config) -> - {error,function_missing}; -render_function(FDocs, #docs_v1{ docs = Docs } = D, Config) -> +-spec render_function([chunk_entry()], #docs_v1{}, atom(), map()) -> + unicode:chardata() | {'error', 'function_missing'}. +render_function([], _D, _Module, _Config) -> + {error, function_missing}; +render_function(FDocs, #docs_v1{docs = Docs} = D, Module, Config) -> Grouping = lists:foldl( - fun({_Group,_Anno,_Sig,_Doc,#{ equiv := Group }} = Func,Acc) -> - Members = maps:get(Group, Acc, []), - Acc#{ Group => [Func|Members] }; - ({Group, _Anno, _Sig, _Doc, _Meta} = Func, Acc) -> - Members = maps:get(Group, Acc, []), - Acc#{ Group => [Func|Members] } - end, #{}, lists:sort(FDocs)), + fun + ({_Group, _Anno, _Sig, _Doc, #{equiv := Group}} = Func, Acc) -> + Members = maps:get(Group, Acc, []), + Acc#{Group => [Func | Members]}; + ({Group, _Anno, _Sig, _Doc, _Meta} = Func, Acc) -> + Members = maps:get(Group, Acc, []), + Acc#{Group => [Func | Members]} + end, + #{}, + lists:sort(FDocs) + ), lists:map( - fun({Group,Members}) -> - lists:map( - fun(Member = {_,_,_,Doc,_}) -> - Sig = render_signature(Member), - LocalDoc = - if Doc =:= #{} -> - case lists:keyfind(Group, 1, Docs) of - false -> - get_local_doc(Group, none, D); - {_,_,_,GroupDoc,_} -> - get_local_doc(Group, GroupDoc, D) - end; - true -> - get_local_doc(Group, Doc, D) + fun({Group, Members}) -> + lists:map( + fun(Member = {_, _, _, Doc, _}) -> + Sig = render_signature(Member, Module), + LocalDoc = + if + Doc =:= #{} -> + case lists:keyfind(Group, 1, Docs) of + false -> + get_local_doc(Group, none, D); + {_, _, _, GroupDoc, _} -> + get_local_doc(Group, GroupDoc, D) + end; + true -> + get_local_doc(Group, Doc, D) end, - render_headers_and_docs( - [Sig], LocalDoc, D, Config) - end, Members) - end, maps:to_list(Grouping)). + render_headers_and_docs( + [Sig], LocalDoc, D, Config + ) + end, + Members + ) + end, + maps:to_list(Grouping) + ). %% Render the signature of either function, type, or anything else really. --spec render_signature(chunk_entry()) -> chunk_elements(). -render_signature({{_Type,_F,_A},_Anno,_Sigs,_Docs,#{ signature := Specs } = Meta}) -> +-spec render_signature(chunk_entry(), module()) -> chunk_elements() | els_poi:poi(). +render_signature({{_Type, _F, _A}, _Anno, _Sigs, _Docs, #{signature := Specs} = Meta}, _Module) -> lists:flatmap( - fun(ASTSpec) -> - PPSpec = erl_pp:attribute(ASTSpec,[{encoding,utf8}]), - Spec = - case ASTSpec of - {_Attribute, _Line, opaque, _} -> - %% We do not want show the internals of the opaque type - hd(string:split(PPSpec,"::")); - _ -> - trim_spec(PPSpec) - end, - BinSpec = - unicode:characters_to_binary( - string:trim(Spec, trailing, "\n")), - [{pre,[],BinSpec}, - {hr,[],[]}|render_meta(Meta)] - end, Specs); -render_signature({{_Type,_F,_A},_Anno,Sigs,_Docs,Meta}) -> - [{pre,[],Sigs},{hr,[],[]} | render_meta(Meta)]. + fun(ASTSpec) -> + PPSpec = erl_pp:attribute(ASTSpec, [{encoding, utf8}]), + Spec = + case ASTSpec of + {_Attribute, _Line, opaque, _} -> + %% We do not want show the internals of the opaque type + hd(string:split(PPSpec, "::")); + _ -> + trim_spec(PPSpec) + end, + BinSpec = + unicode:characters_to_binary( + string:trim(Spec, trailing, "\n") + ), + [ + {pre, [], BinSpec}, + {hr, [], []} + | render_meta(Meta) + ] + end, + Specs + ); +render_signature({{_Type, F, A}, _Anno, Sigs, _Docs, Meta}, Module) -> + case els_dt_signatures:lookup({Module, F, A}) of + {ok, [#{spec := <<"-spec ", Spec/binary>>}]} -> + [{pre, [], Spec}, {hr, [], []} | render_meta(Meta)]; + {ok, _} -> + [{pre, [], Sigs}, {hr, [], []} | render_meta(Meta)] + end. -spec trim_spec(unicode:chardata()) -> unicode:chardata(). trim_spec(Spec) -> unicode:characters_to_binary( - string:trim( - lists:join($\n,trim_spec(string:split(Spec, "\n", all),0)), - trailing, "\n")). + string:trim( + lists:join($\n, trim_spec(string:split(Spec, "\n", all), 0)), + trailing, + "\n" + ) + ). -spec trim_spec([unicode:chardata()], non_neg_integer()) -> unicode:chardata(). -trim_spec(["-spec " ++ Spec|T], 0) -> +trim_spec(["-spec " ++ Spec | T], 0) -> [Spec | trim_spec(T, 6)]; -trim_spec([H|T], N) -> - case re:run(H,io_lib:format("(\\s{~p}\\s+)when",[N]),[{capture,all_but_first}]) of - {match,[{0,Indent}]} -> - trim_spec([H|T],Indent); +trim_spec([H | T], N) -> + case re:run(H, io_lib:format("(\\s{~p}\\s+)when", [N]), [{capture, all_but_first}]) of + {match, [{0, Indent}]} -> + trim_spec([H | T], Indent); nomatch -> - case string:trim(string:slice(H, 0, N),both) of + case string:trim(string:slice(H, 0, N), both) of "" -> - [re:replace(string:slice(H, N, infinity)," "," ",[global])|trim_spec(T, N)]; + [ + re:replace(string:slice(H, N, infinity), " ", " ", [global]) + | trim_spec(T, N) + ]; _ -> - [re:replace(H," "," ",[global])|trim_spec(T, N)] + [re:replace(H, " ", " ", [global]) | trim_spec(T, N)] end end; trim_spec([], _N) -> @@ -323,44 +470,69 @@ trim_spec([], _N) -> -spec render_meta(map()) -> chunk_elements(). render_meta(Meta) -> - case lists:flatmap( - fun({since,Vsn}) -> - [{em,[],<<"Since:">>}, <<" ">>, Vsn]; - ({ deprecated, Depr }) -> - [{em,[],<<"Deprecated: ">>}, <<" ">>, Depr]; - (_) -> [] - end, maps:to_list(Meta)) of + case + lists:flatmap( + fun + ({since, Vsn}) -> + [{em, [], <<"Since:">>}, <<" ">>, Vsn]; + ({deprecated, Depr}) -> + [{em, [], <<"Deprecated: ">>}, <<" ">>, Depr]; + (_) -> + [] + end, + maps:to_list(Meta) + ) + of [] -> []; Docs -> Docs end. --spec render_headers_and_docs([chunk_elements()], chunk_elements(), #docs_v1{}, map()) -> unicode:chardata(). +-spec render_headers_and_docs([chunk_elements()], chunk_elements(), #docs_v1{}, map()) -> + unicode:chardata(). render_headers_and_docs(Headers, DocContents, D, Config) -> render_headers_and_docs(Headers, DocContents, init_config(D, Config)). --spec render_headers_and_docs([chunk_elements()], chunk_elements(), #config{}) -> unicode:chardata(). +-spec render_headers_and_docs([chunk_elements()], chunk_elements(), #config{}) -> + unicode:chardata(). render_headers_and_docs(Headers, DocContents, #config{} = Config) -> - [render_docs( - lists:flatmap( - fun(Header) -> - [{br,[],[]},Header] - end, Headers), Config), - "\n", - render_docs(DocContents, 0, Config)]. - --spec render_typecb_docs([TypeCB] | TypeCB, #config{}) -> unicode:chardata() | {'error', 'type_missing'} when - TypeCB :: {{type | callback, Name :: atom(), Arity :: non_neg_integer()}, - Encoding :: binary(), Sig :: [binary()], none | hidden | #{ binary() => chunk_elements()}}. -render_typecb_docs([], _C) -> - {error,type_missing}; -render_typecb_docs(TypeCBs, #config{} = C) when is_list(TypeCBs) -> - [render_typecb_docs(TypeCB, C) || TypeCB <- TypeCBs]; -render_typecb_docs({F,_,_Sig,Docs,_Meta} = TypeCB, #config{docs = D} = C) -> - render_headers_and_docs(render_signature(TypeCB), get_local_doc(F,Docs,D), C). --spec render_typecb_docs(chunk_elements(), #docs_v1{}, _) -> unicode:chardata() | {'error', 'type_missing'}. -render_typecb_docs(Docs, D, Config) -> - render_typecb_docs(Docs, init_config(D, Config)). + [ + render_docs( + lists:flatmap( + fun(Header) -> + [{br, [], []}, Header] + end, + Headers + ), + Config + ), + "\n", + render_docs(DocContents, 0, Config) + ]. + +-spec render_typecb_docs([TypeCB] | TypeCB, module(), #config{}) -> + unicode:chardata() | {'error', 'type_missing'} +when + TypeCB :: { + {type | callback, Name :: atom(), Arity :: non_neg_integer()}, + Encoding :: binary(), + Sig :: [binary()], + none | hidden | #{binary() => chunk_elements()} + }. +render_typecb_docs([], _Module, _C) -> + {error, type_missing}; +render_typecb_docs(TypeCBs, Module, #config{} = C) when is_list(TypeCBs) -> + [render_typecb_docs(TypeCB, Module, C) || TypeCB <- TypeCBs]; +render_typecb_docs({F, _, _Sig, Docs, _Meta} = TypeCB, Module, #config{docs = D} = C) -> + render_headers_and_docs( + render_signature(TypeCB, Module), + get_local_doc(F, Docs, D), + C + ). +-spec render_typecb_docs(chunk_elements(), #docs_v1{}, module(), _) -> + unicode:chardata() | {'error', 'type_missing'}. +render_typecb_docs(Docs, D, Module, Config) -> + render_typecb_docs(Docs, Module, init_config(D, Config)). %%% General rendering functions -spec render_docs([chunk_element()], #config{}) -> unicode:chardata(). @@ -368,28 +540,38 @@ render_docs(DocContents, #config{} = Config) -> render_docs(DocContents, 0, Config). -spec render_docs([chunk_element()], 0, #config{}) -> unicode:chardata(). render_docs(DocContents, Ind, D = #config{}) when is_integer(Ind) -> - {Doc,_} = trimnl(render_docs(DocContents, [], 0, Ind, D)), + {Doc, _} = trimnl(render_docs(DocContents, [], 0, Ind, D)), Doc. -spec init_config(#docs_v1{}, _) -> #config{}. init_config(D, _Config) -> - #config{ docs = D }. - --spec render_docs(Elems :: [chunk_element()], - Stack :: [chunk_element_type()], - non_neg_integer(), - non_neg_integer(), - #config{}) -> + #config{docs = D}. + +-spec render_docs( + Elems :: [chunk_element()], + Stack :: [chunk_element_type()], + non_neg_integer(), + non_neg_integer(), + #config{} +) -> {unicode:chardata(), non_neg_integer()}. -render_docs(Elems,State,Pos,Ind,D) when is_list(Elems) -> +render_docs(Str, State, Pos, Ind, D) when + is_list(Str), + is_integer(hd(Str)) +-> + %% This is a string, convert it to binary. + render_docs([unicode:characters_to_binary(Str)], State, Pos, Ind, D); +render_docs(Elems, State, Pos, Ind, D) when is_list(Elems) -> lists:mapfoldl( - fun(Elem,P) -> - render_docs(Elem,State,P,Ind,D) - end,Pos,Elems); -render_docs(Elem,State,Pos,Ind,D) -> -% io:format("Elem: ~p (~p) (~p,~p)~n",[Elem,State,Pos,Ind]), - render_element(Elem,State,Pos,Ind,D). - + fun(Elem, P) -> + render_docs(Elem, State, P, Ind, D) + end, + Pos, + Elems + ); +render_docs(Elem, State, Pos, Ind, D) -> + % io:format("Elem: ~p (~p) (~p,~p)~n",[Elem,State,Pos,Ind]), + render_element(Elem, State, Pos, Ind, D). %%% The function is the main element rendering function %%% @@ -406,215 +588,244 @@ render_docs(Elem,State,Pos,Ind,D) -> %%% Any block elements (i.e. p, ul, li etc) are responsible for trimming %%% extra new lines. eg. <ul><li><p>content</p></li></ul> should only %%% have two newlines at the end. --spec render_element(Elem :: chunk_element(), - Stack :: [chunk_element_type()], - Pos :: non_neg_integer(), - Indent :: non_neg_integer(), - Config :: #config{}) -> - {unicode:chardata(), Pos :: non_neg_integer()}. +-spec render_element( + Elem :: chunk_element(), + Stack :: [chunk_element_type()], + Pos :: non_neg_integer(), + Indent :: non_neg_integer(), + Config :: #config{} +) -> + {unicode:chardata(), Pos :: non_neg_integer()}. %% render_element({IgnoreMe,_,Content}, State, Pos, Ind,D) %% when IgnoreMe =:= a -> %% render_docs(Content, State, Pos, Ind,D); %% Catch h* before the padding is done as they reset padding -render_element({h1,_,Content},State,0 = Pos,_Ind,D) -> - {Docs, NewPos} = render_docs(Content,State,Pos,0,D), +render_element({h1, _, Content}, State, 0 = Pos, _Ind, D) -> + {Docs, NewPos} = render_docs(Content, State, Pos, 0, D), trimnlnl({["# ", Docs], NewPos}); -render_element({h2,_,Content},State,0 = Pos,_Ind,D) -> - {Docs, NewPos} = render_docs(Content,State,Pos,0,D), +render_element({h2, _, Content}, State, 0 = Pos, _Ind, D) -> + {Docs, NewPos} = render_docs(Content, State, Pos, 0, D), trimnlnl({["## ", Docs], NewPos}); -render_element({h3,_,Content},State,Pos,_Ind,D) when Pos =< 2 -> - {Docs, NewPos} = render_docs(Content,State,Pos,0,D), +render_element({h3, _, Content}, State, Pos, _Ind, D) when Pos =< 2 -> + {Docs, NewPos} = render_docs(Content, State, Pos, 0, D), trimnlnl({["### ", Docs], NewPos}); -render_element({h4,_,Content},State,Pos,_Ind,D) when Pos =< 2 -> - {Docs, NewPos} = render_docs(Content,State,Pos,0,D), +render_element({h4, _, Content}, State, Pos, _Ind, D) when Pos =< 2 -> + {Docs, NewPos} = render_docs(Content, State, Pos, 0, D), trimnlnl({["#### ", Docs], NewPos}); -render_element({h5,_,Content},State,Pos,_Ind,D) when Pos =< 2 -> - {Docs, NewPos} = render_docs(Content,State,Pos,0,D), +render_element({h5, _, Content}, State, Pos, _Ind, D) when Pos =< 2 -> + {Docs, NewPos} = render_docs(Content, State, Pos, 0, D), trimnlnl({["##### ", Docs], NewPos}); -render_element({h6,_,Content},State,Pos,_Ind,D) when Pos =< 2 -> - {Docs, NewPos} = render_docs(Content,State,Pos,0,D), +render_element({h6, _, Content}, State, Pos, _Ind, D) when Pos =< 2 -> + {Docs, NewPos} = render_docs(Content, State, Pos, 0, D), trimnlnl({["###### ", Docs], NewPos}); - -render_element({pre,_Attr,_Content} = E,State,Pos,Ind,D) when Pos > Ind -> +render_element({pre, _Attr, _Content} = E, State, Pos, Ind, D) when Pos > Ind -> %% We pad `pre` with two newlines if the previous section did not indent the region. - {Docs,NewPos} = render_element(E,State,0,Ind,D), - {["\n\n",Docs],NewPos}; -render_element({Elem,_Attr,_Content} = E,State,Pos,Ind,D) when Pos > Ind, ?IS_BLOCK(Elem) -> - {Docs,NewPos} = render_element(E,State,0,Ind,D), - {["\n",Docs],NewPos}; -render_element({'div',[{class,What}],Content},State,Pos,Ind,D) -> - Title = unicode:characters_to_binary([string:titlecase(What),":"]), - {Header, 0} = render_element({h3,[],[Title]},State,Pos,Ind,D), - {Docs, 0} = render_element({'div',[],Content},['div'|State], 0, Ind+2, D), - {[Header,Docs],0}; -render_element({Tag,_,Content},State,Pos,Ind,D) when Tag =:= p; Tag =:= 'div' -> - trimnlnl(render_docs(Content, [Tag|State], Pos, Ind, D)); - -render_element(Elem,State,Pos,Ind,D) when Pos < Ind -> - {Docs,NewPos} = render_element(Elem,State,Ind,Ind,D), - {[pad(Ind - Pos), Docs],NewPos}; - -render_element({a,Attr,Content}, State, Pos, Ind,D) -> + {Docs, NewPos} = render_element(E, State, 0, Ind, D), + {["\n\n", Docs], NewPos}; +render_element({Elem, _Attr, _Content} = E, State, Pos, Ind, D) when Pos > Ind, ?IS_BLOCK(Elem) -> + {Docs, NewPos} = render_element(E, State, 0, Ind, D), + {["\n", Docs], NewPos}; +render_element({'div', [{class, What}], Content}, State, Pos, Ind, D) -> + Title = unicode:characters_to_binary([string:titlecase(What), ":"]), + {Header, 0} = render_element({h3, [], [Title]}, State, Pos, Ind, D), + {Docs, 0} = render_element({'div', [], Content}, ['div' | State], 0, Ind + 2, D), + {[Header, Docs], 0}; +render_element({Tag, _, Content}, State, Pos, Ind, D) when Tag =:= p; Tag =:= 'div' -> + trimnlnl(render_docs(Content, [Tag | State], Pos, Ind, D)); +render_element(Elem, State, Pos, Ind, D) when Pos < Ind -> + {Docs, NewPos} = render_element(Elem, State, Ind, Ind, D), + {[pad(Ind - Pos), Docs], NewPos}; +render_element({a, Attr, Content}, State, Pos, Ind, D) -> {Docs, NewPos} = render_docs(Content, State, Pos, Ind, D), Href = proplists:get_value(href, Attr), IsOTPLink = Href =/= undefined andalso string:find(Href, ":") =/= nomatch, - case proplists:get_value(rel,Attr) of + case proplists:get_value(rel, Attr) of _ when not IsOTPLink -> - {Docs, NewPos}; + {Docs, NewPos}; <<"https://erlang.org/doc/link/seemfa">> -> - [_App, MFA] = string:split(Href,":"), - [Mod, FA] = string:split(MFA,"#"), - [Func, Arity] = string:split(FA,"/"), - {["[",Docs,"](https://erlang.org/doc/man/",Mod,".html#",Func,"-",Arity,")"],NewPos}; + [_App, MFA] = string:split(Href, ":"), + [Mod, FA] = string:split(MFA, "#"), + [Func, Arity] = string:split(FA, "/"), + { + ["[", Docs, "](https://erlang.org/doc/man/", Mod, ".html#", Func, "-", Arity, ")"], + NewPos + }; <<"https://erlang.org/doc/link/seetype">> -> - case string:lexemes(Href,":#/") of + case string:lexemes(Href, ":#/") of [_App, Mod, Type, Arity] -> - {["[",Docs,"](https://erlang.org/doc/man/",Mod,".html#","type-",Type,"-",Arity,")"],NewPos}; + { + [ + "[", + Docs, + "](https://erlang.org/doc/man/", + Mod, + ".html#", + "type-", + Type, + "-", + Arity, + ")" + ], + NewPos + }; [_App, Mod, Type] -> - {["[",Docs,"](https://erlang.org/doc/man/",Mod,".html#","type-",Type,")"],NewPos} + { + [ + "[", + Docs, + "](https://erlang.org/doc/man/", + Mod, + ".html#", + "type-", + Type, + ")" + ], + NewPos + } end; <<"https://erlang.org/doc/link/seeerl">> -> - [_App, Mod|Anchor] = string:lexemes(Href,":#"), - {["[",Docs,"](https://erlang.org/doc/man/",Mod,".html#",Anchor,")"],NewPos}; + [_App, Mod | Anchor] = string:lexemes(Href, ":#"), + {["[", Docs, "](https://erlang.org/doc/man/", Mod, ".html#", Anchor, ")"], NewPos}; _ -> - {Docs,NewPos} + {Docs, NewPos} end; - -render_element({code,_,Content},[pre|_] = State,Pos,Ind,D) -> +render_element({code, _, Content}, [pre | _] = State, Pos, Ind, D) -> %% When code is within a pre we don't emit any underline - render_docs(Content, [code|State], Pos, Ind,D); -render_element({code,_,Content},State,Pos,Ind,D) -> - {Docs, NewPos} = render_docs(Content, [code|State], Pos, Ind,D), - {["`",Docs,"`"], NewPos}; - -render_element({em,Attr,Content},State,Pos,Ind,D) -> - render_element({i,Attr,Content},State,Pos,Ind,D); -render_element({i,_,Content},State,Pos,Ind,D) -> - {Docs, NewPos} = render_docs(Content, [i|State], Pos, Ind,D), - case lists:member(pre,State) of + render_docs(Content, [code | State], Pos, Ind, D); +render_element({code, _, Content}, State, Pos, Ind, D) -> + {Docs, NewPos} = render_docs(Content, [code | State], Pos, Ind, D), + {["`", Docs, "`"], NewPos}; +render_element({em, Attr, Content}, State, Pos, Ind, D) -> + render_element({i, Attr, Content}, State, Pos, Ind, D); +render_element({i, _, Content}, State, Pos, Ind, D) -> + {Docs, NewPos} = render_docs(Content, [i | State], Pos, Ind, D), + case lists:member(pre, State) of true -> {[Docs], NewPos}; false -> - {["*",Docs,"*"], NewPos} + {["*", Docs, "*"], NewPos} end; - -render_element({hr,[],[]},_State,Pos,_Ind,_D) -> - {"---\n",Pos}; - -render_element({br,[],[]},_State,Pos,_Ind,_D) -> - {"",Pos}; - -render_element({strong,Attr,Content},State,Pos,Ind,D) -> - render_element({b,Attr,Content},State,Pos,Ind,D); -render_element({b,_,Content},State,Pos,Ind,D) -> - {Docs, NewPos} = render_docs(Content, State, Pos, Ind,D), - case lists:member(pre,State) of +render_element({hr, [], []}, _State, Pos, _Ind, _D) -> + {"---\n", Pos}; +render_element({br, [], []}, _State, Pos, _Ind, _D) -> + {"", Pos}; +render_element({strong, Attr, Content}, State, Pos, Ind, D) -> + render_element({b, Attr, Content}, State, Pos, Ind, D); +render_element({b, _, Content}, State, Pos, Ind, D) -> + {Docs, NewPos} = render_docs(Content, State, Pos, Ind, D), + case lists:member(pre, State) of true -> {[Docs], NewPos}; false -> - {["**",Docs,"**"], NewPos} + {["**", Docs, "**"], NewPos} end; - -render_element({pre,_,Content},State,Pos,Ind,D) -> +render_element({pre, _, Content}, State, Pos, Ind, D) -> %% For pre we make sure to respect the newlines in pre - {Docs, _} = trimnl(render_docs(Content, [pre|State], Pos, Ind, D)), - trimnlnl(["```erlang\n",pad(Ind),Docs,pad(Ind),"```"]); - -render_element({ul,[{class,<<"types">>}],Content},State,_Pos,Ind,D) -> - {Docs, _} = render_docs(Content, [types|State], 0, Ind, D), + {Docs, _} = trimnl(render_docs(Content, [pre | State], Pos, Ind, D)), + trimnlnl(["```erlang\n", pad(Ind), Docs, pad(Ind), "```"]); +render_element({ul, [{class, <<"types">>}], Content}, State, _Pos, Ind, D) -> + {Docs, _} = render_docs(Content, [types | State], 0, Ind, D), trimnlnl(Docs); -render_element({li,Attr,Content},[types|_] = State,Pos,Ind,C) -> +render_element({li, Attr, Content}, [types | _] = State, Pos, Ind, C) -> Doc = - case {proplists:get_value(name, Attr),proplists:get_value(class, Attr)} of - {undefined,Class} when Class =:= undefined; Class =:= <<"type">> -> + case {proplists:get_value(name, Attr), proplists:get_value(class, Attr)} of + {undefined, Class} when Class =:= undefined; Class =:= <<"type">> -> %% Inline html for types - render_docs(Content ++ [<<" ">>],[type|State],Pos,Ind,C); - {_,<<"description">>} -> + render_docs(Content ++ [<<" ">>], [type | State], Pos, Ind, C); + {_, <<"description">>} -> %% Inline html for type descriptions - render_docs(Content ++ [<<" ">>],[type|State],Pos,Ind+2,C); - {Name,_} -> + render_docs(Content ++ [<<" ">>], [type | State], Pos, Ind + 2, C); + {Name, _} -> %% Try to render from type metadata - case render_type_signature(binary_to_atom(Name),C) of + case render_type_signature(binary_to_atom(Name), C) of undefined when Content =:= [] -> %% Failed and no content, emit place-holder - {["```erlang\n-type ",Name,"() :: term().```"],0}; + {["```erlang\n-type ", Name, "() :: term().```"], 0}; undefined -> %% Failed with metadata, render the content - render_docs(Content ++ [<<" ">>],[type|State],Pos,Ind,C); + render_docs(Content ++ [<<" ">>], [type | State], Pos, Ind, C); Type -> %% Emit the erl_pp typespec - {["```erlang\n",Type,"```"],0} + {["```erlang\n", Type, "```"], 0} end end, trimnl(Doc); -render_element({ul,[],Content},State,Pos,Ind,D) -> - trimnlnl(render_docs(Content, [ul|State], Pos, Ind,D)); -render_element({ol,[],Content},State,Pos,Ind,D) -> - trimnlnl(render_docs(Content, [ol|State], Pos, Ind,D)); -render_element({li,[],Content},[ul | _] = State, Pos, Ind,D) -> - {Docs, _NewPos} = render_docs(Content, [li | State], Pos + 2,Ind + 2, D), - trimnl(["* ",Docs]); -render_element({li,[],Content},[ol | _] = State, Pos, Ind,D) -> - {Docs, _NewPos} = render_docs(Content, [li | State], Pos + 2,Ind + 2, D), +render_element({ul, [], Content}, State, Pos, Ind, D) -> + trimnlnl(render_docs(Content, [ul | State], Pos, Ind, D)); +render_element({ol, [], Content}, State, Pos, Ind, D) -> + trimnlnl(render_docs(Content, [ol | State], Pos, Ind, D)); +render_element({li, [], Content}, [ul | _] = State, Pos, Ind, D) -> + {Docs, _NewPos} = render_docs(Content, [li | State], Pos + 2, Ind + 2, D), + trimnl(["* ", Docs]); +render_element({li, [], Content}, [ol | _] = State, Pos, Ind, D) -> + {Docs, _NewPos} = render_docs(Content, [li | State], Pos + 2, Ind + 2, D), trimnl(["1. ", Docs]); - -render_element({dl,_,Content},State,Pos,Ind,D) -> - trimnlnl(render_docs(Content, [dl|State], Pos, Ind,D)); -render_element({dt,_,Content},[dl | _] = State,Pos,Ind,D) -> - {Docs, NewPos} = render_docs([{b,[],Content}], - [li | State], Pos + 2, Ind + 2, D), +render_element({dl, _, Content}, State, Pos, Ind, D) -> + trimnlnl(render_docs(Content, [dl | State], Pos, Ind, D)); +render_element({dt, _, Content}, [dl | _] = State, Pos, Ind, D) -> + {Docs, NewPos} = render_docs( + [{b, [], Content}], + [li | State], + Pos + 2, + Ind + 2, + D + ), trimnl({["* ", string:trim(Docs, trailing, "\n"), " "], NewPos}); -render_element({dd,_,Content},[dl | _] = State,Pos,Ind,D) -> - {Docs, _NewPos} = render_docs(Content, [li | State], Pos+2, Ind + 2, D), +render_element({dd, _, Content}, [dl | _] = State, Pos, Ind, D) -> + {Docs, _NewPos} = render_docs(Content, [li | State], Pos + 2, Ind + 2, D), trimnlnl([pad(2 + Ind - Pos), Docs]); - -render_element(B, State, Pos, Ind,_D) when is_binary(B) -> - Pre = string:replace(B,"\n",[nlpad(Ind)],all), +render_element(B, State, Pos, Ind, _D) when is_binary(B) -> + Pre = string:replace(B, "\n", [nlpad(Ind)], all), EscapeChars = [ - "\\", - "`", - "*", - "_", - "{", - "}", - "[", - "]", - "<", - ">", - "(", - ")", - "#", - "+", - "-", - ".", - "!", - "|" - ], + "\\", + "`", + "*", + "_", + "{", + "}", + "[", + "]", + "<", + ">", + "(", + ")", + "#", + "+", + "-", + ".", + "!", + "|" + ], Str = case State of - [pre|_] -> Pre; - [code|_] -> Pre; + [pre | _] -> + Pre; + [code | _] -> + Pre; _ -> - re:replace(Pre, ["(",lists:join($|,[["\\",C] || C <- EscapeChars]),")"], - "\\\\\\1", [global]) + re:replace( + Pre, + ["(", lists:join($|, [["\\", C] || C <- EscapeChars]), ")"], + "\\\\\\1", + [global] + ) end, {Str, Pos + lastline(Str)}; - -render_element({Tag,Attr,Content}, State, Pos, Ind,D) -> - case lists:member(Tag,?ALL_ELEMENTS) of +render_element({Tag, Attr, Content}, State, Pos, Ind, D) -> + case lists:member(Tag, ?ALL_ELEMENTS) of true -> - throw({unhandled_element,Tag,Attr,Content}); + throw({unhandled_element, Tag, Attr, Content}); false -> %% We ignore tags that we do not care about ok end, - render_docs(Content, State, Pos, Ind,D). + render_docs(Content, State, Pos, Ind, D). -spec render_type_signature(atom(), #config{}) -> 'undefined' | unicode:chardata(). -render_type_signature(Name, #config{ docs = #docs_v1{ metadata = #{ types := AllTypes }}}) -> - case [Type || Type = {TName,_} <- maps:keys(AllTypes), TName =:= Name] of +render_type_signature(Name, #config{docs = #docs_v1{metadata = #{types := AllTypes}}}) -> + case [Type || Type = {TName, _} <- maps:keys(AllTypes), TName =:= Name] of [] -> undefined; Types -> @@ -624,36 +835,39 @@ render_type_signature(Name, #config{ docs = #docs_v1{ metadata = #{ types := All %% Pad N spaces (and possibly pre-prend newline), disabling any ansi formatting while doing so. -spec pad(non_neg_integer()) -> unicode:chardata(). pad(N) -> - pad(N,""). + pad(N, ""). -spec nlpad(non_neg_integer()) -> unicode:chardata(). nlpad(N) -> - pad(N,"\n"). + pad(N, "\n"). -spec pad(non_neg_integer(), unicode:chardata()) -> unicode:chardata(). pad(N, Extra) -> - Pad = lists:duplicate(N,[$ ]), + Pad = lists:duplicate(N, [$\s]), [Extra, Pad]. -spec lastline(unicode:chardata()) -> non_neg_integer(). %% Look for the length of the last line of a string lastline(Str) -> - LastStr = case string:find(Str,"\n",trailing) of - nomatch -> - Str; - Match -> - tl(string:next_codepoint(Match)) - end, + LastStr = + case string:find(Str, "\n", trailing) of + nomatch -> + Str; + Match -> + tl(string:next_codepoint(Match)) + end, string:length(LastStr). %% These functions make sure that we trim extra newlines added %% by the renderer. For example if we do <li><p></p></li> %% that would add 4 \n at after the last </li>. This is trimmed %% here to only be 2 \n --spec trimnlnl(unicode:chardata() | {unicode:chardata(), non_neg_integer()}) -> {unicode:chardata(), 0}. +-spec trimnlnl(unicode:chardata() | {unicode:chardata(), non_neg_integer()}) -> + {unicode:chardata(), 0}. trimnlnl({Chars, _Pos}) -> nl(nl(string:trim(Chars, trailing, "\n"))); trimnlnl(Chars) -> nl(nl(string:trim(Chars, trailing, "\n"))). --spec trimnl(unicode:chardata() | {unicode:chardata(), non_neg_integer()}) -> {unicode:chardata(), 0}. +-spec trimnl(unicode:chardata() | {unicode:chardata(), non_neg_integer()}) -> + {unicode:chardata(), 0}. trimnl({Chars, _Pos}) -> nl(string:trim(Chars, trailing, "\n")); trimnl(Chars) -> @@ -662,7 +876,7 @@ trimnl(Chars) -> nl({Chars, _Pos}) -> nl(Chars); nl(Chars) -> - {[Chars,"\n"],0}. + {[Chars, "\n"], 0}. -endif. -endif. diff --git a/apps/els_lsp/src/els_elvis_diagnostics.erl b/apps/els_lsp/src/els_elvis_diagnostics.erl index 6c31bc65a..97e0b3550 100644 --- a/apps/els_lsp/src/els_elvis_diagnostics.erl +++ b/apps/els_lsp/src/els_elvis_diagnostics.erl @@ -11,10 +11,11 @@ %%============================================================================== %% Exports %%============================================================================== --export([ is_default/0 - , run/1 - , source/0 - ]). +-export([ + is_default/0, + run/1, + source/0 +]). %%============================================================================== %% Includes @@ -28,55 +29,61 @@ -spec is_default() -> boolean(). is_default() -> - true. + true. -spec run(uri()) -> [els_diagnostics:diagnostic()]. run(Uri) -> - case els_utils:project_relative(Uri) of - {error, not_relative} -> []; - RelFile -> - %% Note: elvis_core:rock_this requires a file path relative to the - %% project root, formatted as a string. - try - Filename = get_elvis_config_path(), - Config = elvis_config:from_file(Filename), - elvis_core:rock_this(RelFile, Config) - of - ok -> []; - {fail, Problems} -> lists:flatmap(fun format_diagnostics/1, Problems) - catch Err -> - ?LOG_WARNING("Elvis error.[Err=~p] ", [Err]), - [] - end + case els_utils:project_relative(Uri) of + {error, not_relative} -> + []; + RelFile -> + %% Note: elvis_core:rock_this requires a file path relative to the + %% project root, formatted as a string. + try + Filename = get_elvis_config_path(), + Config = elvis_config:from_file(Filename), + elvis_core:rock_this(RelFile, Config) + of + ok -> []; + {fail, Problems} -> lists:flatmap(fun format_diagnostics/1, Problems) + catch + Err -> + ?LOG_WARNING("Elvis error.[Err=~p] ", [Err]), + [] + end end. -spec source() -> binary(). source() -> - <<"Elvis">>. + <<"Elvis">>. %%============================================================================== %% Internal Functions %%============================================================================== -spec format_diagnostics(any()) -> [map()]. format_diagnostics(#{file := _File, rules := Rules}) -> - R = format_rules(Rules), - lists:flatten(R). + R = format_rules(Rules), + lists:flatten(R). %%% This section is based directly on elvis_result:print_rules -spec format_rules([any()]) -> [[map()]]. format_rules([]) -> - []; + []; format_rules([#{error_msg := Msg, info := Info} | Items]) -> - [diagnostic(<<"Config Error">>, Msg, 1, Info, ?DIAGNOSTIC_ERROR) | - format_rules(Items)]; + [ + diagnostic(<<"Config Error">>, Msg, 1, Info, ?DIAGNOSTIC_ERROR) + | format_rules(Items) + ]; format_rules([#{warn_msg := Msg, info := Info} | Items]) -> - [diagnostic(<<"Config Warning">>, Msg, 1, Info, ?DIAGNOSTIC_WARNING) | - format_rules(Items)]; + [ + diagnostic(<<"Config Warning">>, Msg, 1, Info, ?DIAGNOSTIC_WARNING) + | format_rules(Items) + ]; format_rules([#{items := []} | Items]) -> - format_rules(Items); + format_rules(Items); format_rules([#{items := Items, name := Name} | EItems]) -> - ItemDiags = format_item(Name, Items), - [lists:flatten(ItemDiags) | format_rules(EItems)]. + ItemDiags = format_item(Name, Items), + [lists:flatten(ItemDiags) | format_rules(EItems)]. %% Item -spec format_item(any(), [any()]) -> [[map()]]. @@ -88,26 +95,48 @@ format_item(_Name, []) -> %%% End of section based directly on elvis_result:print_rules --spec diagnostic(any(), any(), integer(), [any()], - els_diagnostics:severity()) -> [map()]. +-spec diagnostic( + any(), + any(), + integer(), + [any()], + els_diagnostics:severity() +) -> [map()]. diagnostic(Name, Msg, Ln, Info, Severity) -> - FMsg = io_lib:format(Msg, Info), - Range = els_protocol:range(#{from => {Ln, 1}, to => {Ln + 1, 1}}), - Message = els_utils:to_binary(FMsg), - [#{ range => Range - , severity => Severity - , code => Name - , source => source() - , message => Message - , relatedInformation => [] - }]. + %% Avoid negative line numbers + DiagLine = make_protocol_line(Ln), + FMsg = io_lib:format(Msg, Info), + Range = els_protocol:range(#{ + from => {DiagLine, 1}, + to => {DiagLine + 1, 1} + }), + Message = els_utils:to_binary(FMsg), + [ + #{ + range => Range, + severity => Severity, + code => Name, + source => source(), + message => Message, + relatedInformation => [] + } + ]. + +-spec make_protocol_line(Line :: number()) -> number(). +make_protocol_line(Line) when + Line =< 0 orelse + Line =:= unknown +-> + 1; +make_protocol_line(Line) -> + Line. -spec get_elvis_config_path() -> file:filename_all(). get_elvis_config_path() -> - case els_config:get(elvis_config_path) of - undefined -> - RootPath = els_uri:path(els_config:get(root_uri)), - filename:join([RootPath, "elvis.config"]); - FilePath -> - FilePath - end. + case els_config:get(elvis_config_path) of + undefined -> + RootPath = els_uri:path(els_config:get(root_uri)), + filename:join([RootPath, "elvis.config"]); + FilePath -> + FilePath + end. diff --git a/apps/els_lsp/src/els_eqwalizer_diagnostics.erl b/apps/els_lsp/src/els_eqwalizer_diagnostics.erl new file mode 100644 index 000000000..2e6509495 --- /dev/null +++ b/apps/els_lsp/src/els_eqwalizer_diagnostics.erl @@ -0,0 +1,110 @@ +%%============================================================================== +%% EqWAlizer diagnostics +%%============================================================================== +-module(els_eqwalizer_diagnostics). + +%%============================================================================== +%% Behaviours +%%============================================================================== +-behaviour(els_diagnostics). + +%%============================================================================== +%% Exports +%%============================================================================== +-export([ + is_default/0, + run/1, + source/0 +]). + +%% Exported to ease mocking during tests +-export([eqwalize/2]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include("els_lsp.hrl"). +-include_lib("kernel/include/logger.hrl"). + +%%============================================================================== +%% Callback Functions +%%============================================================================== + +-spec is_default() -> false. +is_default() -> + false. + +-spec run(uri()) -> [els_diagnostics:diagnostic()]. +run(Uri) -> + case filename:extension(Uri) of + <<".erl">> -> + Project = els_uri:path(els_config:get(root_uri)), + Module = els_uri:module(Uri), + %% Fully qualified call to ensure it's mockable + lists:filtermap(fun make_diagnostic/1, ?MODULE:eqwalize(Project, Module)); + _ -> + [] + end. + +-spec source() -> binary(). +source() -> + <<"EqWAlizer">>. + +%%============================================================================== +%% Internal Functions +%%============================================================================== + +-spec eqwalize(binary(), atom()) -> [string()]. +eqwalize(Project, Module) -> + Cmd = lists:flatten( + io_lib:format("elp eqwalize ~p --format json-lsp --project ~s", [Module, Project]) + ), + string:split(string:trim(os:cmd(Cmd)), "\n", all). + +-spec make_diagnostic(binary()) -> {true, els_diagnostics:diagnostic()} | false. +make_diagnostic(Message) -> + try json:decode(els_utils:to_binary(Message)) of + #{ + <<"relative_path">> := _RelativePath, + <<"diagnostic">> := + #{ + <<"severity">> := Severity, + <<"message">> := Description, + <<"range">> := + #{ + <<"start">> := + #{ + <<"line">> := FromLine, + <<"character">> := FromChar + }, + <<"end">> := + #{ + <<"line">> := ToLine, + <<"character">> := ToChar + } + } + } + } -> + Range = els_protocol:range(#{ + from => {FromLine + 1, FromChar + 1}, + to => {ToLine + 1, ToChar + 1} + }), + Diagnostic = els_diagnostics:make_diagnostic( + Range, + Description, + Severity, + source() + ), + {true, Diagnostic}; + DecodedMessage -> + ?LOG_WARNING("Unrecognized Eqwalizer diagnostic (~p)", [ + DecodedMessage + ]), + false + catch + C:E:St -> + ?LOG_WARNING("Issue while running Eqwalizer (~p:~p:~p) for message ~p", [ + C, E, St, Message + ]), + false + end. diff --git a/apps/els_lsp/src/els_erlfmt_ast.erl b/apps/els_lsp/src/els_erlfmt_ast.erl index 0d92759ed..925e8ef4c 100644 --- a/apps/els_lsp/src/els_erlfmt_ast.erl +++ b/apps/els_lsp/src/els_erlfmt_ast.erl @@ -38,7 +38,7 @@ %% Hence, the erl_syntax:set_pos() function is used for all annotations. erlfmt_to_st(Node) -> - Context = get('$erlfmt_ast_context$'), + Context = get('$erlfmt_ast_context$'), case Node of %% --------------------------------------------------------------------- %% The following cases can be easily rewritten without losing information @@ -94,7 +94,7 @@ erlfmt_to_st(Node) -> _ -> erlfmt_to_st(F) end - || F <- Fields + || F <- Fields ], Tuple1 = update_tree_with_meta(erl_syntax:tuple(Fields1), TPos), update_tree_with_meta( @@ -110,125 +110,206 @@ erlfmt_to_st(Node) -> %% Representation for types is in general the same as for %% corresponding values. The `type` node is not used at all. This %% means new binary operators `|`, `::`, and `..` inside types. - {attribute, Pos, {atom, _, Tag} = Name, [Def]} when Tag =:= type; Tag =:= opaque -> - put('$erlfmt_ast_context$', type), - {op, OPos, '::', Type, Definition} = Def, - {TypeName, Args} = - case Type of - {call, _CPos, TypeName0, Args0} -> - {TypeName0, Args0}; - {macro_call, CPos, {_, MPos, _} = MacroName, Args0} -> - EndLoc = maps:get(end_location, MPos), - TypeName0 = {macro_call, CPos#{end_location => EndLoc}, MacroName, none}, - {TypeName0, Args0} - end, - Tree = - update_tree_with_meta( - erl_syntax:attribute(erlfmt_to_st(Name), - [update_tree_with_meta( - erl_syntax:tuple([erlfmt_to_st(TypeName), - erlfmt_to_st(Definition), - erl_syntax:list([erlfmt_to_st(A) || A <- Args])]), - OPos)]), - Pos), - erase('$erlfmt_ast_context$'), - Tree; - {attribute, Pos, {atom, _, RawName} = Name, Args} when RawName =:= callback; - RawName =:= spec -> - put('$erlfmt_ast_context$', type), - [{spec, SPos, FName, Clauses}] = Args, - {spec_clause, _, {args, _, ClauseArgs}, _, _} = hd(Clauses), - Arity = length(ClauseArgs), - Tree = - update_tree_with_meta( - erl_syntax:attribute(erlfmt_to_st(Name), - [update_tree_with_meta( - erl_syntax:tuple([erl_syntax:tuple([erlfmt_to_st(FName), erl_syntax:integer(Arity)]), - erl_syntax:list([erlfmt_to_st(C) || C <- Clauses])]), - SPos)]), - Pos), - erase('$erlfmt_ast_context$'), - Tree; - {spec_clause, Pos, {args, _HeadMeta, Args}, ReturnType, empty} -> - update_tree_with_meta( - erl_syntax_function_type([erlfmt_to_st(A) || A <- Args], - erlfmt_to_st(ReturnType)), - Pos); - {spec_clause, Pos, {args, _HeadMeta, Args}, ReturnType, GuardOr} -> - FunctionType = - update_tree_with_meta( - erl_syntax_function_type([erlfmt_to_st(A) || A <- Args], - erlfmt_to_st(ReturnType)), - Pos), - FunctionConstraint = erlfmt_guard_to_st(GuardOr), - - update_tree_with_meta( - erl_syntax:constrained_function_type(FunctionType, [FunctionConstraint]), - Pos); - {op, Pos, '|', A, B} when Context =:= type -> - update_tree_with_meta( - erl_syntax:type_union([erlfmt_to_st(A), - erlfmt_to_st(B)]), - Pos); - {op, Pos, '..', A, B} when Context =:= type -> - %% erlfmt_to_st_1({type, Pos, range, [A, B]}), - update_tree_with_meta( - erl_syntax:integer_range_type(erlfmt_to_st(A), - erlfmt_to_st(B)), - Pos); - %%{op, Pos, '::', A, B} when Context =:= type -> - %% update_tree_with_meta( - %% erl_syntax:annotated_type(erlfmt_to_st(A), - %% erlfmt_to_st(B)), - %% Pos); - {record, Pos, Name, Fields} when Context =:= type -> - %% The record name is represented as node instead of a raw atom - %% and typed record fields are represented as '::' ops - Fields1 = [ - case F of - {op, FPos, '::', B, T} -> - B1 = erlfmt_to_st(B), - T1 = erlfmt_to_st(T), - update_tree_with_meta( - erl_syntax:record_type_field(B1, T1), - FPos + {attribute, Pos, {atom, _, Tag} = Name, [{op, OPos, '::', Type, Definition}]} when + Tag =:= type; Tag =:= opaque + -> + put('$erlfmt_ast_context$', type), + {TypeName, Args} = + case Type of + {call, _CPos, TypeName0, Args0} -> + {TypeName0, Args0}; + {macro_call, _, _, _} -> + %% Note: in the following example the arguments belong to the macro + %% so we set empty type args. + %% `-type ?M(A) :: A.' + %% The erlang preprocessor also prefers the M/1 macro if both M/0 + %% and M/1 are defined, but it also allows only M/0. Unfortunately + %% erlang_ls doesn't know what macros are defined. + {Type, []}; + _ -> + %% whatever stands at the left side of '::', let's keep it. + %% erlfmt_parser allows atoms and vars too + {Type, []} + end, + Tree = + update_tree_with_meta( + erl_syntax:attribute( + erlfmt_to_st(Name), + [ + update_tree_with_meta( + erl_syntax:tuple([ + erlfmt_to_st(TypeName), + erlfmt_to_st(Definition), + erl_syntax:list([erlfmt_to_st(A) || A <- Args]) + ]), + OPos + ) + ] + ), + Pos + ), + erase('$erlfmt_ast_context$'), + Tree; + {attribute, Pos, {atom, _, Tag} = Name, [Def]} when Tag =:= type; Tag =:= opaque -> + %% an incomplete attribute, where `::` operator and the definition missing + %% eg "-type t()." + put('$erlfmt_ast_context$', type), + OPos = element(2, Def), + {TypeName, Args} = + case Def of + {call, _CPos, TypeName0, Args0} -> + {TypeName0, Args0}; + _ -> + {Def, []} + end, + %% Set definition as an empty tuple for which els_parser generates no POIs + EmptyDef = erl_syntax:tuple([]), + Tree = + update_tree_with_meta( + erl_syntax:attribute( + erlfmt_to_st(Name), + [ + update_tree_with_meta( + erl_syntax:tuple([ + erlfmt_to_st(TypeName), + EmptyDef, + erl_syntax:list([erlfmt_to_st(A) || A <- Args]) + ]), + OPos + ) + ] + ), + Pos + ), + erase('$erlfmt_ast_context$'), + Tree; + {attribute, Pos, {atom, _, RawName} = Name, Args} when + RawName =:= callback; + RawName =:= spec + -> + put('$erlfmt_ast_context$', type), + [{spec, SPos, FName, Clauses}] = Args, + {spec_clause, _, {args, _, ClauseArgs}, _, _} = hd(Clauses), + Arity = length(ClauseArgs), + Tree = + update_tree_with_meta( + erl_syntax:attribute( + erlfmt_to_st(Name), + [ + update_tree_with_meta( + erl_syntax:tuple([ + erl_syntax:tuple([ + erlfmt_to_st(FName), erl_syntax:integer(Arity) + ]), + erl_syntax:list([erlfmt_to_st(C) || C <- Clauses]) + ]), + SPos + ) + ] + ), + Pos + ), + erase('$erlfmt_ast_context$'), + Tree; + {spec_clause, Pos, {args, _HeadMeta, Args}, ReturnType, empty} -> + update_tree_with_meta( + erl_syntax_function_type( + [erlfmt_to_st(A) || A <- Args], + erlfmt_to_st(ReturnType) + ), + Pos + ); + {spec_clause, Pos, {args, _HeadMeta, Args}, ReturnType, GuardOr} -> + FunctionType = + update_tree_with_meta( + erl_syntax_function_type( + [erlfmt_to_st(A) || A <- Args], + erlfmt_to_st(ReturnType) + ), + Pos + ), + FunctionConstraint = erlfmt_guard_to_st(GuardOr), + + update_tree_with_meta( + erl_syntax:constrained_function_type(FunctionType, [FunctionConstraint]), + Pos + ); + {op, Pos, '|', A, B} when Context =:= type -> + update_tree_with_meta( + erl_syntax:type_union([ + erlfmt_to_st(A), + erlfmt_to_st(B) + ]), + Pos + ); + {op, Pos, '..', A, B} when Context =:= type -> + %% erlfmt_to_st_1({type, Pos, range, [A, B]}), + update_tree_with_meta( + erl_syntax:integer_range_type( + erlfmt_to_st(A), + erlfmt_to_st(B) + ), + Pos + ); + %%{op, Pos, '::', A, B} when Context =:= type -> + %% update_tree_with_meta( + %% erl_syntax:annotated_type(erlfmt_to_st(A), + %% erlfmt_to_st(B)), + %% Pos); + {record, Pos, Name, Fields} when Context =:= type -> + %% The record name is represented as node instead of a raw atom + %% and typed record fields are represented as '::' ops + Fields1 = [ + case F of + {op, FPos, '::', B, T} -> + B1 = erlfmt_to_st(B), + T1 = erlfmt_to_st(T), + update_tree_with_meta( + erl_syntax:record_type_field(B1, T1), + FPos ); - _ -> - erlfmt_to_st(F) - end - || F <- Fields - ], - - update_tree_with_meta( - erl_syntax:record_type( - erlfmt_to_st(Name), - Fields1 - ), - Pos - ); - {call, Pos, {remote, _, _, _} = Name, Args} when Context =:= type -> - update_tree_with_meta( - erl_syntax:type_application(erlfmt_to_st(Name), - [erlfmt_to_st(A) || A <- Args]), - Pos); - {call, Pos, Name, Args} when Context =:= type -> - TypeTag = - case Name of - {atom, _, NameAtom} -> - Arity = length(Args), - case erl_internal:is_type(NameAtom, Arity) of - true -> - type_application; - false -> - user_type_application - end; - _ -> - user_type_application - end, - update_tree_with_meta( - erl_syntax:TypeTag(erlfmt_to_st(Name), - [erlfmt_to_st(A) || A <- Args]), - Pos); + _ -> + erlfmt_to_st(F) + end + || F <- Fields + ], + + update_tree_with_meta( + erl_syntax:record_type( + erlfmt_to_st(Name), + Fields1 + ), + Pos + ); + {call, Pos, {remote, _, _, _} = Name, Args} when Context =:= type -> + update_tree_with_meta( + erl_syntax:type_application( + erlfmt_to_st(Name), + [erlfmt_to_st(A) || A <- Args] + ), + Pos + ); + {call, Pos, Name, Args} when Context =:= type -> + TypeTag = + case Name of + {atom, _, NameAtom} -> + Arity = length(Args), + case erl_internal:is_type(NameAtom, Arity) of + true -> + type_application; + false -> + user_type_application + end; + _ -> + user_type_application + end, + update_tree_with_meta( + erl_syntax:TypeTag( + erlfmt_to_st(Name), + [erlfmt_to_st(A) || A <- Args] + ), + Pos + ); {attribute, Pos, {atom, _, define} = Tag, [Name, empty]} -> %% the erlfmt parser allows defines with empty bodies (with the %% closing parens following after the comma); we must turn the @@ -289,18 +370,20 @@ erlfmt_to_st(Node) -> {'try', Pos, {body, _, _} = Body, Clauses, Handlers, After} -> %% TODO: preserving annotations on bodies and clause groups Body1 = [erlfmt_to_st(Body)], - Clauses1 = case Clauses of - {clauses, _, CList} -> - [erlfmt_clause_to_st(C) || C <- CList]; - none -> - [] - end, - Handlers1 = case Handlers of - {clauses, _, HList} -> - [erlfmt_clause_to_st(C) || C <- HList]; - none -> - [] - end, + Clauses1 = + case Clauses of + {clauses, _, CList} -> + [erlfmt_clause_to_st(C) || C <- CList]; + none -> + [] + end, + Handlers1 = + case Handlers of + {clauses, _, HList} -> + [erlfmt_clause_to_st(C) || C <- HList]; + none -> + [] + end, After1 = [erlfmt_to_st(E) || E <- After], update_tree_with_meta( erl_syntax:try_expr( @@ -320,8 +403,12 @@ erlfmt_to_st(Node) -> {clause, _, _, _, _} = Clause -> %% clauses of case/if/receive/try erlfmt_clause_to_st(Clause); + {else_clause, Pos, Clauses} -> + %% The else clause of a maybe expression - in OTP it is just called + %% 'else' but has the same format and content + erlfmt_to_st_1({'else', Pos, Clauses}); %% Lists are represented as a `list` node instead of a chain of `cons` - %% and `nil` nodes, similar to the `tuple` node. The last elemenent of + %% and `nil` nodes, similar to the `tuple` node. The last element of %% the list can be a `cons` node representing explicit consing syntax. {list, Pos, Elements} -> %% a "cons" node here means 'H | T' in isolation @@ -443,38 +530,45 @@ erlfmt_to_st(Node) -> FPos ), update_tree_with_meta(erl_syntax:implicit_fun(FName), Pos); - {'fun', Pos, type} -> - update_tree_with_meta(erl_syntax:fun_type(), Pos); - {'fun', Pos, {type, _, {args, _, Args}, Res}} -> - update_tree_with_meta( - erl_syntax_function_type( - [erlfmt_to_st(A) || A <- Args], - erlfmt_to_st(Res)), - Pos); - {'bin', Pos, Elements} when Context =:= type -> - %% Note: we loose a lot of Annotation info here - %% Note2: erl_parse assigns the line number (with no column) to the dummy zeros - {M, N} = - case Elements of - [{bin_element, _, {var, _, '_'}, {bin_size, _, {var, _, '_'}, NNode}, default}] -> - {{integer, dummy_anno(), 0}, NNode}; - [{bin_element, _, {var, _, '_'}, MNode, default}] -> - {MNode, {integer, dummy_anno(), 0}}; - [{bin_element, _, {var, _, '_'}, MNode, default}, - {bin_element, _, {var, _, '_'}, {bin_size, _, {var, _, '_'}, NNode}, default}] -> - {MNode, NNode}; - [] -> - {{integer, dummy_anno(), 0}, {integer, dummy_anno(), 0}}; - _ -> - %% No idea what this is - what ST should we create? - %% maybe just a binary(), or an empty text node - {{integer, dummy_anno(), 0}, {integer, dummy_anno(), 1}} - end, - update_tree_with_meta( - erl_syntax:bitstring_type( - erlfmt_to_st(M), - erlfmt_to_st(N)), - Pos); + {'fun', Pos, type} -> + update_tree_with_meta(erl_syntax:fun_type(), Pos); + {'fun', Pos, {type, _, {args, _, Args}, Res}} -> + update_tree_with_meta( + erl_syntax_function_type( + [erlfmt_to_st(A) || A <- Args], + erlfmt_to_st(Res) + ), + Pos + ); + {'bin', Pos, Elements} when Context =:= type -> + %% Note: we loose a lot of Annotation info here + %% Note2: erl_parse assigns the line number (with no column) to the dummy zeros + {M, N} = + case Elements of + [{bin_element, _, {var, _, '_'}, {bin_size, _, {var, _, '_'}, NNode}, default}] -> + {{integer, dummy_anno(), 0}, NNode}; + [{bin_element, _, {var, _, '_'}, MNode, default}] -> + {MNode, {integer, dummy_anno(), 0}}; + [ + {bin_element, _, {var, _, '_'}, MNode, default}, + {bin_element, _, {var, _, '_'}, {bin_size, _, {var, _, '_'}, NNode}, + default} + ] -> + {MNode, NNode}; + [] -> + {{integer, dummy_anno(), 0}, {integer, dummy_anno(), 0}}; + _ -> + %% No idea what this is - what ST should we create? + %% maybe just a binary(), or an empty text node + {{integer, dummy_anno(), 0}, {integer, dummy_anno(), 1}} + end, + update_tree_with_meta( + erl_syntax:bitstring_type( + erlfmt_to_st(M), + erlfmt_to_st(N) + ), + Pos + ); %% Bit type definitions inside binaries are represented as full nodes %% instead of raw atoms and integers. The unit notation `unit:Int` is %% represented with a `{remote, Anno, {atom, Anno, unit}, Int}` node. @@ -507,24 +601,27 @@ erlfmt_to_st(Node) -> ), Pos ); - {'receive', Pos, {clauses, _PosClauses, ClauseList}} -> - update_tree_with_meta( - erl_syntax:receive_expr([erlfmt_to_st(C) || C <- ClauseList]), - Pos); - {'receive', Pos, Clauses, {after_clause, _PosAfter, Timeout, Action}} -> - Clauses1 = case Clauses of - empty -> - []; - {clauses, _PosClauses, ClauseList} -> - [erlfmt_to_st(C) || C <- ClauseList] - end, - update_tree_with_meta( - erl_syntax:receive_expr( - Clauses1, - erlfmt_to_st(Timeout), - [erlfmt_to_st(A) || A <- Action]), - Pos); - + {'receive', Pos, {clauses, _PosClauses, ClauseList}} -> + update_tree_with_meta( + erl_syntax:receive_expr([erlfmt_to_st(C) || C <- ClauseList]), + Pos + ); + {'receive', Pos, Clauses, {after_clause, _PosAfter, Timeout, Action}} -> + Clauses1 = + case Clauses of + empty -> + []; + {clauses, _PosClauses, ClauseList} -> + [erlfmt_to_st(C) || C <- ClauseList] + end, + update_tree_with_meta( + erl_syntax:receive_expr( + Clauses1, + erlfmt_to_st(Timeout), + [erlfmt_to_st(A) || A <- Action] + ), + Pos + ); %% --------------------------------------------------------------------- %% The remaining cases have been added by erlfmt and need special handling %% (many are represented as magically-tagged tuples for now) @@ -583,6 +680,11 @@ erlfmt_to_st(Node) -> {args, Pos, Args} -> AAnno = dummy_anno(), erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*args*'} | Args]}); + {sigil, Pos, _SigilPrefix, {string, StringPos0, Text}, _SigilSuffix} -> + %% erl_syntax doesn't handle sigils, so we just extract the string + %% Move start of string to start of sigil + StringPos = StringPos0#{location := maps:get(location, Pos)}, + erlfmt_to_st_1({string, StringPos, Text}); %% TODO: %% New `{spec_clause, Anno, Head, Body, Guards}` node for clauses %% inside `spec` and `callback` attributes, similar to the `clause` @@ -600,11 +702,11 @@ erlfmt_to_st(Node) -> %% So first replace the Meta from Node with proper erl_syntax pos+annotation to %% make dialyzer happy. -spec erlfmt_to_st_1(erlfmt() | syntax_tools()) -> syntax_tools(). -erlfmt_to_st_1(Node) when is_map(element(2, Node))-> - Node2 = convert_meta_to_anno(Node), - erlfmt_to_st_2(Node2); +erlfmt_to_st_1(Node) when is_map(element(2, Node)) -> + Node2 = convert_meta_to_anno(Node), + erlfmt_to_st_2(Node2); erlfmt_to_st_1(Node) -> - erlfmt_to_st_2(Node). + erlfmt_to_st_2(Node). -spec erlfmt_to_st_2(syntax_tools()) -> syntax_tools(). erlfmt_to_st_2(Node) -> @@ -623,9 +725,9 @@ erlfmt_subtrees_to_st(Groups) -> [ [ erlfmt_to_st(Subtree) - || Subtree <- Group + || Subtree <- Group ] - || Group <- Groups + || Group <- Groups ]. -spec get_function_name(maybe_improper_list()) -> any(). @@ -674,7 +776,7 @@ erlfmt_clause_to_st(Other) -> %% might be a macro call erlfmt_to_st(Other). --spec erlfmt_clause_to_st(_,[any()],_,[any()]) -> any(). +-spec erlfmt_clause_to_st(_, [any()], _, [any()]) -> any(). erlfmt_clause_to_st(Pos, Patterns, Guard, Body) -> Groups = [ Patterns, @@ -694,7 +796,7 @@ erlfmt_guard_to_st({guard_or, Pos, List}) -> update_tree_with_meta( erl_syntax:disjunction([ erlfmt_guard_to_st(E) - || E <- List + || E <- List ]), Pos ); @@ -702,7 +804,7 @@ erlfmt_guard_to_st({guard_and, Pos, List}) -> update_tree_with_meta( erl_syntax:conjunction([ erlfmt_guard_to_st(E) - || E <- List + || E <- List ]), Pos ); @@ -746,7 +848,7 @@ fold_arity_qualifier(Node) -> -spec dummy_anno() -> erl_anno:anno(). dummy_anno() -> - erl_anno:set_generated(true, erl_anno:new({0, 1})). + erl_anno:set_generated(true, erl_anno:new({0, 1})). %% erlfmt ast utilities @@ -762,43 +864,43 @@ set_anno(Node, Loc) -> %% erl_syntax:function_type/2 has wrong spec before OTP 24 -spec erl_syntax_function_type('any_arity' | [syntax_tools()], syntax_tools()) -> syntax_tools(). erl_syntax_function_type(Arguments, Return) -> - apply(erl_syntax, function_type, [Arguments, Return]). + apply(erl_syntax, function_type, [Arguments, Return]). %% Convert erlfmt_scan:anno to erl_syntax pos+annotation %% %% Note: nothing from meta is stored in annotation %% as erlang_ls only needs start and end locations. --spec update_tree_with_meta(syntax_tools(), erlfmt_scan:anno()) - -> syntax_tools(). +-spec update_tree_with_meta(syntax_tools(), erlfmt_scan:anno()) -> + syntax_tools(). update_tree_with_meta(Tree, Meta) -> - Anno = meta_to_anno(Meta), - Tree2 = erl_syntax:set_pos(Tree, Anno), - %% erl_syntax:set_ann(Tree2, [{meta, Meta}]). - Tree2. + Anno = meta_to_anno(Meta), + Tree2 = erl_syntax:set_pos(Tree, Anno), + %% erl_syntax:set_ann(Tree2, [{meta, Meta}]). + Tree2. -spec convert_meta_to_anno(erlfmt()) -> syntax_tools(). convert_meta_to_anno(Node) -> - Meta = get_anno(Node), - Node2 = set_anno(Node, meta_to_anno(Meta)), - %% erl_syntax:set_ann(Node2, [{meta, Meta}]). - Node2. + Meta = get_anno(Node), + Node2 = set_anno(Node, meta_to_anno(Meta)), + %% erl_syntax:set_ann(Node2, [{meta, Meta}]). + Node2. -spec meta_to_anno(erlfmt_scan:anno()) -> erl_anno:anno(). meta_to_anno(Meta) -> - %% Recommenting can modify the start and end locations of certain trees - %% see erlfmt_recomment:put_(pre|post)_comments/1 - From = - case maps:is_key(pre_comments, Meta) of - true -> - maps:get(inner_location, Meta); - false -> - maps:get(location, Meta) - end, - To = - case maps:is_key(post_comments, Meta) of - true -> - maps:get(inner_end_location, Meta); - false -> - maps:get(end_location, Meta) - end, - erl_anno:from_term([{location, From}, {end_location, To}]). + %% Recommenting can modify the start and end locations of certain trees + %% see erlfmt_recomment:put_(pre|post)_comments/1 + From = + case maps:is_key(pre_comments, Meta) of + true -> + maps:get(inner_location, Meta); + false -> + maps:get(location, Meta) + end, + To = + case maps:is_key(post_comments, Meta) of + true -> + maps:get(inner_end_location, Meta); + false -> + maps:get(end_location, Meta) + end, + erl_anno:from_term([{location, From}, {end_location, To}]). diff --git a/apps/els_lsp/src/els_eunit_diagnostics.erl b/apps/els_lsp/src/els_eunit_diagnostics.erl new file mode 100644 index 000000000..f03b29b12 --- /dev/null +++ b/apps/els_lsp/src/els_eunit_diagnostics.erl @@ -0,0 +1,177 @@ +-module(els_eunit_diagnostics). +-behaviour(els_diagnostics). + +%%% els_diagnostics callbacks +-export([run/1]). +-export([is_default/0]). +-export([source/0]). +-include("els_lsp.hrl"). +-include_lib("kernel/include/logger.hrl"). + +%%% els_diagnostics callbacks +-spec is_default() -> boolean(). +is_default() -> + false. + +-spec source() -> binary(). +source() -> + <<"EUnit">>. + +-spec run(uri()) -> [els_diagnostics:diagnostic()]. +run(Uri) -> + case run_eunit_on_remote_node(Uri) of + ignore -> + ?LOG_INFO("No remote node to run on."), + []; + _Res -> + receive + {result, Collected} -> + lists:flatmap( + fun(Data) -> + handle_result(Uri, Data) + end, + Collected + ) + after 5000 -> + ?LOG_INFO("EUnit Timeout."), + [] + end + end. + +-spec run_eunit_on_remote_node(uri()) -> ignore | any(). +run_eunit_on_remote_node(Uri) -> + Ext = filename:extension(Uri), + case els_config:get(code_reload) of + #{"node" := NodeOrNodes} when Ext == <<".erl">> -> + Module = els_uri:module(Uri), + case NodeOrNodes of + [Node | _] when is_list(Node) -> + rpc_eunit(Node, Module); + Node when is_list(Node) -> + rpc_eunit(Node, Module); + _ -> + ignore + end; + _ -> + ignore + end. + +-spec rpc_eunit(string(), module()) -> any(). +rpc_eunit(NodeStr, Module) -> + ?LOG_INFO("Running EUnit tests for ~p on ~s.", [Module, NodeStr]), + Listener = els_eunit_listener:start([{parent_pid, self()}]), + rpc:call( + node_name(NodeStr), + eunit, + test, + [ + Module, + [ + {report, Listener}, + {exact_execution, true}, + {no_tty, true} + ] + ] + ). + +-spec node_name(string()) -> atom(). +node_name(N) -> + els_utils:compose_node_name(N, els_config_runtime:get_name_type()). + +-spec handle_result(uri(), any()) -> [els_diagnostics:diagnostic()]. +handle_result(Uri, Data) -> + Status = proplists:get_value(status, Data), + case Status of + {error, {error, {Assertion, Info0}, _Stack}} when + Assertion == assert; + Assertion == assertNot; + Assertion == assertMatch; + Assertion == assertNotMatch; + Assertion == assertEqual; + Assertion == assertNotEqual; + Assertion == assertException; + Assertion == assertNotException; + Assertion == assertError; + Assertion == assertExit; + Assertion == assertThrow; + Assertion == assertCmd; + Assertion == assertCmdOutput + -> + Info1 = lists:keydelete(module, 1, Info0), + {value, {line, Line}, Info2} = lists:keytake(line, 1, Info1), + Msg = + io_lib:format("~p failed.\n", [Assertion]) ++ + [format_info_value(K, V) || {K, V} <- Info2] ++ + format_output(Data), + [diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)]; + ok -> + Line = get_line(Uri, Data), + Msg = "Test passed." ++ format_output(Data), + [diagnostic(Line, Msg, ?DIAGNOSTIC_INFO)]; + {error, {error, Error, Stack}} -> + UriM = els_uri:module(Uri), + case [X || {M, _, _, _} = X <- Stack, M == UriM] of + [] -> + %% Current module not in stacktrace + %% Error will be placed on line 0 + Msg = io_lib:format("Test crashed: ~p\n~p", [Error, Stack]), + Line = 0, + [diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)]; + [{M, F, A, Info0} | _] -> + Msg = io_lib:format("Test crashed: ~p", [Error]), + Line = get_line(Uri, [{source, {M, F, A}} | Info0]), + [diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)] + end; + Error -> + Line = proplists:get_value(line, Data), + Msg = io_lib:format("Test crashed: ~p", [Error]), + [diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)] + end. + +-spec get_line(uri(), any()) -> non_neg_integer(). +get_line(Uri, Data) -> + case proplists:get_value(line, Data) of + 0 -> + {M, F, A} = proplists:get_value(source, Data), + {ok, Document} = els_utils:lookup_document(Uri), + UriM = els_uri:module(Uri), + case UriM == M of + true -> + POIs = els_dt_document:pois(Document, [function]), + case [R || #{id := Id, range := R} <- POIs, Id == {F, A}] of + [] -> + 0; + [#{from := {Line, _}} | _] -> + Line + end; + false -> + 0 + end; + Line -> + Line + end. + +-spec format_output(any()) -> iolist(). +format_output(Data) -> + case proplists:get_value(output, Data) of + [<<>>] -> + []; + [Output] -> + io_lib:format("\noutput:\n~s", [Output]) + end. + +-spec diagnostic(non_neg_integer(), iolist(), els_diagnostics:severity()) -> + els_diagnostics:diagnostic(). +diagnostic(Line, Msg, Severity) -> + #{ + range => els_protocol:range(#{from => {Line, 1}, to => {Line + 1, 1}}), + severity => Severity, + source => source(), + message => list_to_binary(Msg) + }. + +-spec format_info_value(atom(), any()) -> iolist(). +format_info_value(K, V) when is_list(V) -> + io_lib:format("~p: ~s\n", [K, V]); +format_info_value(K, V) -> + io_lib:format("~p: ~p\n", [K, V]). diff --git a/apps/els_lsp/src/els_eunit_listener.erl b/apps/els_lsp/src/els_eunit_listener.erl new file mode 100644 index 000000000..9ac7f15d3 --- /dev/null +++ b/apps/els_lsp/src/els_eunit_listener.erl @@ -0,0 +1,61 @@ +%% @doc Simple EUnit listener that collects the results of the tests and +%% sends them back to the parent process. + +-module(els_eunit_listener). + +-behaviour(eunit_listener). + +-export([start/0, start/1]). +-export([init/1, handle_begin/3, handle_end/3, handle_cancel/3, terminate/2]). + +-record(state, { + result = [] :: list(), + parent_pid :: pid() +}). + +-type state() :: #state{}. + +-spec start() -> pid(). +start() -> + start([]). + +-spec start(list()) -> pid(). +start(Options) -> + eunit_listener:start(?MODULE, Options). + +-spec init(list()) -> state(). +init(Options) -> + Pid = proplists:get_value(parent_pid, Options), + receive + {start, _Reference} -> + #state{parent_pid = Pid} + end. + +-spec terminate(any(), state()) -> any(). +terminate({ok, _Data}, #state{parent_pid = Pid, result = Result}) -> + Pid ! {result, Result}, + ok; +terminate({error, _Reason}, _State) -> + sync_end(error). + +-spec sync_end(any()) -> ok. +sync_end(Result) -> + receive + {stop, Reference, ReplyTo} -> + ReplyTo ! {result, Reference, Result}, + ok + end. + +-spec handle_begin(atom(), any(), state()) -> state(). +handle_begin(_Kind, _Data, State) -> + State. + +-spec handle_end(atom(), any(), state()) -> state(). +handle_end(group, _Data, State) -> + State; +handle_end(test, Data, State) -> + State#state{result = [Data | State#state.result]}. + +-spec handle_cancel(atom(), any(), state()) -> state(). +handle_cancel(_Kind, _Data, State) -> + State. diff --git a/apps/els_lsp/src/els_execute_command_provider.erl b/apps/els_lsp/src/els_execute_command_provider.erl index 5c6499849..8271887d3 100644 --- a/apps/els_lsp/src/els_execute_command_provider.erl +++ b/apps/els_lsp/src/els_execute_command_provider.erl @@ -2,10 +2,10 @@ -behaviour(els_provider). --export([ handle_request/2 - , is_enabled/0 - , options/0 - ]). +-export([ + options/0, + handle_request/1 +]). %%============================================================================== %% Includes @@ -13,104 +13,513 @@ -include("els_lsp.hrl"). -include_lib("kernel/include/logger.hrl"). --type state() :: any(). - %%============================================================================== %% els_provider functions %%============================================================================== - --spec is_enabled() -> boolean(). -is_enabled() -> true. - -spec options() -> map(). options() -> - #{ commands => [ els_command:with_prefix(<<"replace-lines">>) - , els_command:with_prefix(<<"server-info">>) - , els_command:with_prefix(<<"ct-run-test">>) - , els_command:with_prefix(<<"show-behaviour-usages">>) - , els_command:with_prefix(<<"suggest-spec">>) - , els_command:with_prefix(<<"function-references">>) - ] }. - --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({workspace_executecommand, Params}, State) -> - #{ <<"command">> := PrefixedCommand } = Params, - Arguments = maps:get(<<"arguments">>, Params, []), - Result = execute_command( els_command:without_prefix(PrefixedCommand) - , Arguments), - {Result, State}. + Commands = [ + <<"server-info">>, + <<"ct-run-test">>, + <<"show-behaviour-usages">>, + <<"suggest-spec">>, + <<"function-references">>, + <<"refactor.extract">>, + <<"add-behaviour-callbacks">>, + <<"bump-variables">>, + <<"browse-error">>, + <<"browse-docs">> + ], + #{ + commands => [ + els_command:with_prefix(Cmd) + || Cmd <- Commands ++ wrangler_handler:enabled_commands() + ] + }. + +-spec handle_request(any()) -> {response, any()}. +handle_request({workspace_executecommand, Params}) -> + #{<<"command">> := PrefixedCommand} = Params, + Arguments = maps:get(<<"arguments">>, Params, []), + Result = execute_command( + els_command:without_prefix(PrefixedCommand), + Arguments + ), + {response, Result}. %%============================================================================== %% Internal Functions %%============================================================================== -spec execute_command(els_command:command_id(), [any()]) -> [map()]. -execute_command(<<"replace-lines">> - , [#{ <<"uri">> := Uri - , <<"lines">> := Lines - , <<"from">> := LineFrom - , <<"to">> := LineTo }]) -> - Method = <<"workspace/applyEdit">>, - Params = #{ edit => - els_text_edit:edit_replace_text(Uri, Lines, LineFrom, LineTo) - }, - els_server:send_request(Method, Params), - []; execute_command(<<"server-info">>, _Arguments) -> - {ok, Version} = application:get_key(?APP, vsn), - BinVersion = list_to_binary(Version), - Root = filename:basename(els_uri:path(els_config:get(root_uri))), - ConfigPath = case els_config:get(config_path) of - undefined -> <<"undefined">>; - Path -> list_to_binary(Path) - end, - - OtpPathConfig = list_to_binary(els_config:get(otp_path)), - OtpRootDir = list_to_binary(code:root_dir()), - OtpMessage = case OtpRootDir == OtpPathConfig of - true -> - <<", OTP root ", OtpRootDir/binary>>; - false -> - <<", OTP root(code):" - , OtpRootDir/binary - , ", OTP root(config):" - , OtpPathConfig/binary>> - end, - Message = <<"Erlang LS (in ", Root/binary, "), version: " - , BinVersion/binary - , ", config from " - , ConfigPath/binary - , OtpMessage/binary - >>, - els_server:send_notification(<<"window/showMessage">>, - #{ type => ?MESSAGE_TYPE_INFO, - message => Message - }), - []; + {ok, Version} = application:get_key(?APP, vsn), + BinVersion = list_to_binary(Version), + Root = filename:basename(els_uri:path(els_config:get(root_uri))), + ConfigPath = + case els_config:get(config_path) of + undefined -> <<"undefined">>; + Path -> list_to_binary(Path) + end, + + OtpPathConfig = list_to_binary(els_config:get(otp_path)), + OtpRootDir = list_to_binary(code:root_dir()), + OtpMessage = + case OtpRootDir == OtpPathConfig of + true -> + <<", OTP root ", OtpRootDir/binary>>; + false -> + <<", OTP root(code):", OtpRootDir/binary, ", OTP root(config):", + OtpPathConfig/binary>> + end, + Message = + <<"Erlang LS (in ", Root/binary, "), version: ", BinVersion/binary, ", config from ", + ConfigPath/binary, OtpMessage/binary>>, + els_server:send_notification( + <<"window/showMessage">>, + #{ + type => ?MESSAGE_TYPE_INFO, + message => Message + } + ), + []; execute_command(<<"ct-run-test">>, [Params]) -> - els_command_ct_run_test:execute(Params), - []; + els_command_ct_run_test:execute(Params), + []; execute_command(<<"function-references">>, [_Params]) -> - []; + []; execute_command(<<"show-behaviour-usages">>, [_Params]) -> - []; + []; execute_command(<<"suggest-spec">>, []) -> - []; -execute_command(<<"suggest-spec">>, [#{ <<"uri">> := Uri - , <<"line">> := Line - , <<"spec">> := Spec - }]) -> - Method = <<"workspace/applyEdit">>, - {ok, #{text := Text}} = els_utils:lookup_document(Uri), - LineText = els_text:line(Text, Line - 1), - NewText = <<Spec/binary, "\n", LineText/binary, "\n">>, - Params = - #{ edit => - els_text_edit:edit_replace_text(Uri, NewText, Line - 1, Line) - }, - els_server:send_request(Method, Params), - []; + []; +execute_command(<<"suggest-spec">>, [ + #{ + <<"uri">> := Uri, + <<"line">> := Line, + <<"spec">> := Spec + } +]) -> + Method = <<"workspace/applyEdit">>, + {ok, #{text := Text}} = els_utils:lookup_document(Uri), + LineText = els_text:line(Text, Line - 1), + NewText = <<Spec/binary, "\n", LineText/binary, "\n">>, + Params = + #{ + edit => + els_text_edit:edit_replace_text(Uri, NewText, Line - 1, Line) + }, + els_server:send_request(Method, Params), + []; +execute_command(<<"refactor.extract">>, [ + #{ + <<"uri">> := Uri, + <<"range">> := Range + } +]) -> + ok = extract_function(Uri, Range), + []; +execute_command(<<"bump-variables">>, [ + #{ + <<"uri">> := Uri, + <<"range">> := Range, + <<"name">> := Name + } +]) -> + ok = bump_variables(Uri, Range, Name), + []; +execute_command(<<"add-behaviour-callbacks">>, [ + #{ + <<"uri">> := Uri, + <<"behaviour">> := Behaviour + } +]) -> + {ok, Document} = els_utils:lookup_document(Uri), + case els_utils:find_module(binary_to_atom(Behaviour, utf8)) of + {error, _} -> + []; + {ok, BeUri} -> + %% Put exported callback functions after -behaviour() or -export() + #{range := #{to := {ExportLine, _Col}}} = + lists:last( + els_poi:sort( + els_dt_document:pois( + Document, + [behaviour, export] + ) + ) + ), + ExportPos = {ExportLine + 1, 1}, + + %% Put callback functions after the last function + CallbacksPos = + case els_poi:sort(els_dt_document:pois(Document, [function])) of + [] -> + {ExportLine + 2, 1}; + POIs -> + #{data := #{wrapping_range := #{to := Pos}}} = lists:last(POIs), + Pos + end, + {ok, BeDoc} = els_utils:lookup_document(BeUri), + CallbackPOIs = els_poi:sort(els_dt_document:pois(BeDoc, [callback])), + FunPOIs = els_dt_document:pois(Document, [function]), + + %% Only add missing callback functions, existing functions are kept. + Funs = [Id || #{id := Id} <- FunPOIs], + Callbacks = [ + Cb + || #{id := Id} = Cb <- CallbackPOIs, + not lists:member(Id, Funs) + ], + Comment = ["\n%%% ", Behaviour, " callbacks\n"], + ExportText = Comment ++ [export_text(Id) || Id <- Callbacks], + Text = Comment ++ [fun_text(Cb, BeDoc) || Cb <- Callbacks], + Method = <<"workspace/applyEdit">>, + Params = + #{ + edit => + #{ + changes => #{ + Uri => + [ + #{ + newText => iolist_to_binary(ExportText), + range => els_protocol:range( + #{ + from => ExportPos, + to => ExportPos + } + ) + }, + #{ + newText => iolist_to_binary(Text), + range => els_protocol:range( + #{ + from => CallbacksPos, + to => CallbacksPos + } + ) + } + ] + } + } + }, + els_server:send_request(Method, Params), + [] + end; +execute_command(<<"browse-error">>, [#{<<"source">> := Source, <<"code">> := ErrorCodeBin}]) -> + Url = make_url_browse_error(Source, ErrorCodeBin), + launch_browser(Url); +execute_command(<<"browse-docs">>, [ + #{ + <<"source">> := <<"otp">>, + <<"app">> := App, + <<"module">> := Module, + <<"function">> := Function, + <<"arity">> := Arity, + <<"kind">> := Kind + } +]) -> + Prefix = + case Kind of + <<"function">> -> ""; + <<"type">> -> "t:" + end, + Url = io_lib:format( + "https://www.erlang.org/doc/apps/~s/~s.html#~s~s/~p", + [App, Module, Prefix, Function, Arity] + ), + %% TODO: Function + launch_browser(Url); +execute_command(<<"browse-docs">>, [ + #{ + <<"source">> := <<"hex">>, + <<"app">> := App, + <<"module">> := Module, + <<"function">> := Function, + <<"arity">> := Arity, + <<"kind">> := Kind + } +]) -> + %% Edoc uses #function-arity while ExDoc uses #function/arity + %% We just support ExDoc for now. + %% Suppose we could add special handling for known edoc apps. + Prefix = + case Kind of + <<"function">> -> ""; + <<"type">> -> "t:" + end, + + Url = io_lib:format( + "https://hexdocs.pm/~s/~s.html#~s~s/~p", + [App, Module, Prefix, Function, Arity] + ), + launch_browser(Url); execute_command(Command, Arguments) -> - ?LOG_INFO("Unsupported command: [Command=~p] [Arguments=~p]" - , [Command, Arguments]), - []. + case wrangler_handler:execute_command(Command, Arguments) of + true -> + ok; + _ -> + ?LOG_INFO( + "Unsupported command: [Command=~p] [Arguments=~p]", + [Command, Arguments] + ) + end, + []. + +-spec make_url_browse_error(binary(), binary()) -> string(). +make_url_browse_error(<<"Compiler">>, ErrorCodeBin) -> + [Prefix | _] = ErrorCode = binary_to_list(ErrorCodeBin), + "https://whatsapp.github.io/erlang-language-platform/" ++ + "docs/erlang-error-index/" ++ + string:lowercase([Prefix]) ++ "/" ++ ErrorCode ++ "/"; +make_url_browse_error(<<"Elvis">>, ErrorCodeBin) -> + ErrorCode = binary_to_list(ErrorCodeBin), + "https://github.com/inaka/elvis_core/blob/main/doc_rules/elvis_style/" ++ + ErrorCode ++ ".md". + +-spec launch_browser(_) -> ok. +launch_browser(Url) -> + case os:type() of + {win32, _} -> + %% TODO: Not sure if this is the correct way to open a browser on Windows + os:cmd("start " ++ Url); + {_, linux} -> + os:cmd("xdg-open " ++ Url); + {_, darwin} -> + os:cmd("open " ++ Url); + {_, _} -> + not_supported + end, + ok. + +-spec bump_variables(uri(), range(), binary()) -> ok. +bump_variables(Uri, Range, VarName) -> + {Name, Number} = split_variable(VarName), + {ok, Document} = els_utils:lookup_document(Uri), + VarPOIs = els_poi:sort(els_dt_document:pois(Document, [variable])), + VarRange = els_range:to_poi_range(Range), + ScopeRange = els_scope:variable_scope_range(VarRange, Document), + Changes = + [ + bump_variable_change(POI) + || POI <- pois_in(VarPOIs, ScopeRange), + should_bump_variable(POI, Name, Number) + ], + Method = <<"workspace/applyEdit">>, + Params = #{edit => #{changes => #{Uri => Changes}}}, + els_server:send_request(Method, Params). + +-spec should_bump_variable(els_poi:poi(), binary(), binary()) -> boolean(). +should_bump_variable(#{id := Id}, Name, Number) -> + case split_variable(Id) of + {PName, PNumber} when PName == Name -> + binary_to_integer(PNumber) >= binary_to_integer(Number); + _ -> + false + end. + +-spec bump_variable_change(els_poi:poi()) -> map(). +bump_variable_change(#{id := Id, range := PoiRange}) -> + {Name, Number} = split_variable(Id), + NewNumber = integer_to_binary(binary_to_integer(Number) + 1), + NewId = binary_to_atom(<<Name/binary, NewNumber/binary>>, utf8), + #{ + newText => NewId, + range => els_protocol:range(PoiRange) + }. + +-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 split_variable(atom() | binary() | list()) -> {binary(), binary()} | error. +split_variable(Name) when is_atom(Name) -> + split_variable(atom_to_list(Name)); +split_variable(Name) when is_binary(Name) -> + split_variable(unicode:characters_to_list(Name)); +split_variable(Name) when is_list(Name) -> + split_variable(lists:reverse(Name), []). + +-spec split_variable(string(), string()) -> {binary(), binary()} | error. +split_variable([H | T], Acc) when $0 =< H, H =< $9 -> + split_variable(T, [H | Acc]); +split_variable(_Name, []) -> + error; +split_variable(Name, Acc) -> + {list_to_binary(lists:reverse(Name)), list_to_binary(Acc)}. + +-spec extract_function(uri(), range()) -> ok. +extract_function(Uri, Range) -> + {ok, [#{text := Text} = Document]} = els_dt_document:lookup(Uri), + ExtractRange = extract_range(Document, Range), + #{from := {FromL, FromC} = From, to := {ToL, ToC}} = ExtractRange, + ExtractString0 = els_text:range(Text, From, {ToL, ToC}), + %% Trim whitespace + ExtractString = string:trim(ExtractString0, both, " \n\r\t"), + %% Trim trailing termination symbol + ExtractStringTrimmed = string:trim(ExtractString, trailing, ",.;"), + Method = <<"workspace/applyEdit">>, + case els_dt_document:wrapping_functions(Document, FromL, FromC) of + [WrappingFunPOI | _] when ExtractStringTrimmed /= <<>> -> + %% WrappingFunPOI is the function that we are currently in + #{ + data := #{ + wrapping_range := + #{ + from := {FunBeginLine, _}, + to := {FunEndLine, _} + } + } + } = WrappingFunPOI, + %% Get args needed for the new function + Args = get_args(ExtractRange, Document, FromL, FunBeginLine), + ArgsBin = unicode:characters_to_binary(string:join(Args, ", ")), + FunClause = <<"new_function(", ArgsBin/binary, ")">>, + %% Place the new function after the current function + EndSymbol = end_symbol(ExtractString), + NewRange = els_protocol:range( + #{from => {FunEndLine + 1, 1}, to => {FunEndLine + 1, 1}} + ), + FunBody = unicode:characters_to_list( + <<FunClause/binary, " ->\n", ExtractStringTrimmed/binary, ".">> + ), + {ok, FunBodyFormatted, _} = erlfmt:format_string(FunBody, []), + NewFun = unicode:characters_to_binary(FunBodyFormatted ++ "\n"), + Changes = [ + #{ + newText => <<FunClause/binary, EndSymbol/binary>>, + range => els_protocol:range(ExtractRange) + }, + #{ + newText => NewFun, + range => NewRange + } + ], + Params = #{edit => #{changes => #{Uri => Changes}}}, + els_server:send_request(Method, Params); + _ -> + ?LOG_INFO("No wrapping function found"), + ok + end. + +-spec end_symbol(binary()) -> binary(). +end_symbol(ExtractString) -> + case binary:last(ExtractString) of + $. -> <<".">>; + $, -> <<",">>; + $; -> <<";">>; + _ -> <<>> + end. + +%% @doc Find all variables defined in the function before the current. +%% If they are used inside the selected range, they need to be +%% sent in as arguments to the new function. +-spec get_args( + els_poi:poi_range(), + els_dt_document:item(), + non_neg_integer(), + non_neg_integer() +) -> [string()]. +get_args(PoiRange, Document, FromL, FunBeginLine) -> + BeforeRange = #{from => {FunBeginLine, 1}, to => {FromL, 1}}, + VarPOIsBefore = els_dt_document:pois_in_range(Document, [variable], BeforeRange), + %% Remove all variables inside LCs or keyword expressions + LCPOIs = els_dt_document:pois(Document, [list_comp]), + FunExprPOIs = [ + POI + || #{id := fun_expr} = POI <- els_dt_document:pois(Document, [keyword_expr]) + ], + %% Only consider fun exprs that doesn't contain the selected range + ExcludePOIs = [ + POI + || #{range := R} = POI <- FunExprPOIs ++ LCPOIs, not els_range:in(PoiRange, R) + ], + VarsBefore = [ + Id + || #{range := VarRange, id := Id} <- VarPOIsBefore, + not_in_any_range(VarRange, ExcludePOIs) + ], + %% Find all variables defined before the current function that are used + %% inside the selected range. + VarPOIsInside = els_dt_document:pois_in_range(Document, [variable], PoiRange), + els_utils:uniq([ + atom_to_list(Id) + || #{id := Id} <- els_poi:sort(VarPOIsInside), + lists:member(Id, VarsBefore) + ]). + +-spec not_in_any_range(els_poi:poi_range(), [els_poi:poi()]) -> boolean(). +not_in_any_range(VarRange, POIs) -> + not lists:any( + fun(#{range := Range}) -> + els_range:in(VarRange, Range) + end, + POIs + ). + +-spec extract_range(els_dt_document:item(), range()) -> els_poi:poi_range(). +extract_range(#{text := Text} = Document, Range) -> + PoiRange = els_range:to_poi_range(Range), + #{from := {CurrL, CurrC} = From, to := To} = PoiRange, + POIs = els_dt_document:get_element_at_pos(Document, CurrL, CurrC), + MarkedText = els_text:range(Text, From, To), + case els_text:is_keyword_expr(MarkedText) of + true -> + case sort_by_range_size([P || #{kind := keyword_expr} = P <- POIs]) of + [] -> + PoiRange; + [{_Size, #{range := SmallestRange}} | _] -> + SmallestRange + end; + false -> + PoiRange + end. + +-spec sort_by_range_size(_) -> _. +sort_by_range_size(POIs) -> + lists:sort([{range_size(P), P} || P <- POIs]). + +-spec range_size(_) -> _. +range_size(#{range := #{from := {FromL, FromC}, to := {ToL, ToC}}}) -> + {ToL - FromL, ToC - FromC}. + +-spec spec_text(binary()) -> binary(). +spec_text(<<"-callback", Rest/binary>>) -> + <<"-spec", Rest/binary>>; +spec_text(Text) -> + Text. + +-spec fun_text(els_poi:poi(), els_dt_document:item()) -> iolist(). +fun_text(#{id := {Name, Arity}, range := Range}, #{text := Text}) -> + #{from := From, to := To} = Range, + %% TODO: Assuming 2 space indentation + CallbackText = els_text:range(Text, From, To), + SpecText = spec_text(CallbackText), + [ + io_lib:format("~s", [SpecText]), + "\n", + atom_to_binary(Name, utf8), + "(", + args_text(Arity, 1), + ") ->\n", + " error(not_implemented).\n\n" + ]. + +-spec export_text(els_poi:poi()) -> iolist(). +export_text(#{id := {Name, Arity}}) -> + [ + "-export([", + atom_to_binary(Name, utf8), + "/", + integer_to_list(Arity), + "]).\n" + ]. + +-spec args_text(integer(), integer()) -> iolist(). +args_text(0, 1) -> + []; +args_text(Arity, Arity) -> + ["_"]; +args_text(Arity, N) -> + ["_, " | args_text(Arity, N + 1)]. diff --git a/apps/els_lsp/src/els_folding_range_provider.erl b/apps/els_lsp/src/els_folding_range_provider.erl index b9552a9a0..0507a830c 100644 --- a/apps/els_lsp/src/els_folding_range_provider.erl +++ b/apps/els_lsp/src/els_folding_range_provider.erl @@ -4,44 +4,44 @@ -include("els_lsp.hrl"). --export([ handle_request/2 - , is_enabled/0 - ]). +-export([ + handle_request/1 +]). %%============================================================================== %% Type Definitions %%============================================================================== - -type folding_range_result() :: [folding_range()] | null. --type state() :: any(). %%============================================================================== %% els_provider functions %%============================================================================== - --spec is_enabled() -> boolean(). -is_enabled() -> - true. - --spec handle_request(tuple(), state()) -> {folding_range_result(), state()}. -handle_request({document_foldingrange, Params}, State) -> - #{ <<"textDocument">> := #{<<"uri">> := Uri} } = Params, - {ok, Document} = els_utils:lookup_document(Uri), - POIs = els_dt_document:pois(Document, [folding_range]), - Response = case [folding_range(Range) || #{range := Range} <- POIs] of - [] -> null; - Ranges -> Ranges - end, - {Response, State}. +-spec handle_request(tuple()) -> {response, folding_range_result()}. +handle_request({document_foldingrange, Params}) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + {ok, Document} = els_utils:lookup_document(Uri), + POIs = els_dt_document:pois(Document, [function, record]), + Response = + case + [ + poi_range_to_folding_range(els_poi:folding_range(POI)) + || POI <- POIs, els_poi:folding_range(POI) =/= oneliner + ] + of + [] -> null; + Ranges -> Ranges + end, + {response, Response}. %%============================================================================== %% Internal functions %%============================================================================== --spec folding_range(poi_range()) -> folding_range(). -folding_range(#{from := {FromLine, FromCol}, to := {ToLine, ToCol}}) -> - #{ startLine => FromLine - 1 - , startCharacter => FromCol - , endLine => ToLine - 1 - , endCharacter => ToCol - }. +-spec poi_range_to_folding_range(els_poi:poi_range()) -> folding_range(). +poi_range_to_folding_range(#{from := {FromLine, FromCol}, to := {ToLine, ToCol}}) -> + #{ + startLine => FromLine - 1, + startCharacter => FromCol, + endLine => ToLine - 1, + endCharacter => ToCol + }. diff --git a/apps/els_lsp/src/els_formatting_provider.erl b/apps/els_lsp/src/els_formatting_provider.erl index 6994549d7..a09756b64 100644 --- a/apps/els_lsp/src/els_formatting_provider.erl +++ b/apps/els_lsp/src/els_formatting_provider.erl @@ -2,13 +2,9 @@ -behaviour(els_provider). --export([ init/0 - , handle_request/2 - , is_enabled/0 - , is_enabled_document/0 - , is_enabled_range/0 - , is_enabled_on_type/0 - ]). +-export([ + handle_request/1 +]). %%============================================================================== %% Includes @@ -16,146 +12,213 @@ -include("els_lsp.hrl"). -include_lib("kernel/include/logger.hrl"). -%%============================================================================== -%% Types -%%============================================================================== --type formatter() :: fun((string(), string(), formatting_options()) -> - boolean()). --type state() :: [formatter()]. - %%============================================================================== %% Macro Definitions %%============================================================================== -define(DEFAULT_SUB_INDENT, 2). +-type formatter_name() :: + sr_formatter + | erlfmt_formatter + | otp_formatter + | default_formatter. %%============================================================================== %% els_provider functions %%============================================================================== --spec init() -> state(). -init() -> - case els_config:get(bsp_enabled) of - false -> - [ fun format_document_local/3 ]; - _ -> - [ fun format_document_bsp/3, fun format_document_local/3 ] - end. - -%% Keep the behaviour happy --spec is_enabled() -> boolean(). -is_enabled() -> is_enabled_document(). - --spec is_enabled_document() -> boolean(). -is_enabled_document() -> true. - --spec is_enabled_range() -> boolean(). -is_enabled_range() -> - false. - +-spec handle_request(any()) -> {response, any()}. +handle_request({document_formatting, Params}) -> + #{ + <<"options">> := Options, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + Path = els_uri:path(Uri), + {ok, Document} = els_utils:lookup_document(Uri), + case els_utils:project_relative(Uri) of + {error, not_relative} -> + {response, []}; + RelativePath -> + format_document(Path, Document, RelativePath, Options) + end; +handle_request({document_rangeformatting, Params}) -> + #{ + <<"range">> := #{ + <<"start">> := StartPos, + <<"end">> := EndPos + }, + <<"options">> := Options, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + Range = #{start => StartPos, 'end' => EndPos}, + {ok, Document} = els_utils:lookup_document(Uri), + {ok, TextEdit} = rangeformat_document(Uri, Document, Range, Options), + {response, TextEdit}; %% NOTE: because erlang_ls does not send incremental document changes %% via `textDocument/didChange`, this kind of formatting does not %% make sense. --spec is_enabled_on_type() -> document_ontypeformatting_options(). -is_enabled_on_type() -> false. - --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({document_formatting, Params}, State) -> - #{ <<"options">> := Options - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - Path = els_uri:path(Uri), - case els_utils:project_relative(Uri) of - {error, not_relative} -> - {[], State}; - RelativePath -> - format_document(Path, RelativePath, Options, State) - end; -handle_request({document_rangeformatting, Params}, State) -> - #{ <<"range">> := #{ <<"start">> := StartPos - , <<"end">> := EndPos - } - , <<"options">> := Options - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - Range = #{ start => StartPos, 'end' => EndPos }, - {ok, Document} = els_utils:lookup_document(Uri), - case rangeformat_document(Uri, Document, Range, Options) of - {ok, TextEdit} -> {TextEdit, State} - end; -handle_request({document_ontypeformatting, Params}, State) -> - #{ <<"position">> := #{ <<"line">> := Line - , <<"character">> := Character - } - , <<"ch">> := Char - , <<"options">> := Options - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - {ok, Document} = els_utils:lookup_document(Uri), - case ontypeformat_document(Uri, Document, Line + 1, Character + 1, Char - , Options) of - {ok, TextEdit} -> {TextEdit, State} - end. +handle_request({document_ontypeformatting, Params}) -> + #{ + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"ch">> := Char, + <<"options">> := Options, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + {ok, Document} = els_utils:lookup_document(Uri), + {ok, TextEdit} = + ontypeformat_document( + Uri, + Document, + Line + 1, + Character + 1, + Char, + Options + ), + {response, TextEdit}. %%============================================================================== %% Internal functions %%============================================================================== --spec format_document(binary(), string(), formatting_options(), state()) -> - {[text_edit()], state()}. -format_document(Path, RelativePath, Options, Formatters) -> - Fun = fun(Dir) -> - NewFormatters = lists:dropwhile( - fun(F) -> - not F(Dir, RelativePath, Options) - end, - Formatters - ), - Outfile = filename:join(Dir, RelativePath), - {els_text_edit:diff_files(Path, Outfile), NewFormatters} - end, - tempdir:mktmp(Fun). - --spec format_document_bsp(string(), string(), formatting_options()) -> - boolean(). -format_document_bsp(Dir, RelativePath, _Options) -> - Method = <<"rebar3/run">>, - Params = #{ <<"args">> => ["format", "-o", Dir, "-f", RelativePath] }, - try - case els_bsp_provider:request(Method, Params) of - {error, Reason} -> - error(Reason); - {reply, #{ error := _Error } = Result} -> - error(Result); - {reply, Result} -> - ?LOG_DEBUG("BSP format succeeded. [result=~p]", [Result]), - true - end - catch - C:E:S -> - ?LOG_WARNING("format_document_bsp failed. ~p:~p ~p", [C, E, S]), - false - end. - --spec format_document_local(string(), string(), formatting_options()) -> - boolean(). -format_document_local(Dir, RelativePath, - #{ <<"insertSpaces">> := InsertSpaces - , <<"tabSize">> := TabSize } = Options) -> - SubIndent = maps:get(<<"subIndent">>, Options, ?DEFAULT_SUB_INDENT), - Opts = #{ remove_tabs => InsertSpaces - , break_indent => TabSize - , sub_indent => SubIndent - , output_dir => Dir - }, - Formatter = rebar3_formatter:new(default_formatter, Opts, unused), - rebar3_formatter:format_file(RelativePath, Formatter), - true. - --spec rangeformat_document(uri(), map(), range(), formatting_options()) - -> {ok, [text_edit()]}. +-spec format_document(binary(), els_dt_document:item(), string(), formatting_options()) -> + {response, [text_edit()]}. +format_document(Path, Document, RelativePath, Options) -> + Config = els_config:get(formatting), + case {get_formatter_files(Config), get_formatter_exclude_files(Config)} of + {all, ExcludeFiles} -> + case lists:member(Path, ExcludeFiles) of + true -> + {response, []}; + false -> + do_format_document(Document, RelativePath, Options) + end; + {Files, ExcludeFiles} -> + case lists:member(Path, Files) of + true -> + case lists:member(Path, ExcludeFiles) of + true -> + {response, []}; + false -> + do_format_document(Document, RelativePath, Options) + end; + false -> + {response, []} + end + end. + +-spec do_format_document(els_dt_document:item(), string(), formatting_options()) -> + {response, [text_edit()]}. +do_format_document(#{text := Text}, RelativePath, Options) -> + Fun = fun(Dir) -> + {ok, OldCwd} = file:get_cwd(), + ok = file:set_cwd(Dir), + try + Basename = filename:basename(RelativePath), + InFile = filename:join(Dir, Basename), + OutDir = filename:join([Dir, "formatted"]), + OutFile = filename:join(OutDir, Basename), + ok = filelib:ensure_dir(OutFile), + ok = file:write_file(InFile, Text), + ok = format_document_local(OutDir, Basename, Options), + Diff = els_text_edit:diff_files(InFile, OutFile), + {response, Diff} + after + ok = file:set_cwd(OldCwd) + end + end, + tempdir:mktmp(Fun). + +-spec format_document_local(string(), string(), formatting_options()) -> ok. +format_document_local( + Dir, + RelativePath, + #{ + <<"insertSpaces">> := InsertSpaces, + <<"tabSize">> := TabSize + } = Options +) -> + SubIndent = get_sub_indent(Options), + Opts0 = #{ + remove_tabs => InsertSpaces, + break_indent => TabSize, + sub_indent => SubIndent, + output_dir => Dir + }, + Config = els_config:get(formatting), + FormatterName = get_formatter_name(Config), + Opts = maybe_set_width(FormatterName, Opts0, get_width(Config)), + ?LOG_INFO("Format using ~p with options: ~p", [FormatterName, Opts]), + Formatter = rebar3_formatter:new(FormatterName, Opts, unused), + rebar3_formatter:format_file(RelativePath, Formatter), + ok. + +-spec get_sub_indent(map()) -> integer(). +get_sub_indent(Options) -> + maps:get("subIndent", Options, ?DEFAULT_SUB_INDENT). + +-spec maybe_set_width(formatter_name(), map(), integer() | undefined) -> map(). +maybe_set_width(erlfmt_formatter, Opts, Width) when is_integer(Width) -> + Opts#{print_width => Width}; +maybe_set_width(default_formatter, Opts, Width) when is_integer(Width) -> + Opts#{paper => Width}; +maybe_set_width(otp_formatter, Opts, Width) when is_integer(Width) -> + Opts#{paper => Width}; +maybe_set_width(_Formatter, Opts, _) -> + Opts. + +-spec get_width(map()) -> integer() | undefined. +get_width(Config) -> + maps:get("width", Config, undefined). + +-spec rangeformat_document(uri(), map(), range(), formatting_options()) -> + {ok, [text_edit()]}. rangeformat_document(_Uri, _Document, _Range, _Options) -> {ok, []}. --spec ontypeformat_document(binary(), map() - , number(), number(), string(), formatting_options()) - -> {ok, [text_edit()]}. +-spec ontypeformat_document( + binary(), + map(), + number(), + number(), + string(), + formatting_options() +) -> + {ok, [text_edit()]}. ontypeformat_document(_Uri, _Document, _Line, _Col, _Char, _Options) -> {ok, []}. + +-spec get_formatter_files(map()) -> [binary()] | all. +get_formatter_files(Config) -> + RootPath = els_uri:path(els_config:get(root_uri)), + case maps:get("files", Config, all) of + all -> + all; + Globs -> + lists:flatten([expand_glob(RootPath, Glob) || Glob <- Globs]) + end. + +-spec get_formatter_exclude_files(map()) -> [binary()]. +get_formatter_exclude_files(Config) -> + RootPath = els_uri:path(els_config:get(root_uri)), + Globs = maps:get("exclude_files", Config, []), + lists:flatten([expand_glob(RootPath, Glob) || Glob <- Globs]). + +-spec expand_glob(binary(), binary()) -> [binary()]. +expand_glob(RootPath, Glob) -> + Wildcard = unicode:characters_to_list(filename:join(RootPath, Glob), utf8), + [unicode:characters_to_binary(Path) || Path <- filelib:wildcard(Wildcard)]. + +-spec get_formatter_name(map() | undefined) -> formatter_name(). +get_formatter_name(undefined) -> + default_formatter; +get_formatter_name(Config) -> + case maps:get("formatter", Config, undefined) of + "sr" -> + sr_formatter; + "erlfmt" -> + erlfmt_formatter; + "otp" -> + otp_formatter; + _ -> + default_formatter + end. diff --git a/apps/els_lsp/src/els_fungraph.erl b/apps/els_lsp/src/els_fungraph.erl index 19da5d1ec..adc2d935d 100644 --- a/apps/els_lsp/src/els_fungraph.erl +++ b/apps/els_lsp/src/els_fungraph.erl @@ -12,34 +12,34 @@ -spec new(id_fun(NodeT), edges_fun(NodeT)) -> graph(NodeT). new(IdFun, EdgesFun) -> - {?MODULE, IdFun, EdgesFun}. + {?MODULE, IdFun, EdgesFun}. -type acc_fun(NodeT, AccT) :: - fun((_Node :: NodeT, _From :: NodeT, AccT) -> AccT). + fun((_Node :: NodeT, _From :: NodeT, AccT) -> AccT). -spec traverse(acc_fun(NodeT, AccT), AccT, NodeT, graph(NodeT)) -> AccT. traverse(AccFun, Acc, From, G) -> - traverse(AccFun, Acc, [From], sets:new(), G). + traverse(AccFun, Acc, [From], sets:new(), G). -spec traverse(acc_fun(NodeT, AccT), AccT, [NodeT], sets:set(), graph(NodeT)) -> - AccT. + AccT. traverse(AccFun, Acc, [Node | Rest], Visited, G = {?MODULE, IdFun, EdgesFun}) -> - {AdjacentNodes, VisitedNext} = lists:foldr( - fun(Adjacent, {NodesAcc, VisitedAcc}) -> - ID = IdFun(Adjacent), - case sets:is_element(ID, VisitedAcc) of - false -> {[Adjacent | NodesAcc], sets:add_element(ID, VisitedAcc)}; - true -> {NodesAcc, VisitedAcc} - end - end, - {[], Visited}, - EdgesFun(Node) - ), - AccNext = lists:foldl( - fun(Adjacent, AccIn) -> AccFun(Adjacent, Node, AccIn) end, - Acc, - AdjacentNodes - ), - traverse(AccFun, AccNext, AdjacentNodes ++ Rest, VisitedNext, G); + {AdjacentNodes, VisitedNext} = lists:foldr( + fun(Adjacent, {NodesAcc, VisitedAcc}) -> + ID = IdFun(Adjacent), + case sets:is_element(ID, VisitedAcc) of + false -> {[Adjacent | NodesAcc], sets:add_element(ID, VisitedAcc)}; + true -> {NodesAcc, VisitedAcc} + end + end, + {[], Visited}, + EdgesFun(Node) + ), + AccNext = lists:foldl( + fun(Adjacent, AccIn) -> AccFun(Adjacent, Node, AccIn) end, + Acc, + AdjacentNodes + ), + traverse(AccFun, AccNext, AdjacentNodes ++ Rest, VisitedNext, G); traverse(_AccFun, Acc, [], _Visited, _G) -> - Acc. + Acc. diff --git a/apps/els_lsp/src/els_general_provider.erl b/apps/els_lsp/src/els_general_provider.erl index 52737d78c..b73b390be 100644 --- a/apps/els_lsp/src/els_general_provider.erl +++ b/apps/els_lsp/src/els_general_provider.erl @@ -1,12 +1,13 @@ -module(els_general_provider). -behaviour(els_provider). --export([ handle_request/2 - , is_enabled/0 - ]). +-export([ + default_providers/0, + enabled_providers/0, + handle_request/1 +]). --export([ server_capabilities/0 - ]). +-export([server_capabilities/0]). %%============================================================================== %% Includes @@ -20,18 +21,21 @@ -type server_capabilities() :: map(). -type initialize_request() :: {initialize, initialize_params()}. --type initialize_params() :: #{ processId := number() | null - , rootPath => binary() | null - , rootUri := uri() | null - , initializationOptions => any() - , capabilities := client_capabilities() - , trace => off - | messages - | verbose - , workspaceFolders => [workspace_folder()] - | null - }. --type initialize_result() :: #{ capabilities => server_capabilities() }. +-type initialize_params() :: #{ + processId := number() | null, + rootPath => binary() | null, + rootUri := uri() | null, + initializationOptions => any(), + capabilities := client_capabilities(), + trace => + off + | messages + | verbose, + workspaceFolders => + [workspace_folder()] + | null +}. +-type initialize_result() :: #{capabilities => server_capabilities()}. -type initialized_request() :: {initialized, initialized_params()}. -type initialized_params() :: #{}. -type initialized_result() :: null. @@ -41,137 +45,269 @@ -type exit_request() :: {exit, exit_params()}. -type exit_params() :: #{status => atom()}. -type exit_result() :: null. --type state() :: any(). +-type provider_id() :: string(). %%============================================================================== %% els_provider functions %%============================================================================== - --spec is_enabled() -> boolean(). -is_enabled() -> true. - --spec handle_request( initialize_request() - | initialized_request() - | shutdown_request() - | exit_request() - , state()) -> - { initialize_result() +-spec handle_request( + initialize_request() + | initialized_request() + | shutdown_request() + | exit_request() +) -> + {response, + initialize_result() | initialized_result() | shutdown_result() - | exit_result() - , state() - }. -handle_request({initialize, Params}, State) -> - #{ <<"rootUri">> := RootUri0 - , <<"capabilities">> := Capabilities - } = Params, - RootUri = case RootUri0 of - null -> + | exit_result()}. +handle_request({initialize, Params}) -> + #{ + <<"rootUri">> := RootUri0, + <<"capabilities">> := Capabilities + } = Params, + RootUri = + case RootUri0 of + null -> {ok, Cwd} = file:get_cwd(), els_uri:uri(els_utils:to_binary(Cwd)); - _ -> RootUri0 - end, - InitOptions = case maps:get(<<"initializationOptions">>, Params, #{}) of - InitOptions0 when is_map(InitOptions0) -> - InitOptions0; - _ -> #{} - end, - ok = els_config:initialize(RootUri, Capabilities, InitOptions, true), - NewState = State#{ root_uri => RootUri, init_options => InitOptions}, - {server_capabilities(), NewState}; -handle_request({initialized, _Params}, State) -> - #{root_uri := RootUri} = State, - - NodeName = els_distribution_server:node_name( <<"erlang_ls">> - , filename:basename(RootUri)), - els_distribution_server:start_distribution(NodeName), - ?LOG_INFO("Started distribution for: [~p]", [NodeName]), - - case els_bsp_provider:maybe_start(RootUri) of - {error, Reason} -> - case els_config:get(bsp_enabled) of - true -> - ?LOG_ERROR( "BSP server startup failed, shutting down. [reason=~p]" - , [Reason] - ), - els_utils:halt(1); - auto -> - ?LOG_INFO("BSP server startup failed. [reason=~p]", [Reason]), - ok - end; - _ -> - ok - end, - case els_bsp_provider:info(is_running) of - true -> %% The BSP provider will start indexing when it's ready - ok; - false -> %% We need to start indexing here - els_indexing:maybe_start() - end, - - {null, State}; -handle_request({shutdown, _Params}, State) -> - {null, State}; -handle_request({exit, #{status := Status}}, State) -> - ?LOG_INFO("Language server stopping..."), - ExitCode = case Status of - shutdown -> 0; - _ -> 1 - end, - els_utils:halt(ExitCode), - {null, State}. + _ -> + RootUri0 + end, + InitOptions = + case maps:get(<<"initializationOptions">>, Params, #{}) of + InitOptions0 when is_map(InitOptions0) -> + InitOptions0; + _ -> + #{} + end, + ok = els_config:initialize(RootUri, Capabilities, InitOptions, lsp_notification), + {response, server_capabilities()}; +handle_request({initialized, _Params}) -> + RootUri = els_config:get(root_uri), + NodeName = els_distribution_server:node_name( + <<"erlang_ls">>, + filename:basename(RootUri) + ), + register_capabilities(), + els_distribution_server:start_distribution(NodeName), + ?LOG_INFO("Started distribution for: [~p]", [NodeName]), + els_indexing:maybe_start(), + {response, null}; +handle_request({shutdown, _Params}) -> + {response, null}; +handle_request({exit, #{status := Status}}) -> + ?LOG_INFO("Language server stopping..."), + ExitCode = + case Status of + shutdown -> 0; + _ -> 1 + end, + els_utils:halt(ExitCode), + {response, null}. %%============================================================================== %% API %%============================================================================== +%% @doc Give all available providers +-spec available_providers() -> [provider_id()]. +available_providers() -> + [ + "text-document-sync", + "hover", + "completion", + "signature-help", + "definition", + "references", + "document-highlight", + "document-symbol", + "workspace-symbol", + "code-action", + "document-formatting", + "document-range-formatting", + "document-on-type-formatting", + "folding-range", + "implementation", + "execute-command", + "code-lens", + "rename", + "call-hierarchy", + "semantic-tokens", + "inlay-hint" + ]. + +%% @doc Give the list of all providers enabled by default. +-spec default_providers() -> [provider_id()]. +default_providers() -> + available_providers() -- + [ + "document-range-formatting", + %% NOTE: because erlang_ls does not send incremental document changes + %% via `textDocument/didChange', this kind of formatting does not + %% make sense. + "document-on-type-formatting", + %% Signature help is experimental. + "signature-help" + ]. + +%% @doc Give the list of all providers enabled by the current configuration. +-spec enabled_providers() -> [provider_id()]. +enabled_providers() -> + Config = els_config:get(providers), + Default = default_providers(), + Enabled = maps:get("enabled", Config, []), + Disabled = maps:get("disabled", Config, []), + lists:usort((Default ++ valid(Enabled)) -- valid(Disabled)). + +%% @doc Give the LSP server capabilities map for all capabilities enabled by +%% the current configuration. -spec server_capabilities() -> server_capabilities(). server_capabilities() -> - {ok, Version} = application:get_key(?APP, vsn), - #{ capabilities => - #{ textDocumentSync => - #{ openClose => true - , change => els_text_synchronization:sync_mode() - , save => #{includeText => false} - } - , hoverProvider => - els_hover_provider:is_enabled() - , completionProvider => - #{ resolveProvider => true - , triggerCharacters => - els_completion_provider:trigger_characters() - } - , definitionProvider => - els_definition_provider:is_enabled() - , referencesProvider => - els_references_provider:is_enabled() - , documentHighlightProvider => - els_document_highlight_provider:is_enabled() - , documentSymbolProvider => - els_document_symbol_provider:is_enabled() - , workspaceSymbolProvider => - els_workspace_symbol_provider:is_enabled() - , codeActionProvider => - els_code_action_provider:is_enabled() - , documentFormattingProvider => - els_formatting_provider:is_enabled_document() - , documentRangeFormattingProvider => - els_formatting_provider:is_enabled_range() - , foldingRangeProvider => - els_folding_range_provider:is_enabled() - , implementationProvider => - els_implementation_provider:is_enabled() - , executeCommandProvider => - els_execute_command_provider:options() - , codeLensProvider => - els_code_lens_provider:options() - , renameProvider => - els_rename_provider:is_enabled() - , callHierarchyProvider => - els_call_hierarchy_provider:is_enabled() + {ok, Version} = application:get_key(?APP, vsn), + AvailableCapabilities = + #{ + textDocumentSync => + els_text_synchronization_provider:options(), + hoverProvider => true, + completionProvider => + #{ + resolveProvider => true, + triggerCharacters => + els_completion_provider:trigger_characters() + }, + signatureHelpProvider => + #{ + triggerCharacters => + els_signature_help_provider:trigger_characters() + }, + definitionProvider => true, + referencesProvider => true, + documentHighlightProvider => true, + documentSymbolProvider => true, + workspaceSymbolProvider => true, + codeActionProvider => true, + documentFormattingProvider => true, + documentRangeFormattingProvider => false, + foldingRangeProvider => true, + implementationProvider => true, + executeCommandProvider => + els_execute_command_provider:options(), + codeLensProvider => + els_code_lens_provider:options(), + renameProvider => + els_rename_provider:options(), + callHierarchyProvider => true, + inlayHintProvider => + els_inlay_hint_provider:options(), + semanticTokensProvider => + #{ + legend => + #{ + tokenTypes => wrangler_handler:semantic_token_types(), + tokenModifiers => wrangler_handler:semantic_token_modifiers() + }, + range => false, + full => wrangler_handler:is_enabled() + } }, - serverInfo => - #{ name => <<"Erlang LS">> - , version => els_utils:to_binary(Version) + EnabledProviders = enabled_providers(), + ConfiguredCapabilities = + maps:filter( + fun(Provider, _Config) -> + lists:member(provider_id(Provider), EnabledProviders) + end, + AvailableCapabilities + ), + #{ + capabilities => ConfiguredCapabilities, + serverInfo => + #{ + name => <<"Erlang LS">>, + version => els_utils:to_binary(Version) + } + }. + +-spec register_capabilities() -> ok. +register_capabilities() -> + Methods = [<<"didChangeWatchedFiles">>], + ClientCapabilities = els_config:get(capabilities), + Registrations = [ + dynamic_registration_options(Method) + || Method <- Methods, is_dynamic_registration_enabled(Method, ClientCapabilities) + ], + case Registrations of + [] -> + ?LOG_INFO("Skipping dynamic capabilities registration"); + _ -> + Params = #{registrations => Registrations}, + els_server:send_request(<<"client/registerCapability">>, Params) + end. + +-spec is_dynamic_registration_enabled(binary(), map()) -> boolean(). +is_dynamic_registration_enabled(Method, ClientCapabilities) -> + maps:get( + <<"dynamicRegistration">>, + maps:get(Method, maps:get(<<"workspace">>, ClientCapabilities, #{}), #{}), + false + ). + +-spec dynamic_registration_options(binary()) -> map(). +dynamic_registration_options(<<"didChangeWatchedFiles">>) -> + RootPath = els_uri:path(els_config:get(root_uri)), + GlobPattern = filename:join([RootPath, "**", "*.{e,h}rl"]), + #{ + id => <<"workspace/didChangeWatchedFiles">>, + method => <<"workspace/didChangeWatchedFiles">>, + registerOptions => #{ + watchers => [#{globPattern => GlobPattern}] } - }. + }. + +-spec valid([any()]) -> [provider_id()]. +valid(ProviderIds) -> + {Valid, Invalid} = lists:partition(fun is_valid_provider_id/1, ProviderIds), + case Invalid of + [] -> + ok; + _ -> + Fmt = "Discarding invalid providers 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 is_valid_provider_id(any()) -> boolean(). +is_valid_provider_id(ProviderId) -> + lists:member(ProviderId, available_providers()). + +-spec provider_id(atom()) -> provider_id(). +provider_id(textDocumentSync) -> "text-document-sync"; +provider_id(completionProvider) -> "completion"; +provider_id(hoverProvider) -> "hover"; +provider_id(signatureHelpProvider) -> "signature-help"; +provider_id(definitionProvider) -> "definition"; +provider_id(referencesProvider) -> "references"; +provider_id(documentHighlightProvider) -> "document-highlight"; +provider_id(documentSymbolProvider) -> "document-symbol"; +provider_id(workspaceSymbolProvider) -> "workspace-symbol"; +provider_id(codeActionProvider) -> "code-action"; +provider_id(documentFormattingProvider) -> "document-formatting"; +provider_id(documentRangeFormattingProvider) -> "document-range-formatting"; +provider_id(documentOnTypeFormattingProvider) -> "document-on-type-formatting"; +provider_id(foldingRangeProvider) -> "folding-range"; +provider_id(implementationProvider) -> "implementation"; +provider_id(executeCommandProvider) -> "execute-command"; +provider_id(codeLensProvider) -> "code-lens"; +provider_id(renameProvider) -> "rename"; +provider_id(callHierarchyProvider) -> "call-hierarchy"; +provider_id(semanticTokensProvider) -> "semantic-tokens"; +provider_id(inlayHintProvider) -> "inlay-hint". diff --git a/apps/els_lsp/src/els_gradualizer_diagnostics.erl b/apps/els_lsp/src/els_gradualizer_diagnostics.erl index 598dba094..f515f32a9 100644 --- a/apps/els_lsp/src/els_gradualizer_diagnostics.erl +++ b/apps/els_lsp/src/els_gradualizer_diagnostics.erl @@ -11,10 +11,11 @@ %%============================================================================== %% Exports %%============================================================================== --export([ is_default/0 - , run/1 - , source/0 - ]). +-export([ + is_default/0, + run/1, + source/0 +]). %%============================================================================== %% Includes @@ -28,63 +29,65 @@ -spec is_default() -> boolean(). is_default() -> - false. + false. -spec run(uri()) -> [els_diagnostics:diagnostic()]. run(Uri) -> - case start_and_load() of - true -> - Path = unicode:characters_to_list(els_uri:path(Uri)), - Includes = [{i, I} || I <- els_config:get(include_paths)], - Opts = [return_errors] ++ Includes, - Errors = gradualizer:type_check_files([Path], Opts), - lists:flatmap(fun analyzer_error/1, Errors); - false -> - [] - end. + Files = lists:flatmap( + fun(Dir) -> + filelib:wildcard(filename:join(Dir, "*.erl")) + end, + els_config:get(apps_paths) ++ els_config:get(deps_paths) + ), + ?LOG_INFO("Importing files into gradualizer_db: ~p", [Files]), + ok = gradualizer_db:import_erl_files( + Files, + els_config:get(include_paths) + ), + Path = unicode:characters_to_list(els_uri:path(Uri)), + Includes = [{i, I} || I <- els_config:get(include_paths)], + Opts = [return_errors] ++ Includes, + Errors = gradualizer:type_check_files([Path], Opts), + ?LOG_INFO("Gradualizer diagnostics: ~p", [Errors]), + lists:flatmap(fun analyzer_error/1, Errors). -spec source() -> binary(). source() -> - <<"Gradualizer">>. + <<"Gradualizer">>. %%============================================================================== %% Internal Functions %%============================================================================== --spec start_and_load() -> boolean(). -start_and_load() -> - try application:ensure_all_started(gradualizer) of - {ok, [gradualizer]} -> - Files = lists:flatmap( - fun(Dir) -> - filelib:wildcard(filename:join(Dir, "*.erl")) - end, - els_config:get(apps_paths) ++ els_config:get(deps_paths)), - ok = gradualizer_db:import_erl_files(Files, - els_config:get(include_paths)), - true; - {ok, []} -> - true - catch E:R -> - ?LOG_ERROR("Could not start gradualizer: ~p ~p", [E, R]), - false - end. - -spec analyzer_error(any()) -> any(). analyzer_error({_Path, Error}) -> - FmtOpts = [{fmt_location, brief}, {color, never}], - FmtError = gradualizer_fmt:format_type_error(Error, FmtOpts), - case re:run(FmtError, "([0-9]+):([0-9]+:)? (.*)", - [{capture, all_but_first, binary}, dotall]) of - {match, [BinLine, _BinCol, Msg]} -> - Line = case binary_to_integer(BinLine) of - 0 -> 1; - L -> L - end, - Range = els_protocol:range(#{ from => {Line, 1}, - to => {Line + 1, 1} }), - [els_diagnostics:make_diagnostic(Range, Msg, ?DIAGNOSTIC_WARNING, - source())]; - _ -> - [] - end. + FmtOpts = [{fmt_location, brief}, {color, never}], + FmtError = gradualizer_fmt:format_type_error(Error, FmtOpts), + case + re:run( + FmtError, + "([0-9]+):([0-9]+:)? (.*)", + [{capture, all_but_first, binary}, dotall] + ) + of + {match, [BinLine, _BinCol, Msg]} -> + Line = + case binary_to_integer(BinLine) of + 0 -> 1; + L -> L + end, + Range = els_protocol:range(#{ + from => {Line, 1}, + to => {Line + 1, 1} + }), + [ + els_diagnostics:make_diagnostic( + Range, + Msg, + ?DIAGNOSTIC_WARNING, + source() + ) + ]; + _ -> + [] + end. diff --git a/apps/els_lsp/src/els_group_leader_server.erl b/apps/els_lsp/src/els_group_leader_server.erl index feb309c92..19ea02f3f 100644 --- a/apps/els_lsp/src/els_group_leader_server.erl +++ b/apps/els_lsp/src/els_group_leader_server.erl @@ -7,26 +7,28 @@ %%============================================================================== %% API %%============================================================================== --export([ new/0 - , flush/1 - , stop/1 - ]). +-export([ + new/0, + flush/1, + stop/1 +]). %%============================================================================== %% Supervisor API %%============================================================================== --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 @@ -41,13 +43,15 @@ %%============================================================================== %% Type Definitions %%============================================================================== --type config() :: #{ caller := pid() - , gl := pid() - }. --type state() :: #{ acc := [any()] - , caller := pid() - , gl := pid() - }. +-type config() :: #{ + caller := pid(), + gl := pid() +}. +-type state() :: #{ + acc := [any()], + caller := pid(), + gl := pid() +}. %%============================================================================== %% API @@ -56,24 +60,24 @@ %% @doc Create a new server -spec new() -> {ok, pid()}. new() -> - Caller = self(), - GL = group_leader(), - supervisor:start_child(els_group_leader_sup, [#{caller => Caller, gl => GL}]). + Caller = self(), + GL = group_leader(), + supervisor:start_child(els_group_leader_sup, [#{caller => Caller, gl => GL}]). -spec flush(pid()) -> binary(). flush(Server) -> - gen_server:call(Server, {flush}). + gen_server:call(Server, {flush}). -spec stop(pid()) -> ok. stop(Server) -> - gen_server:call(Server, {stop}). + gen_server:call(Server, {stop}). %%============================================================================== %% Supervisor API %%============================================================================== -spec start_link(config()) -> {ok, pid()}. start_link(Config) -> - gen_server:start_link(?MODULE, Config, []). + gen_server:start_link(?SERVER, Config, []). %%============================================================================== %% gen_server callbacks @@ -81,51 +85,53 @@ start_link(Config) -> -spec init(config()) -> {ok, state()}. init(#{caller := Caller, gl := GL}) -> - process_flag(trap_exit, true), - ?LOG_INFO("Starting group leader server [caller=~p] [gl=~p]", [Caller, GL]), - group_leader(self(), Caller), - {ok, #{acc => [], caller => Caller, gl => GL}}. + process_flag(trap_exit, true), + ?LOG_INFO("Starting group leader server [caller=~p] [gl=~p]", [Caller, GL]), + group_leader(self(), Caller), + {ok, #{acc => [], caller => Caller, gl => GL}}. -spec handle_call(any(), {pid(), any()}, state()) -> {noreply, state()}. handle_call({flush}, _From, #{acc := Acc} = State) -> - Res = els_utils:to_binary(lists:flatten(lists:reverse(Acc))), - {reply, Res, State}; + Res = els_utils:to_binary(lists:flatten(lists:reverse(Acc))), + {reply, Res, State}; handle_call({stop}, _From, State) -> - {stop, normal, ok, State}; + {stop, normal, ok, 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({io_request, From, ReplyAs, Request}, State) -> - #{acc := Acc} = State, - case Request of - {put_chars, Encoding, Characters} -> - List = unicode:characters_to_list(Characters, Encoding), - From ! {io_reply, ReplyAs, ok}, - {noreply, State#{acc => [List|Acc]}}; - {put_chars, Encoding, M, F, A} -> - String = try erlang:apply(M, F, A) - catch - C:E:S -> - io_lib:format("~p", [{C, E, S}]) - end, - List = unicode:characters_to_list(String, Encoding), - From ! {io_reply, ReplyAs, ok}, - {noreply, State#{acc => [List|Acc]}}; - Else -> - ?LOG_WARNING("[Group Leader] Request not implemented", [Else]), - From ! {io_reply, ReplyAs, {error, not_implemented}}, - {noreply, State} - end; + #{acc := Acc} = State, + case Request of + {put_chars, Encoding, Characters} -> + List = unicode:characters_to_list(Characters, Encoding), + From ! {io_reply, ReplyAs, ok}, + {noreply, State#{acc => [List | Acc]}}; + {put_chars, Encoding, M, F, A} -> + String = + try + erlang:apply(M, F, A) + catch + C:E:S -> + io_lib:format("~p", [{C, E, S}]) + end, + List = unicode:characters_to_list(String, Encoding), + From ! {io_reply, ReplyAs, ok}, + {noreply, State#{acc => [List | Acc]}}; + Else -> + ?LOG_WARNING("[Group Leader] Request not implemented", [Else]), + From ! {io_reply, ReplyAs, {error, not_implemented}}, + {noreply, State} + end; handle_info(Request, State) -> - ?LOG_WARNING("[Group Leader] Unexpected request", [Request]), - {noreply, State}. + ?LOG_WARNING("[Group Leader] Unexpected request", [Request]), + {noreply, State}. -spec terminate(any(), state()) -> ok. terminate(_Reason, #{caller := Caller, gl := GL} = _State) -> - group_leader(GL, Caller), - ok. + group_leader(GL, Caller), + ok. diff --git a/apps/els_lsp/src/els_group_leader_sup.erl b/apps/els_lsp/src/els_group_leader_sup.erl index 852ef463f..f5a8564c6 100644 --- a/apps/els_lsp/src/els_group_leader_sup.erl +++ b/apps/els_lsp/src/els_group_leader_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_group_leader_server - , start => {els_group_leader_server, start_link, []} - , restart => temporary - , shutdown => 5000 - }], - {ok, {SupFlags, ChildSpecs}}. + SupFlags = #{ + strategy => simple_one_for_one, + intensity => 5, + period => 60 + }, + ChildSpecs = [ + #{ + id => els_group_leader_server, + start => {els_group_leader_server, start_link, []}, + restart => temporary, + shutdown => 5000 + } + ], + {ok, {SupFlags, ChildSpecs}}. diff --git a/apps/els_lsp/src/els_hover_provider.erl b/apps/els_lsp/src/els_hover_provider.erl index b46b90b9c..491a411d1 100644 --- a/apps/els_lsp/src/els_hover_provider.erl +++ b/apps/els_lsp/src/els_hover_provider.erl @@ -5,65 +5,35 @@ -behaviour(els_provider). --export([ handle_info/2 - , handle_request/2 - , is_enabled/0 - , init/0 - , cancel_request/2 - ]). +-export([ + handle_request/1 +]). -include("els_lsp.hrl"). -include_lib("kernel/include/logger.hrl"). --define(SERVER, ?MODULE). - %%============================================================================== %% Types %%============================================================================== --type state() :: #{in_progress => [progress_entry()]}. --type progress_entry() :: {uri(), job()}. --type job() :: pid(). %%============================================================================== %% els_provider functions %%============================================================================== --spec is_enabled() -> boolean(). -is_enabled() -> - true. - --spec init() -> state(). -init() -> - #{ in_progress => []}. - --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({hover, Params}, State) -> - #{in_progress := InProgress} = State, - #{ <<"position">> := #{ <<"line">> := Line - , <<"character">> := Character - } - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - ?LOG_DEBUG("Starting hover job ""[uri=~p, line=~p, character=~p]" - , [Uri, Line, Character] - ), - Job = run_hover_job(Uri, Line, Character), - {Job, State#{in_progress => [{Uri, Job}|InProgress]}}. - - --spec handle_info(any(), state()) -> state(). -handle_info({result, HoverResp, Job}, State) -> - ?LOG_DEBUG("Received hover result [job=~p]", [Job]), - #{ in_progress := InProgress } = State, - els_server:send_response(Job, HoverResp), - State#{ in_progress => lists:keydelete(Job, 2, InProgress) }. - --spec cancel_request(job(), state()) -> state(). -cancel_request(Job, State) -> - ?LOG_DEBUG("Cancelling hover [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({hover, Params}) -> + #{ + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + ?LOG_DEBUG( + "Starting hover job " "[uri=~p, line=~p, character=~p]", + [Uri, Line, Character] + ), + Job = run_hover_job(Uri, Line, Character), + {async, Uri, Job}. %%============================================================================== %% Internal Functions @@ -71,32 +41,32 @@ cancel_request(Job, State) -> -spec run_hover_job(uri(), line(), column()) -> pid(). run_hover_job(Uri, Line, Character) -> - Config = #{ task => fun get_docs/2 - , entries => [{Uri, Line, Character}] - , title => <<"Hover">> - , on_complete => - fun(HoverResp) -> - ?SERVER ! {result, HoverResp, self()}, - ok - end - }, - {ok, Pid} = els_background_job:new(Config), - Pid. - --spec get_docs({uri(), integer(), integer()}, undefined) -> map() | null. -get_docs({Uri, Line, Character}, _) -> - {ok, Doc} = els_utils:lookup_document(Uri), - POIs = els_dt_document:get_element_at_pos(Doc, Line + 1, Character + 1), - do_get_docs(Uri, POIs). - - --spec do_get_docs(uri(), [poi()]) -> map() | null. -do_get_docs(_Uri, []) -> - null; -do_get_docs(Uri, [POI|Rest]) -> - case els_docs:docs(Uri, POI) of - [] -> - do_get_docs(Uri, Rest); - Entries -> - #{contents => els_markup_content:new(Entries)} - end. + {ok, Doc} = els_utils:lookup_document(Uri), + POIs = els_dt_document:get_element_at_pos(Doc, Line + 1, Character + 1), + Config = #{ + task => fun get_docs/2, + entries => [{Uri, POIs}], + title => <<"Hover">>, + on_complete => fun els_server:register_result/1 + }, + {ok, Pid} = els_background_job:new(Config), + Pid. + +-spec get_docs({uri(), [els_poi:poi()]}, undefined) -> map() | null. +get_docs({Uri, POIs}, _) -> + Pid = self(), + spawn(fun() -> do_get_docs(Uri, POIs, Pid) end), + receive + HoverResp -> HoverResp + end. + +-spec do_get_docs(uri(), [els_poi:poi()], pid()) -> map() | null. +do_get_docs(_Uri, [], Pid) -> + Pid ! null; +do_get_docs(Uri, [POI | Rest], Pid) -> + case els_docs:docs(Uri, POI) of + [] -> + do_get_docs(Uri, Rest, Pid); + Entries -> + Pid ! #{contents => els_markup_content:new(Entries)} + end. diff --git a/apps/els_lsp/src/els_implementation_provider.erl b/apps/els_lsp/src/els_implementation_provider.erl index 0b3234e55..0c667fe2b 100644 --- a/apps/els_lsp/src/els_implementation_provider.erl +++ b/apps/els_lsp/src/els_implementation_provider.erl @@ -4,110 +4,128 @@ -include("els_lsp.hrl"). --export([ handle_request/2 - , is_enabled/0 - ]). +-export([ + handle_request/1 +]). %%============================================================================== %% els_provider functions %%============================================================================== - --spec is_enabled() -> boolean(). -is_enabled() -> - true. - --spec handle_request(tuple(), els_provider:state()) -> - {[location()], els_provider:state()}. -handle_request({implementation, Params}, State) -> - #{ <<"position">> := #{ <<"line">> := Line - , <<"character">> := Character - } - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - {ok, Document} = els_utils:lookup_document(Uri), - Implementations = find_implementation(Document, Line, Character), - Locations = [#{uri => U, range => els_protocol:range(Range)} || - {U, #{range := Range}} <- Implementations], - {Locations, State}. +-spec handle_request(tuple()) -> {response, [location()]}. +handle_request({implementation, Params}) -> + #{ + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + {ok, Document} = els_utils:lookup_document(Uri), + Implementations = find_implementation(Document, Line, Character), + Locations = [ + #{uri => U, range => els_protocol:range(Range)} + || {U, #{range := Range}} <- Implementations + ], + {response, Locations}. %%============================================================================== %% Internal functions %%============================================================================== --spec find_implementation( els_dt_document:item() - , non_neg_integer() - , non_neg_integer() - ) -> [{uri(), poi()}]. +-spec find_implementation( + els_dt_document:item(), + non_neg_integer(), + non_neg_integer() +) -> [{uri(), els_poi:poi()}]. find_implementation(Document, Line, Character) -> - case els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1) of - [POI|_] -> implementation(Document, POI); - [] -> [] - end. + case els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1) of + [POI | _] -> implementation(Document, POI); + [] -> [] + end. --spec implementation(els_dt_document:item(), poi()) -> [{uri(), poi()}]. +-spec implementation(els_dt_document:item(), els_poi:poi()) -> [{uri(), els_poi:poi()}]. +implementation( + Document, + #{ + kind := Kind, + id := {_M, F, A}, + data := #{mod_is_variable := true} + } = POI +) when + Kind =:= application; + Kind =:= implicit_fun +-> + %% Try to handle Mod:function() by assuming it is a behaviour callback. + implementation(Document, POI#{kind => callback, id => {F, A}}); implementation(Document, #{kind := application, id := MFA}) -> - #{uri := Uri} = Document, - case callback(MFA) of - {CF, CA} -> - POIs = els_dt_document:pois(Document, [function]), - [{Uri, POI} || #{id := {F, A}} = POI <- POIs, F =:= CF, A =:= CA]; - undefined -> - [] - end; -implementation(Document, #{kind := callback, id := {CF, CA}}) -> - #{uri := Uri} = Document, - {ok, Refs} = els_dt_references:find_by_id(behaviour, els_uri:module(Uri)), - lists:flatmap( - fun(#{uri := U}) -> - case els_utils:lookup_document(U) of - {ok, D} -> - [{U, POI} || #{id := {F, A}} = POI - <- els_dt_document:pois(D, [function]), - F =:= CF, A =:= CA]; - {error, _Reason} -> + #{uri := Uri} = Document, + case callback(MFA) of + {CF, CA} -> + POIs = els_dt_document:pois(Document, [function]), + [{Uri, POI} || #{id := {F, A}} = POI <- POIs, F =:= CF, A =:= CA]; + undefined -> [] - end - end, Refs); + end; +implementation(Document, #{kind := callback, id := {CF, CA}}) -> + #{uri := Uri} = Document, + {ok, Refs} = els_dt_references:find_by_id(behaviour, els_uri:module(Uri)), + lists:flatmap( + fun(#{uri := U}) -> + case els_utils:lookup_document(U) of + {ok, D} -> + [ + {U, POI} + || #{id := {F, A}} = POI <- + els_dt_document:pois(D, [function]), + F =:= CF, + A =:= CA + ]; + {error, _Reason} -> + [] + end + end, + Refs + ); implementation(_Document, _POI) -> - []. + []. -spec callback(mfa()) -> {atom(), non_neg_integer()} | undefined. %% gen_event -callback({gen_event, add_handler, 3}) -> {init, 1}; +callback({gen_event, add_handler, 3}) -> {init, 1}; callback({gen_event, add_sup_handler, 3}) -> {init, 1}; -callback({gen_event, call, 3}) -> {handle_call, 2}; -callback({gen_event, call, 4}) -> {handle_call, 2}; -callback({gen_event, delete_handler, 1}) -> {terminate, 2}; -callback({gen_event, notify, 2}) -> {handle_event, 2}; -callback({gen_event, sync_notify, 2}) -> {handle_event, 2}; -callback({gen_event, stop, 1}) -> {terminate, 2}; -callback({gen_event, stop, 3}) -> {terminate, 2}; +callback({gen_event, call, 3}) -> {handle_call, 2}; +callback({gen_event, call, 4}) -> {handle_call, 2}; +callback({gen_event, delete_handler, 1}) -> {terminate, 2}; +callback({gen_event, notify, 2}) -> {handle_event, 2}; +callback({gen_event, sync_notify, 2}) -> {handle_event, 2}; +callback({gen_event, stop, 1}) -> {terminate, 2}; +callback({gen_event, stop, 3}) -> {terminate, 2}; %% gen_server -callback({gen_server, abcast, 2}) -> {handle_cast, 2}; -callback({gen_server, abcast, 3}) -> {handle_cast, 2}; -callback({gen_server, call, 2}) -> {handle_call, 3}; -callback({gen_server, call, 3}) -> {handle_call, 3}; -callback({gen_server, cast, 2}) -> {handle_cast, 2}; -callback({gen_server, multi_call, 2}) -> {handle_call, 3}; -callback({gen_server, multi_call, 3}) -> {handle_call, 3}; -callback({gen_server, multi_call, 4}) -> {handle_call, 3}; -callback({gen_server, start, 3}) -> {init, 1}; -callback({gen_server, start, 4}) -> {init, 1}; -callback({gen_server, start_link, 3}) -> {init, 1}; -callback({gen_server, start_link, 4}) -> {init, 1}; -callback({gen_server, stop, 1}) -> {terminate, 2}; -callback({gen_server, stop, 3}) -> {terminate, 2}; +callback({gen_server, abcast, 2}) -> {handle_cast, 2}; +callback({gen_server, abcast, 3}) -> {handle_cast, 2}; +callback({gen_server, call, 2}) -> {handle_call, 3}; +callback({gen_server, call, 3}) -> {handle_call, 3}; +callback({gen_server, cast, 2}) -> {handle_cast, 2}; +callback({gen_server, multi_call, 2}) -> {handle_call, 3}; +callback({gen_server, multi_call, 3}) -> {handle_call, 3}; +callback({gen_server, multi_call, 4}) -> {handle_call, 3}; +callback({gen_server, start, 3}) -> {init, 1}; +callback({gen_server, start, 4}) -> {init, 1}; +callback({gen_server, start_link, 3}) -> {init, 1}; +callback({gen_server, start_link, 4}) -> {init, 1}; +callback({gen_server, stop, 1}) -> {terminate, 2}; +callback({gen_server, stop, 3}) -> {terminate, 2}; %% gen_statem -callback({gen_statem, call, 2}) -> {handle_event, 4}; -callback({gen_statem, call, 3}) -> {handle_event, 4}; -callback({gen_statem, cast, 2}) -> {handle_event, 4}; -callback({gen_statem, start, 3}) -> {init, 1}; -callback({gen_statem, start, 4}) -> {init, 1}; -callback({gen_statem, start_link, 3}) -> {init, 1}; -callback({gen_statem, start_link, 4}) -> {init, 1}; -callback({gen_statem, stop, 1}) -> {terminate, 3}; -callback({gen_statem, stop, 3}) -> {terminate, 3}; +callback({gen_statem, call, 2}) -> {handle_event, 4}; +callback({gen_statem, call, 3}) -> {handle_event, 4}; +callback({gen_statem, cast, 2}) -> {handle_event, 4}; +callback({gen_statem, start, 3}) -> {init, 1}; +callback({gen_statem, start, 4}) -> {init, 1}; +callback({gen_statem, start_link, 3}) -> {init, 1}; +callback({gen_statem, start_link, 4}) -> {init, 1}; +callback({gen_statem, stop, 1}) -> {terminate, 3}; +callback({gen_statem, stop, 3}) -> {terminate, 3}; %% supervisor -callback({supervisor, start_link, 2}) -> {init, 1}; -callback({supervisor, start_link, 3}) -> {init, 1}; +callback({supervisor, start_link, 2}) -> {init, 1}; +callback({supervisor, start_link, 3}) -> {init, 1}; %% Everything else -callback(_) -> undefined. +callback(_) -> undefined. diff --git a/apps/els_lsp/src/els_include_paths.erl b/apps/els_lsp/src/els_include_paths.erl new file mode 100644 index 000000000..a64061379 --- /dev/null +++ b/apps/els_lsp/src/els_include_paths.erl @@ -0,0 +1,108 @@ +-module(els_include_paths). +-export([includes/1]). +-export([include_libs/0]). +-export([include_libs/1]). + +-include_lib("els_core/include/els_core.hrl"). + +-spec includes(els_dt_document:item()) -> [binary()]. +includes(#{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 include_libs() -> [binary()]. +include_libs() -> + {ok, Headers} = els_dt_document_index:find_by_kind(header), + Uris = [Uri || #{uri := Uri} <- Headers], + include_libs(Uris). + +-spec include_libs([uri()]) -> [binary()]. +include_libs(Uris) -> + Paths = + els_config:get(otp_paths) ++ + els_config:get(deps_paths) ++ + els_config:get(apps_paths) ++ + els_config:get(include_paths), + lists:flatmap( + fun(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, + Uris + ). + +-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. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +strip_app_version_test() -> + ?assertEqual(<<"foo">>, strip_app_version(<<"foo">>)), + ?assertEqual(<<"foo">>, strip_app_version(<<"foo-1.2.3">>)), + ?assertEqual(<<"">>, strip_app_version(<<"">>)), + ?assertEqual(<<"foo-bar">>, strip_app_version(<<"foo-bar">>)), + ?assertEqual(<<"foo-bar">>, strip_app_version(<<"foo-bar-1.2.3">>)), + ?assertEqual(<<"foo-bar-baz">>, strip_app_version(<<"foo-bar-baz">>)), + ?assertEqual(<<"foo-bar-baz">>, strip_app_version(<<"foo-bar-baz-1.2.3">>)). + +-endif. diff --git a/apps/els_lsp/src/els_incomplete_parser.erl b/apps/els_lsp/src/els_incomplete_parser.erl new file mode 100644 index 000000000..f2981a76a --- /dev/null +++ b/apps/els_lsp/src/els_incomplete_parser.erl @@ -0,0 +1,28 @@ +-module(els_incomplete_parser). +-export([parse_after/2]). +-export([parse_line/2]). + +-spec parse_after(binary(), integer()) -> [els_poi:poi()]. +parse_after(Text, Line) -> + {_, AfterText} = els_text:split_at_line(Text, Line), + {ok, POIs} = els_parser:parse(AfterText), + POIs. + +-spec parse_line(binary(), integer()) -> [els_poi:poi()]. +parse_line(Text, Line) -> + LineText0 = string:trim(els_text:line(Text, Line), trailing, ",;"), + case els_parser:parse(LineText0) of + {ok, []} -> + LineStr = els_utils:to_list(LineText0), + case lists:reverse(LineStr) of + %% Kludge to parse "case foo() of" + "fo " ++ _ -> + LineText1 = <<LineText0/binary, " _ -> _ end">>, + {ok, POIs} = els_parser:parse(LineText1), + POIs; + _ -> + [] + end; + {ok, POIs} -> + POIs + end. diff --git a/apps/els_lsp/src/els_index_buffer.erl b/apps/els_lsp/src/els_index_buffer.erl deleted file mode 100644 index d97adf8e1..000000000 --- a/apps/els_lsp/src/els_index_buffer.erl +++ /dev/null @@ -1,110 +0,0 @@ -%% @doc Buffer textedits to avoid spamming indexing for every keystroke -%% -%% FIXME: Currently implemented as a simple process. -%% Might be nicer to rewrite this as a gen_server -%% FIXME: Edits are bottlenecked by this single process, so this could slow -%% down indexing when making changes in multiple files, this could be -%% mitigated by using a worker pool or spawning a process per Uri. --module(els_index_buffer). - --export([ start/0 - , stop/0 - , apply_edits_async/2 - , flush/1 - ]). - --include_lib("kernel/include/logger.hrl"). --include("els_lsp.hrl"). - --define(SERVER, ?MODULE). - --spec start() -> {ok, pid()} | {error, _}. -start() -> - case whereis(?SERVER) of - undefined -> - Pid = proc_lib:spawn_link(fun loop/0), - true = erlang:register(?SERVER, Pid), - ?LOG_INFO("[~p] Started.", [?SERVER]), - {ok, Pid}; - Pid -> - {error, {already_started, Pid}} - end. - --spec stop() -> ok. -stop() -> - ?SERVER ! stop, - erlang:unregister(?SERVER), - ?LOG_INFO("[~p] Stopped.", [?SERVER]), - ok. - --spec apply_edits_async(uri(), [els_text:edit()]) -> ok. -apply_edits_async(Uri, Edits) -> - ?SERVER ! {apply_edits, Uri, Edits}, - ok. - --spec flush(uri()) -> ok. -flush(Uri) -> - Ref = make_ref(), - ?SERVER ! {flush, self(), Ref, Uri}, - receive - {Ref, done} -> - ok - end. - --spec loop() -> ok. -loop() -> - receive - stop -> ok; - {apply_edits, Uri, Edits} -> - try - do_apply_edits(Uri, Edits) - catch E:R:St -> - ?LOG_ERROR("[~p] Crashed while applying edits ~p: ~p", - [?SERVER, Uri, {E, R, St}]) - end, - loop(); - {flush, Pid, Ref, Uri} -> - try - do_flush(Uri) - catch E:R:St -> - ?LOG_ERROR("[~p] Crashed while flushing ~p: ~p", - [?SERVER, Uri, {E, R, St}]) - end, - Pid ! {Ref, done}, - loop() - end. - --spec do_apply_edits(uri(), [els_text:edit()]) -> ok. -do_apply_edits(Uri, Edits0) -> - ?LOG_DEBUG("[~p] Processing index request for ~p", [?SERVER, Uri]), - {ok, [#{text := Text0}]} = els_dt_document:lookup(Uri), - ?LOG_DEBUG("[~p] Apply edits: ~p", [?SERVER, Edits0]), - Text1 = els_text:apply_edits(Text0, Edits0), - Text = receive_all(Uri, Text1), - ?LOG_DEBUG("[~p] Started indexing ~p", [?SERVER, Uri]), - {Duration, ok} = timer:tc(fun() -> els_indexing:index(Uri, Text, 'deep') end), - ?LOG_DEBUG("[~p] Done indexing ~p [duration: ~pms]", - [?SERVER, Uri, Duration div 1000]), - ok. - --spec do_flush(uri()) -> ok. -do_flush(Uri) -> - ?LOG_DEBUG("[~p] Flushing ~p", [?SERVER, Uri]), - {ok, [#{text := Text0}]} = els_dt_document:lookup(Uri), - Text = receive_all(Uri, Text0), - {Duration, ok} = timer:tc(fun() -> els_indexing:index(Uri, Text, 'deep') end), - ?LOG_DEBUG("[~p] Done flushing ~p [duration: ~pms]", - [?SERVER, Uri, Duration div 1000]), - ok. - --spec receive_all(uri(), binary()) -> binary(). -receive_all(Uri, Text0) -> - receive - {apply_edits, Uri, Edits} -> - ?LOG_DEBUG("[~p] Received more edits ~p.", [?SERVER, Uri]), - ?LOG_DEBUG("[~p] Apply edits: ~p", [?SERVER, Edits]), - Text = els_text:apply_edits(Text0, Edits), - receive_all(Uri, Text) - after - 0 -> Text0 - end. diff --git a/apps/els_lsp/src/els_indexing.erl b/apps/els_lsp/src/els_indexing.erl index 0b3ece720..99eca4ee1 100644 --- a/apps/els_lsp/src/els_indexing.erl +++ b/apps/els_lsp/src/els_indexing.erl @@ -3,13 +3,17 @@ -callback index(els_dt_document:item()) -> ok. %% API --export([ find_and_index_file/1 - , index_file/1 - , index/3 - , index_dir/2 - , start/0 - , maybe_start/0 - ]). +-export([ + find_and_deeply_index_file/1, + index_dir/2, + start/0, + maybe_start/0, + ensure_deeply_indexed/1, + shallow_index/2, + shallow_index/3, + deep_index/2, + remove/1 +]). %%============================================================================== %% Includes @@ -20,200 +24,319 @@ %%============================================================================== %% Types %%============================================================================== --type mode() :: 'deep' | 'shallow'. - -%%============================================================================== -%% Macros -%%============================================================================== --define(SERVER, ?MODULE). +-type version() :: null | integer(). %%============================================================================== %% Exported functions %%============================================================================== --spec find_and_index_file(string()) -> - {ok, uri()} | {error, any()}. -find_and_index_file(FileName) -> - SearchPaths = els_config:get(search_paths), - case file:path_open( SearchPaths - , els_utils:to_binary(FileName) - , [read] - ) - of - {ok, IoDevice, FullName} -> - %% TODO: Avoid opening file twice - file:close(IoDevice), - index_file(FullName); - {error, Error} -> - {error, Error} - end. - --spec index_file(binary()) -> {ok, uri()}. -index_file(Path) -> - try_index_file(Path, 'deep'), - {ok, els_uri:uri(Path)}. - --spec index(uri(), binary(), mode()) -> ok. -index(Uri, Text, Mode) -> - MD5 = erlang:md5(Text), - case els_dt_document:lookup(Uri) of - {ok, [#{md5 := MD5}]} -> - ok; - {ok, LookupResult} -> - Document = els_dt_document:new(Uri, Text), - do_index(Document, Mode, LookupResult =/= []) - end. - --spec do_index(els_dt_document:item(), mode(), boolean()) -> ok. -do_index(#{uri := Uri, id := Id, kind := Kind} = Document, Mode, Reset) -> - case Mode of - 'deep' -> - ok = els_dt_document:insert(Document); - 'shallow' -> - %% Don't store detailed POIs when "shallow" indexing. - %% They will be reloaded and inserted when needed - %% by calling els_utils:lookup_document/1 - ok - end, - %% Mapping from document id to uri - ModuleItem = els_dt_document_index:new(Id, Uri, Kind), - ok = els_dt_document_index:insert(ModuleItem), - index_signatures(Document), - index_references(Document, Mode, Reset). - --spec index_signatures(els_dt_document:item()) -> ok. -index_signatures(#{id := Id, text := Text} = Document) -> - Specs = els_dt_document:pois(Document, [spec]), - [ begin - #{from := From, to := To} = Range, - Spec = els_text:range(Text, From, To), - els_dt_signatures:insert(#{ mfa => {Id, F, A} , spec => Spec}) - end - || #{id := {F, A}, range := Range} <- Specs - ], - ok. - --spec index_references(els_dt_document:item(), mode(), boolean()) -> ok. -index_references(#{uri := Uri} = Document, 'deep', true) -> - %% Optimization to only do (non-optimized) match_delete when necessary - ok = els_dt_references:delete_by_uri(Uri), - index_references(Document, 'deep', false); -index_references(#{uri := Uri} = Document, 'deep', false) -> - %% References - POIs = els_dt_document:pois(Document, [ application - , behaviour - , implicit_fun - , include - , include_lib - , type_application - , import_entry - ]), - [register_reference(Uri, POI) || POI <- POIs], - ok; -index_references(_Document, 'shallow', _) -> - ok. +-spec find_and_deeply_index_file(string()) -> + {ok, uri()} | {error, any()}. +find_and_deeply_index_file(FileName) -> + case get({?MODULE, find_and_deep_index_file, FileName}) of + undefined -> + SearchPaths = els_config:get(search_paths), + case + file:path_open( + SearchPaths, + els_utils:to_binary(FileName), + [read] + ) + of + {ok, IoDevice, Path} -> + %% TODO: Avoid opening file twice + file:close(IoDevice), + Uri = els_uri:uri(Path), + ensure_deeply_indexed(Uri), + {ok, Uri}; + {error, _} = Error -> + %% Optimization! + %% If we can't find the file, then memoize the error result + %% by storing it in the process dictionary as we most + %% likely won't find it if we try again. + put({?MODULE, find_and_deeply_index_file, FileName}, Error), + Error + end; + {error, _} = Error -> + Error + end. + +-spec is_generated_file(binary(), string()) -> boolean(). +is_generated_file(Text, Tag) -> + [Line | _] = string:split(Text, "\n", leading), + case re:run(Line, Tag) of + {match, _} -> + true; + nomatch -> + false + end. + +-spec ensure_deeply_indexed(uri()) -> els_dt_document:item(). +ensure_deeply_indexed(Uri) -> + {ok, #{pois := POIs} = Document} = els_utils:lookup_document(Uri), + case POIs of + ondemand -> + deep_index(Document, _UpdateWords = true); + _ -> + Document + end. + +-spec deep_index(els_dt_document:item(), boolean()) -> els_dt_document:item(). +deep_index(Document0, UpdateWords) -> + #{ + id := Id, + uri := Uri, + text := Text, + source := Source, + version := Version + } = Document0, + {ok, POIs} = els_parser:parse(Text), + Document = + case UpdateWords of + true -> + Words = els_dt_document:get_words(Text), + Document0#{pois => POIs, words => Words}; + false -> + Document0#{pois => POIs} + end, + case els_dt_document:versioned_insert(Document) of + ok -> + index_functions(Id, Uri, POIs, Version), + index_signatures(Id, Uri, Text, POIs, Version), + case Source of + otp -> + ok; + S when S =:= app orelse S =:= dep -> + index_references(Id, Uri, POIs, Version) + end; + {error, condition_not_satisfied} -> + ?LOG_DEBUG("Skip indexing old version [uri=~p]", [Uri]), + ok + end, + Document. + +-spec index_signatures(atom(), uri(), binary(), [els_poi:poi()], version()) -> ok. +index_signatures(Id, Uri, Text, POIs, Version) -> + ok = els_dt_signatures:versioned_delete_by_uri(Uri, Version), + [index_signature(Id, Text, POI, Version) || #{kind := spec} = POI <- POIs], + ok. + +-spec index_signature(atom(), binary(), els_poi:poi(), version()) -> ok. +index_signature(_M, _Text, #{id := undefined}, _Version) -> + ok; +index_signature(M, Text, #{id := {F, A}, range := Range, data := #{args := Args}}, Version) -> + #{from := From, to := To} = Range, + Spec = els_text:range(Text, From, To), + els_dt_signatures:versioned_insert(#{ + mfa => {M, F, A}, + spec => Spec, + version => Version, + args => Args + }). + +-spec index_functions(atom(), uri(), [els_poi:poi()], version()) -> ok. +index_functions(M, Uri, POIs, Version) -> + ok = els_dt_functions:versioned_delete_by_uri(Uri, Version), + [index_function(M, POI, Version) || #{kind := function} = POI <- POIs], + ok. + +-spec index_function(atom(), els_poi:poi(), version()) -> ok. +index_function(M, #{id := {F, A}}, Version) -> + els_dt_functions:versioned_insert(#{ + mfa => {M, F, A}, + version => Version + }). + +-spec index_references(atom(), uri(), [els_poi:poi()], version()) -> ok. +index_references(Id, Uri, POIs, Version) -> + ok = els_dt_references:versioned_delete_by_uri(Uri, Version), + %% Function + ReferenceKinds = [ + application, + implicit_fun, + import_entry, + %% Include + include, + include_lib, + %% Behaviour + behaviour, + %% Type + type_application, + %% Atom + atom, + %% Macro + macro, + %% Record + record_expr, + record_field + ], + [ + index_reference(Id, Uri, POI, Version) + || #{kind := Kind} = POI <- POIs, + lists:member(Kind, ReferenceKinds) + ], + ok. + +-spec index_reference(atom(), uri(), els_poi:poi(), version()) -> ok. +index_reference(M, Uri, #{kind := Kind, id := {F, A}} = POI, Version) when + Kind =/= macro, + Kind =/= record_field +-> + index_reference(M, Uri, POI#{id => {M, F, A}}, Version); +index_reference(_M, Uri, #{kind := Kind, id := Id, range := Range}, Version) -> + els_dt_references:versioned_insert(Kind, #{ + id => Id, + uri => Uri, + range => Range, + version => Version + }). + +-spec shallow_index(binary(), els_dt_document:source()) -> {ok, uri()}. +shallow_index(Path, Source) -> + Uri = els_uri:uri(Path), + {ok, Text} = file:read_file(Path), + shallow_index(Uri, Text, Source), + {ok, Uri}. + +-spec shallow_index(uri(), binary(), els_dt_document:source()) -> ok. +shallow_index(Uri, Text, Source) -> + Document = els_dt_document:new(Uri, Text, Source), + case els_dt_document:versioned_insert(Document) of + ok -> + #{id := Id, kind := Kind} = Document, + ModuleItem = els_dt_document_index:new(Id, Uri, Kind), + ok = els_dt_document_index:insert(ModuleItem); + {error, condition_not_satisfied} -> + ?LOG_DEBUG("Skip indexing old version [uri=~p]", [Uri]), + ok + end. -spec maybe_start() -> true | false. maybe_start() -> - IndexingEnabled = els_config:get(indexing_enabled), - case IndexingEnabled of - true -> - start(); - false -> - ?LOG_INFO("Skipping Indexing (disabled via InitOptions)") - end, - IndexingEnabled. + IndexingEnabled = els_config:get(indexing_enabled), + case IndexingEnabled of + true -> + start(); + false -> + ?LOG_INFO("Skipping Indexing (disabled via InitOptions)") + end, + IndexingEnabled. -spec start() -> ok. start() -> - start(<<"OTP">>, entries_otp()), - start(<<"Applications">>, entries_apps()), - start(<<"Dependencies">>, entries_deps()). - --spec start(binary(), [{string(), 'deep' | 'shallow'}]) -> ok. -start(Group, Entries) -> - Task = fun({Dir, Mode}, _) -> index_dir(Dir, Mode) end, - Config = #{ task => Task - , entries => Entries - , title => <<"Indexing ", Group/binary>> - }, - {ok, _Pid} = els_background_job:new(Config), - ok. + Skip = els_config_indexing:get_skip_generated_files(), + SkipTag = els_config_indexing:get_generated_files_tag(), + ?LOG_INFO("Start indexing. [skip=~p] [skip_tag=~p]", [Skip, SkipTag]), + start(<<"OTP">>, Skip, SkipTag, els_config:get(otp_paths), otp), + start(<<"Applications">>, Skip, SkipTag, els_config:get(apps_paths), app), + start(<<"Dependencies">>, Skip, SkipTag, els_config:get(deps_paths), dep). + +-spec start( + binary(), + boolean(), + string(), + [string()], + els_dt_document:source() +) -> ok. +start(Group, Skip, SkipTag, Entries, Source) -> + Task = fun(Dir, {Succeeded0, Skipped0, Failed0}) -> + {Su, Sk, Fa} = index_dir(Dir, Skip, SkipTag, Source), + {Succeeded0 + Su, Skipped0 + Sk, Failed0 + Fa} + end, + Start = erlang:monotonic_time(millisecond), + Config = #{ + task => Task, + entries => Entries, + title => <<"Indexing ", Group/binary>>, + initial_state => {0, 0, 0}, + on_complete => + fun({Succeeded, Skipped, Failed}) -> + End = erlang:monotonic_time(millisecond), + Duration = End - Start, + Event = #{ + group => Group, + duration_ms => Duration, + succeeded => Succeeded, + skipped => Skipped, + failed => Failed, + type => <<"indexing">> + }, + ?LOG_INFO( + "Completed indexing for ~s " + "(succeeded: ~p, skipped: ~p, failed: ~p, duration: ~p ms)", + [Group, Succeeded, Skipped, Failed, Duration] + ), + els_telemetry:send_notification(Event) + end + }, + {ok, _Pid} = els_background_job:new(Config), + ok. + +-spec remove(uri()) -> ok. +remove(Uri) -> + ok = els_dt_document:delete(Uri), + ok = els_dt_document_index:delete_by_uri(Uri), + ok = els_dt_references:delete_by_uri(Uri), + ok = els_dt_signatures:delete_by_uri(Uri), + ok = els_dt_functions:delete_by_uri(Uri). %%============================================================================== %% Internal functions %%============================================================================== -%% @doc Try indexing a file. --spec try_index_file(binary(), mode()) -> ok | {error, any()}. -try_index_file(FullName, Mode) -> - Uri = els_uri:uri(FullName), - try - ?LOG_DEBUG("Indexing file. [filename=~s, uri=~s]", [FullName, Uri]), +-spec shallow_index(binary(), boolean(), string(), els_dt_document:source()) -> + ok | skipped. +shallow_index(FullName, SkipGeneratedFiles, GeneratedFilesTag, Source) -> + Uri = els_uri:uri(FullName), + ?LOG_DEBUG( + "Shallow indexing file. [filename=~s] [uri=~s]", + [FullName, Uri] + ), {ok, Text} = file:read_file(FullName), - ok = index(Uri, Text, Mode) - catch Type:Reason:St -> - ?LOG_ERROR("Error indexing file " - "[filename=~s, uri=~s] " - "~p:~p:~p", [FullName, Uri, Type, Reason, St]), - {error, {Type, Reason}} - end. - --spec register_reference(uri(), poi()) -> ok. -register_reference(Uri, #{id := {F, A}} = POI) -> - M = els_uri:module(Uri), - register_reference(Uri, POI#{id => {M, F, A}}); -register_reference(Uri, #{kind := Kind, id := Id, range := Range}) - when %% Include - Kind =:= include; - Kind =:= include_lib; - %% Function - Kind =:= application; - Kind =:= implicit_fun; - Kind =:= import_entry; - %% Type - Kind =:= type_application; - %% Behaviour - Kind =:= behaviour -> - els_dt_references:insert( Kind - , #{id => Id, uri => Uri, range => Range} - ). - --spec index_dir(string(), mode()) -> {non_neg_integer(), non_neg_integer()}. -index_dir(Dir, Mode) -> - ?LOG_DEBUG("Indexing directory. [dir=~s] [mode=~s]", [Dir, Mode]), - F = fun(FileName, {Succeeded, Failed}) -> - case try_index_file(els_utils:to_binary(FileName), Mode) of - ok -> {Succeeded + 1, Failed}; - {error, _Error} -> {Succeeded, Failed + 1} - end - end, - Filter = fun(Path) -> - Ext = filename:extension(Path), - lists:member(Ext, [".erl", ".hrl", ".escript"]) - end, - - {Time, {Succeeded, Failed}} = timer:tc( els_utils - , fold_files - , [ F - , Filter - , Dir - , {0, 0} - ] - ), - ?LOG_DEBUG("Finished indexing directory. [dir=~s] [mode=~s] [time=~p] " - "[succeeded=~p] " - "[failed=~p]", [Dir, Mode, Time/1000/1000, Succeeded, Failed]), - {Succeeded, Failed}. - --spec entries_apps() -> [{string(), 'deep' | 'shallow'}]. -entries_apps() -> - [{Dir, 'deep'} || Dir <- els_config:get(apps_paths)]. - --spec entries_deps() -> [{string(), 'deep' | 'shallow'}]. -entries_deps() -> - [{Dir, 'deep'} || Dir <- els_config:get(deps_paths)]. - --spec entries_otp() -> [{string(), 'deep' | 'shallow'}]. -entries_otp() -> - [{Dir, 'shallow'} || Dir <- els_config:get(otp_paths)]. + case SkipGeneratedFiles andalso is_generated_file(Text, GeneratedFilesTag) of + true -> + ?LOG_DEBUG("Skip indexing for generated file ~p", [Uri]), + skipped; + false -> + shallow_index(Uri, Text, Source) + end. + +-spec index_dir(string(), els_dt_document:source()) -> + {non_neg_integer(), non_neg_integer(), non_neg_integer()}. +index_dir(Dir, Source) -> + Skip = els_config_indexing:get_skip_generated_files(), + SkipTag = els_config_indexing:get_generated_files_tag(), + index_dir(Dir, Skip, SkipTag, Source). + +-spec index_dir(string(), boolean(), string(), els_dt_document:source()) -> + {non_neg_integer(), non_neg_integer(), non_neg_integer()}. +index_dir(Dir, Skip, SkipTag, Source) -> + ?LOG_DEBUG("Indexing directory. [dir=~s]", [Dir]), + F = fun(FileName, {Succeeded, Skipped, Failed}) -> + BinaryName = els_utils:to_binary(FileName), + case shallow_index(BinaryName, Skip, SkipTag, Source) of + ok -> {Succeeded + 1, Skipped, Failed}; + skipped -> {Succeeded, Skipped + 1, Failed} + end + end, + Filter = fun(Path) -> + Ext = filename:extension(Path), + lists:member(Ext, [".erl", ".hrl", ".escript"]) + end, + + {Time, {Succeeded, Skipped, Failed}} = timer:tc( + els_utils, + fold_files, + [ + F, + Filter, + Dir, + {0, 0, 0} + ] + ), + ?LOG_DEBUG( + "Finished indexing directory. [dir=~s] [time=~p] " + "[succeeded=~p] [skipped=~p] [failed=~p]", + [Dir, Time / 1000 / 1000, Succeeded, Skipped, Failed] + ), + {Succeeded, Skipped, Failed}. diff --git a/apps/els_lsp/src/els_inlay_hint_provider.erl b/apps/els_lsp/src/els_inlay_hint_provider.erl new file mode 100644 index 000000000..76a82ea03 --- /dev/null +++ b/apps/els_lsp/src/els_inlay_hint_provider.erl @@ -0,0 +1,180 @@ +-module(els_inlay_hint_provider). + +-behaviour(els_provider). + +-export([ + handle_request/1, + options/0 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include("els_lsp.hrl"). +-include_lib("els_core/include/els_core.hrl"). +-include_lib("kernel/include/logger.hrl"). + +%%============================================================================== +%% els_provider functions +%%============================================================================== +-spec handle_request(any()) -> {async, uri(), pid()}. +handle_request({inlay_hint, Params}) -> + #{ + <<"range">> := Range, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + ?LOG_DEBUG( + "Inlay hint provider was called with params: ~p", + [Params] + ), + PoiRange = els_range:to_poi_range(Range), + {ok, Job} = run_inlay_hints_job(Uri, PoiRange), + {async, Uri, Job}. + +-spec options() -> boolean(). +options() -> + els_config:get(inlay_hints_enabled). + +%%============================================================================== +%% Internal functions +%%============================================================================== +-spec run_inlay_hints_job(uri(), els_poi:poi_range()) -> {ok, pid()}. +run_inlay_hints_job(Uri, Range) -> + Config = #{ + task => fun get_inlay_hints/2, + entries => [{Uri, Range}], + title => <<"Inlay Hints">>, + on_complete => fun els_server:register_result/1 + }, + els_background_job:new(Config). + +-spec get_inlay_hints({uri(), els_poi:poi_range()}, any()) -> + [inlay_hint()]. +get_inlay_hints({Uri, Range}, _) -> + %% Wait for indexing job to finish, so that we have the updated document + wait_for_indexing_job(Uri), + %% Read the document to get the latest version + TS = erlang:timestamp(), + {ok, [Document]} = els_dt_document:lookup(Uri), + %% Fetch all application POIs that are in the given range + AppPOIs = els_dt_document:pois_in_range(Document, [application], Range), + ArgHints = lists:flatmap(fun(POI) -> arg_hints(Uri, POI) end, AppPOIs), + + %% Fetch all function_clause POIs that are in the given range + FunPOIs = els_dt_document:pois_in_range(Document, [function_clause], Range), + %% Fetch all export entries + ExportPOIs = els_dt_document:pois(Document, [export_entry]), + FunHints = lists:flatmap(fun(POI) -> fun_hints(POI, ExportPOIs) end, FunPOIs), + + ?LOG_DEBUG( + "Inlay hints took ~p ms", + [timer:now_diff(erlang:timestamp(), TS) div 1000] + ), + ArgHints ++ FunHints. + +%% @doc Output inlay hints for a function clause, +%% indicate if a function is exported or not +-spec fun_hints(els_poi:poi(), [els_poi:poi()]) -> [inlay_hint()]. +fun_hints(#{id := {F, A, _}, range := Range}, ExportPOIs) -> + case lists:any(fun(#{id := Id}) -> Id == {F, A} end, ExportPOIs) of + true -> + [fun_exported_hint(Range)]; + false -> + [] + end. + +-spec fun_exported_hint(els_poi:poi_range()) -> inlay_hint(). +fun_exported_hint(#{from := {FromL, FromC}}) -> + #{ + position => #{line => FromL - 1, character => FromC - 1}, + label => unicode:characters_to_binary("exp"), + paddingRight => true, + kind => ?INLAY_HINT_KIND_TYPE + }. + +-spec arg_hints(uri(), els_poi:poi()) -> [inlay_hint()]. +arg_hints(Uri, #{kind := application, data := #{args := CallArgs}} = POI) -> + lists:flatmap( + fun(#{index := N, range := ArgRange, name := Name}) -> + case els_code_navigation:goto_definition(Uri, POI) of + {ok, [{DefUri, DefPOI} | _]} -> + DefArgs = els_arg:get_args(DefUri, DefPOI), + DefArgName = arg_name(N, DefArgs), + case should_show_arg_hint(Name, DefArgName) of + true -> + [arg_hint(ArgRange, DefArgName)]; + false -> + [] + end; + {error, _} -> + [] + end + end, + CallArgs + ); +arg_hints(_Uri, _POI) -> + []. + +-spec arg_hint(els_poi:poi_range(), string()) -> inlay_hint(). +arg_hint(#{from := {FromL, FromC}}, ArgName) -> + #{ + position => #{line => FromL - 1, character => FromC - 1}, + label => unicode:characters_to_binary( + remove_leading_underscore(ArgName) ++ ":" + ), + paddingRight => true, + kind => ?INLAY_HINT_KIND_PARAMETER + }. + +-spec should_show_arg_hint( + string() | undefined, + string() | undefined +) -> + boolean(). +should_show_arg_hint(Name, Name) -> + false; +should_show_arg_hint(_Name, undefined) -> + false; +should_show_arg_hint(undefined, _Name) -> + true; +should_show_arg_hint(Name, DefArgName) -> + normalize(Name) /= normalize(DefArgName). + +-spec normalize(string()) -> string(). +normalize(String) -> + remove_leading_underscore( + strip_trailing_digits(String) + ). + +-spec remove_leading_underscore(string()) -> string(). +remove_leading_underscore(String) -> + string:trim(String, leading, "_"). + +-spec strip_trailing_digits(string()) -> string(). +strip_trailing_digits(String) -> + string:trim(String, trailing, "0123456789"). + +-spec wait_for_indexing_job(uri()) -> ok. +wait_for_indexing_job(Uri) -> + %% Add delay to allowing indexing job to start + timer:sleep(10), + JobTitles = els_background_job:list_titles(), + case lists:member(<<"Indexing ", Uri/binary>>, JobTitles) of + false -> + %% No indexing job is running, we're ready! + ok; + true -> + %% Indexing job is still running, retry until it finishes + wait_for_indexing_job(Uri) + end. + +-spec arg_name(non_neg_integer(), els_arg:args()) -> string() | undefined. +arg_name(_N, []) -> + undefined; +arg_name(N, Args) -> + case lists:nth(N, Args) of + #{name := "_" ++ Name} -> + Name; + #{name := Name} -> + Name + end. diff --git a/apps/els_lsp/src/els_log_notification.erl b/apps/els_lsp/src/els_log_notification.erl index 1a4572b9e..8bdf08fe8 100644 --- a/apps/els_lsp/src/els_log_notification.erl +++ b/apps/els_lsp/src/els_log_notification.erl @@ -9,16 +9,19 @@ -define(LSP_MESSAGE_TYPE_INFO, 3). -define(LSP_MESSAGE_TYPE_LOG, 4). --type lsp_message_type() :: ?LSP_MESSAGE_TYPE_ERROR | - ?LSP_MESSAGE_TYPE_WARNING | - ?LSP_MESSAGE_TYPE_INFO | - ?LSP_MESSAGE_TYPE_LOG. +-type lsp_message_type() :: + ?LSP_MESSAGE_TYPE_ERROR + | ?LSP_MESSAGE_TYPE_WARNING + | ?LSP_MESSAGE_TYPE_INFO + | ?LSP_MESSAGE_TYPE_LOG. -spec log(logger:log_event(), logger:config_handler()) -> ok. log(#{level := Level} = LogEvent, _Config) -> try - Msg = logger_formatter:format( LogEvent - , #{ template => ?LSP_LOG_FORMAT}), + Msg = logger_formatter:format( + LogEvent, + #{template => ?LSP_LOG_FORMAT} + ), els_server:send_notification(<<"window/logMessage">>, #{ <<"message">> => unicode:characters_to_binary(Msg), <<"type">> => otp_log_level_to_lsp(Level) @@ -26,8 +29,10 @@ log(#{level := Level} = LogEvent, _Config) -> catch E:R:ST -> ErrMsg = - io_lib:format( "Logger Exception ({~w, ~w}): ~n~p" - , [E, R, ST]), + io_lib:format( + "Logger Exception ({~w, ~w}): ~n~p", + [E, R, ST] + ), els_server:send_notification(<<"window/logMessage">>, #{ <<"message">> => unicode:characters_to_binary(ErrMsg), <<"type">> => 1 diff --git a/apps/els_lsp/src/els_lsp.app.src b/apps/els_lsp/src/els_lsp.app.src index 5e62a74dd..4e6014d92 100644 --- a/apps/els_lsp/src/els_lsp.app.src +++ b/apps/els_lsp/src/els_lsp.app.src @@ -6,6 +6,7 @@ {applications, [ kernel, stdlib, + edoc, docsh, elvis_core, rebar3_format, @@ -17,6 +18,9 @@ els_core, gradualizer ]}, + {optional_applications, [ + edoc + ]}, {env, []}, {modules, []}, {maintainers, []}, diff --git a/apps/els_lsp/src/els_markup_content.erl b/apps/els_lsp/src/els_markup_content.erl index 4498f942f..453e35cb2 100644 --- a/apps/els_lsp/src/els_markup_content.erl +++ b/apps/els_lsp/src/els_markup_content.erl @@ -1,23 +1,32 @@ -module(els_markup_content). --export([ new/1 ]). +-export([new/1]). -include_lib("els_core/include/els_core.hrl"). --type doc_entry() :: { 'text' | 'h1' | 'h2' | 'h3' | 'h4' - | 'code_block_line' | 'code_block_begin' | 'code_block_end' - | 'code_line' | 'code_inline' - , string() - }. --export_type([ doc_entry/0 ]). +-type doc_entry() :: { + 'text' + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'code_block_line' + | 'code_block_begin' + | 'code_block_end' + | 'code_line' + | 'code_inline', + string() +}. +-export_type([doc_entry/0]). -spec new([doc_entry()]) -> markup_content(). new(Entries) -> - MarkupKind = markup_kind(), - Value = value(MarkupKind, Entries), - #{ kind => MarkupKind - , value => Value - }. + MarkupKind = markup_kind(), + Value = value(MarkupKind, Entries), + #{ + kind => MarkupKind, + value => Value + }. %%------------------------------------------------------------------------------ %% @doc markup_kind/0 @@ -27,52 +36,52 @@ new(Entries) -> %%------------------------------------------------------------------------------ -spec markup_kind() -> markup_kind(). markup_kind() -> - ContentFormat = - case els_config:get(capabilities) of - #{<<"textDocument">> := #{<<"hover">> := #{<<"contentFormat">> := X}}} -> - X; - _ -> - [] - end, - case lists:member(atom_to_binary(?MARKDOWN, utf8), ContentFormat) of - true -> ?MARKDOWN; - false -> ?PLAINTEXT - end. + ContentFormat = + case els_config:get(capabilities) of + #{<<"textDocument">> := #{<<"hover">> := #{<<"contentFormat">> := X}}} -> + X; + _ -> + [] + end, + case lists:member(atom_to_binary(?MARKDOWN, utf8), ContentFormat) of + true -> ?MARKDOWN; + false -> ?PLAINTEXT + end. -spec value(markup_kind(), [doc_entry()]) -> binary(). value(MarkupKind, Entries) -> - Separator = separator(MarkupKind), - FormattedEntries = [format_entry(Entry, MarkupKind) || Entry <- Entries], - unicode:characters_to_binary(string:join(FormattedEntries, Separator)). + Separator = separator(MarkupKind), + FormattedEntries = [format_entry(Entry, MarkupKind) || Entry <- Entries], + unicode:characters_to_binary(string:join(FormattedEntries, Separator)). -spec separator(markup_kind()) -> string(). separator(?MARKDOWN) -> - "\n\n"; + "\n\n"; separator(?PLAINTEXT) -> - "\n". + "\n". -spec format_entry(doc_entry(), markup_kind()) -> string(). format_entry({h1, String}, _MarkupKind) -> - "# " ++ String; + "# " ++ String; format_entry({h2, String}, _MarkupKind) -> - "## " ++ String; + "## " ++ String; format_entry({h3, String}, _MarkupKind) -> - "### " ++ String; + "### " ++ String; format_entry({h4, String}, _MarkupKind) -> - "#### " ++ String; + "#### " ++ String; format_entry({code_block_line, String}, _MarkupKind) -> - " " ++ String; + " " ++ String; format_entry({code_block_begin, _Language}, ?PLAINTEXT) -> - ""; + ""; format_entry({code_block_begin, Language}, ?MARKDOWN) -> - "```" ++ Language; + "```" ++ Language; format_entry({code_block_end, _Language}, ?PLAINTEXT) -> - ""; + ""; format_entry({code_block_end, _Language}, ?MARKDOWN) -> - "```"; + "```"; format_entry({code_inline, String}, ?PLAINTEXT) -> - String; + String; format_entry({code_line, String}, ?MARKDOWN) -> - "```erlang\n" ++ String ++ "\n```"; + "```erlang\n" ++ String ++ "\n```"; format_entry({_Kind, String}, _MarkupKind) -> - String. + String. diff --git a/apps/els_lsp/src/els_methods.erl b/apps/els_lsp/src/els_methods.erl index 4f0c3f8a4..cd5b8b440 100644 --- a/apps/els_lsp/src/els_methods.erl +++ b/apps/els_lsp/src/els_methods.erl @@ -1,39 +1,43 @@ -module(els_methods). --export([ dispatch/4 - ]). - --export([ exit/2 - , initialize/2 - , initialized/2 - , shutdown/2 - , textdocument_completion/2 - , completionitem_resolve/2 - , textdocument_didopen/2 - , textdocument_didchange/2 - , textdocument_didsave/2 - , textdocument_didclose/2 - , textdocument_documentsymbol/2 - , textdocument_hover/2 - , textdocument_definition/2 - , textdocument_implementation/2 - , textdocument_references/2 - , textdocument_documenthighlight/2 - , textdocument_formatting/2 - , textdocument_rangeformatting/2 - , textdocument_ontypeformatting/2 - , textdocument_foldingrange/2 - , workspace_didchangeconfiguration/2 - , textdocument_codeaction/2 - , textdocument_codelens/2 - , textdocument_rename/2 - , textdocument_preparecallhierarchy/2 - , callhierarchy_incomingcalls/2 - , callhierarchy_outgoingcalls/2 - , workspace_executecommand/2 - , workspace_didchangewatchedfiles/2 - , workspace_symbol/2 - ]). +-export([dispatch/4]). + +-export([ + exit/2, + initialize/2, + initialized/2, + shutdown/2, + textdocument_completion/2, + completionitem_resolve/2, + textdocument_didopen/2, + textdocument_didchange/2, + textdocument_didsave/2, + textdocument_didclose/2, + textdocument_documentsymbol/2, + textdocument_hover/2, + textdocument_definition/2, + textdocument_implementation/2, + textdocument_references/2, + textdocument_documenthighlight/2, + textdocument_formatting/2, + textdocument_rangeformatting/2, + textdocument_ontypeformatting/2, + textdocument_foldingrange/2, + workspace_didchangeconfiguration/2, + textdocument_codeaction/2, + textdocument_codelens/2, + textdocument_rename/2, + textdocument_preparerename/2, + textdocument_preparecallhierarchy/2, + textdocument_semantictokens_full/2, + textdocument_signaturehelp/2, + callhierarchy_incomingcalls/2, + callhierarchy_outgoingcalls/2, + workspace_executecommand/2, + workspace_didchangewatchedfiles/2, + workspace_symbol/2 +]). +-export([textdocument_inlayhint/2]). %%============================================================================== %% Includes @@ -41,405 +45,530 @@ -include("els_lsp.hrl"). -include_lib("kernel/include/logger.hrl"). --type method_name() :: binary(). --type state() :: map(). --type params() :: map(). --type result() :: {response, params() | null, state()} - | {error, params(), state()} - | {noresponse, state()} - | {noresponse, {els_provider:provider(), pid()}, state()} - | {notification, binary(), params(), state()}. +-type method_name() :: binary(). +-type params() :: map(). +-type result() :: + {response, params() | null, els_server:state()} + | {error, params(), els_server:state()} + | {noresponse, els_server:state()} + | {noresponse, uri(), pid(), els_server:state()} + | {notification, binary(), params(), els_server:state()} + | {diagnostics, uri(), [pid()], els_server:state()} + | {async, uri(), pid(), els_server: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(). +-spec dispatch(method_name(), params(), request_type(), els_server:state()) -> result(). dispatch(<<"$/", Method/binary>>, Params, notification, State) -> - Msg = "Ignoring $/ notification [method=~p] [params=~p]", - Fmt = [Method, Params], - ?LOG_DEBUG(Msg, Fmt), - {noresponse, State}; + Msg = "Ignoring $/ notification [method=~p] [params=~p]", + Fmt = [Method, Params], + ?LOG_DEBUG(Msg, Fmt), + {noresponse, State}; dispatch(<<"$/", Method/binary>>, Params, request, State) -> - Msg = "Ignoring $/ request [method=~p] [params=~p]", - Fmt = [Method, Params], - ?LOG_DEBUG(Msg, Fmt), - Error = #{ code => ?ERR_METHOD_NOT_FOUND - , message => <<"Method not found: ", Method/binary>> - }, - {error, Error, State}; -dispatch(Method, Params, _Type, State) -> - Function = method_to_function_name(Method), - ?LOG_DEBUG("Dispatching request [method=~p] [params=~p]", [Method, Params]), - try do_dispatch(Function, Params, State) - catch - error:undef -> - not_implemented_method(Method, 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 ", Method/binary>> - }, - {error, Error, State} - end. - --spec do_dispatch(atom(), params(), state()) -> result(). + Msg = "Ignoring $/ request [method=~p] [params=~p]", + Fmt = [Method, Params], + ?LOG_DEBUG(Msg, Fmt), + Error = #{ + code => ?ERR_METHOD_NOT_FOUND, + message => <<"Method not found: ", Method/binary>> + }, + {error, Error, State}; +dispatch(Method, Params, MessageType, State) -> + Function = method_to_function_name(Method), + ?LOG_DEBUG("Dispatching request [method=~p] [params=~p]", [Method, Params]), + try + do_dispatch(Function, Params, State) + catch + error:undef:Stack -> + ?LOG_ERROR( + "Internal [type=~p] [error=~p] [stack=~p]", + [error, undef, Stack] + ), + not_implemented_method(Method, State); + Type:Reason:Stack -> + ?LOG_ERROR( + "Internal [type=~p] [error=~p] [stack=~p]", + [Type, Reason, Stack] + ), + case MessageType of + request -> + Error = #{ + type => Type, + reason => Reason, + stack => Stack, + method => Method, + params => Params + }, + ErrorMsg = els_utils:to_binary(lists:flatten(io_lib:format("~p", [Error]))), + ErrorResponse = #{ + code => ?ERR_INTERNAL_ERROR, + message => <<"Internal Error: ", ErrorMsg/binary>> + }, + {error, ErrorResponse, State}; + notification -> + {noresponse, State} + end + end. + +-spec do_dispatch(atom(), params(), els_server:state()) -> result(). do_dispatch(exit, Params, State) -> - els_methods:exit(Params, State); + els_methods:exit(Params, State); do_dispatch(_Function, _Params, #{status := shutdown} = State) -> - Message = <<"Server is shutting down">>, - Result = #{ code => ?ERR_INVALID_REQUEST - , message => Message - }, - {error, Result, State}; + Message = <<"Server is shutting down">>, + Result = #{ + code => ?ERR_INVALID_REQUEST, + message => Message + }, + {error, Result, State}; do_dispatch(initialize, Params, State) -> - els_methods:initialize(Params, State); + els_methods:initialize(Params, State); do_dispatch(Function, Params, #{status := initialized} = State) -> - els_methods:Function(Params, State); + els_methods:Function(Params, State); do_dispatch(_Function, _Params, 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(). + 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(), els_server:state()) -> result(). not_implemented_method(Method, State) -> - ?LOG_WARNING("[Method not implemented] [method=~s]", [Method]), - Message = <<"Method not implemented: ", Method/binary>>, - Method1 = <<"window/showMessage">>, - Params = #{ type => ?MESSAGE_TYPE_INFO - , message => Message - }, - {notification, Method1, Params, State}. + ?LOG_WARNING("[Method not implemented] [method=~s]", [Method]), + Message = <<"Method not implemented: ", Method/binary>>, + Method1 = <<"window/showMessage">>, + Params = #{ + type => ?MESSAGE_TYPE_INFO, + message => Message + }, + {notification, Method1, Params, State}. -spec method_to_function_name(method_name()) -> atom(). method_to_function_name(<<"$/", Method/binary>>) -> - method_to_function_name(<<"$_", Method/binary>>); + method_to_function_name(<<"$_", Method/binary>>); method_to_function_name(Method) -> - Replaced = string:replace(Method, <<"/">>, <<"_">>), - Lower = string:lowercase(Replaced), - Binary = els_utils:to_binary(Lower), - binary_to_atom(Binary, utf8). + Replaced = string:replace(Method, <<"/">>, <<"_">>, all), + Lower = string:lowercase(Replaced), + Binary = els_utils:to_binary(Lower), + binary_to_atom(Binary, utf8). %%============================================================================== %% Initialize %%============================================================================== --spec initialize(params(), state()) -> result(). +-spec initialize(params(), els_server:state()) -> result(). initialize(Params, State) -> - Provider = els_general_provider, - Request = {initialize, Params}, - Response = els_provider:handle_request(Provider, Request), - {response, Response, State#{status => initialized}}. + Provider = els_general_provider, + Request = {initialize, Params}, + {response, Response} = els_provider:handle_request(Provider, Request), + {response, Response, State#{status => initialized}}. %%============================================================================== %% Initialized %%============================================================================== --spec initialized(params(), state()) -> result(). +-spec initialized(params(), els_server:state()) -> result(). initialized(Params, State) -> - Provider = els_general_provider, - Request = {initialized, Params}, - _Response = els_provider:handle_request(Provider, Request), - %% Report to the user the server version - {ok, Version} = application:get_key(?APP, vsn), - ?LOG_INFO("initialized: [App=~p] [Version=~p]", [?APP, Version]), - BinVersion = els_utils:to_binary(Version), - Root = filename:basename(els_uri:path(els_config:get(root_uri))), - OTPVersion = els_utils:to_binary(erlang:system_info(otp_release)), - Message = <<"Erlang LS (in ", Root/binary, "), version: " - , BinVersion/binary, ", OTP version: " - , OTPVersion/binary>>, - NMethod = <<"window/showMessage">>, - NParams = #{ type => ?MESSAGE_TYPE_INFO - , message => Message - }, - {notification, NMethod, NParams, State}. + Provider = els_general_provider, + Request = {initialized, Params}, + {response, _Response} = els_provider:handle_request(Provider, Request), + %% Report to the user the server version + {ok, Version} = application:get_key(?APP, vsn), + ?LOG_INFO("initialized: [App=~p] [Version=~p]", [?APP, Version]), + BinVersion = els_utils:to_binary(Version), + Root = filename:basename(els_uri:path(els_config:get(root_uri))), + OTPVersion = els_utils:to_binary(erlang:system_info(otp_release)), + Message = + <<"Erlang LS (in ", Root/binary, "), version: ", BinVersion/binary, ", OTP version: ", + OTPVersion/binary>>, + NMethod = <<"window/showMessage">>, + NParams = #{ + type => ?MESSAGE_TYPE_INFO, + message => Message + }, + {notification, NMethod, NParams, State}. %%============================================================================== %% shutdown %%============================================================================== --spec shutdown(params(), state()) -> result(). +-spec shutdown(params(), els_server:state()) -> result(). shutdown(Params, State) -> - Provider = els_general_provider, - Request = {shutdown, Params}, - Response = els_provider:handle_request(Provider, Request), - {response, Response, State#{status => shutdown}}. + Provider = els_general_provider, + Request = {shutdown, Params}, + {response, Response} = els_provider:handle_request(Provider, Request), + {response, Response, State#{status => shutdown}}. %%============================================================================== %% exit %%============================================================================== --spec exit(params(), state()) -> no_return(). +-spec exit(params(), els_server:state()) -> no_return(). exit(_Params, State) -> - Provider = els_general_provider, - Request = {exit, #{status => maps:get(status, State, undefined)}}, - _Response = els_provider:handle_request(Provider, Request), - {noresponse, #{}}. + Provider = els_general_provider, + Request = {exit, #{status => maps:get(status, State)}}, + {response, _Response} = els_provider:handle_request(Provider, Request), + %% Only reached by property-based test (where halt/1 is mocked for + %% faster iteration + {noresponse, State#{status => exiting}}. %%============================================================================== %% textDocument/didopen %%============================================================================== --spec textdocument_didopen(params(), state()) -> result(). -textdocument_didopen(Params, State) -> - ok = els_text_synchronization:did_open(Params), - {noresponse, State}. +-spec textdocument_didopen(params(), els_server:state()) -> result(). +textdocument_didopen(Params, #{open_buffers := OpenBuffers} = State) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + Provider = els_text_synchronization_provider, + Request = {did_open, Params}, + {diagnostics, Uri, Jobs} = els_provider:handle_request(Provider, Request), + {diagnostics, Uri, Jobs, State#{open_buffers => sets:add_element(Uri, OpenBuffers)}}. %%============================================================================== %% textDocument/didchange %%============================================================================== --spec textdocument_didchange(params(), state()) -> result(). -textdocument_didchange(Params, State) -> - ok = els_text_synchronization:did_change(Params), - {noresponse, State}. +-spec textdocument_didchange(params(), els_server:state()) -> result(). +textdocument_didchange(Params, State0) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + State = cancel_request_by_uri(Uri, State0), + Provider = els_text_synchronization_provider, + Request = {did_change, Params}, + _ = els_provider:handle_request(Provider, Request), + {noresponse, State}. %%============================================================================== %% textDocument/didsave %%============================================================================== --spec textdocument_didsave(params(), state()) -> result(). +-spec textdocument_didsave(params(), els_server:state()) -> result(). textdocument_didsave(Params, State) -> - ok = els_text_synchronization:did_save(Params), - {noresponse, State}. + Provider = els_text_synchronization_provider, + Request = {did_save, Params}, + {diagnostics, Uri, Jobs} = els_provider:handle_request(Provider, Request), + {diagnostics, Uri, Jobs, State}. %%============================================================================== %% textDocument/didclose %%============================================================================== --spec textdocument_didclose(params(), state()) -> result(). -textdocument_didclose(Params, State) -> - ok = els_text_synchronization:did_close(Params), - {noresponse, State}. +-spec textdocument_didclose(params(), els_server:state()) -> result(). +textdocument_didclose(Params, #{open_buffers := OpenBuffers} = State) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + Provider = els_text_synchronization_provider, + Request = {did_close, Params}, + noresponse = els_provider:handle_request(Provider, Request), + {noresponse, State#{open_buffers => sets:del_element(Uri, OpenBuffers)}}. %%============================================================================== %% textdocument/documentSymbol %%============================================================================== --spec textdocument_documentsymbol(params(), state()) -> result(). +-spec textdocument_documentsymbol(params(), els_server:state()) -> result(). textdocument_documentsymbol(Params, State) -> - Provider = els_document_symbol_provider, - Request = {document_symbol, Params}, - Response = els_provider:handle_request(Provider, Request), - {response, Response, State}. + Provider = els_document_symbol_provider, + Request = {document_symbol, Params}, + {response, Response} = els_provider:handle_request(Provider, Request), + {response, Response, State}. %%============================================================================== %% textDocument/hover %%============================================================================== --spec textdocument_hover(params(), state()) -> result(). +-spec textdocument_hover(params(), els_server:state()) -> result(). textdocument_hover(Params, State) -> - Provider = els_hover_provider, - Job = els_provider:handle_request(Provider, {hover, Params}), - {noresponse, {Provider, Job}, State}. + Provider = els_hover_provider, + {async, Uri, Job} = els_provider:handle_request(Provider, {hover, Params}), + {async, Uri, Job, State}. %%============================================================================== %% textDocument/completion %%============================================================================== --spec textdocument_completion(params(), state()) -> result(). +-spec textdocument_completion(params(), els_server:state()) -> result(). textdocument_completion(Params, State) -> - Provider = els_completion_provider, - Response = els_provider:handle_request(Provider, {completion, Params}), - {response, Response, State}. + Provider = els_completion_provider, + {async, Uri, Job} = + els_provider:handle_request(Provider, {completion, Params}), + {async, Uri, Job, State}. %%============================================================================== %% completionItem/resolve %%============================================================================== --spec completionitem_resolve(params(), state()) -> result(). +-spec completionitem_resolve(params(), els_server:state()) -> result(). completionitem_resolve(Params, State) -> - Provider = els_completion_provider, - Response = els_provider:handle_request(Provider, {resolve, Params}), - {response, Response, State}. + Provider = els_completion_provider, + {response, Response} = + els_provider:handle_request(Provider, {resolve, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/definition %%============================================================================== --spec textdocument_definition(params(), state()) -> result(). +-spec textdocument_definition(params(), els_server:state()) -> result(). textdocument_definition(Params, State) -> - Provider = els_definition_provider, - Response = els_provider:handle_request(Provider, {definition, Params}), - {response, Response, State}. + Provider = els_definition_provider, + case els_provider:handle_request(Provider, {definition, Params}) of + {response, Response} -> + {response, Response, State}; + {async, Uri, Job} -> + {async, Uri, Job, State} + end. %%============================================================================== %% textDocument/references %%============================================================================== --spec textdocument_references(params(), state()) -> result(). +-spec textdocument_references(params(), els_server:state()) -> result(). textdocument_references(Params, State) -> - Provider = els_references_provider, - Response = els_provider:handle_request(Provider, {references, Params}), - {response, Response, State}. + Provider = els_references_provider, + {async, Uri, Job} = els_provider:handle_request(Provider, {references, Params}), + {async, Uri, Job, State}. %%============================================================================== %% textDocument/documentHightlight %%============================================================================== --spec textdocument_documenthighlight(params(), state()) -> result(). +-spec textdocument_documenthighlight(params(), els_server:state()) -> result(). textdocument_documenthighlight(Params, State) -> - Provider = els_document_highlight_provider, - Response = els_provider:handle_request(Provider, - {document_highlight, Params}), - {response, Response, State}. + Provider = els_document_highlight_provider, + {response, Response} = + els_provider:handle_request(Provider, {document_highlight, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/formatting %%============================================================================== --spec textdocument_formatting(params(), state()) -> result(). +-spec textdocument_formatting(params(), els_server:state()) -> result(). textdocument_formatting(Params, State) -> - Provider = els_formatting_provider, - Response = els_provider:handle_request(Provider, - {document_formatting, Params}), - {response, Response, State}. + Provider = els_formatting_provider, + {response, Response} = + els_provider:handle_request(Provider, {document_formatting, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/rangeFormatting %%============================================================================== --spec textdocument_rangeformatting(params(), state()) -> result(). +-spec textdocument_rangeformatting(params(), els_server:state()) -> result(). textdocument_rangeformatting(Params, State) -> - Provider = els_formatting_provider, - Response = els_provider:handle_request(Provider, - {document_rangeformatting, Params}), - {response, Response, State}. + Provider = els_formatting_provider, + {response, Response} = + els_provider:handle_request(Provider, {document_rangeformatting, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/onTypeFormatting %%============================================================================== --spec textdocument_ontypeformatting(params(), state()) -> result(). +-spec textdocument_ontypeformatting(params(), els_server:state()) -> result(). textdocument_ontypeformatting(Params, State) -> - Provider = els_formatting_provider, - Response = els_provider:handle_request(Provider, - {document_ontypeformatting, Params}), - {response, Response, State}. + Provider = els_formatting_provider, + {response, Response} = + els_provider:handle_request(Provider, {document_ontypeformatting, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/foldingRange %%============================================================================== --spec textdocument_foldingrange(params(), state()) -> result(). +-spec textdocument_foldingrange(params(), els_server:state()) -> result(). textdocument_foldingrange(Params, State) -> - Provider = els_folding_range_provider, - Response = els_provider:handle_request( Provider - , {document_foldingrange, Params}), - {response, Response, State}. + Provider = els_folding_range_provider, + {response, Response} = + els_provider:handle_request(Provider, {document_foldingrange, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/implementation %%============================================================================== --spec textdocument_implementation(params(), state()) -> result(). +-spec textdocument_implementation(params(), els_server:state()) -> result(). textdocument_implementation(Params, State) -> - Provider = els_implementation_provider, - Response = els_provider:handle_request(Provider, {implementation, Params}), - {response, Response, State}. + Provider = els_implementation_provider, + {response, Response} = + els_provider:handle_request(Provider, {implementation, Params}), + {response, Response, State}. %%============================================================================== %% workspace/didChangeConfiguration %%============================================================================== --spec workspace_didchangeconfiguration(params(), state()) -> result(). +-spec workspace_didchangeconfiguration(params(), els_server:state()) -> result(). workspace_didchangeconfiguration(_Params, State) -> - %% Some clients send this notification on startup, even though we - %% have no server-side config. So swallow it without complaining. - {noresponse, State}. + %% Some clients send this notification on startup, even though we + %% have no server-side config. So swallow it without complaining. + {noresponse, State}. %%============================================================================== %% textDocument/codeAction %%============================================================================== --spec textdocument_codeaction(params(), state()) -> result(). +-spec textdocument_codeaction(params(), els_server:state()) -> result(). textdocument_codeaction(Params, State) -> - Provider = els_code_action_provider, - Response = els_provider:handle_request(Provider, - {document_codeaction, Params}), - {response, Response, State}. + Provider = els_code_action_provider, + {response, Response} = + els_provider:handle_request(Provider, {document_codeaction, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/codeLens %%============================================================================== --spec textdocument_codelens(params(), state()) -> result(). +-spec textdocument_codelens(params(), els_server:state()) -> result(). textdocument_codelens(Params, State) -> - Provider = els_code_lens_provider, - Job = els_provider:handle_request(Provider, {document_codelens, Params}), - {noresponse, {Provider, Job}, State}. + Provider = els_code_lens_provider, + {async, Uri, Job} = + els_provider:handle_request(Provider, {document_codelens, Params}), + {async, Uri, Job, State}. %%============================================================================== %% textDocument/rename %%============================================================================== --spec textdocument_rename(params(), state()) -> result(). +-spec textdocument_rename(params(), els_server:state()) -> result(). textdocument_rename(Params, State) -> - Provider = els_rename_provider, - Response = els_provider:handle_request(Provider, {rename, Params}), - {response, Response, State}. + Provider = els_rename_provider, + {response, Response} = + els_provider:handle_request(Provider, {rename, Params}), + {response, Response, State}. + +%%============================================================================== +%% textDocument/prepareRename +%%============================================================================= +-spec textdocument_preparerename(params(), els_server:state()) -> result(). +textdocument_preparerename(Params, State) -> + Provider = els_prepare_rename_provider, + {response, Response} = + els_provider:handle_request(Provider, {prepare_rename, Params}), + {response, Response, State}. + +%%============================================================================== +%% textDocument/inlayHint +%%============================================================================= +-spec textdocument_inlayhint(params(), els_server:state()) -> result(). +textdocument_inlayhint(Params, State) -> + Provider = els_inlay_hint_provider, + {async, Uri, Job} = + els_provider:handle_request(Provider, {inlay_hint, Params}), + {async, Uri, Job, State}. + +%%============================================================================== +%% textDocument/semanticTokens/full +%%============================================================================== + +-spec textdocument_semantictokens_full(params(), els_server:state()) -> result(). +textdocument_semantictokens_full(Params, State) -> + Provider = els_semantic_token_provider, + {response, Response} = + els_provider:handle_request(Provider, {semantic_tokens, Params}), + {response, Response, State}. %%============================================================================== %% textDocument/preparePreparecallhierarchy %%============================================================================== --spec textdocument_preparecallhierarchy(params(), state()) -> result(). +-spec textdocument_preparecallhierarchy(params(), els_server:state()) -> result(). textdocument_preparecallhierarchy(Params, State) -> - Provider = els_call_hierarchy_provider, - Response = els_provider:handle_request(Provider, {prepare, Params}), - {response, Response, State}. + Provider = els_call_hierarchy_provider, + {response, Response} = + els_provider:handle_request(Provider, {prepare, Params}), + {response, Response, State}. + +%%============================================================================== +%% textDocument/signatureHelp +%%============================================================================== + +-spec textdocument_signaturehelp(params(), els_server:state()) -> result(). +textdocument_signaturehelp(Params, State) -> + Provider = els_signature_help_provider, + {response, Response} = + els_provider:handle_request(Provider, {signature_help, Params}), + {response, Response, State}. %%============================================================================== %% callHierarchy/incomingCalls %%============================================================================== --spec callhierarchy_incomingcalls(params(), state()) -> result(). +-spec callhierarchy_incomingcalls(params(), els_server:state()) -> result(). callhierarchy_incomingcalls(Params, State) -> - Provider = els_call_hierarchy_provider, - Response = els_provider:handle_request(Provider, {incoming_calls, Params}), - {response, Response, State}. + Provider = els_call_hierarchy_provider, + {response, Response} = + els_provider:handle_request(Provider, {incoming_calls, Params}), + {response, Response, State}. %%============================================================================== %% callHierarchy/outgoingCalls %%============================================================================== --spec callhierarchy_outgoingcalls(params(), state()) -> result(). +-spec callhierarchy_outgoingcalls(params(), els_server:state()) -> result(). callhierarchy_outgoingcalls(Params, State) -> - Provider = els_call_hierarchy_provider, - Response = els_provider:handle_request(Provider, {outgoing_calls, Params}), - {response, Response, State}. + Provider = els_call_hierarchy_provider, + {response, Response} = + els_provider:handle_request(Provider, {outgoing_calls, Params}), + {response, Response, State}. %%============================================================================== %% workspace/executeCommand %%============================================================================== --spec workspace_executecommand(params(), state()) -> result(). +-spec workspace_executecommand(params(), els_server:state()) -> result(). workspace_executecommand(Params, State) -> - Provider = els_execute_command_provider, - Response = els_provider:handle_request(Provider, - {workspace_executecommand, Params}), - {response, Response, State}. + Provider = els_execute_command_provider, + {response, Response} = + els_provider:handle_request(Provider, {workspace_executecommand, Params}), + {response, Response, State}. %%============================================================================== %% workspace/didChangeWatchedFiles %%============================================================================== --spec workspace_didchangewatchedfiles(map(), state()) -> result(). -workspace_didchangewatchedfiles(_Params, State) -> - %% Some clients rely on these notifications to be successful. - %% Let's just ignore them. - {noresponse, State}. +-spec workspace_didchangewatchedfiles(map(), els_server:state()) -> result(). +workspace_didchangewatchedfiles(Params0, State) -> + #{open_buffers := OpenBuffers} = State, + #{<<"changes">> := Changes0} = Params0, + Changes = [ + C + || #{<<"uri">> := Uri} = C <- Changes0, + not sets:is_element(Uri, OpenBuffers) + ], + Params = Params0#{<<"changes">> => Changes}, + Provider = els_text_synchronization_provider, + Request = {did_change_watched_files, Params}, + noresponse = els_provider:handle_request(Provider, Request), + {noresponse, State}. %%============================================================================== %% workspace/symbol %%============================================================================== --spec workspace_symbol(map(), state()) -> result(). +-spec workspace_symbol(map(), els_server:state()) -> result(). workspace_symbol(Params, State) -> - Provider = els_workspace_symbol_provider, - Response = els_provider:handle_request(Provider, {symbol, Params}), - {response, Response, State}. + Provider = els_workspace_symbol_provider, + {response, Response} = + els_provider:handle_request(Provider, {symbol, Params}), + {response, Response, State}. + +%%============================================================================== +%% Internal Functions +%%============================================================================== +-spec cancel_request_by_uri(uri(), els_server:state()) -> els_server:state(). +cancel_request_by_uri(Uri, State) -> + #{in_progress := InProgress0} = State, + Fun = fun({U, Job}) -> + case U =:= Uri of + true -> + els_background_job:stop(Job), + false; + false -> + true + end + end, + InProgress = lists:filtermap(Fun, InProgress0), + ?LOG_DEBUG("Cancelling requests by Uri [uri=~p]", [Uri]), + State#{in_progress => InProgress}. diff --git a/apps/els_lsp/src/els_parser.erl b/apps/els_lsp/src/els_parser.erl index 18d851228..943f06e71 100644 --- a/apps/els_lsp/src/els_parser.erl +++ b/apps/els_lsp/src/els_parser.erl @@ -1,15 +1,22 @@ %%============================================================================== -%% The erlang_ls parser. It uses the epp_dodger OTP library. +%% The erlang_ls parser. It uses the parser of erlfmt library. %%============================================================================== -module(els_parser). %%============================================================================== %% Exports %%============================================================================== --export([ parse/1 - , parse_file/1 - , parse_text/1 - ]). +-export([ + parse/1, + parse_incomplete_text/2, + points_of_interest/1 +]). + +%% For manual use only, to test the parser +-export([ + parse_file/1, + parse_text/1 +]). %%============================================================================== %% Includes @@ -19,60 +26,186 @@ -type deep_list(T) :: [T | deep_list(T)]. +%%============================================================================== +%% Dialyzer +%%============================================================================== + +%% Spec of erlfmt:read_nodes_string is wrong, it CAN return {skip, _} + +-dialyzer([{nowarn_function, parse/1}]). +-dialyzer([{nowarn_function, parse_text/1}]). +-dialyzer([{nowarn_function, fix_erlfmt/1}]). + +%% Spec of erlfmt_parse:parse_node/1 is wrong, +%% error location can be returned in various formats +%% see https://github.com/WhatsApp/erlfmt/pull/352 + +-dialyzer([{nowarn_function, loc_to_pos/1}]). + %%============================================================================== %% API %%============================================================================== --spec parse(binary()) -> {ok, [poi()]}. +-spec parse(binary()) -> {ok, [els_poi:poi()]}. parse(Text) -> - String = els_utils:to_list(Text), - case erlfmt:read_nodes_string("nofile", String) of - {ok, Forms, _ErrorInfo} -> - {ok, lists:flatten(parse_forms(Forms))}; - {error, _ErrorInfo} -> - {ok, []} - end. + String = els_utils:to_list(Text), + case erlfmt:read_nodes_string("nofile", String) of + {ok, Forms, _ErrorInfo} -> + {ok, lists:flatten(parse_forms(Forms))}; + {skip, _} -> + ?LOG_INFO("Erlfmt skipped parsing, try to fix it."), + parse(fix_erlfmt(Text)); + {error, _ErrorInfo} -> + {ok, []} + end. + +-spec fix_erlfmt(binary()) -> binary(). +fix_erlfmt(Text) -> + %% erlfmt will skip if it finds pragma @noformat, so we remove it + binary:replace(Text, <<"@noformat">>, <<"@doformat">>). -spec parse_file(file:name_all()) -> {ok, [tree()]} | {error, term()}. parse_file(FileName) -> - forms_to_ast(erlfmt:read_nodes(FileName)). + forms_to_ast(erlfmt:read_nodes(FileName)). +%% Spec of erlfmt:read_nodes_string is wrong, it can return {skip, _} -spec parse_text(binary()) -> {ok, [tree()]} | {error, term()}. parse_text(Text) -> - String = els_utils:to_list(Text), - forms_to_ast(erlfmt:read_nodes_string("nofile", String)). + String = els_utils:to_list(Text), + case erlfmt:read_nodes_string("nofile", String) of + {skip, _} -> + ?LOG_INFO("Erlfmt skipped parsing, try to fix it."), + parse_text(fix_erlfmt(Text)); + Result -> + forms_to_ast(Result) + end. -spec forms_to_ast(tuple()) -> {ok, [tree()]} | {error, term()}. forms_to_ast({ok, Forms, _ErrorInfo}) -> - TreeList = - [els_erlfmt_ast:erlfmt_to_st(Form) || Form <- Forms], - {ok, TreeList}; + TreeList = + [els_erlfmt_ast:erlfmt_to_st(Form) || Form <- Forms], + {ok, TreeList}; forms_to_ast({error, _ErrorInfo} = Error) -> - Error. + Error. + +-spec parse_incomplete_text(string(), {erl_anno:line(), erl_anno:column()}) -> + {ok, tree()} | error. +parse_incomplete_text(Text, {_Line, _Col} = StartLoc) -> + Tokens = scan_text(Text, StartLoc), + case parse_incomplete_tokens(Tokens) of + {ok, Form} -> + Tree = els_erlfmt_ast:erlfmt_to_st(Form), + {ok, Tree}; + error -> + error + end. %%============================================================================== %% Internal Functions %%============================================================================== --spec parse_forms([erlfmt_parse:abstract_node()]) -> deep_list(poi()). +-spec parse_forms([erlfmt_parse:abstract_node()]) -> deep_list(els_poi:poi()). parse_forms(Forms) -> - [try - parse_form(Form) - catch Type:Reason:St -> - ?LOG_WARNING("Please report error parsing form ~p:~p:~p~n~p~n", - [Type, Reason, St, Form]), - [] - end - || Form <- Forms]. - --spec parse_form(erlfmt_parse:abstract_node()) -> deep_list(poi()). + [ + try + parse_form(Form) + catch + Type:Reason:St -> + ?LOG_WARNING( + "Please report error parsing form ~p:~p:~p~n~p~n", + [Type, Reason, St, Form] + ), + [] + end + || Form <- Forms + ]. + +-spec parse_form(erlfmt_parse:abstract_node()) -> deep_list(els_poi:poi()). parse_form({raw_string, Anno, Text}) -> - Start = erlfmt_scan:get_anno(location, Anno), - {ok, RangeTokens, _EndLocation} = erl_scan:string(Text, Start, [text]), - find_attribute_tokens(RangeTokens); + StartLoc = erlfmt_scan:get_anno(location, Anno), + RangeTokens = scan_text(Text, StartLoc), + case parse_incomplete_tokens(RangeTokens) of + {ok, Form} -> + parse_form(Form); + error -> + find_attribute_tokens(RangeTokens) + end; parse_form(Form) -> - Tree = els_erlfmt_ast:erlfmt_to_st(Form), - POIs = points_of_interest(Tree), - POIs. + Tree = els_erlfmt_ast:erlfmt_to_st(Form), + POIs = points_of_interest(Tree), + POIs. + +-spec scan_text(string(), {erl_anno:line(), erl_anno:column()}) -> + [erlfmt_scan:token()]. +scan_text(Text, StartLoc) -> + PaddedText = pad_text(Text, StartLoc), + {ok, Tokens, _Comments, _Cont} = erlfmt_scan:string_node(PaddedText), + case Tokens of + [] -> []; + _ -> ensure_dot(Tokens) + end. + +-spec parse_incomplete_tokens([erlfmt_scan:token()]) -> + {ok, erlfmt_parse:abstract_node()} | error. +parse_incomplete_tokens([{dot, _}]) -> + error; +parse_incomplete_tokens([]) -> + error; +parse_incomplete_tokens(Tokens) -> + case erlfmt_parse:parse_node(Tokens) of + {ok, Form} -> + {ok, Form}; + {error, {ErrorLoc, erlfmt_parse, _Reason}} -> + ErrorPos = loc_to_pos(ErrorLoc), + TrimmedTokens = tokens_until(Tokens, ErrorPos), + parse_incomplete_tokens(TrimmedTokens) + end. + +%% Convert location in various formats to a consistent position that can always be compared +-spec loc_to_pos(erlfmt_scan:anno() | erl_anno:location()) -> pos(). +loc_to_pos(#{location := Loc}) -> + %% erlfmt_scan:anno() + loc_to_pos(Loc); +loc_to_pos({Line, Col} = Loc) when is_integer(Line), is_integer(Col) -> + Loc; +loc_to_pos(Line) when is_integer(Line) -> + {Line, 0}. + +%% @doc Drop tokens after given location but keep final dot, to preserve its +%% location +-spec tokens_until([erlfmt_scan:token()], pos()) -> + [erlfmt_scan:token()]. +tokens_until([_Hd, {dot, _} = Dot], _Pos) -> + %% We need to drop at least one token before the dot. + %% Otherwise if error location is at the dot, we cannot just drop the dot and + %% add a dot again, because it would result in an infinite loop. + [Dot]; +tokens_until([Hd | Tail], Pos) -> + case erlfmt_scan:get_anno(location, Hd) < Pos of + true -> + [Hd | tokens_until(Tail, Pos)]; + false -> + tokens_until(Tail, Pos) + end. + +%% `erlfmt_scan' does not support start location other than {1,1} +%% so we have to shift the text with newlines and spaces +-spec pad_text(string(), {erl_anno:line(), erl_anno:column()}) -> string(). +pad_text(Text, {StartLine, StartColumn}) -> + lists:duplicate(StartLine - 1, $\n) ++ + lists:duplicate(StartColumn - 1, $\s) ++ + Text. + +-spec ensure_dot([erlfmt_scan:token(), ...]) -> [erlfmt_scan:token(), ...]. +ensure_dot(Tokens) -> + case lists:last(Tokens) of + {dot, _} -> + Tokens; + T -> + EndLocation = erlfmt_scan:get_anno(end_location, T), + %% Add a dot which has zero length (invisible) so it does not modify the + %% end location of the whole form + Tokens ++ [{dot, #{location => EndLocation, end_location => EndLocation}}] + end. %% @doc Resolve POI for specific sections %% @@ -81,696 +214,1132 @@ parse_form(Form) -> %% completion items. Using the tokens provides accurate position for the %% beginning and end for this sections, and can also handle the situations when %% the code is not parsable. --spec find_attribute_tokens([erl_scan:token()]) -> [poi()]. -find_attribute_tokens([ {'-', Anno}, {atom, _, Name} | [_|_] = Rest]) - when Name =:= export; - Name =:= export_type -> - From = erl_anno:location(Anno), - To = token_end_location(lists:last(Rest)), - [poi({From, To}, Name, From)]; -find_attribute_tokens([ {'-', Anno}, {atom, _, spec} | [_|_] = Rest]) -> - From = erl_anno:location(Anno), - To = token_end_location(lists:last(Rest)), - [poi({From, To}, spec, undefined)]; +-spec find_attribute_tokens([erlfmt_scan:token()]) -> [els_poi:poi()]. +find_attribute_tokens([{'-', Anno}, {atom, _, Name} | [_ | _] = Rest]) when + Name =:= export; + Name =:= export_type; + Name =:= nifs +-> + From = erlfmt_scan:get_anno(location, Anno), + To = erlfmt_scan:get_anno(end_location, lists:last(Rest)), + [poi({From, To}, Name, From)]; +find_attribute_tokens([{'-', Anno}, {atom, _, spec} | [_ | _] = Rest]) -> + From = erlfmt_scan:get_anno(location, Anno), + To = erlfmt_scan:get_anno(end_location, lists:last(Rest)), + [poi({From, To}, spec, undefined)]; find_attribute_tokens(_) -> - []. - -%% Inspired by erlfmt_scan:dot_anno --spec token_end_location(erl_scan:token()) -> erl_anno:location(). -token_end_location({dot, Anno}) -> - %% Special handling for dot tokens, which by definition contain a dot char - %% followed by a whitespace char. We don't want to count the whitespace (which - %% is usually a newline) as part of the form. - {Line, Col} = erl_anno:location(Anno), - {Line, Col + 1}; -token_end_location(Token) -> - erl_scan:end_location(Token). - --spec points_of_interest(tree()) -> [[poi()]]. + []. + +-spec points_of_interest(tree()) -> [[els_poi:poi()]]. points_of_interest(Tree) -> - FoldFun = fun(T, Acc) -> [do_points_of_interest(T) | Acc] end, - fold(FoldFun, [], Tree). + FoldFun = fun(T, Acc) -> [do_points_of_interest(T) | Acc] end, + fold(FoldFun, [], Tree). %% @doc Return the list of points of interest for a given `Tree'. --spec do_points_of_interest(tree()) -> [poi()]. +-spec do_points_of_interest(tree()) -> [els_poi:poi()]. do_points_of_interest(Tree) -> - try - case erl_syntax:type(Tree) of - application -> application(Tree); - attribute -> attribute(Tree); - function -> function(Tree); - implicit_fun -> implicit_fun(Tree); - macro -> macro(Tree); - record_access -> record_access(Tree); - record_expr -> record_expr(Tree); - variable -> variable(Tree); - atom -> atom(Tree); - Type when Type =:= type_application; - Type =:= user_type_application -> - type_application(Tree); - record_type -> record_type(Tree); - _ -> [] - end - catch throw:syntax_error -> [] - end. - --spec application(tree()) -> [poi()]. + try + case erl_syntax:type(Tree) of + application -> + application(Tree); + attribute -> + attribute(Tree); + function -> + function(Tree); + implicit_fun -> + implicit_fun(Tree); + macro -> + macro(Tree); + record_access -> + record_access(Tree); + record_index_expr -> + record_index_expr(Tree); + record_expr -> + record_expr(Tree); + list_comp -> + list_comp(Tree); + variable -> + variable(Tree); + atom -> + atom(Tree); + Type when + Type =:= type_application; + Type =:= user_type_application + -> + type_application(Tree); + record_type -> + record_type(Tree); + Type when + Type == block_expr; + Type == case_expr; + Type == if_expr; + Type == implicit_fun; + Type == maybe_expr; + Type == receive_expr; + Type == try_expr; + Type == fun_expr + -> + keyword_expr(Type, Tree); + _Other -> + [] + end + catch + throw:syntax_error -> [] + end. + +-spec keyword_expr(atom(), tree()) -> [els_poi:poi()]. +keyword_expr(Type, Tree) -> + Pos = erl_syntax:get_pos(Tree), + [poi(Pos, keyword_expr, Type)]. + +-spec application(tree()) -> [els_poi:poi()]. application(Tree) -> - case application_mfa(Tree) of - undefined -> []; - {F, A} -> - Pos = erl_syntax:get_pos(erl_syntax:application_operator(Tree)), - case erl_internal:bif(F, A) of - %% Call to a function from the `erlang` module - true -> [poi(Pos, application, {erlang, F, A}, #{imported => true})]; - %% Local call - false -> [poi(Pos, application, {F, A})] - end; - MFA -> - ModFunTree = erl_syntax:application_operator(Tree), - Pos = erl_syntax:get_pos(ModFunTree), - FunTree = erl_syntax:module_qualifier_body(ModFunTree), - [poi(Pos, application, MFA, - #{name_range => els_range:range(erl_syntax:get_pos(FunTree))})] - end. + case application_mfa(Tree) of + undefined -> + []; + {{variable, F}, A} -> + Pos = erl_syntax:get_pos(erl_syntax:application_operator(Tree)), + [ + poi(Pos, application, {F, A}, #{fun_is_variable => true}), + poi(Pos, variable, F) + ]; + {F, A} -> + Pos = erl_syntax:get_pos(erl_syntax:application_operator(Tree)), + case erl_internal:bif(F, A) of + %% Call to a function from the `erlang` module + true -> + [poi(Pos, application, {erlang, F, A}, #{imported => true})]; + %% Local call + false -> + Args = erl_syntax:application_arguments(Tree), + [ + poi( + Pos, + application, + {F, A}, + #{args => args_from_subtrees(Args)} + ) + ] + end; + {{ModType, M}, {FunType, F}, A} -> + ModFunTree = erl_syntax:application_operator(Tree), + Pos = erl_syntax:get_pos(ModFunTree), + FunTree = erl_syntax:module_qualifier_body(ModFunTree), + ModTree = erl_syntax:module_qualifier_argument(ModFunTree), + FunPos = erl_syntax:get_pos(FunTree), + ModPos = erl_syntax:get_pos(ModTree), + Args = erl_syntax:application_arguments(Tree), + Data = #{ + name_range => els_range:range(FunPos), + mod_range => els_range:range(ModPos), + fun_is_variable => FunType =:= variable, + mod_is_variable => ModType =:= variable, + args => args_from_subtrees(Args) + }, + [poi(Pos, application, {M, F, A}, Data)] ++ + [poi(ModPos, variable, M) || ModType =:= variable] ++ + [poi(FunPos, variable, F) || FunType =:= variable]; + MFA -> + ModFunTree = erl_syntax:application_operator(Tree), + Pos = erl_syntax:get_pos(ModFunTree), + FunTree = erl_syntax:module_qualifier_body(ModFunTree), + ModTree = erl_syntax:module_qualifier_argument(ModFunTree), + Args = erl_syntax:application_arguments(Tree), + + Data = #{ + name_range => els_range:range(erl_syntax:get_pos(FunTree)), + mod_range => els_range:range(erl_syntax:get_pos(ModTree)), + args => args_from_subtrees(Args) + }, + [poi(Pos, application, MFA, Data)] + end. -spec application_mfa(tree()) -> - {module(), atom(), arity()} | {atom(), arity()} | undefined. + {module(), atom(), arity()} + | {atom(), arity()} + | {{atom(), atom()}, {atom(), atom()}, arity()} + | {{atom(), atom()}, arity()} + | undefined. application_mfa(Tree) -> - case erl_syntax_lib:analyze_application(Tree) of - %% Remote call - {M, {F, A}} -> - {M, F, A}; - {F, A} -> - {F, A}; - A when is_integer(A) -> - %% If the function is not explicitly named (e.g. a variable is - %% used as the module qualifier or the function name), only the - %% arity A is returned. - %% In the special case where the macro `?MODULE` is used as the - %% module qualifier, we can consider it as a local call. - Operator = erl_syntax:application_operator(Tree), - case erl_syntax:type(Operator) of - module_qualifier -> application_with_variable(Operator, A); - _ -> undefined - end - end. + case erl_syntax_lib:analyze_application(Tree) of + %% Remote call + {M, {F, A}} -> + {M, F, A}; + {F, A} -> + {F, A}; + A when is_integer(A) -> + %% If the function is not explicitly named (e.g. a variable is + %% used as the module qualifier or the function name), only the + %% arity A is returned. + %% In the special case where the macro `?MODULE` is used as the + %% module qualifier, we can consider it as a local call. + Operator = erl_syntax:application_operator(Tree), + case erl_syntax:type(Operator) of + module_qualifier -> application_with_variable(Operator, A); + variable -> {{variable, node_name(Operator)}, A}; + _ -> undefined + end + end. -spec application_with_variable(tree(), arity()) -> - {atom(), arity()} | undefined. + {{atom(), atom()}, {atom(), atom()}, arity()} + | {atom(), arity()} + | undefined. application_with_variable(Operator, A) -> - Module = erl_syntax:module_qualifier_argument(Operator), - Function = erl_syntax:module_qualifier_body(Operator), - case {erl_syntax:type(Module), erl_syntax:type(Function)} of - %% The usage of the ?MODULE macro as the module name for - %% fully qualified calls is so common that it is worth a - %% specific clause. - {macro, atom} -> - ModuleName = macro_name(Module), - FunctionName = node_name(Function), - case {ModuleName, FunctionName} of - {'MODULE', F} -> {F, A}; - _ -> undefined - end; - _ -> undefined - end. + Module = erl_syntax:module_qualifier_argument(Operator), + Function = erl_syntax:module_qualifier_body(Operator), + case {erl_syntax:type(Module), erl_syntax:type(Function)} of + %% The usage of the ?MODULE macro as the module name for + %% fully qualified calls is so common that it is worth a + %% specific clause. + {macro, atom} -> + ModuleName = macro_name(Module), + FunctionName = node_name(Function), + case {ModuleName, FunctionName} of + {'MODULE', F} -> {F, A}; + _ -> undefined + end; + {ModType, FunType} when + ModType =:= variable orelse ModType =:= atom, + FunType =:= variable orelse FunType =:= atom + -> + ModuleName = node_name(Module), + FunctionName = node_name(Function), + {{ModType, ModuleName}, {FunType, FunctionName}, A}; + _ -> + undefined + end. --spec attribute(tree()) -> [poi()]. +-spec attribute(tree()) -> [els_poi:poi()]. attribute(Tree) -> - Pos = erl_syntax:get_pos(Tree), - try {attribute_name_atom(Tree), erl_syntax:attribute_arguments(Tree)} of - %% Yes, Erlang allows both British and American spellings for - %% keywords. - {AttrName, [Arg]} when AttrName =:= behaviour; - AttrName =:= behavior -> - case is_atom_node(Arg) of - {true, Behaviour} -> - [poi(Pos, behaviour, Behaviour)]; - false -> - [] - end; - {module, [Module, _Args]} -> - case is_atom_node(Module) of - {true, ModuleName} -> - [poi(erl_syntax:get_pos(Module), module, ModuleName)]; - _ -> - [] - end; - {module, [Module]} -> - case is_atom_node(Module) of - {true, ModuleName} -> - [poi(erl_syntax:get_pos(Module), module, ModuleName)]; - _ -> - [] - end; - {compile, [Arg]} -> - %% When we encounter a compiler attribute, we include a POI - %% indicating the fact. This is useful, for example, to avoid - %% marking header files including parse transforms or other - %% compiler attributes as unused. See #1047 - Marker = poi(erl_syntax:get_pos(Arg), compile, unused), - [Marker|find_compile_options_pois(Arg)]; - {AttrName, [Arg]} when AttrName =:= export; - AttrName =:= export_type -> - find_export_pois(Tree, AttrName, Arg); - {import, [ModTree, ImportList]} -> - case is_atom_node(ModTree) of - {true, M} -> - Imports = erl_syntax:list_elements(ImportList), - find_import_entry_pois(M, Imports); - _ -> - [] - end; - {define, [Define|Value]} -> - DefinePos = case erl_syntax:type(Define) of + Pos = erl_syntax:get_pos(Tree), + try {attribute_name_atom(Tree), erl_syntax:attribute_arguments(Tree)} of + %% Yes, Erlang allows both British and American spellings for + %% keywords. + {AttrName, [Arg]} when + AttrName =:= behaviour; + AttrName =:= behavior + -> + case is_atom_node(Arg) of + {true, Behaviour} -> + Data = #{mod_range => els_range:range(erl_syntax:get_pos(Arg))}, + [poi(Pos, behaviour, Behaviour, Data)]; + false -> + [] + end; + {module, [Module, _Args]} -> + case is_atom_node(Module) of + {true, ModuleName} -> + [poi(erl_syntax:get_pos(Module), module, ModuleName)]; + _ -> + [] + end; + {module, [Module]} -> + case is_atom_node(Module) of + {true, ModuleName} -> + [poi(erl_syntax:get_pos(Module), module, ModuleName)]; + _ -> + [] + end; + {compile, [Arg]} -> + %% When we encounter a compiler attribute, we include a POI + %% indicating the fact. This is useful, for example, to avoid + %% marking header files including parse transforms or other + %% compiler attributes as unused. See #1047 + Marker = poi(erl_syntax:get_pos(Arg), compile, unused), + [Marker | find_compile_options_pois(Arg)]; + {AttrName, [Arg]} when + AttrName =:= export; + AttrName =:= export_type + -> + find_export_pois(Tree, AttrName, Arg); + {nifs, [Arg]} -> + Nifs = erl_syntax:list_elements(Arg), + NifsEntries = find_nifs_entry_pois(Nifs), + [ + poi(erl_syntax:get_pos(Tree), nifs, get_start_location(Tree)) + | NifsEntries + ]; + {import, [ModTree, ImportList]} -> + case is_atom_node(ModTree) of + {true, _} -> + Imports = erl_syntax:list_elements(ImportList), + find_import_entry_pois(ModTree, Imports); + _ -> + [] + end; + {define, [Define | Value]} -> + DefinePos = + case erl_syntax:type(Define) of application -> - Operator = erl_syntax:application_operator(Define), - erl_syntax:get_pos(Operator); + Operator = erl_syntax:application_operator(Define), + erl_syntax:get_pos(Operator); _ -> - erl_syntax:get_pos(Define) - end, - ValueRange = #{ from => get_start_location(hd(Value)) - , to => get_end_location(lists:last(Value)) - }, - Args = define_args(Define), - Data = #{value_range => ValueRange, args => Args}, - [poi(DefinePos, define, define_name(Define), Data)]; - {include, [String]} -> - [poi(Pos, include, erl_syntax:string_value(String))]; - {include_lib, [String]} -> - [poi(Pos, include_lib, erl_syntax:string_value(String))]; - {record, [Record, Fields]} -> - case is_record_name(Record) of - {true, RecordName} -> - record_attribute_pois(Tree, Record, RecordName, Fields); - false -> - [] - end; - {AttrName, [ArgTuple]} when AttrName =:= type; - AttrName =:= opaque -> - [Type, _, ArgsListTree] = erl_syntax:tuple_elements(ArgTuple), - TypeArgs = erl_syntax:list_elements(ArgsListTree), - case is_atom_node(Type) of - {true, TypeName} -> - Id = {TypeName, length(TypeArgs)}, - [poi(Pos, type_definition, Id, - #{ name_range => els_range:range(erl_syntax:get_pos(Type)), - args => type_args(TypeArgs)})]; + erl_syntax:get_pos(Define) + end, + ValueRange = #{ + from => get_start_location(hd(Value)), + to => get_end_location(lists:last(Value)) + }, + Args = define_args(Define), + Data = #{value_range => ValueRange, args => Args}, + [poi(DefinePos, define, define_name(Define), Data)]; + {include, [Node]} -> + include_pois(Pos, include, Node); + {include_lib, [Node]} -> + include_pois(Pos, include_lib, Node); + {record, [Record, Fields]} -> + case is_record_name(Record) of + {true, RecordName} -> + record_attribute_pois(Tree, Record, RecordName, Fields); + false -> + [] + end; + {AttrName, [ArgTuple]} when + AttrName =:= type; + AttrName =:= opaque + -> + [Type, _, ArgsListTree] = erl_syntax:tuple_elements(ArgTuple), + TypeArgs = erl_syntax:list_elements(ArgsListTree), + case is_atom_node(Type) of + {true, TypeName} -> + Id = {TypeName, length(TypeArgs)}, + [ + poi( + Pos, + type_definition, + Id, + #{ + name_range => els_range:range(erl_syntax:get_pos(Type)), + args => args_from_subtrees(TypeArgs) + } + ) + ]; + _ -> + [] + end; + {callback, [ArgTuple]} -> + [FATree | _] = erl_syntax:tuple_elements(ArgTuple), + case spec_function_name(FATree) of + {F, A} -> + [FTree, _] = erl_syntax:tuple_elements(FATree), + [ + poi( + Pos, + callback, + {F, A}, + #{name_range => els_range:range(erl_syntax:get_pos(FTree))} + ) + ]; + undefined -> + [] + end; + {spec, [ArgTuple]} -> + [FATree, SpecTree] = erl_syntax:tuple_elements(ArgTuple), + Args = get_spec_args(SpecTree), + case spec_function_name(FATree) of + {F, A} -> + [FTree, _] = erl_syntax:tuple_elements(FATree), + [ + poi( + Pos, + spec, + {F, A}, + #{ + args => Args, + name_range => els_range:range(erl_syntax:get_pos(FTree)) + } + ) + ]; + undefined -> + [poi(Pos, spec, undefined)] + end; + {Attribute, [{Type, Anno, Name}]} when + (Attribute =:= ifdef orelse + Attribute =:= ifndef orelse + Attribute =:= undef), + (Type =:= var orelse + Type =:= atom) + -> + poi(Anno, macro, Name); _ -> - [] - end; - {callback, [ArgTuple]} -> - [FATree | _] = erl_syntax:tuple_elements(ArgTuple), - case spec_function_name(FATree) of - {F, A} -> - [FTree, _] = erl_syntax:tuple_elements(FATree), - [poi(Pos, callback, {F, A}, - #{name_range => els_range:range(erl_syntax:get_pos(FTree))})]; - undefined -> - [] - end; - {spec, [ArgTuple]} -> - [FATree | _] = erl_syntax:tuple_elements(ArgTuple), - case spec_function_name(FATree) of - {F, A} -> - [FTree, _] = erl_syntax:tuple_elements(FATree), - [poi(Pos, spec, {F, A}, - #{name_range => els_range:range(erl_syntax:get_pos(FTree))})]; - undefined -> - [poi(Pos, spec, undefined)] - end; - _ -> - [] - catch throw:syntax_error -> - [] - end. - --spec record_attribute_pois(tree(), tree(), atom(), tree()) -> [poi()]. + [] + catch + throw:syntax_error:St -> + ?LOG_INFO("Syntax error: ~p", [St]), + [] + end. + +-spec get_spec_args(tree()) -> els_arg:args(). +get_spec_args(Tree) -> + %% Just fetching from the first spec clause for simplicity + [SpecArg | _] = erl_syntax:list_elements(Tree), + do_get_spec_args(SpecArg). + +-spec do_get_spec_args(tree()) -> els_arg:args(). +do_get_spec_args(Tree) -> + case erl_syntax:type(Tree) of + constrained_function_type -> + Body = erl_syntax:constrained_function_type_body(Tree), + do_get_spec_args(Body); + function_type -> + TypeArgs = erl_syntax:function_type_arguments(Tree), + args_from_subtrees(TypeArgs); + _OtherType -> + [] + end. + +-spec record_attribute_pois(tree(), tree(), atom(), tree()) -> [els_poi:poi()]. record_attribute_pois(Tree, Record, RecordName, Fields) -> - FieldList = record_def_field_name_list(Fields), - ValueRange = #{ from => get_start_location(Tree), - to => get_end_location(Tree)}, - Data = #{field_list => FieldList, value_range => ValueRange}, - [poi(erl_syntax:get_pos(Record), record, RecordName, Data) - | record_def_fields(Fields, RecordName)]. - --spec find_compile_options_pois(tree()) -> [poi()]. + FieldList = record_def_field_name_list(Fields), + {StartLine, StartColumn} = get_start_location(Tree), + {EndLine, EndColumn} = get_end_location(Tree), + ValueRange = #{ + from => {StartLine, StartColumn}, + to => {EndLine, EndColumn} + }, + FoldingRange = exceeds_one_line(StartLine, EndLine), + Data = #{ + field_list => FieldList, + value_range => ValueRange, + folding_range => FoldingRange + }, + [ + poi(erl_syntax:get_pos(Record), record, RecordName, Data) + | record_def_fields(Fields, RecordName) + ]. + +-spec find_compile_options_pois(tree()) -> [els_poi:poi()]. find_compile_options_pois(Arg) -> - case erl_syntax:type(Arg) of - list -> - L = erl_syntax:list_elements(Arg), - lists:flatmap(fun find_compile_options_pois/1, L); - tuple -> - case erl_syntax:tuple_elements(Arg) of - [K, V] -> - case {is_atom_node(K), is_atom_node(V)} of - {{true, parse_transform}, {true, PT}} -> - [poi(erl_syntax:get_pos(V), parse_transform, PT)]; - _ -> - [] - end; + case erl_syntax:type(Arg) of + list -> + L = erl_syntax:list_elements(Arg), + lists:flatmap(fun find_compile_options_pois/1, L); + tuple -> + case erl_syntax:tuple_elements(Arg) of + [K, V] -> + case {is_atom_node(K), is_atom_node(V)} of + {{true, parse_transform}, {true, PT}} -> + [poi(erl_syntax:get_pos(V), parse_transform, PT)]; + _ -> + [] + end; + _ -> + [] + end; + atom -> + %% currently there is no atom compile option that we are interested in + []; _ -> - [] - end; - atom -> - %% currently there is no atom compile option that we are interested in - []; - _ -> - [] - end. - --spec find_export_pois(tree(), export | export_type, tree()) -> [poi()]. + [] + end. + +-spec find_export_pois(tree(), export | export_type, tree()) -> [els_poi:poi()]. find_export_pois(Tree, AttrName, Arg) -> - Exports = erl_syntax:list_elements(Arg), - EntryPoiKind = case AttrName of - export -> export_entry; - export_type -> export_type_entry - end, - ExportEntries = find_export_entry_pois(EntryPoiKind, Exports), - [ poi(erl_syntax:get_pos(Tree), AttrName, get_start_location(Tree)) - | ExportEntries ]. - --spec find_export_entry_pois(export_entry | export_type_entry, [tree()]) - -> [poi()]. + Exports = erl_syntax:list_elements(Arg), + EntryPoiKind = + case AttrName of + export -> export_entry; + export_type -> export_type_entry + end, + ExportEntries = find_export_entry_pois(EntryPoiKind, Exports), + [ + poi(erl_syntax:get_pos(Tree), AttrName, get_start_location(Tree)) + | ExportEntries + ]. + +-spec find_export_entry_pois(export_entry | export_type_entry, [tree()]) -> + [els_poi:poi()]. find_export_entry_pois(EntryPoiKind, Exports) -> - lists:flatten( - [ case get_name_arity(FATree) of - {F, A} -> - FTree = erl_syntax:arity_qualifier_body(FATree), - poi(erl_syntax:get_pos(FATree), EntryPoiKind, {F, A}, - #{name_range => els_range:range(erl_syntax:get_pos(FTree))}); - false -> - [] - end - || FATree <- Exports - ]). - --spec find_import_entry_pois(atom(), [tree()]) -> [poi()]. -find_import_entry_pois(M, Imports) -> - lists:flatten( - [ case get_name_arity(FATree) of - {F, A} -> - FTree = erl_syntax:arity_qualifier_body(FATree), - poi(erl_syntax:get_pos(FATree), import_entry, {M, F, A}, - #{name_range => els_range:range(erl_syntax:get_pos(FTree))}); - false -> - [] - end - || FATree <- Imports - ]). + lists:flatten( + [ + case get_name_arity(FATree) of + {F, A} -> + FTree = erl_syntax:arity_qualifier_body(FATree), + poi( + erl_syntax:get_pos(FATree), + EntryPoiKind, + {F, A}, + #{name_range => els_range:range(erl_syntax:get_pos(FTree))} + ); + false -> + [] + end + || FATree <- Exports + ] + ). + +-spec find_nifs_entry_pois([tree()]) -> + [els_poi:poi()]. +find_nifs_entry_pois(Nifs) -> + lists:flatten( + [ + case get_name_arity(FATree) of + {F, A} -> + FTree = erl_syntax:arity_qualifier_body(FATree), + poi( + erl_syntax:get_pos(FATree), + nifs_entry, + {F, A}, + #{name_range => els_range:range(erl_syntax:get_pos(FTree))} + ); + false -> + [] + end + || FATree <- Nifs + ] + ). + +-spec find_import_entry_pois(tree(), [tree()]) -> [els_poi:poi()]. +find_import_entry_pois(ModTree, Imports) -> + M = erl_syntax:atom_value(ModTree), + lists:flatten( + [ + case get_name_arity(FATree) of + {F, A} -> + FTree = erl_syntax:arity_qualifier_body(FATree), + Data = #{ + name_range => els_range:range(erl_syntax:get_pos(FTree)), + mod_range => els_range:range(erl_syntax:get_pos(ModTree)) + }, + poi(erl_syntax:get_pos(FATree), import_entry, {M, F, A}, Data); + false -> + [] + end + || FATree <- Imports + ] + ). -spec spec_function_name(tree()) -> {atom(), arity()} | undefined. spec_function_name(FATree) -> - %% concrete will throw an error if `FATree' contains any macro - try erl_syntax:concrete(FATree) of - {F, A} -> {F, A}; - _ -> undefined - catch _:_ -> - undefined - end. - --spec type_args([tree()]) -> [{integer(), string()}]. -type_args(Args) -> - [ case erl_syntax:type(T) of - variable -> {N, erl_syntax:variable_literal(T)}; - _ -> {N, "Type" ++ integer_to_list(N)} - end - || {N, T} <- lists:zip(lists:seq(1, length(Args)), Args) - ]. - --spec function(tree()) -> [poi()]. + %% concrete will throw an error if `FATree' contains any macro + try erl_syntax:concrete(FATree) of + {F, A} -> {F, A}; + _ -> undefined + catch + _:_ -> + undefined + end. + +-spec function(tree()) -> [els_poi:poi()]. function(Tree) -> - FunName = erl_syntax:function_name(Tree), - Clauses = erl_syntax:function_clauses(Tree), - {F, A, Args} = analyze_function(FunName, Clauses), - - IndexedClauses = lists:zip(lists:seq(1, length(Clauses)), Clauses), - %% FIXME function_clause range should be the range of the name atom however - %% that is not present in the clause Tree (it is in the erlfmt_parse node) - ClausesPOIs = [ poi( get_start_location(Clause) - , function_clause - , {F, A, I} - , pretty_print_clause(Clause) - ) - || {I, Clause} <- IndexedClauses, - erl_syntax:type(Clause) =:= clause], - {StartLine, StartColumn} = StartLocation = get_start_location(Tree), - {EndLine, _EndColumn} = get_end_location(Tree), - %% It only makes sense to fold a function if the function contains - %% at least one line apart from its signature. - FoldingRanges = case EndLine > StartLine of - true -> - Range = #{ from => {StartLine, ?END_OF_LINE} - , to => {EndLine, ?END_OF_LINE} - }, - [ els_poi:new(Range, folding_range, StartLocation) ]; - false -> - [] - end, - FunctionPOI = poi(erl_syntax:get_pos(FunName), function, {F, A}, - #{ args => Args - , wrapping_range => #{ from => {StartLine, StartColumn} - , to => {EndLine + 1, 0} - } - }), - lists:append([ [ FunctionPOI ] - , FoldingRanges - , ClausesPOIs - ]). + FunName = erl_syntax:function_name(Tree), + Clauses = erl_syntax:function_clauses(Tree), + {F, A, Args} = analyze_function(FunName, Clauses), + + IndexedClauses = lists:zip(lists:seq(1, length(Clauses)), Clauses), + %% FIXME function_clause range should be the range of the name atom however + %% that is not present in the clause Tree (it is in the erlfmt_parse node) + ClausesPOIs = [ + poi( + get_start_location(Clause), + function_clause, + {F, A, I}, + pretty_print_clause(Clause) + ) + || {I, Clause} <- IndexedClauses, + erl_syntax:type(Clause) =:= clause + ], + {StartLine, StartColumn} = get_start_location(Tree), + {EndLine, EndColumn} = get_end_location(Tree), + FoldingRange = exceeds_one_line(StartLine, EndLine), + FunctionPOI = poi( + erl_syntax:get_pos(FunName), + function, + {F, A}, + #{ + args => Args, + wrapping_range => #{ + from => {StartLine, StartColumn}, + to => {EndLine + 1, 0} + }, + symbol_range => #{ + from => {StartLine, StartColumn}, + to => {EndLine, EndColumn} + }, + folding_range => FoldingRange + } + ), + lists:append([ + [FunctionPOI], + ClausesPOIs + ]). -spec analyze_function(tree(), [tree()]) -> - {atom(), arity(), [{integer(), string()}]}. -analyze_function(FunName, Clauses) -> - F = case is_atom_node(FunName) of - {true, FAtom} -> FAtom; - false -> throw(syntax_error) - end, - - case lists:dropwhile(fun(T) -> erl_syntax:type(T) =/= clause end, Clauses) of - [Clause | _] -> - {Arity, Args} = function_args(Clause), - {F, Arity, Args}; - [] -> - throw(syntax_error) - end. - --spec function_args(tree()) -> {arity(), [{integer(), string()}]}. -function_args(Clause) -> - Patterns = erl_syntax:clause_patterns(Clause), - Arity = length(Patterns), - Args = args_from_subtrees(Patterns), - {Arity, Args}. + {atom(), arity(), els_arg:args()}. +analyze_function(FunName, Clauses0) -> + F = + case is_atom_node(FunName) of + {true, FAtom} -> FAtom; + false -> throw(syntax_error) + end, + case lists:dropwhile(fun(T) -> erl_syntax:type(T) =/= clause end, Clauses0) of + [] -> + throw(syntax_error); + Clauses -> + %% Extract args from clauses and choose the best + FunArgs = [function_args(Clause) || Clause <- Clauses], + SortedFunArgs = lists:sort( + fun({_, A1}, {_, A2}) -> + %% Sort by count of undefined names + A1Count = lists:sum([1 || #{name := undefined} <- A1]), + A2Count = lists:sum([1 || #{name := undefined} <- A2]), + A1Count =< A2Count + end, + FunArgs + ), + %% The first one in the list should have the least undefined names. + %% So it should be the "best". + {Arity, Args} = hd(SortedFunArgs), + {F, Arity, Args} + end. + +-spec function_args(tree()) -> {arity(), els_arg:args()}. +function_args(Clause) -> + Patterns = erl_syntax:clause_patterns(Clause), + Arity = length(Patterns), + Args = args_from_subtrees(Patterns), + {Arity, Args}. --spec args_from_subtrees([tree()]) -> [{integer(), string()}]. +-spec args_from_subtrees([tree()]) -> els_arg:args(). args_from_subtrees(Trees) -> - Arity = length(Trees), - [ case erl_syntax:type(T) of - %% TODO: Handle literals - variable -> {N, erl_syntax:variable_literal(T)}; - _ -> {N, "Arg" ++ integer_to_list(N)} - end - || {N, T} <- lists:zip(lists:seq(1, Arity), Trees) - ]. - --spec implicit_fun(tree()) -> [poi()]. + Arity = length(Trees), + [ + #{ + index => N, + name => extract_variable(T), + range => els_range:range(erl_syntax:get_pos(T)) + } + || {N, T} <- lists:zip(lists:seq(1, Arity), Trees) + ]. + +-spec extract_variable(tree()) -> + string() | undefined | {type, string() | undefined}. +extract_variable(T) -> + case erl_syntax:type(T) of + %% TODO: Handle literals + variable -> + erl_syntax:variable_literal(T); + match_expr -> + Body = erl_syntax:match_expr_body(T), + Pattern = erl_syntax:match_expr_pattern(T), + case {extract_variable(Pattern), extract_variable(Body)} of + {undefined, Result} -> + Result; + {Result, _} -> + Result + end; + record_expr -> + RecordNode = erl_syntax:record_expr_type(T), + atom_to_name(RecordNode); + annotated_type -> + TypeName = erl_syntax:annotated_type_name(T), + case erl_syntax:type(TypeName) of + variable -> + erl_syntax:variable_literal(TypeName); + _ -> + undefined + end; + type_application -> + TypeName = erl_syntax:type_application_name(T), + case erl_syntax:type(TypeName) of + atom -> + {type, atom_to_name(TypeName)}; + module_qualifier -> + Fun = erl_syntax:module_qualifier_body(TypeName), + {type, atom_to_name(Fun)}; + _ -> + undefined + end; + user_type_application -> + TypeName = erl_syntax:user_type_application_name(T), + {type, atom_to_name(TypeName)}; + list -> + try erl_syntax:list_elements(T) of + [H | _] -> + case extract_variable(H) of + undefined -> + undefined; + {type, Name} when is_list(Name) -> + {type, Name ++ "s"}; + Name when is_list(Name) -> + Name ++ "s"; + Name -> + Name + end; + _ -> + undefined + catch + error:_ -> + undefined + end; + record_type -> + TypeName = erl_syntax:record_type_name(T), + {type, atom_to_name(TypeName)}; + Type -> + ?LOG_DEBUG("Unknown type: ~p", [Type]), + undefined + end. + +-spec atom_to_name(tree()) -> string() | undefined. +atom_to_name(T) -> + case erl_syntax:type(T) of + atom -> + NameAtom = erl_syntax:atom_value(T), + NameBin = els_utils:camel_case(atom_to_binary(NameAtom, utf8)), + unicode:characters_to_list(NameBin); + _ -> + undefined + end. + +-spec implicit_fun(tree()) -> [els_poi:poi()]. implicit_fun(Tree) -> - FunSpec = try erl_syntax_lib:analyze_implicit_fun(Tree) of - {M, {F, A}} -> {M, F, A}; - {F, A} -> {F, A} - catch throw:syntax_error -> + FunSpec = + try erl_syntax_lib:analyze_implicit_fun(Tree) of + {M, {F, A}} -> {M, F, A}; + {F, A} -> {F, A} + catch + throw:syntax_error -> undefined - end, - case FunSpec of - undefined -> []; - _ -> - NameTree = erl_syntax:implicit_fun_name(Tree), - FunTree = - case FunSpec of - {_, _, _} -> - erl_syntax:arity_qualifier_body( - erl_syntax:module_qualifier_body(NameTree)); - {_, _} -> - erl_syntax:arity_qualifier_body(NameTree) end, - [poi(erl_syntax:get_pos(Tree), implicit_fun, FunSpec, - #{name_range => els_range:range(erl_syntax:get_pos(FunTree))})] - end. --spec macro(tree()) -> [poi()]. + case FunSpec of + undefined -> + NameTree = erl_syntax:implicit_fun_name(Tree), + case try_analyze_implicit_fun(Tree) of + {{ModType, Mod}, {FunType, Function}, Arity} -> + ModTree = erl_syntax:module_qualifier_argument(NameTree), + FunTree = erl_syntax:arity_qualifier_body( + erl_syntax:module_qualifier_body(NameTree) + ), + Data = #{ + name_range => els_range:range(erl_syntax:get_pos(FunTree)), + mod_range => els_range:range(erl_syntax:get_pos(ModTree)), + mod_is_variable => ModType =:= variable, + fun_is_variable => FunType =:= variable + }, + [poi(erl_syntax:get_pos(Tree), implicit_fun, {Mod, Function, Arity}, Data)]; + {Function, Arity} -> + ModTree = erl_syntax:module_qualifier_argument(NameTree), + FunTree = erl_syntax:arity_qualifier_body( + erl_syntax:module_qualifier_body(NameTree) + ), + Data = #{ + name_range => els_range:range(erl_syntax:get_pos(FunTree)), + mod_range => els_range:range(erl_syntax:get_pos(ModTree)) + }, + [poi(erl_syntax:get_pos(Tree), implicit_fun, {Function, Arity}, Data)]; + _ -> + [] + end; + _ -> + NameTree = erl_syntax:implicit_fun_name(Tree), + Data = + case FunSpec of + {_, _, _} -> + ModTree = erl_syntax:module_qualifier_argument(NameTree), + FunTree = erl_syntax:arity_qualifier_body( + erl_syntax:module_qualifier_body(NameTree) + ), + #{ + name_range => els_range:range(erl_syntax:get_pos(FunTree)), + mod_range => els_range:range(erl_syntax:get_pos(ModTree)) + }; + {_, _} -> + FunTree = erl_syntax:arity_qualifier_body(NameTree), + #{name_range => els_range:range(erl_syntax:get_pos(FunTree))} + end, + [poi(erl_syntax:get_pos(Tree), implicit_fun, FunSpec, Data)] + end. + +-spec try_analyze_implicit_fun(tree()) -> + {{atom(), atom()}, {atom(), atom()}, arity()} + | {atom(), arity()} + | undefined. +try_analyze_implicit_fun(Tree) -> + FunName = erl_syntax:implicit_fun_name(Tree), + case erl_syntax:type(FunName) of + module_qualifier -> + ModQBody = erl_syntax:module_qualifier_body(FunName), + ModQArg = erl_syntax:module_qualifier_argument(FunName), + case erl_syntax:type(ModQBody) of + arity_qualifier -> + try_analyze_arity_qualifier(ModQBody, ModQArg); + _Type -> + undefined + end; + _Type -> + undefined + end. + +-spec try_analyze_arity_qualifier(tree(), tree()) -> + {{atom(), atom()}, {atom(), atom()}, arity()} + | {atom(), arity()} + | undefined. +try_analyze_arity_qualifier(ModQBody, ModQArg) -> + AqBody = erl_syntax:arity_qualifier_body(ModQBody), + AqArg = erl_syntax:arity_qualifier_argument(ModQBody), + case {erl_syntax:type(ModQArg), erl_syntax:type(AqBody), erl_syntax:type(AqArg)} of + {macro, atom, integer} -> + M = erl_syntax:variable_name(erl_syntax:macro_name(ModQArg)), + F = erl_syntax:atom_value(AqBody), + A = erl_syntax:integer_value(AqArg), + case M of + 'MODULE' -> + {F, A}; + _ -> + undefined + end; + {ModType, FunType, integer} when + ModType =:= variable orelse ModType =:= atom, + FunType =:= variable orelse FunType =:= atom + -> + M = node_name(ModQArg), + F = node_name(AqBody), + A = erl_syntax:integer_value(AqArg), + {{ModType, M}, {FunType, F}, A}; + _Types -> + undefined + end. + +-spec macro(tree()) -> [els_poi:poi()]. macro(Tree) -> - Anno = macro_location(Tree), - [poi(Anno, macro, macro_name(Tree))]. - --spec map_record_def_fields(Fun, tree(), atom()) -> [Result] - when Fun :: fun((tree(), atom()) -> Result). + Anno = macro_location(Tree), + [poi(Anno, macro, macro_name(Tree))]. + +-spec list_comp(tree()) -> [els_poi:poi()]. +list_comp(Tree) -> + Pos = erl_syntax:get_pos(Tree), + Body = erl_syntax:list_comp_body(Tree), + PatRanges = [ + els_range:range( + erl_syntax:get_pos( + erl_syntax:generator_pattern(Gen) + ) + ) + || Gen <- Body, + erl_syntax:type(Gen) =:= generator + ], + [poi(Pos, list_comp, undefined, #{pattern_ranges => PatRanges})]. + +-spec map_record_def_fields(Fun, tree(), atom()) -> [Result] when + Fun :: fun((tree(), atom()) -> Result). map_record_def_fields(Fun, Fields, RecordName) -> - case erl_syntax:type(Fields) of - tuple -> - lists:append( - [ case erl_syntax:type(FieldTree) of - record_field -> - FieldNode = FieldTree, - Fun(FieldNode, RecordName); - typed_record_field -> - FieldNode = erl_syntax:typed_record_field_body(FieldTree), - Fun(FieldNode, RecordName); - _ -> - [] - end - || FieldTree <- erl_syntax:tuple_elements(Fields) - ]); - _ -> - [] - end. + case erl_syntax:type(Fields) of + tuple -> + lists:append( + [ + case erl_syntax:type(FieldTree) of + record_field -> + FieldNode = FieldTree, + Fun(FieldNode, RecordName); + typed_record_field -> + FieldNode = erl_syntax:typed_record_field_body(FieldTree), + Fun(FieldNode, RecordName); + _ -> + [] + end + || FieldTree <- erl_syntax:tuple_elements(Fields) + ] + ); + _ -> + [] + end. %% Fields with macro name are skipped -spec record_def_field_name_list(tree()) -> [atom()]. record_def_field_name_list(Fields) -> - map_record_def_fields( - fun(FieldNode, _) -> - FieldName = erl_syntax:record_field_name(FieldNode), - case is_atom_node(FieldName) of - {true, NameAtom} -> - [NameAtom]; - false -> - [] - end - end, - Fields, - undefined). + map_record_def_fields( + fun(FieldNode, _) -> + FieldName = erl_syntax:record_field_name(FieldNode), + case is_atom_node(FieldName) of + {true, NameAtom} -> + [NameAtom]; + false -> + [] + end + end, + Fields, + undefined + ). --spec record_def_fields(tree(), atom()) -> [poi()]. +-spec record_def_fields(tree(), atom()) -> [els_poi:poi()]. record_def_fields(Fields, RecordName) -> - map_record_def_fields( - fun(F, R) -> - record_field_name(F, R, record_def_field) - end, - Fields, - RecordName). - --spec record_access(tree()) -> [poi()]. + map_record_def_fields( + fun(F, R) -> + record_field_name(F, R, record_def_field) + end, + Fields, + RecordName + ). + +-spec record_access(tree()) -> [els_poi:poi()]. record_access(Tree) -> - RecordNode = erl_syntax:record_access_type(Tree), - case is_record_name(RecordNode) of - {true, Record} -> - record_access_pois(Tree, Record); - false -> - [] - end. - --spec record_access_pois(tree(), atom()) -> [poi()]. + RecordNode = erl_syntax:record_access_type(Tree), + case is_record_name(RecordNode) of + {true, Record} -> + record_access_pois(Tree, Record); + false -> + [] + end. + +-spec record_access_pois(tree(), atom()) -> [els_poi:poi()]. record_access_pois(Tree, Record) -> - FieldNode = erl_syntax:record_access_field(Tree), - FieldPoi = - case is_atom_node(FieldNode) of - {true, FieldName} -> - [poi(erl_syntax:get_pos(FieldNode), record_field, {Record, FieldName})]; - _ -> - [] - end, - Anno = record_access_location(Tree), - [ poi(Anno, record_expr, Record) - | FieldPoi ]. - --spec record_expr(tree()) -> [poi()]. + FieldNode = erl_syntax:record_access_field(Tree), + FieldPoi = + case is_atom_node(FieldNode) of + {true, FieldName} -> + [poi(erl_syntax:get_pos(FieldNode), record_field, {Record, FieldName})]; + _ -> + [] + end, + Anno = record_access_location(Tree), + [ + poi(Anno, record_expr, Record) + | FieldPoi + ]. + +-spec record_index_expr(tree()) -> [els_poi:poi()]. +record_index_expr(Tree) -> + RecordNode = erl_syntax:record_index_expr_type(Tree), + case is_record_name(RecordNode) of + {true, Record} -> + record_index_expr_pois(Tree, Record); + false -> + [] + end. + +-spec record_index_expr_pois(tree(), atom()) -> [els_poi:poi()]. +record_index_expr_pois(Tree, Record) -> + FieldNode = erl_syntax:record_index_expr_field(Tree), + FieldPoi = + case is_atom_node(FieldNode) of + {true, FieldName} -> + [poi(erl_syntax:get_pos(FieldNode), record_field, {Record, FieldName})]; + _ -> + [] + end, + Anno = record_index_expr_location(Tree), + [poi(Anno, record_expr, Record) | FieldPoi]. + +-spec record_expr(tree()) -> [els_poi:poi()]. record_expr(Tree) -> - RecordNode = erl_syntax:record_expr_type(Tree), - case is_record_name(RecordNode) of - {true, Record} -> - record_expr_pois(Tree, RecordNode, Record); - false -> - [] - end. - --spec record_expr_pois(tree(), tree(), atom()) -> [poi()]. + RecordNode = erl_syntax:record_expr_type(Tree), + case is_record_name(RecordNode) of + {true, Record} -> + record_expr_pois(Tree, RecordNode, Record); + false -> + [] + end. + +-spec record_expr_pois(tree(), tree(), atom()) -> [els_poi:poi()]. record_expr_pois(Tree, RecordNode, Record) -> - FieldPois = lists:append( - [record_field_name(F, Record, record_field) - || F <- erl_syntax:record_expr_fields(Tree)]), - Anno = record_expr_location(Tree, RecordNode), - [ poi(Anno, record_expr, Record) - | FieldPois ]. - --spec record_type(tree()) -> [poi()]. + FieldPois = lists:append( + [ + record_field_name(F, Record, record_field) + || F <- erl_syntax:record_expr_fields(Tree) + ] + ), + Anno = record_expr_location(Tree, RecordNode), + [ + poi(Anno, record_expr, Record) + | FieldPois + ]. + +-spec record_type(tree()) -> [els_poi:poi()]. record_type(Tree) -> - RecordNode = erl_syntax:record_type_name(Tree), - case is_record_name(RecordNode) of - {true, Record} -> - record_type_pois(Tree, RecordNode, Record); - false -> - [] - end. - --spec record_type_pois(tree(), tree(), atom()) -> [poi()]. + RecordNode = erl_syntax:record_type_name(Tree), + case is_record_name(RecordNode) of + {true, Record} -> + record_type_pois(Tree, RecordNode, Record); + false -> + [] + end. + +-spec record_type_pois(tree(), tree(), atom()) -> [els_poi:poi()]. record_type_pois(Tree, RecordNode, Record) -> - FieldPois = lists:append( - [record_field_name(F, Record, record_field) - || F <- erl_syntax:record_type_fields(Tree)]), - Anno = record_expr_location(Tree, RecordNode), - [ poi(Anno, record_expr, Record) - | FieldPois ]. - --spec record_field_name(tree(), atom(), poi_kind()) -> [poi()]. + FieldPois = lists:append( + [ + record_field_name(F, Record, record_field) + || F <- erl_syntax:record_type_fields(Tree) + ] + ), + Anno = record_expr_location(Tree, RecordNode), + [ + poi(Anno, record_expr, Record) + | FieldPois + ]. + +-spec record_field_name(tree(), atom(), els_poi:poi_kind()) -> [els_poi:poi()]. record_field_name(FieldNode, Record, Kind) -> - NameNode = - case erl_syntax:type(FieldNode) of - record_field -> - erl_syntax:record_field_name(FieldNode); - record_type_field -> - erl_syntax:record_type_field_name(FieldNode) - end, - case is_atom_node(NameNode) of - {true, NameAtom} -> - Pos = erl_syntax:get_pos(NameNode), - [poi(Pos, Kind, {Record, NameAtom})]; - _ -> - [] - end. + NameNode = + case erl_syntax:type(FieldNode) of + record_field -> + erl_syntax:record_field_name(FieldNode); + record_type_field -> + erl_syntax:record_type_field_name(FieldNode); + comment -> + undefined + end, + case is_atom_node(NameNode) of + {true, NameAtom} -> + Pos = erl_syntax:get_pos(NameNode), + [poi(Pos, Kind, {Record, NameAtom})]; + _ -> + [] + end. -spec is_record_name(tree()) -> {true, atom()} | false. is_record_name(RecordNameNode) -> - case erl_syntax:type(RecordNameNode) of - atom -> - NameAtom = erl_syntax:atom_value(RecordNameNode), - {true, NameAtom}; - macro -> - case macro_name(RecordNameNode) of - 'MODULE' -> - %% [#1052] Let's handle the common ?MODULE macro case explicitly - {true, '?MODULE'}; + case erl_syntax:type(RecordNameNode) of + atom -> + NameAtom = erl_syntax:atom_value(RecordNameNode), + {true, NameAtom}; + macro -> + case macro_name(RecordNameNode) of + 'MODULE' -> + %% [#1052] Let's handle the common ?MODULE macro case explicitly + {true, '?MODULE'}; + _ -> + false + end; _ -> - false - end; - _ -> - false - end. + false + end. --spec type_application(tree()) -> [poi()]. +-spec type_application(tree()) -> [els_poi:poi()]. type_application(Tree) -> - Type = erl_syntax:type(Tree), - case erl_syntax_lib:analyze_type_application(Tree) of - {Module, {Name, Arity}} -> - %% remote type - Id = {Module, Name, Arity}, - ModTypeTree = erl_syntax:type_application_name(Tree), - Pos = erl_syntax:get_pos(ModTypeTree), - TypeTree = erl_syntax:module_qualifier_body(ModTypeTree), - [poi(Pos, type_application, Id, - #{name_range => els_range:range(erl_syntax:get_pos(TypeTree))})]; - {Name, Arity} when Type =:= user_type_application -> - %% user-defined local type - Id = {Name, Arity}, - Pos = erl_syntax:get_pos(erl_syntax:user_type_application_name(Tree)), - [poi(Pos, type_application, Id)]; - {Name, Arity} when Type =:= type_application -> - %% Built-in types - Id = {erlang, Name, Arity}, - Pos = erl_syntax:get_pos(erl_syntax:type_application_name(Tree)), - [poi(Pos, type_application, Id)] - end. - --spec variable(tree()) -> [poi()]. + Type = erl_syntax:type(Tree), + case erl_syntax_lib:analyze_type_application(Tree) of + {Module, {Name, Arity}} -> + %% remote type + Id = {Module, Name, Arity}, + ModTypeTree = erl_syntax:type_application_name(Tree), + Pos = erl_syntax:get_pos(ModTypeTree), + TypeTree = erl_syntax:module_qualifier_body(ModTypeTree), + ModTree = erl_syntax:module_qualifier_argument(ModTypeTree), + Data = #{ + name_range => els_range:range(erl_syntax:get_pos(TypeTree)), + mod_range => els_range:range(erl_syntax:get_pos(ModTree)) + }, + [poi(Pos, type_application, Id, Data)]; + {Name, Arity} when Type =:= user_type_application -> + %% user-defined local type + Id = {Name, Arity}, + Pos = erl_syntax:get_pos(erl_syntax:user_type_application_name(Tree)), + [poi(Pos, type_application, Id)]; + {Name, Arity} when Type =:= type_application -> + %% Built-in types + Id = {erlang, Name, Arity}, + Pos = erl_syntax:get_pos(erl_syntax:type_application_name(Tree)), + [poi(Pos, type_application, Id)] + end. + +-spec variable(tree()) -> [els_poi:poi()]. variable(Tree) -> - Pos = erl_syntax:get_pos(Tree), - case Pos of - 0 -> []; - _ -> [poi(Pos, variable, node_name(Tree))] - end. + Pos = erl_syntax:get_pos(Tree), + case Pos of + 0 -> []; + _ -> [poi(Pos, variable, node_name(Tree))] + end. --spec atom(tree()) -> [poi()]. +-spec atom(tree()) -> [els_poi:poi()]. atom(Tree) -> - Pos = erl_syntax:get_pos(Tree), - case Pos of - 0 -> []; - _ -> [poi(Pos, atom, node_name(Tree))] - end. + Pos = erl_syntax:get_pos(Tree), + case Pos of + 0 -> []; + _ -> [poi(Pos, atom, node_name(Tree))] + end. -spec define_name(tree()) -> atom(). define_name(Tree) -> - case erl_syntax:type(Tree) of - application -> - Operator = erl_syntax:application_operator(Tree), - Args = erl_syntax:application_arguments(Tree), - macro_name(Operator, Args); - variable -> - erl_syntax:variable_name(Tree); - atom -> - erl_syntax:atom_value(Tree); - underscore -> - '_' - end. - --spec define_args(tree()) -> none | [{integer(), string()}]. + case erl_syntax:type(Tree) of + application -> + Operator = erl_syntax:application_operator(Tree), + Args = erl_syntax:application_arguments(Tree), + macro_name(Operator, Args); + variable -> + erl_syntax:variable_name(Tree); + atom -> + erl_syntax:atom_value(Tree); + underscore -> + '_' + end. + +-spec define_args(tree()) -> none | els_arg:args(). define_args(Define) -> - case erl_syntax:type(Define) of - application -> - Args = erl_syntax:application_arguments(Define), - args_from_subtrees(Args); - _ -> - none - end. + case erl_syntax:type(Define) of + application -> + Args = erl_syntax:application_arguments(Define), + args_from_subtrees(Args); + _ -> + none + end. -spec node_name(tree()) -> atom(). node_name(Tree) -> - case erl_syntax:type(Tree) of - atom -> - erl_syntax:atom_value(Tree); - variable -> - erl_syntax:variable_name(Tree); - underscore -> - '_' - end. + case erl_syntax:type(Tree) of + atom -> + erl_syntax:atom_value(Tree); + variable -> + erl_syntax:variable_name(Tree); + underscore -> + '_' + end. -spec macro_name(tree()) -> atom() | {atom(), non_neg_integer()}. macro_name(Tree) -> - macro_name(erl_syntax:macro_name(Tree), erl_syntax:macro_arguments(Tree)). + macro_name(erl_syntax:macro_name(Tree), erl_syntax:macro_arguments(Tree)). -spec macro_name(tree(), [tree()] | none) -> - atom() | {atom(), non_neg_integer()}. + atom() | {atom(), non_neg_integer()}. macro_name(Name, none) -> node_name(Name); macro_name(Name, Args) -> {node_name(Name), length(Args)}. --spec is_atom_node(tree()) -> {true, atom()} | false. +-spec is_atom_node(tree() | undefined) -> {true, atom()} | false. +is_atom_node(undefined) -> + false; is_atom_node(Tree) -> - case erl_syntax:type(Tree) of - atom -> - {true, erl_syntax:atom_value(Tree)}; - _ -> - false - end. + case erl_syntax:type(Tree) of + atom -> + {true, erl_syntax:atom_value(Tree)}; + _ -> + false + end. -spec get_name_arity(tree()) -> {atom(), integer()} | false. get_name_arity(Tree) -> - case erl_syntax:type(Tree) of - arity_qualifier -> - A = erl_syntax:arity_qualifier_argument(Tree), - case erl_syntax:type(A) of - integer -> - F = erl_syntax:arity_qualifier_body(Tree), - case is_atom_node(F) of - {true, Name} -> - Arity = erl_syntax:integer_value(A), - {Name, Arity}; - false -> - false - end; + case erl_syntax:type(Tree) of + arity_qualifier -> + A = erl_syntax:arity_qualifier_argument(Tree), + case erl_syntax:type(A) of + integer -> + F = erl_syntax:arity_qualifier_body(Tree), + case is_atom_node(F) of + {true, Name} -> + Arity = erl_syntax:integer_value(A), + {Name, Arity}; + false -> + false + end; + _ -> + false + end; _ -> - false - end; - _ -> - false - end. + false + end. --spec poi(pos() | {pos(), pos()} | erl_anno:anno(), poi_kind(), any()) -> poi(). +-spec poi(pos() | {pos(), pos()} | erl_anno:anno(), els_poi:poi_kind(), any()) -> + els_poi:poi(). poi(Pos, Kind, Id) -> - poi(Pos, Kind, Id, undefined). + poi(Pos, Kind, Id, undefined). --spec poi(pos() | {pos(), pos()} | erl_anno:anno(), poi_kind(), any(), any()) -> - poi(). +-spec poi(pos() | {pos(), pos()} | erl_anno:anno(), els_poi:poi_kind(), any(), any()) -> + els_poi:poi(). poi(Pos, Kind, Id, Data) -> - Range = els_range:range(Pos, Kind, Id, Data), - els_poi:new(Range, Kind, Id, Data). + Range = els_range:range(Pos, Kind, Id, Data), + els_poi:new(Range, Kind, Id, Data). %% @doc Fold over nodes in the tree %% @@ -778,284 +1347,347 @@ poi(Pos, Kind, Id, Data) -> %% what subtrees should be folded over for certain types of nodes. -spec fold(fun((tree(), term()) -> term()), term(), tree()) -> term(). fold(F, S, Tree) -> - case subtrees(Tree, erl_syntax:type(Tree)) of - [] -> F(Tree, S); - Gs -> F(Tree, fold1(F, S, Gs)) - end. + case subtrees(Tree, erl_syntax:type(Tree)) of + [] -> F(Tree, S); + Gs -> F(Tree, fold1(F, S, Gs)) + end. -spec fold1(fun((tree(), term()) -> term()), term(), [[tree()]]) -> - term(). + term(). fold1(F, S, [L | Ls]) -> - fold1(F, fold2(F, S, L), Ls); + fold1(F, fold2(F, S, L), Ls); fold1(_, S, []) -> - S. + S. -spec fold2(fun((tree(), term()) -> term()), term(), [tree()]) -> - term(). + term(). fold2(F, S, [T | Ts]) -> - fold2(F, fold(F, S, T), Ts); + fold2(F, fold(F, S, T), Ts); fold2(_, S, []) -> - S. + S. -spec subtrees(tree(), atom()) -> [[tree()]]. subtrees(Tree, application) -> - [ case application_mfa(Tree) of - undefined -> - [erl_syntax:application_operator(Tree)]; - _ -> - [] - end - , erl_syntax:application_arguments(Tree)]; + [ + case application_mfa(Tree) of + undefined -> + [erl_syntax:application_operator(Tree)]; + _ -> + [] + end, + erl_syntax:application_arguments(Tree) + ]; subtrees(Tree, function) -> - [erl_syntax:function_clauses(Tree)]; + [erl_syntax:function_clauses(Tree)]; subtrees(_Tree, implicit_fun) -> - []; + []; subtrees(Tree, macro) -> - case erl_syntax:macro_arguments(Tree) of - none -> []; - Args -> [Args] - end; + case erl_syntax:macro_arguments(Tree) of + none -> []; + Args -> [Args] + end; subtrees(Tree, record_access) -> - NameNode = erl_syntax:record_access_type(Tree), - FieldNode = erl_syntax:record_access_field(Tree), - [ [erl_syntax:record_access_argument(Tree)] - , skip_record_name_atom(NameNode) - , skip_name_atom(FieldNode) - ]; + NameNode = erl_syntax:record_access_type(Tree), + FieldNode = erl_syntax:record_access_field(Tree), + [ + [erl_syntax:record_access_argument(Tree)], + skip_record_name_atom(NameNode), + skip_name_atom(FieldNode) + ]; +subtrees(Tree, record_index_expr) -> + NameNode = erl_syntax:record_index_expr_type(Tree), + FieldNode = erl_syntax:record_index_expr_field(Tree), + [ + skip_record_name_atom(NameNode), + skip_name_atom(FieldNode) + ]; subtrees(Tree, record_expr) -> - NameNode = erl_syntax:record_expr_type(Tree), - Fields = erl_syntax:record_expr_fields(Tree), - [ case erl_syntax:record_expr_argument(Tree) of - none -> []; - Arg -> [Arg] - end - , skip_record_name_atom(NameNode) - , Fields - ]; + NameNode = erl_syntax:record_expr_type(Tree), + Fields = erl_syntax:record_expr_fields(Tree), + [ + case erl_syntax:record_expr_argument(Tree) of + none -> []; + Arg -> [Arg] + end, + skip_record_name_atom(NameNode), + Fields + ]; subtrees(Tree, record_field) -> - NameNode = erl_syntax:record_field_name(Tree), - [ skip_name_atom(NameNode) - , case erl_syntax:record_field_value(Tree) of - none -> - []; - V -> - [V] - end]; + NameNode = erl_syntax:record_field_name(Tree), + [ + skip_name_atom(NameNode), + case erl_syntax:record_field_value(Tree) of + none -> + []; + V -> + [V] + end + ]; subtrees(Tree, record_type) -> - NameNode = erl_syntax:record_type_name(Tree), - [ skip_record_name_atom(NameNode) - , erl_syntax:record_type_fields(Tree) - ]; + NameNode = erl_syntax:record_type_name(Tree), + [ + skip_record_name_atom(NameNode), + erl_syntax:record_type_fields(Tree) + ]; subtrees(Tree, record_type_field) -> - NameNode = erl_syntax:record_type_field_name(Tree), - [ skip_name_atom(NameNode) - , [erl_syntax:record_type_field_type(Tree)] - ]; + NameNode = erl_syntax:record_type_field_name(Tree), + [ + skip_name_atom(NameNode), + [erl_syntax:record_type_field_type(Tree)] + ]; subtrees(Tree, user_type_application) -> - NameNode = erl_syntax:user_type_application_name(Tree), - [ skip_name_atom(NameNode) - , erl_syntax:user_type_application_arguments(Tree) - ]; + NameNode = erl_syntax:user_type_application_name(Tree), + [ + skip_name_atom(NameNode), + erl_syntax:user_type_application_arguments(Tree) + ]; subtrees(Tree, type_application) -> - NameNode = erl_syntax:type_application_name(Tree), - [ skip_type_name_atom(NameNode) - , erl_syntax:type_application_arguments(Tree) - ]; + NameNode = erl_syntax:type_application_name(Tree), + [ + skip_type_name_atom(NameNode), + erl_syntax:type_application_arguments(Tree) + ]; subtrees(Tree, attribute) -> - AttrName = attribute_name_atom(Tree), - Args = case erl_syntax:attribute_arguments(Tree) of - none -> []; - Args0 -> Args0 - end, - attribute_subtrees(AttrName, Args); + AttrName = attribute_name_atom(Tree), + Args = + case erl_syntax:attribute_arguments(Tree) of + none -> []; + Args0 -> Args0 + end, + attribute_subtrees(AttrName, Args); subtrees(Tree, _) -> - erl_syntax:subtrees(Tree). + erl_syntax:subtrees(Tree). -spec attribute_name_atom(tree()) -> atom() | tree(). attribute_name_atom(Tree) -> - NameNode = erl_syntax:attribute_name(Tree), - case erl_syntax:type(NameNode) of - atom -> - erl_syntax:atom_value(NameNode); - _ -> - NameNode - end. + NameNode = erl_syntax:attribute_name(Tree), + case erl_syntax:type(NameNode) of + atom -> + erl_syntax:atom_value(NameNode); + _ -> + NameNode + end. -spec attribute_subtrees(atom() | tree(), [tree()]) -> [[tree()]]. -attribute_subtrees(AttrName, [Mod]) - when AttrName =:= module; - AttrName =:= behavior; - AttrName =:= behaviour -> - [skip_name_atom(Mod)]; +attribute_subtrees(AttrName, [Mod]) when + AttrName =:= module; + AttrName =:= behavior; + AttrName =:= behaviour +-> + [skip_name_atom(Mod)]; attribute_subtrees(record, [RecordName, FieldsTuple]) -> - [ skip_record_name_atom(RecordName) - , [FieldsTuple] - ]; + [ + skip_record_name_atom(RecordName), + [FieldsTuple] + ]; attribute_subtrees(import, [Mod, Imports]) -> - [ skip_name_atom(Mod) - , skip_function_entries(Imports) ]; -attribute_subtrees(AttrName, [Exports]) - when AttrName =:= export; - AttrName =:= export_type -> - [ skip_function_entries(Exports) ]; -attribute_subtrees(define, [_Name | Definition]) -> - %% The definition can contain commas, in which case it will look like as if - %% the attribute would have more than two arguments. Eg.: `-define(M, a, b).' - [Definition]; -attribute_subtrees(AttrName, _) - when AttrName =:= include; - AttrName =:= include_lib -> - []; -attribute_subtrees(AttrName, [ArgTuple]) - when AttrName =:= callback; - AttrName =:= spec -> - case erl_syntax:type(ArgTuple) of - tuple -> - [FATree | Rest] = erl_syntax:tuple_elements(ArgTuple), - [ case spec_function_name(FATree) of - {_, _} -> []; - undefined -> [FATree] - end - , Rest ]; - _ -> - [[ArgTuple]] - end; -attribute_subtrees(AttrName, [ArgTuple]) - when AttrName =:= type; - AttrName =:= opaque -> - case erl_syntax:type(ArgTuple) of - tuple -> - [Type | Rest] = erl_syntax:tuple_elements(ArgTuple), - [skip_name_atom(Type), Rest]; - _ -> - [ArgTuple] - end; -attribute_subtrees(AttrName, Args) - when is_atom(AttrName) -> - [Args]; + [ + skip_name_atom(Mod), + skip_function_entries(Imports) + ]; +attribute_subtrees(AttrName, [Exports]) when + AttrName =:= export; + AttrName =:= export_type +-> + [skip_function_entries(Exports)]; +attribute_subtrees(define, [Name | Definition]) -> + %% The definition can contain commas, in which case it will look like as if + %% the attribute would have more than two arguments. Eg.: `-define(M, a, b).' + Args = + case erl_syntax:type(Name) of + application -> erl_syntax:application_arguments(Name); + _ -> [] + end, + [Args, Definition]; +attribute_subtrees(AttrName, _) when + AttrName =:= include; + AttrName =:= include_lib +-> + []; +attribute_subtrees(AttrName, [ArgTuple]) when + AttrName =:= callback; + AttrName =:= spec +-> + case erl_syntax:type(ArgTuple) of + tuple -> + [FATree | Rest] = erl_syntax:tuple_elements(ArgTuple), + [ + case spec_function_name(FATree) of + {_, _} -> []; + undefined -> [FATree] + end, + Rest + ]; + _ -> + [[ArgTuple]] + end; +attribute_subtrees(AttrName, [ArgTuple]) when + AttrName =:= type; + AttrName =:= opaque +-> + case erl_syntax:type(ArgTuple) of + tuple -> + [Type | Rest] = erl_syntax:tuple_elements(ArgTuple), + [skip_name_atom(Type), Rest]; + _ -> + [ArgTuple] + end; +attribute_subtrees(AttrName, Args) when + is_atom(AttrName) +-> + [Args]; attribute_subtrees(AttrName, Args) -> - %% Attribute name not an atom, probably a macro - [[AttrName], Args]. + %% Attribute name not an atom, probably a macro + [[AttrName], Args]. %% Skip visiting atoms of import/export entries -spec skip_function_entries(tree()) -> [tree()]. skip_function_entries(FunList) -> - case erl_syntax:type(FunList) of - list -> - lists:filter( - fun(FATree) -> - case get_name_arity(FATree) of - {_, _} -> false; - false -> true - end - end, erl_syntax:list_elements(FunList)); - _ -> - [FunList] - end. + case erl_syntax:type(FunList) of + list -> + lists:filter( + fun(FATree) -> + case get_name_arity(FATree) of + {_, _} -> false; + false -> true + end + end, + erl_syntax:list_elements(FunList) + ); + _ -> + [FunList] + end. + +%% Helpers for determining valid Folding Ranges +-spec exceeds_one_line(erl_anno:line(), erl_anno:line()) -> + els_poi:poi_range() | oneliner. +exceeds_one_line(StartLine, EndLine) when EndLine > StartLine -> + #{ + from => {StartLine, ?END_OF_LINE}, + to => {EndLine, ?END_OF_LINE} + }; +exceeds_one_line(_, _) -> + oneliner. %% Skip visiting atoms of record names as they are already %% represented as `record_expr' pois -spec skip_record_name_atom(tree()) -> [tree()]. skip_record_name_atom(NameNode) -> - case is_record_name(NameNode) of - {true, _} -> - []; - _ -> - [NameNode] - end. + case is_record_name(NameNode) of + {true, _} -> + []; + _ -> + [NameNode] + end. %% Skip visiting atoms as they are already represented as other pois -spec skip_name_atom(tree()) -> [tree()]. skip_name_atom(NameNode) -> - case erl_syntax:type(NameNode) of - atom -> - []; - _ -> - [NameNode] - end. + case erl_syntax:type(NameNode) of + atom -> + []; + _ -> + [NameNode] + end. -spec skip_type_name_atom(tree()) -> [tree()]. skip_type_name_atom(NameNode) -> - case erl_syntax:type(NameNode) of - atom -> - []; - module_qualifier -> - skip_name_atom(erl_syntax:module_qualifier_body(NameNode)) - ++ - skip_name_atom(erl_syntax:module_qualifier_argument(NameNode)); - _ -> - [NameNode] - end. + case erl_syntax:type(NameNode) of + atom -> + []; + module_qualifier -> + skip_name_atom(erl_syntax:module_qualifier_body(NameNode)) ++ + skip_name_atom(erl_syntax:module_qualifier_argument(NameNode)); + _ -> + [NameNode] + end. -spec pretty_print_clause(tree()) -> binary(). pretty_print_clause(Tree) -> - Patterns = erl_syntax:clause_patterns(Tree), - PrettyPatterns = [ erl_prettypr:format(P) || P <- Patterns], - Guard = erl_syntax:clause_guard(Tree), - PrettyGuard = case Guard of - none -> - ""; - _ -> - "when " ++ erl_prettypr:format(Guard) - end, - PrettyClause = io_lib:format( "(~ts) ~ts" - , [ string:join(PrettyPatterns, ", ") - , PrettyGuard - ]), - els_utils:to_binary(PrettyClause). + Patterns = erl_syntax:clause_patterns(Tree), + PrettyPatterns = [erl_prettypr:format(P) || P <- Patterns], + Guard = erl_syntax:clause_guard(Tree), + PrettyGuard = + case Guard of + none -> + ""; + _ -> + "when " ++ erl_prettypr:format(Guard) + end, + PrettyClause = io_lib:format( + "(~ts) ~ts", + [ + string:join(PrettyPatterns, ", "), + PrettyGuard + ] + ), + els_utils:to_binary(PrettyClause). -spec record_access_location(tree()) -> erl_anno:anno(). record_access_location(Tree) -> - %% erlfmt_parser sets start at the start of the argument expression - %% we don't have an exact location of '#' - %% best approximation is the end of the argument - Start = get_end_location(erl_syntax:record_access_argument(Tree)), - Anno = erl_syntax:get_pos(erl_syntax:record_access_type(Tree)), - erl_anno:set_location(Start, Anno). + %% erlfmt_parser sets start at the start of the argument expression + %% we don't have an exact location of '#' + %% best approximation is the end of the argument + Start = get_end_location(erl_syntax:record_access_argument(Tree)), + Anno = erl_syntax:get_pos(erl_syntax:record_access_type(Tree)), + erl_anno:set_location(Start, Anno). + +-spec record_index_expr_location(tree()) -> erl_anno:anno(). +record_index_expr_location(Tree) -> + Start = get_start_location(Tree), + Anno = erl_syntax:get_pos(erl_syntax:record_index_expr_type(Tree)), + erl_anno:set_location(Start, Anno). -spec record_expr_location(tree(), tree()) -> erl_anno:anno(). record_expr_location(Tree, RecordName) -> - %% set start location at '#' - %% and end location at the end of record name - Start = record_expr_start_location(Tree), - Anno = erl_syntax:get_pos(RecordName), - erl_anno:set_location(Start, Anno). + %% set start location at '#' + %% and end location at the end of record name + Start = record_expr_start_location(Tree), + Anno = erl_syntax:get_pos(RecordName), + erl_anno:set_location(Start, Anno). -spec record_expr_start_location(tree()) -> erl_anno:location(). record_expr_start_location(Tree) -> - %% If this is a new record creation or record type - %% the tree start location is at '#'. - %% However if this is a record update, then - %% we don't have an exact location of '#', - %% best approximation is the end of the argument. - case erl_syntax:type(Tree) of - record_expr -> - case erl_syntax:record_expr_argument(Tree) of - none -> - get_start_location(Tree); - RecordArg -> - get_end_location(RecordArg) - end; - record_type -> - get_start_location(Tree) - end. + %% If this is a new record creation or record type + %% the tree start location is at '#'. + %% However if this is a record update, then + %% we don't have an exact location of '#', + %% best approximation is the end of the argument. + case erl_syntax:type(Tree) of + record_expr -> + case erl_syntax:record_expr_argument(Tree) of + none -> + get_start_location(Tree); + RecordArg -> + get_end_location(RecordArg) + end; + record_type -> + get_start_location(Tree) + end. -spec macro_location(tree()) -> erl_anno:anno(). macro_location(Tree) -> - %% set start location at '?' - %% and end location at the end of macro name - %% (exclude arguments) - Start = get_start_location(Tree), - MacroName = erl_syntax:macro_name(Tree), - Anno = erl_syntax:get_pos(MacroName), - erl_anno:set_location(Start, Anno). + %% set start location at '?' + %% and end location at the end of macro name + %% (exclude arguments) + Start = get_start_location(Tree), + MacroName = erl_syntax:macro_name(Tree), + Anno = erl_syntax:get_pos(MacroName), + erl_anno:set_location(Start, Anno). -spec get_start_location(tree()) -> erl_anno:location(). get_start_location(Tree) -> - erl_anno:location(erl_syntax:get_pos(Tree)). + erl_anno:location(erl_syntax:get_pos(Tree)). -spec get_end_location(tree()) -> erl_anno:location(). get_end_location(Tree) -> - %% erl_anno:end_location(erl_syntax:get_pos(Tree)). - Anno = erl_syntax:get_pos(Tree), - proplists:get_value(end_location, erl_anno:to_term(Anno)). + %% erl_anno:end_location(erl_syntax:get_pos(Tree)). + Anno = erl_syntax:get_pos(Tree), + proplists:get_value(end_location, erl_anno:to_term(Anno)). + +-spec include_pois(pos(), include | include_lib, tree()) -> [els_poi:poi()]. +include_pois(Pos, Type, Node) -> + case erl_syntax:type(Node) of + string -> [poi(Pos, Type, erl_syntax:string_value(Node))]; + _ -> [] + end. diff --git a/apps/els_lsp/src/els_poi.erl b/apps/els_lsp/src/els_poi.erl deleted file mode 100644 index a4c2faf88..000000000 --- a/apps/els_lsp/src/els_poi.erl +++ /dev/null @@ -1,58 +0,0 @@ -%%============================================================================== -%% 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 - ]). - -%%============================================================================== -%% Includes -%%============================================================================== --include("els_lsp.hrl"). - -%%============================================================================== -%% 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). - -%%============================================================================== -%% Internal Functions -%%============================================================================== - --spec compare(poi(), poi()) -> boolean(). -compare(#{range := A}, #{range := B}) -> - els_range:compare(A, B). diff --git a/apps/els_lsp/src/els_poi_define.erl b/apps/els_lsp/src/els_poi_define.erl new file mode 100644 index 000000000..ac11471de --- /dev/null +++ b/apps/els_lsp/src/els_poi_define.erl @@ -0,0 +1,19 @@ +-module(els_poi_define). + +-behaviour(els_poi). +-export([label/1, symbol_kind/0]). + +-include("els_lsp.hrl"). + +-opaque id() :: {atom(), arity()}. +-export_type([id/0]). + +-spec label(els_poi:poi()) -> binary(). +label(#{id := {Name, Arity}}) -> + els_utils:to_binary(io_lib:format("~s/~p", [Name, Arity])); +label(#{id := Name}) when is_atom(Name) -> + atom_to_binary(Name, utf8). + +-spec symbol_kind() -> ?SYMBOLKIND_CONSTANT. +symbol_kind() -> + ?SYMBOLKIND_CONSTANT. diff --git a/apps/els_lsp/src/els_poi_function.erl b/apps/els_lsp/src/els_poi_function.erl new file mode 100644 index 000000000..e57f7ea7d --- /dev/null +++ b/apps/els_lsp/src/els_poi_function.erl @@ -0,0 +1,17 @@ +-module(els_poi_function). + +-behaviour(els_poi). +-export([label/1, symbol_kind/0]). + +-include("els_lsp.hrl"). + +-opaque id() :: {atom(), arity()}. +-export_type([id/0]). + +-spec label(els_poi:poi()) -> binary(). +label(#{id := {F, A}}) -> + els_utils:to_binary(io_lib:format("~s/~p", [F, A])). + +-spec symbol_kind() -> ?SYMBOLKIND_FUNCTION. +symbol_kind() -> + ?SYMBOLKIND_FUNCTION. diff --git a/apps/els_lsp/src/els_poi_record.erl b/apps/els_lsp/src/els_poi_record.erl new file mode 100644 index 000000000..19132ee1c --- /dev/null +++ b/apps/els_lsp/src/els_poi_record.erl @@ -0,0 +1,17 @@ +-module(els_poi_record). + +-behaviour(els_poi). +-export([label/1, symbol_kind/0]). + +-include("els_lsp.hrl"). + +-opaque id() :: {atom(), arity()}. +-export_type([id/0]). + +-spec label(els_poi:poi()) -> binary(). +label(#{id := Name}) when is_atom(Name) -> + atom_to_binary(Name, utf8). + +-spec symbol_kind() -> ?SYMBOLKIND_STRUCT. +symbol_kind() -> + ?SYMBOLKIND_STRUCT. diff --git a/apps/els_lsp/src/els_poi_type_definition.erl b/apps/els_lsp/src/els_poi_type_definition.erl new file mode 100644 index 000000000..b2ce74a38 --- /dev/null +++ b/apps/els_lsp/src/els_poi_type_definition.erl @@ -0,0 +1,17 @@ +-module(els_poi_type_definition). + +-behaviour(els_poi). +-export([label/1, symbol_kind/0]). + +-include("els_lsp.hrl"). + +-opaque id() :: {atom(), arity()}. +-export_type([id/0]). + +-spec label(els_poi:poi()) -> binary(). +label(#{id := {Name, Arity}}) -> + els_utils:to_binary(io_lib:format("~s/~p", [Name, Arity])). + +-spec symbol_kind() -> ?SYMBOLKIND_TYPE_PARAMETER. +symbol_kind() -> + ?SYMBOLKIND_TYPE_PARAMETER. diff --git a/apps/els_lsp/src/els_prepare_rename_provider.erl b/apps/els_lsp/src/els_prepare_rename_provider.erl new file mode 100644 index 000000000..178c35300 --- /dev/null +++ b/apps/els_lsp/src/els_prepare_rename_provider.erl @@ -0,0 +1,84 @@ +-module(els_prepare_rename_provider). + +-behaviour(els_provider). + +-export([ + handle_request/1 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include_lib("kernel/include/logger.hrl"). + +%%============================================================================== +%% Defines +%%============================================================================== + +%%============================================================================== +%% Types +%%============================================================================== + +%%============================================================================== +%% els_provider functions +%%============================================================================== +-spec handle_request(any()) -> {response, any()}. +handle_request({prepare_rename, Params0}) -> + #{ + <<"textDocument">> := #{<<"uri">> := Uri}, + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + } + } = Params0, + case els_utils:lookup_document(Uri) of + {ok, Document} -> + Params = Params0#{<<"newName">> => <<"newName">>}, + POIs = els_dt_document:get_element_at_pos( + Document, + Line + 1, + Character + 1 + ), + case POIs of + [POI | _] -> + try + els_provider:handle_request( + els_rename_provider, + {rename, Params} + ) + of + {response, null} -> + {response, null}; + {response, _} -> + {response, els_protocol:range(rename_range(POI))} + catch + Class:Reason:Stacktrace -> + ?LOG_ERROR( + "prepareRenamed failed: ~p:~p\n" + "Stacktrace:\n~p\n", + [Class, Reason, Stacktrace] + ), + {response, null} + end; + _ -> + {response, null} + end; + {error, Error} -> + ?LOG_WARNING("Failed to read uri: ~p ~p", [Error, Uri]), + {response, null} + end. + +%%============================================================================== +%% Internal functions +%%============================================================================== +-spec rename_range(els_poi:poi()) -> els_poi:poi_range(). +rename_range(#{data := #{name_range := Range}}) -> + Range; +rename_range(#{kind := Kind, range := #{from := {FromL, FromC}, to := To}}) when + Kind =:= macro; + Kind =:= record_expr +-> + %% Don't include # or ? in name.. + #{from => {FromL, FromC + 1}, to => To}; +rename_range(#{range := Range}) -> + Range. diff --git a/apps/els_lsp/src/els_progress.erl b/apps/els_lsp/src/els_progress.erl index b756aa427..936abf8fd 100644 --- a/apps/els_lsp/src/els_progress.erl +++ b/apps/els_lsp/src/els_progress.erl @@ -11,32 +11,36 @@ %%============================================================================== %% Exports %%============================================================================== --export([ send_notification/2 - , token/0 - ]). +-export([ + send_notification/2, + token/0 +]). %%============================================================================== %% Types %%============================================================================== -type token() :: binary(). -type value() :: els_work_done_progress:value(). --type params() :: #{ token := token() - , value := value() - }. --export_type([ token/0 - , params/0 - ]). +-type params() :: #{ + token := token(), + value := value() +}. +-export_type([ + token/0, + params/0 +]). %%============================================================================== %% API %%============================================================================== -spec send_notification(token(), value()) -> ok. send_notification(Token, Value) -> - Params = #{ token => Token - , value => Value - }, - els_server:send_notification(?METHOD, Params). + Params = #{ + token => Token, + value => Value + }, + els_server:send_notification(?METHOD, Params). -spec token() -> token(). token() -> - list_to_binary(uuid:uuid_to_string(uuid:get_v4())). + list_to_binary(uuid:uuid_to_string(uuid:get_v4())). diff --git a/apps/els_lsp/src/els_providers_sup.erl b/apps/els_lsp/src/els_providers_sup.erl deleted file mode 100644 index 80ced1080..000000000 --- a/apps/els_lsp/src/els_providers_sup.erl +++ /dev/null @@ -1,59 +0,0 @@ -%%============================================================================== -%% Providers Supervisor -%%============================================================================== --module(els_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_provider:provider() - , start := {els_provider, start_link, [els_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 => 5 - , period => 60 - }, - ChildSpecs = [provider_specs(P) || P <- els_provider:enabled_providers()], - {ok, {SupFlags, ChildSpecs}}. - --spec provider_specs(els_provider:provider()) -> provider_spec(). -provider_specs(Provider) -> - #{ id => Provider - , start => {els_provider, start_link, [Provider]} - , shutdown => brutal_kill - }. diff --git a/apps/els_lsp/src/els_range.erl b/apps/els_lsp/src/els_range.erl index 89cac8fcd..515233e84 100644 --- a/apps/els_lsp/src/els_range.erl +++ b/apps/els_lsp/src/els_range.erl @@ -2,69 +2,99 @@ -include("els_lsp.hrl"). --export([ compare/2 - , in/2 - , range/4 - , range/1 - , to_poi_range/1 - ]). +-export([ + compare/2, + in/2, + range/4, + range/1, + line/1, + to_poi_range/1, + inclusion_range/2 +]). --spec compare(poi_range(), poi_range()) -> boolean(). -compare( #{from := FromA, to := ToA} - , #{from := FromB, to := ToB} - ) when FromB =< FromA, ToA =< ToB; %% Nested - ToA =< FromB; %% Sequential - FromA =< FromB, ToA =< ToB %% Sequential & Overlapped - -> - true; +-spec compare(els_poi:poi_range(), els_poi:poi_range()) -> boolean(). +compare( + #{from := FromA, to := ToA}, + #{from := FromB, to := ToB} + %% Nested +) when + FromB =< FromA, ToA =< ToB; + %% Sequential + ToA =< FromB; + %% Sequential & Overlapped + FromA =< FromB, ToA =< ToB +-> + true; compare(_, _) -> - false. + false. --spec in(poi_range(), poi_range()) -> boolean(). +-spec in(els_poi:poi_range(), els_poi:poi_range()) -> boolean(). in(#{from := FromA, to := ToA}, #{from := FromB, to := ToB}) -> - FromA >= FromB andalso ToA =< ToB. + FromA >= FromB andalso ToA =< ToB. --spec range(pos() | {pos(), pos()} | erl_anno:anno(), poi_kind(), any(), any()) - -> poi_range(). -range({{_Line, _Column} = From, {_ToLine, _ToColumn} = To}, Name, _, _Data) - when Name =:= export; - Name =:= export_type; - Name =:= spec -> - %% range from unparsable tokens - #{ from => From, to => To }; +-spec range(pos() | {pos(), pos()} | erl_anno:anno(), els_poi:poi_kind(), any(), any()) -> + els_poi:poi_range(). +range({{_Line, _Column} = From, {_ToLine, _ToColumn} = To}, Name, _, _Data) when + Name =:= export; + Name =:= nifs; + Name =:= export_type; + Name =:= spec +-> + %% range from unparsable tokens + #{from => From, to => To}; range({Line, Column}, function_clause, {F, _A, _Index}, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_string(F)), - #{ from => From, to => To }; + From = {Line, Column}, + To = plus(From, atom_to_string(F)), + #{from => From, to => To}; range(Anno, _Type, _Id, _Data) -> - range(Anno). + range(Anno). --spec range(erl_anno:anno()) -> poi_range(). +-spec range(erl_anno:anno()) -> els_poi:poi_range(). range(Anno) -> - From = erl_anno:location(Anno), - %% To = erl_anno:end_location(Anno), - To = proplists:get_value(end_location, erl_anno:to_term(Anno)), - #{ from => From, to => To }. + From = erl_anno:location(Anno), + %% To = erl_anno:end_location(Anno), + To = proplists:get_value(end_location, erl_anno:to_term(Anno)), + #{from => From, to => To}. + +-spec line(els_poi:poi_range()) -> els_poi:poi_range(). +line(#{from := {FromL, _}, to := {ToL, _}}) -> + #{from => {FromL, 1}, to => {ToL + 1, 1}}. %% @doc Converts a LSP range into a POI range --spec to_poi_range(range()) -> poi_range(). +-spec to_poi_range(range()) -> els_poi:poi_range(). to_poi_range(#{'start' := Start, 'end' := End}) -> - #{'line' := LineStart, 'character' := CharStart} = Start, - #{'line' := LineEnd, 'character' := CharEnd} = End, - #{ from => {LineStart + 1, CharStart + 1} - , to => {LineEnd + 1, CharEnd + 1} - }; + #{'line' := LineStart, 'character' := CharStart} = Start, + #{'line' := LineEnd, 'character' := CharEnd} = End, + #{ + from => {LineStart + 1, CharStart + 1}, + to => {LineEnd + 1, CharEnd + 1} + }; to_poi_range(#{<<"start">> := Start, <<"end">> := End}) -> - #{<<"line">> := LineStart, <<"character">> := CharStart} = Start, - #{<<"line">> := LineEnd, <<"character">> := CharEnd} = End, - #{ from => {LineStart + 1, CharStart + 1} - , to => {LineEnd + 1, CharEnd + 1} - }. + #{<<"line">> := LineStart, <<"character">> := CharStart} = Start, + #{<<"line">> := LineEnd, <<"character">> := CharEnd} = End, + #{ + from => {LineStart + 1, CharStart + 1}, + to => {LineEnd + 1, CharEnd + 1} + }. + +-spec inclusion_range(uri(), els_dt_document:item()) -> + {ok, els_poi:poi_range()} | error. +inclusion_range(Uri, Document) -> + Path = binary_to_list(els_uri:path(Uri)), + case + els_compiler_diagnostics:inclusion_range(Path, Document, include) ++ + els_compiler_diagnostics:inclusion_range(Path, Document, include_lib) + of + [Range | _] -> + {ok, Range}; + [] -> + error + end. -spec plus(pos(), string()) -> pos(). plus({Line, Column}, String) -> - {Line, Column + string:length(String)}. + {Line, Column + string:length(String)}. -spec atom_to_string(atom()) -> string(). atom_to_string(Atom) -> - io_lib:write(Atom). + io_lib:write(Atom). diff --git a/apps/els_lsp/src/els_refactorerl_diagnostics.erl b/apps/els_lsp/src/els_refactorerl_diagnostics.erl new file mode 100644 index 000000000..8d1cee5da --- /dev/null +++ b/apps/els_lsp/src/els_refactorerl_diagnostics.erl @@ -0,0 +1,89 @@ +%%============================================================================== +%% RefactorErl Diagnostics +%%============================================================================== +-module(els_refactorerl_diagnostics). + +%%============================================================================== +%% Behaviours +%%============================================================================== +-behaviour(els_diagnostics). + +%%============================================================================== +%% Exports +%%============================================================================== +-export([ + is_default/0, + run/1, + source/0 +]). + +%%============================================================================== +%% Includes & Defines +%%============================================================================== +-include("els_lsp.hrl"). + +%%============================================================================== +%% Types +%%============================================================================== +-type refactorerl_diagnostic_alias() :: atom(). +-type refactorerl_diagnostic_result() :: {range(), string()}. +%-type refactorerl_query() :: [char()]. + +%%============================================================================== +%% Callback Functions +%%============================================================================== + +-spec is_default() -> boolean(). +is_default() -> + false. + +-spec run(uri()) -> [els_diagnostics:diagnostic()]. +run(Uri) -> + case filename:extension(Uri) of + <<".erl">> -> + case els_refactorerl_utils:referl_node() of + {error, _} -> + []; + {ok, _} -> + case els_refactorerl_utils:add(Uri) of + error -> + []; + ok -> + Module = els_uri:module(Uri), + Diags = enabled_diagnostics(), + Results = els_refactorerl_utils:run_diagnostics(Diags, Module), + make_diagnostics(Results) + end + end; + _ -> + [] + end. + +-spec source() -> binary(). +source() -> + els_refactorerl_utils:source_name(). + +%%============================================================================== +%% Internal Functions +%%============================================================================== +% @doc +% Returns the enabled diagnostics by merging default and configed +-spec enabled_diagnostics() -> [refactorerl_diagnostic_alias()]. +enabled_diagnostics() -> + case els_config:get(refactorerl) of + #{"diagnostics" := List} -> + [list_to_atom(Element) || Element <- List]; + _ -> + [] + end. + +% @doc +% Constructs the ELS diagnostic from RefactorErl result +-spec make_diagnostics([refactorerl_diagnostic_result()]) -> any(). +make_diagnostics([{Range, Message} | Tail]) -> + Severity = ?DIAGNOSTIC_WARNING, + Source = source(), + Diag = els_diagnostics:make_diagnostic(Range, Message, Severity, Source), + [Diag | make_diagnostics(Tail)]; +make_diagnostics([]) -> + []. diff --git a/apps/els_lsp/src/els_refactorerl_utils.erl b/apps/els_lsp/src/els_refactorerl_utils.erl new file mode 100644 index 000000000..9bb78ebc0 --- /dev/null +++ b/apps/els_lsp/src/els_refactorerl_utils.erl @@ -0,0 +1,161 @@ +%%============================================================================== +%% Erlang LS & Refactor Erl communication +%%============================================================================== +-module(els_refactorerl_utils). + +%%============================================================================== +%% API +%%============================================================================== +-export([ + referl_node/0, + notification/1, + notification/2, + run_diagnostics/2, + source_name/0, + add/1 +]). + +%%============================================================================== +%% Includes & Defines +%%============================================================================== +-include("els_lsp.hrl"). + +%%============================================================================== +%% API +%%============================================================================== + +%% @doc +%% Returns the RefactorErl node, if it can't, it returns error and its cause. +%% It returns the node given in config, if it is alive. +%% First it runs the validation functions, the result of validation will be +%% notified to the user. +%% If the node once was validated there will be no display messages. +%% +%% The configuration can store the node and its status. +%% - Either a simple NodeString, which needs to be checked and configure +%% - {Node, Status} where boths are atoms +%% Possible statuses: validated, disconnected, disabled +%% 'disabled', when it won't try to reconnect +%% 'disconnected', when it will try to reconnect +%% - notconfigured, the node is not configured in the config file +%% +%% +%% Node can be: +%% - NodeStr +%% - {Status, Node} where both are atoms. +%% - Status can be: +%% - validated: node is running +%% - disconnected: node is not running +%% - disabled: RefactorErl is turned off for this session. T +%% his can happen after an unsuccessfull query attempt. +-spec referl_node() -> + {ok, atom()} + | {error, disconnected} + | {error, disabled} + | {error, other}. +referl_node() -> + case els_config:get(refactorerl) of + #{"node" := {Node, validated}} -> + {ok, Node}; + #{"node" := {Node, disconnected}} -> + connect_node({retry, Node}); + #{"node" := {_Node, disabled}} -> + {error, disabled}; + #{"node" := NodeStr} -> + RT = els_config_runtime:get_name_type(), + Node = els_utils:compose_node_name(NodeStr, RT), + connect_node({validate, Node}); + notconfigured -> + {error, disabled}; + _ -> + {error, other} + end. + +%%@doc +%% Adds a module to the RefactorErl node. Using the UI router +%% Returns 'ok' if successfull +%% Returns 'error' if it fails +-spec add(uri()) -> error | ok. +add(Uri) -> + case els_refactorerl_utils:referl_node() of + {ok, Node} -> + Path = [binary_to_list(els_uri:path(Uri))], + %% returns error | ok + rpc:call(Node, referl_els, add, [Path]); + _ -> + error + end. + +%%@doc +%% Runs list of diagnostic aliases on refactorerl +-spec run_diagnostics(list(), atom()) -> list(). +run_diagnostics(DiagnosticAliases, Module) -> + case els_refactorerl_utils:referl_node() of + {ok, Node} -> + %% returns error | ok + rpc:call(Node, referl_els, run_diagnostics, [DiagnosticAliases, Module]); + % In this case there was probably error. + _ -> + [] + end. + +%%@doc +%% Util for popping up notifications +-spec notification(string(), number()) -> atom(). +notification(Msg, Severity) -> + Param = #{ + type => Severity, + message => list_to_binary(Msg) + }, + els_server:send_notification(<<"window/showMessage">>, Param). + +-spec notification(string()) -> atom(). +notification(Msg) -> + notification(Msg, ?MESSAGE_TYPE_INFO). + +%%============================================================================== +%% Internal Functions +%%============================================================================== + +%%@doc +%% Checks if the given node is running RefactorErl with ELS interface +-spec is_refactorerl(atom()) -> boolean(). +is_refactorerl(Node) -> + case rpc:call(Node, referl_els, ping, [], 500) of + {refactorerl_els, pong} -> true; + _ -> false + end. + +%%@doc +%% Tries to connect to a node. +%% When it status is validate, the node hasn't been checked yet, +%% so it will reports the success, and failure as well, +%% +%% when retry, it won't report. +-spec connect_node({validate | retry, atom()}) -> + {error, disconnected} + | atom(). +connect_node({Status, Node}) -> + Config = els_config:get(refactorerl), + case {Status, is_refactorerl(Node)} of + {validate, false} -> + els_config:set(refactorerl, Config#{"node" => {Node, disconnected}}), + {error, disconnected}; + {retry, false} -> + els_config:set(refactorerl, Config#{"node" => {Node, disconnected}}), + {error, disconnected}; + {_, true} -> + notification("RefactorErl is connected!", ?MESSAGE_TYPE_INFO), + els_config:set(refactorerl, Config#{"node" => {Node, validated}}), + {ok, Node} + end. + +%%============================================================================== +%% Values +%%============================================================================== + +%%@doc +%% Common soruce name for all RefactorErl based backend(s) +-spec source_name() -> binary(). +source_name() -> + <<"RefactorErl">>. diff --git a/apps/els_lsp/src/els_references_provider.erl b/apps/els_lsp/src/els_references_provider.erl index 7f81b8f6e..c20488033 100644 --- a/apps/els_lsp/src/els_references_provider.erl +++ b/apps/els_lsp/src/els_references_provider.erl @@ -2,154 +2,252 @@ -behaviour(els_provider). --export([ handle_request/2 - , is_enabled/0 - ]). +-export([ + handle_request/1 +]). %% For use in other providers --export([ find_references/2 - , find_scoped_references_for_def/2 - ]). +-export([ + find_references/2, + find_scoped_references_for_def/2, + find_references_to_module/1 +]). %%============================================================================== %% Includes %%============================================================================== -include("els_lsp.hrl"). +-include_lib("kernel/include/logger.hrl"). %%============================================================================== %% Types %%============================================================================== --type state() :: any(). %%============================================================================== %% els_provider functions %%============================================================================== - --spec is_enabled() -> boolean(). -is_enabled() -> - true. - --spec handle_request(any(), state()) -> {[location()] | null, state()}. -handle_request({references, Params}, State) -> - #{ <<"position">> := #{ <<"line">> := Line - , <<"character">> := Character - } - , <<"textDocument">> := #{<<"uri">> := Uri} - } = Params, - {ok, Document} = els_utils:lookup_document(Uri), - Refs = - case - els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1) - of - [POI | _] -> find_references(Uri, POI); - [] -> [] - end, - case Refs of - [] -> {null, State}; - Rs -> {Rs, State} - end. +-spec handle_request(any()) -> {async, uri(), pid()}. +handle_request({references, Params}) -> + #{ + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + ?LOG_DEBUG( + "Starting references job " "[uri=~p, line=~p, character=~p]", + [Uri, Line, Character] + ), + Job = run_references_job(Uri, Line, Character), + {async, Uri, Job}. + +-spec run_references_job(uri(), line(), column()) -> pid(). +run_references_job(Uri, Line, Character) -> + Config = #{ + task => fun get_references/2, + entries => [{Uri, Line, Character}], + title => <<"References">>, + on_complete => fun els_server:register_result/1 + }, + {ok, Pid} = els_background_job:new(Config), + Pid. + +-spec get_references({uri(), integer(), integer()}, _) -> null | [location()]. +get_references({Uri, Line, Character}, _) -> + {ok, Document} = els_utils:lookup_document(Uri), + Refs = + case els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1) of + [POI | _] -> find_references(Uri, POI); + [] -> [] + end, + case Refs of + [] -> null; + Rs -> Rs + end. %%============================================================================== %% Internal functions %%============================================================================== --spec find_references(uri(), poi()) -> [location()]. -find_references(Uri, #{ kind := Kind - , id := Id - }) when Kind =:= application; - Kind =:= implicit_fun; - Kind =:= function; - Kind =:= export_entry; - Kind =:= export_type_entry - -> - Key = case Id of - {F, A} -> {els_uri:module(Uri), F, A}; - {M, F, A} -> {M, F, A} +-spec find_references(uri(), els_poi:poi()) -> [location()]. +find_references(Uri, #{ + kind := Kind, + id := Id +}) when + Kind =:= application; + Kind =:= implicit_fun; + Kind =:= function; + Kind =:= export_entry; + Kind =:= export_type_entry; + Kind =:= nifs_entry +-> + Key = + case Id of + {F, A} -> {els_uri:module(Uri), F, A}; + {M, F, A} -> {M, F, A} end, - find_references_for_id(Kind, Key); -find_references(Uri, #{ kind := Kind - , id := Id - }) when Kind =:= function_clause -> - {F, A, _Index} = Id, - Key = {els_uri:module(Uri), F, A}, - find_references_for_id(Kind, Key); -find_references(Uri, Poi = #{kind := Kind}) - when Kind =:= record; - Kind =:= record_def_field; - Kind =:= define -> - uri_pois_to_locations( - find_scoped_references_for_def(Uri, Poi)); -find_references(Uri, Poi = #{kind := Kind, id := Id}) - when Kind =:= type_definition -> - Key = case Id of - {F, A} -> {els_uri:module(Uri), F, A}; - {M, F, A} -> {M, F, A} + find_references_for_id(Kind, Key); +find_references(Uri, #{kind := variable} = Var) -> + POIs = els_code_navigation:find_in_scope(Uri, Var), + [location(Uri, Range) || #{range := Range} = POI <- POIs, POI =/= Var]; +find_references(Uri, #{ + kind := Kind, + id := Id +}) when Kind =:= function_clause -> + {F, A, _Index} = Id, + Key = {els_uri:module(Uri), F, A}, + find_references_for_id(Kind, Key); +find_references(Uri, POI = #{kind := Kind}) when + Kind =:= record; + Kind =:= record_def_field; + Kind =:= define +-> + find_scoped_references_for_def(Uri, POI); +find_references(Uri, POI = #{kind := Kind, id := Id}) when + Kind =:= type_definition +-> + Key = + case Id of + {F, A} -> {els_uri:module(Uri), F, A}; + {M, F, A} -> {M, F, A} end, - lists:usort(find_references_for_id(Kind, Key) ++ - uri_pois_to_locations( - find_scoped_references_for_def(Uri, Poi))); -find_references(Uri, Poi = #{kind := Kind}) - when Kind =:= record_expr; - Kind =:= record_field; - Kind =:= macro; - Kind =:= type_application -> - case els_code_navigation:goto_definition(Uri, Poi) of - {ok, DefUri, DefPoi} -> - find_references(DefUri, DefPoi); - {error, _} -> - %% look for references only in the current document - uri_pois_to_locations( - find_scoped_references_for_def(Uri, Poi)) - end; + lists:usort( + find_references_for_id(Kind, Key) ++ + find_scoped_references_for_def(Uri, POI) + ); +find_references(Uri, POI = #{kind := Kind, id := Id}) when + Kind =:= record_expr; + Kind =:= record_field; + Kind =:= macro; + Kind =:= type_application +-> + case els_code_navigation:goto_definition(Uri, POI) of + {ok, [{DefUri, DefPoi}]} -> + find_references(DefUri, DefPoi); + _ -> + %% look for references only in the current document + local_refs(Uri, Kind, Id) + end; find_references(Uri, #{kind := module}) -> - case els_utils:lookup_document(Uri) of - {ok, Doc} -> - Exports = els_dt_document:pois(Doc, [export_entry]), - ExcludeLocalRefs = - fun(Loc) -> - maps:get(uri, Loc) =/= Uri - end, - Refs = lists:flatmap(fun(E) -> find_references(Uri, E) end, Exports), - lists:filter(ExcludeLocalRefs, Refs) - end; -find_references(_Uri, #{kind := Kind, id := Name}) - when Kind =:= behaviour -> - find_references_for_id(Kind, Name); + Refs = find_references_to_module(Uri), + [location(U, R) || #{uri := U, range := R} <- Refs]; +find_references(_Uri, #{kind := Kind, id := Name}) when + Kind =:= behaviour; + Kind =:= atom +-> + find_references_for_id(Kind, Name); find_references(_Uri, _POI) -> - []. - --spec find_scoped_references_for_def(uri(), poi()) -> [{uri(), poi()}]. -find_scoped_references_for_def(Uri, #{kind := Kind, id := Name}) -> - Kinds = kind_to_ref_kinds(Kind), - Refs = els_scope:local_and_includer_pois(Uri, Kinds), - [{U, Poi} || {U, Pois} <- Refs, - #{id := N} = Poi <- Pois, - N =:= Name]. - --spec kind_to_ref_kinds(poi_kind()) -> [poi_kind()]. -kind_to_ref_kinds(define) -> - [macro]; -kind_to_ref_kinds(record) -> - [record_expr]; -kind_to_ref_kinds(record_def_field) -> - [record_field]; -kind_to_ref_kinds(type_definition) -> - [type_application]; -kind_to_ref_kinds(Kind) -> - [Kind]. - - --spec find_references_for_id(poi_kind(), any()) -> [location()]. + []. + +-spec local_refs(uri(), els_poi:poi_kind(), els_poi:poi_id()) -> + [location()]. +local_refs(Uri, Kind, Id) -> + {ok, Document} = els_utils:lookup_document(Uri), + POIs = els_dt_document:pois(Document, [kind_to_ref_kind(Kind)]), + LocalRefs = [ + location(Uri, R) + || #{range := R, id := IdPoi} <- POIs, + Id == IdPoi + ], + LocalRefs. + +-spec find_scoped_references_for_def(uri(), els_poi:poi()) -> [location()]. +find_scoped_references_for_def(Uri, POI = #{kind := Kind}) when + Kind =:= type_definition +-> + %% TODO: This is a hack, ideally we shouldn't have any special handling for + %% these kinds. + find_scoped_references_naive(Uri, POI); +find_scoped_references_for_def(Uri, POI) -> + %% Finding scoped references can be done in two ways: + %% * Naive, find all POIs that can reach our POI and matches the id. + %% * Indexed, use the index to find all matching POIs, then check if + %% they actually reference our POI by using goto_definition. + %% It varies from case to case which is the fastest, so we race both + %% functions to get the quickest answer. + Naive = fun() -> find_scoped_references_naive(Uri, POI) end, + Index = fun() -> find_scoped_references_with_index(Uri, POI) end, + els_utils:race([Naive, Index], _Timeout = 15000). + +-spec find_scoped_references_naive(uri(), els_poi:poi()) -> [location()]. +find_scoped_references_naive(Uri, #{id := Id, kind := Kind}) -> + RefKind = kind_to_ref_kind(Kind), + Refs = els_scope:local_and_includer_pois(Uri, [RefKind]), + MatchingRefs = [ + location(U, R) + || {U, POIs} <- Refs, + #{id := N, range := R} <- POIs, + N =:= Id + ], + ?LOG_DEBUG( + "Found scoped references (naive) for ~p: ~p", + [Id, length(MatchingRefs)] + ), + MatchingRefs. + +-spec find_scoped_references_with_index(uri(), els_poi:poi()) -> [location()]. +find_scoped_references_with_index(Uri, POI = #{kind := Kind, id := Id}) -> + RefPOI = POI#{kind := kind_to_ref_kind(Kind)}, + Match = fun(#{uri := RefUri}) -> + case els_code_navigation:goto_definition(RefUri, RefPOI) of + {ok, [{Uri, _}]} -> true; + _Else -> false + end + end, + Refs = [Ref || Ref <- find_references_for_id(Kind, Id), Match(Ref)], + ?LOG_DEBUG( + "Found scoped references (with index) for ~p: ~p", + [Id, length(Refs)] + ), + Refs. + +-spec kind_to_ref_kind(els_poi:poi_kind()) -> els_poi:poi_kind(). +kind_to_ref_kind(define) -> + macro; +kind_to_ref_kind(record) -> + record_expr; +kind_to_ref_kind(record_def_field) -> + record_field; +kind_to_ref_kind(type_definition) -> + type_application; +kind_to_ref_kind(Kind) -> + Kind. + +-spec find_references_to_module(uri()) -> [els_dt_references:item()]. +find_references_to_module(Uri) -> + M = els_uri:module(Uri), + {ok, Doc} = els_utils:lookup_document(Uri), + ExportRefs = + lists:flatmap( + fun(#{id := {F, A}}) -> + {ok, Rs} = + els_dt_references:find_by_id(export_entry, {M, F, A}), + Rs + end, + els_dt_document:pois(Doc, [export_entry]) + ), + ExportTypeRefs = + lists:flatmap( + fun(#{id := {F, A}}) -> + {ok, Rs} = + els_dt_references:find_by_id(export_type_entry, {M, F, A}), + Rs + end, + els_dt_document:pois(Doc, [export_type_entry]) + ), + {ok, BehaviourRefs} = els_dt_references:find_by_id(behaviour, M), + ExcludeLocalRefs = fun(Loc) -> maps:get(uri, Loc) =/= Uri end, + lists:filter(ExcludeLocalRefs, ExportRefs ++ ExportTypeRefs ++ BehaviourRefs). + +-spec find_references_for_id(els_poi:poi_kind(), any()) -> [location()]. find_references_for_id(Kind, Id) -> - {ok, Refs} = els_dt_references:find_by_id(Kind, Id), - [location(U, R) || #{uri := U, range := R} <- Refs]. - --spec uri_pois_to_locations([{uri(), poi()}]) -> [location()]. -uri_pois_to_locations(Refs) -> - [location(U, R) || {U, #{range := R}} <- Refs]. + {ok, Refs} = els_dt_references:find_by_id(Kind, Id), + [location(U, R) || #{uri := U, range := R} <- Refs]. --spec location(uri(), poi_range()) -> location(). +-spec location(uri(), els_poi:poi_range()) -> location(). location(Uri, Range) -> - #{ uri => Uri - , range => els_protocol:range(Range) - }. + #{ + uri => Uri, + range => els_protocol:range(Range) + }. diff --git a/apps/els_lsp/src/els_rename_provider.erl b/apps/els_lsp/src/els_rename_provider.erl index 304f7d533..19f1a5edc 100644 --- a/apps/els_lsp/src/els_rename_provider.erl +++ b/apps/els_lsp/src/els_rename_provider.erl @@ -2,9 +2,10 @@ -behaviour(els_provider). --export([ handle_request/2 - , is_enabled/0 - ]). +-export([ + handle_request/1, + options/0 +]). %%============================================================================== %% Includes @@ -19,256 +20,364 @@ %%============================================================================== %% Types %%============================================================================== --type state() :: any(). %%============================================================================== %% els_provider functions %%============================================================================== --spec is_enabled() -> boolean(). -is_enabled() -> true. +-spec handle_request(any()) -> {response, any()}. +handle_request({rename, Params}) -> + #{ + <<"textDocument">> := #{<<"uri">> := Uri}, + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"newName">> := NewName + } = Params, + {ok, Document} = els_utils:lookup_document(Uri), + Elem = els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1), + WorkspaceEdits = workspace_edits(Uri, Elem, NewName), + {response, WorkspaceEdits}. --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({rename, Params}, State) -> - #{ <<"textDocument">> := #{<<"uri">> := Uri} - , <<"position">> := #{ <<"line">> := Line - , <<"character">> := Character - } - , <<"newName">> := NewName - } = Params, - {ok, Document} = els_utils:lookup_document(Uri), - Elem = els_dt_document:get_element_at_pos(Document, Line + 1, Character + 1), - WorkspaceEdits = workspace_edits(Uri, Elem, NewName), - {WorkspaceEdits, State}. +-spec options() -> boolean() | map(). +options() -> + case els_config:get(capabilities) of + #{<<"textDocument">> := #{<<"rename">> := #{<<"prepareSupport">> := true}}} -> + #{prepareProvider => true}; + _ -> + true + end. %%============================================================================== %% Internal functions %%============================================================================== --spec workspace_edits(uri(), [poi()], binary()) -> null | [any()]. +-spec workspace_edits( + uri(), + [els_poi:poi()], + binary() +) -> null | [any()]. workspace_edits(_Uri, [], _NewName) -> - null; -workspace_edits(Uri, [#{kind := function_clause} = POI| _], NewName) -> - #{id := {F, A, _}} = POI, - #{changes => changes(Uri, POI#{kind => function, id => {F, A}}, NewName)}; -workspace_edits(Uri, [#{kind := Kind} = POI| _], NewName) - when Kind =:= define; - Kind =:= record; - Kind =:= record_def_field; - Kind =:= function; - Kind =:= type_definition; - Kind =:= variable -> - #{changes => changes(Uri, POI, NewName)}; -workspace_edits(Uri, [#{kind := Kind} = POI| _], NewName) - when Kind =:= macro; - Kind =:= record_expr; - Kind =:= record_field; - Kind =:= application; - Kind =:= implicit_fun; - Kind =:= export_entry; - Kind =:= import_entry; - Kind =:= export_type_entry; - Kind =:= type_application -> - case els_code_navigation:goto_definition(Uri, POI) of - {ok, DefUri, DefPOI} -> - #{changes => changes(DefUri, DefPOI, NewName)}; - _ -> - null - end; + null; +workspace_edits(OldUri, [#{kind := module} = POI | _], NewName) -> + %% Generate new Uri + Path = els_uri:path(OldUri), + Dir = filename:dirname(Path), + NewPath = filename:join(Dir, <<NewName/binary, ".erl">>), + NewUri = els_uri:uri(NewPath), + %% Find references that needs to be changed + Refs = els_references_provider:find_references_to_module(OldUri), + RefPOIs = convert_references_to_pois(Refs, [ + application, + implicit_fun, + import_entry, + type_application, + behaviour + ]), + Changes = [ + #{ + textDocument => + #{uri => RefUri, version => null}, + edits => [ + #{ + range => editable_range(RefPOI, module), + newText => NewName + } + ] + } + || {RefUri, RefPOI} <- RefPOIs + ], + #{ + documentChanges => + %% Update -module attribute + [ + #{ + textDocument => #{uri => OldUri, version => null}, + edits => [change(POI, NewName)] + }, + %% Rename file + #{kind => rename, oldUri => OldUri, newUri => NewUri} + | Changes + ] + }; +workspace_edits(Uri, [#{kind := function_clause} = POI | _], NewName) -> + #{id := {F, A, _}} = POI, + #{changes => changes(Uri, POI#{kind => function, id => {F, A}}, NewName)}; +workspace_edits(Uri, [#{kind := spec} = POI | _], NewName) -> + #{changes => changes(Uri, POI#{kind => function}, NewName)}; +workspace_edits(Uri, [#{kind := Kind} = POI | _], NewName) when + Kind =:= define; + Kind =:= record; + Kind =:= record_def_field; + Kind =:= function; + Kind =:= type_definition; + Kind =:= variable +-> + #{changes => changes(Uri, POI, NewName)}; +workspace_edits(Uri, [#{kind := Kind} = POI | _], NewName) when + Kind =:= macro; + Kind =:= record_expr; + Kind =:= record_field; + Kind =:= application; + Kind =:= implicit_fun; + Kind =:= export_entry; + Kind =:= import_entry; + Kind =:= export_type_entry; + Kind =:= type_application; + Kind =:= nifs_entry +-> + case els_code_navigation:goto_definition(Uri, POI) of + {ok, [{DefUri, DefPOI}]} -> + #{changes => changes(DefUri, DefPOI, NewName)}; + _ -> + null + end; workspace_edits(Uri, [#{kind := 'callback'} = POI | _], NewName) -> - #{id := {Name, Arity} = Id} = POI, - Module = els_uri:module(Uri), - {ok, Refs} = els_dt_references:find_by_id(behaviour, Module), - Changes = - lists:foldl( - fun(#{uri := U}, Acc) -> - {ok, Doc} = els_utils:lookup_document(U), - ExportEntries = els_dt_document:pois(Doc, [export_entry]), - FunctionClauses = els_dt_document:pois(Doc, [function_clause]), - Specs = els_dt_document:pois(Doc, [spec]), - Acc#{ U => - [ #{ range => editable_range(P) - , newText => NewName - } || #{id := I} = P <- ExportEntries, I =:= Id - ] ++ - [ #{ range => editable_range(P) - , newText => NewName - } || #{id := {N, A, _I}} = P <- FunctionClauses - , N =:= Name - , A =:= Arity - ] ++ - [ #{ range => editable_range(P) - , newText => NewName - } || #{id := I} = P <- Specs, I =:= Id - ] - } - end, #{ Uri => - [#{ range => editable_range(POI) - , newText => NewName - }] - }, Refs), - #{changes => Changes}; + #{id := {Name, Arity} = Id} = POI, + Module = els_uri:module(Uri), + {ok, Refs} = els_dt_references:find_by_id(behaviour, Module), + Changes = + lists:foldl( + fun(#{uri := U}, Acc) -> + {ok, Doc} = els_utils:lookup_document(U), + ExportEntries = els_dt_document:pois(Doc, [export_entry]), + FunctionClauses = els_dt_document:pois(Doc, [function_clause]), + Specs = els_dt_document:pois(Doc, [spec]), + Acc#{ + U => + [ + #{ + range => editable_range(P), + newText => NewName + } + || #{id := I} = P <- ExportEntries, I =:= Id + ] ++ + [ + #{ + range => editable_range(P), + newText => NewName + } + || #{id := {N, A, _I}} = P <- FunctionClauses, + N =:= Name, + A =:= Arity + ] ++ + [ + #{ + range => editable_range(P), + newText => NewName + } + || #{id := I} = P <- Specs, I =:= Id + ] + } + end, + #{ + Uri => + [ + #{ + range => editable_range(POI), + newText => NewName + } + ] + }, + Refs + ), + #{changes => Changes}; workspace_edits(_Uri, _POIs, _NewName) -> - null. + null. --spec editable_range(poi()) -> range(). -editable_range(#{kind := Kind, data := #{name_range := Range}}) - when Kind =:= application; - Kind =:= implicit_fun; - Kind =:= callback; - Kind =:= spec; - Kind =:= export_entry; - Kind =:= export_type_entry; - Kind =:= import_entry; - Kind =:= type_application; - Kind =:= type_definition -> - %% application POI of a local call and - %% type_application POI of a built-in type don't have name_range data - %% they are handled by the next clause - els_protocol:range(Range); -editable_range(#{kind := _Kind, range := Range}) -> - els_protocol:range(Range). +-spec editable_range(els_poi:poi()) -> range(). +editable_range(POI) -> + editable_range(POI, function). --spec changes(uri(), poi(), binary()) -> #{uri() => [text_edit()]} | null. -changes(Uri, #{kind := variable, id := VarId, range := VarRange}, NewName) -> - %% Rename variable in function clause scope - case els_utils:lookup_document(Uri) of - {ok, Document} -> - FunRange = function_clause_range(VarRange, Document), - Changes = [#{range => editable_range(POI), newText => NewName} || - POI <- els_dt_document:pois(Document, [variable]), - maps:get(id, POI) =:= VarId, - els_range:in(maps:get(range, POI), FunRange) - ], - #{Uri => Changes}; - {error, _} -> - null - end; +-spec editable_range(els_poi:poi(), function | module) -> range(). +editable_range(#{kind := Kind, data := #{mod_range := Range}}, module) when + Kind =:= application; + Kind =:= implicit_fun; + Kind =:= import_entry; + Kind =:= type_application; + Kind =:= behaviour +-> + els_protocol:range(Range); +editable_range(#{kind := Kind, data := #{name_range := Range}}, function) when + Kind =:= application; + Kind =:= implicit_fun; + Kind =:= callback; + Kind =:= spec; + Kind =:= export_entry; + Kind =:= export_type_entry; + Kind =:= import_entry; + Kind =:= type_application; + Kind =:= type_definition; + Kind =:= nifs_entry +-> + %% application POI of a local call and + %% type_application POI of a built-in type don't have name_range data + %% they are handled by the next clause + els_protocol:range(Range); +editable_range(#{kind := _Kind, range := Range}, _) -> + els_protocol:range(Range). + +-spec changes(uri(), els_poi:poi(), binary()) -> #{uri() => [text_edit()]} | null. +changes(Uri, #{kind := module} = Mod, NewName) -> + #{Uri => [#{range => editable_range(Mod), newText => NewName}]}; +changes(Uri, #{kind := variable} = Var, NewName) -> + POIs = els_code_navigation:find_in_scope(Uri, Var), + #{Uri => [#{range => editable_range(P), newText => NewName} || P <- POIs]}; changes(Uri, #{kind := type_definition, id := {Name, A}}, NewName) -> - ?LOG_INFO("Renaming type ~p/~p to ~s", [Name, A, NewName]), - {ok, Doc} = els_utils:lookup_document(Uri), - SelfChanges = [change(P, NewName) || - P <- els_dt_document:pois(Doc, [ type_definition - , export_type_entry - ]), - maps:get(id, P) =:= {Name, A} - ], - Key = {els_uri:module(Uri), Name, A}, - {ok, Refs} = els_dt_references:find_by_id(type_application, Key), - RefPOIs = convert_references_to_pois(Refs, [ type_application - ]), - Changes = - lists:foldl( - fun({RefUri, RefPOI}, Acc) -> - Change = change(RefPOI, NewName), - maps:update_with(RefUri, fun(V) -> [Change|V] end, [Change], Acc) - end, #{Uri => SelfChanges}, RefPOIs), - ?LOG_INFO("Done renaming type ~p/~p to ~s. ~p changes in ~p files.", - [Name, A, NewName, length(lists:flatten(maps:values(Changes))), - length(maps:keys(Changes))]), - Changes; + ?LOG_INFO("Renaming type ~p/~p to ~s", [Name, A, NewName]), + {ok, Doc} = els_utils:lookup_document(Uri), + SelfChanges = [ + change(P, NewName) + || P <- els_dt_document:pois(Doc, [ + type_definition, + export_type_entry + ]), + maps:get(id, P) =:= {Name, A} + ], + Key = {els_uri:module(Uri), Name, A}, + {ok, Refs} = els_dt_references:find_by_id(type_application, Key), + RefPOIs = convert_references_to_pois(Refs, [type_application]), + Changes = + lists:foldl( + fun({RefUri, RefPOI}, Acc) -> + Change = change(RefPOI, NewName), + maps:update_with(RefUri, fun(V) -> [Change | V] end, [Change], Acc) + end, + #{Uri => SelfChanges}, + RefPOIs + ), + ?LOG_INFO( + "Done renaming type ~p/~p to ~s. ~p changes in ~p files.", + [ + Name, + A, + NewName, + length(lists:flatten(maps:values(Changes))), + length(maps:keys(Changes)) + ] + ), + Changes; changes(Uri, #{kind := function, id := {F, A}}, NewName) -> - ?LOG_INFO("Renaming function ~p/~p to ~s", [F, A, NewName]), - {ok, Doc} = els_utils:lookup_document(Uri), - IsMatch = fun (#{id := {Fun, Arity}}) -> {Fun, Arity} =:= {F, A}; - (#{id := {Fun, Arity, _}}) -> {Fun, Arity} =:= {F, A}; - (_) -> false + ?LOG_INFO("Renaming function ~p/~p to ~s", [F, A, NewName]), + {ok, Doc} = els_utils:lookup_document(Uri), + IsMatch = fun + (#{id := {Fun, Arity}}) -> {Fun, Arity} =:= {F, A}; + (#{id := {Fun, Arity, _}}) -> {Fun, Arity} =:= {F, A}; + (_) -> false + end, + SelfChanges = [ + change(P, NewName) + || P <- els_dt_document:pois(Doc, [ + export_entry, + function_clause, + spec, + nifs_entry + ]), + IsMatch(P) + ], + Key = {els_uri:module(Uri), F, A}, + {ok, Refs} = els_dt_references:find_by_id(function, Key), + RefPOIs = convert_references_to_pois(Refs, [ + application, + implicit_fun, + import_entry + ]), + Changes = + lists:foldl( + fun({RefUri, RefPOI}, Acc) -> + ImportChanges = import_changes(RefUri, RefPOI, NewName), + Changes = [change(RefPOI, NewName)] ++ ImportChanges, + maps:update_with(RefUri, fun(V) -> Changes ++ V end, Changes, Acc) end, - SelfChanges = [change(P, NewName) || - P <- els_dt_document:pois(Doc, [ export_entry - , function_clause - , spec - ]), - IsMatch(P) - ], - Key = {els_uri:module(Uri), F, A}, - {ok, Refs} = els_dt_references:find_by_id(function, Key), - RefPOIs = convert_references_to_pois(Refs, [ application - , implicit_fun - , import_entry - ]), - Changes = + #{Uri => SelfChanges}, + RefPOIs + ), + ?LOG_INFO( + "Done renaming function ~p/~p to ~s. ~p changes in ~p files.", + [ + F, + A, + NewName, + length(lists:flatten(maps:values(Changes))), + length(maps:keys(Changes)) + ] + ), + Changes; +changes(Uri, #{kind := DefKind} = DefPOI, NewName) when + DefKind =:= define; + DefKind =:= record; + DefKind =:= record_def_field +-> + Self = #{range => editable_range(DefPOI), newText => NewName}, + Refs = els_references_provider:find_scoped_references_for_def(Uri, DefPOI), lists:foldl( - fun({RefUri, RefPOI}, Acc) -> - ImportChanges = import_changes(RefUri, RefPOI, NewName), - Changes = [change(RefPOI, NewName)] ++ ImportChanges, - maps:update_with(RefUri, fun(V) -> Changes ++ V end, Changes, Acc) - end, #{Uri => SelfChanges}, RefPOIs), - ?LOG_INFO("Done renaming function ~p/~p to ~s. ~p changes in ~p files.", - [F, A, NewName, length(lists:flatten(maps:values(Changes))), - length(maps:keys(Changes))]), - Changes; -changes(Uri, #{kind := DefKind} = DefPoi, NewName) - when DefKind =:= define; - DefKind =:= record; - DefKind =:= record_def_field -> - Self = #{range => editable_range(DefPoi), newText => NewName}, - Refs = els_references_provider:find_scoped_references_for_def(Uri, DefPoi), - lists:foldl( - fun({U, Poi}, Acc) -> - Change = #{ range => editable_range(Poi) - , newText => new_name(Poi, NewName) - }, - maps:update_with(U, fun(V) -> [Change|V] end, [Change], Acc) - end, #{Uri => [Self]}, Refs); + fun(#{uri := U, range := R}, Acc) -> + Change = #{ + range => R, + newText => new_name(DefKind, NewName) + }, + maps:update_with(U, fun(V) -> [Change | V] end, [Change], Acc) + end, + #{Uri => [Self]}, + Refs + ); changes(_Uri, _POI, _NewName) -> - null. + null. --spec new_name(poi(), binary()) -> binary(). -new_name(#{kind := macro}, NewName) -> - <<"?", NewName/binary>>; -new_name(#{kind := record_expr}, NewName) -> - <<"#", NewName/binary>>; +-spec new_name(els_poi:poi_kind(), binary()) -> binary(). +new_name(define, NewName) -> + <<"?", NewName/binary>>; +new_name(record, NewName) -> + <<"#", NewName/binary>>; new_name(_, NewName) -> - NewName. - --spec function_clause_range(poi_range(), els_dt_document:item()) -> poi_range(). -function_clause_range(VarRange, Document) -> - FunPOIs = els_poi:sort(els_dt_document:pois(Document, [function_clause])), - %% Find beginning of first function clause before VarRange - From = case [R || #{range := R} <- FunPOIs, els_range:compare(R, VarRange)] of - [] -> {0, 0}; % Beginning of document - FunRanges -> maps:get(from, lists:last(FunRanges)) - end, - %% Find beginning of first function clause after VarRange - To = case [R || #{range := R} <- FunPOIs, els_range:compare(VarRange, R)] of - [] -> {999999999, 999999999}; % End of document - [#{from := End}|_] -> End - end, - #{from => From, to => To}. + NewName. --spec convert_references_to_pois([els_dt_references:item()], [poi_kind()]) -> - [{uri(), poi()}]. +-spec convert_references_to_pois([els_dt_references:item()], [els_poi:poi_kind()]) -> + [{uri(), els_poi:poi()}]. convert_references_to_pois(Refs, Kinds) -> - UriPOIs = lists:foldl(fun(#{uri := Uri}, Acc) when is_map_key(Uri, Acc) -> - Acc; - (#{uri := Uri}, Acc) -> - POIs = case els_utils:lookup_document(Uri) of - {ok, Doc} -> - els_dt_document:pois(Doc, Kinds); - {error, _} -> - [] - end, - maps:put(Uri, POIs, Acc) - end, #{}, Refs), - lists:map(fun(#{uri := Uri, range := #{from := Pos}}) -> - POIs = maps:get(Uri, UriPOIs), - {Uri, hd(els_poi:match_pos(POIs, Pos))} - end, Refs). + UriPOIs = lists:foldl( + fun + (#{uri := Uri}, Acc) when is_map_key(Uri, Acc) -> + Acc; + (#{uri := Uri}, Acc) -> + POIs = + case els_utils:lookup_document(Uri) of + {ok, Doc} -> + els_dt_document:pois(Doc, Kinds); + {error, _} -> + [] + end, + maps:put(Uri, POIs, Acc) + end, + #{}, + Refs + ), + lists:map( + fun(#{uri := Uri, range := #{from := Pos}}) -> + POIs = maps:get(Uri, UriPOIs), + {Uri, hd(els_poi:match_pos(POIs, Pos))} + end, + Refs + ). %% @doc Find all uses of imported function in Uri --spec import_changes(uri(), poi(), binary()) -> [text_edit()]. +-spec import_changes(uri(), els_poi:poi(), binary()) -> [text_edit()]. import_changes(Uri, #{kind := import_entry, id := {_M, F, A}}, NewName) -> - case els_utils:lookup_document(Uri) of - {ok, Doc} -> - [change(P, NewName) || P <- els_dt_document:pois(Doc, [ application - , implicit_fun - ]), - maps:get(id, P) =:= {F, A}]; - {error, _} -> - [] - end; + case els_utils:lookup_document(Uri) of + {ok, Doc} -> + [ + change(P, NewName) + || P <- els_dt_document:pois(Doc, [ + application, + implicit_fun + ]), + maps:get(id, P) =:= {F, A} + ]; + {error, _} -> + [] + end; import_changes(_Uri, _POI, _NewName) -> - []. + []. --spec change(poi(), binary()) -> text_edit(). +-spec change(els_poi:poi(), binary()) -> text_edit(). change(POI, NewName) -> - #{range => editable_range(POI), newText => NewName}. + #{range => editable_range(POI), newText => NewName}. diff --git a/apps/els_lsp/src/els_scope.erl b/apps/els_lsp/src/els_scope.erl index ab12a1d55..37736f5b7 100644 --- a/apps/els_lsp/src/els_scope.erl +++ b/apps/els_lsp/src/els_scope.erl @@ -1,70 +1,174 @@ %%% @doc Library module to calculate various scoping rules -module(els_scope). --export([ local_and_included_pois/2 - , local_and_includer_pois/2 - ]). +-export([ + local_and_included_pois/2, + local_and_includer_pois/2, + variable_scope_range/2, + pois_before/2 +]). -include("els_lsp.hrl"). %% @doc Return POIs of the provided `Kinds' in the document and included files --spec local_and_included_pois(els_dt_document:item(), poi_kind() | [poi_kind()]) - -> [poi()]. +-spec local_and_included_pois(els_dt_document:item(), els_poi:poi_kind() | [els_poi:poi_kind()]) -> + [els_poi:poi()]. local_and_included_pois(Document, Kind) when is_atom(Kind) -> - local_and_included_pois(Document, [Kind]); + local_and_included_pois(Document, [Kind]); local_and_included_pois(Document, Kinds) -> - lists:flatten([ els_dt_document:pois(Document, Kinds) - , included_pois(Document, Kinds) - ]). + lists:flatten([ + els_dt_document:pois(Document, Kinds), + included_pois(Document, Kinds) + ]). %% @doc Return POIs of the provided `Kinds' in included files from `Document' --spec included_pois(els_dt_document:item(), [poi_kind()]) -> [poi()]. +-spec included_pois(els_dt_document:item(), [els_poi:poi_kind()]) -> [els_poi:poi()]. included_pois(Document, Kinds) -> - els_diagnostics_utils:traverse_include_graph( - fun(IncludedDocument, _Includer, Acc) -> - els_dt_document:pois(IncludedDocument, Kinds) ++ Acc - end, - [], - Document). + els_diagnostics_utils:traverse_include_graph( + fun(IncludedDocument, _Includer, Acc) -> + els_dt_document:pois(IncludedDocument, Kinds) ++ Acc + end, + [], + Document + ). %% @doc Return POIs of the provided `Kinds' in the local document and files that %% (maybe recursively) include it --spec local_and_includer_pois(uri(), [poi_kind()]) -> - [{uri(), [poi()]}]. +-spec local_and_includer_pois(uri(), [els_poi:poi_kind()]) -> + [{uri(), [els_poi:poi()]}]. local_and_includer_pois(LocalUri, Kinds) -> - [{Uri, find_pois_by_uri(Uri, Kinds)} - || Uri <- local_and_includers(LocalUri)]. + [ + {Uri, find_pois_by_uri(Uri, Kinds)} + || Uri <- local_and_includers(LocalUri) + ]. --spec find_pois_by_uri(uri(), [poi_kind()]) -> [poi()]. +-spec find_pois_by_uri(uri(), [els_poi:poi_kind()]) -> [els_poi:poi()]. find_pois_by_uri(Uri, Kinds) -> - {ok, Document} = els_utils:lookup_document(Uri), - els_dt_document:pois(Document, Kinds). + {ok, Document} = els_utils:lookup_document(Uri), + els_dt_document:pois(Document, Kinds). -spec local_and_includers(uri()) -> [uri()]. local_and_includers(Uri) -> - find_includers_loop(Uri, [Uri]). + find_includers_loop(Uri, [Uri]). -spec find_includers_loop(uri(), ordsets:ordset(uri())) -> - ordsets:ordset(uri()). + ordsets:ordset(uri()). find_includers_loop(Uri, Acc0) -> - Includers = find_includers(Uri), - case ordsets:subtract(Includers, Acc0) of - [] -> - %% no new uris - Acc0; - New -> - Acc1 = ordsets:union(New, Acc0), - lists:foldl(fun find_includers_loop/2, Acc1, New) - end. + Includers = find_includers(Uri), + case ordsets:subtract(Includers, Acc0) of + [] -> + %% no new uris + Acc0; + New -> + Acc1 = ordsets:union(New, Acc0), + lists:foldl(fun find_includers_loop/2, Acc1, New) + end. -spec find_includers(uri()) -> [uri()]. find_includers(Uri) -> - IncludeId = els_utils:include_id(els_uri:path(Uri)), - IncludeLibId = els_utils:include_lib_id(els_uri:path(Uri)), - lists:usort(find_includers(include, IncludeId) ++ - find_includers(include_lib, IncludeLibId)). + Path = els_uri:path(Uri), + case filename:extension(Path) of + <<".hrl">> -> + IncludeId = els_utils:include_id(Path), + IncludeLibId = els_utils:include_lib_id(Path), + lists:usort( + find_includers(include, IncludeId) ++ + find_includers(include_lib, IncludeLibId) + ); + _ -> + [] + end. --spec find_includers(poi_kind(), string()) -> [uri()]. +-spec find_includers(els_poi:poi_kind(), string()) -> [uri()]. find_includers(Kind, Id) -> - {ok, Items} = els_dt_references:find_by_id(Kind, Id), - [Uri || #{uri := Uri} <- Items]. + {ok, Items} = els_dt_references:find_by_id(Kind, Id), + [Uri || #{uri := Uri} <- Items]. + +%% @doc Find the rough scope of a variable, this is based on heuristics and +%% won't always be correct. +%% `VarRange' is expected to be the range of the variable. +-spec variable_scope_range(els_poi:poi_range(), els_dt_document:item()) -> els_poi:poi_range(). +variable_scope_range(VarRange, Document) -> + Attributes = [spec, callback, define, record, type_definition], + AttrPOIs = els_dt_document:pois(Document, Attributes), + case pois_match(AttrPOIs, VarRange) of + [#{range := Range}] -> + %% Inside attribute, simple. + Range; + [] -> + %% If variable is not inside an attribute we need to figure out where the + %% scope of the variable begins and ends. + %% The scope of variables inside functions are limited by function clauses + %% The scope of variables outside of function are limited by top-level + %% POIs (attributes and functions) before and after. + FunPOIs = els_poi:sort(els_dt_document:pois(Document, [function])), + POIs = els_poi:sort( + els_dt_document:pois(Document, [ + function_clause + | Attributes + ]) + ), + CurrentFunRange = + case pois_match(FunPOIs, VarRange) of + [] -> undefined; + [POI] -> range(POI) + end, + IsInsideFunction = CurrentFunRange /= undefined, + BeforeFunRanges = [range(POI) || POI <- pois_before(FunPOIs, VarRange)], + %% Find where scope should begin + From = + case [R || #{range := R} <- pois_before(POIs, VarRange)] of + [] -> + %% No POIs before + {0, 0}; + [BeforeRange | _] when IsInsideFunction -> + %% Inside function, use beginning of closest function clause + maps:get(from, BeforeRange); + [BeforeRange | _] when BeforeFunRanges == [] -> + %% No function before, use end of closest POI + maps:get(to, BeforeRange); + [BeforeRange | _] -> + %% Use end of closest POI, including functions. + max( + maps:get(to, hd(BeforeFunRanges)), + maps:get(to, BeforeRange) + ) + end, + %% Find when scope should end + To = + case [R || #{range := R} <- pois_after(POIs, VarRange)] of + [] when IsInsideFunction -> + %% No POIs after, use end of function + maps:get(to, CurrentFunRange); + [] -> + %% No POIs after, use end of document + {999999999, 999999999}; + [AfterRange | _] when IsInsideFunction -> + %% Inside function, use closest of end of function *OR* + %% beginning of the next function clause + min(maps:get(to, CurrentFunRange), maps:get(from, AfterRange)); + [AfterRange | _] -> + %% Use beginning of next POI + maps:get(from, AfterRange) + end, + #{from => From, to => To} + end. + +-spec pois_before([els_poi:poi()], els_poi:poi_range()) -> [els_poi:poi()]. +pois_before(POIs, Range) -> + %% Reverse since we are typically interested in the last POI + lists:reverse([POI || POI <- POIs, els_range:compare(range(POI), Range)]). + +-spec pois_after([els_poi:poi()], els_poi:poi_range()) -> [els_poi:poi()]. +pois_after(POIs, VarRange) -> + [POI || POI <- POIs, els_range:compare(VarRange, range(POI))]. + +-spec pois_match([els_poi:poi()], els_poi:poi_range()) -> [els_poi:poi()]. +pois_match(POIs, Range) -> + [POI || POI <- POIs, els_range:in(Range, range(POI))]. + +-spec range(els_poi:poi()) -> els_poi:poi_range(). +range(#{kind := function, data := #{wrapping_range := Range}}) -> + Range; +range(#{range := Range}) -> + Range. diff --git a/apps/els_lsp/src/els_semantic_token_provider.erl b/apps/els_lsp/src/els_semantic_token_provider.erl new file mode 100644 index 000000000..571c1876d --- /dev/null +++ b/apps/els_lsp/src/els_semantic_token_provider.erl @@ -0,0 +1,20 @@ +-module(els_semantic_token_provider). + +-behaviour(els_provider). + +-include("els_lsp.hrl"). +-export([handle_request/1]). + +%%============================================================================== +%% els_provider functions +%%============================================================================== + +-spec handle_request(any()) -> {response, any()}. +handle_request({semantic_tokens, Params}) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + Result = #{<<"data">> => semantic_tokens(Uri)}, + {response, Result}. + +-spec semantic_tokens(uri()) -> [integer()]. +semantic_tokens(Uri) -> + wrangler_handler:get_semantic_tokens(Uri). diff --git a/apps/els_lsp/src/els_server.erl b/apps/els_lsp/src/els_server.erl index eedfb736a..629f5cd47 100644 --- a/apps/els_lsp/src/els_server.erl +++ b/apps/els_lsp/src/els_server.erl @@ -12,227 +12,375 @@ %% Exports %%============================================================================== --export([ start_link/0 - ]). +-export([start_link/0]). %% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - ]). +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2 +]). %% API --export([ process_requests/1 - , set_io_device/1 - , send_notification/2 - , send_request/2 - , send_response/2 - ]). +-export([ + process_requests/1, + set_io_device/1, + send_notification/2, + send_request/2, + send_response/2, + register_result/1, + register_diagonstics/2 +]). %% Testing --export([ reset_internal_state/0 - ]). +-export([reset_state/0]). %%============================================================================== %% Includes %%============================================================================== -include_lib("kernel/include/logger.hrl"). +-include_lib("els_core/include/els_core.hrl"). %%============================================================================== %% Macros %%============================================================================== -define(SERVER, ?MODULE). -%%============================================================================== -%% Record Definitions -%%============================================================================== --record(state, { io_device :: any() - , request_id :: number() - , internal_state :: map() - , pending :: [{number(), els_provider:provider(), pid()}] - }). - %%============================================================================== %% Type Definitions %%============================================================================== --type state() :: #state{}. +-type state() :: + #{ + status := started | initialized | shutdown | exiting, + io_device := pid() | standard_io, + request_id := number(), + pending := [{number(), pid()}], + open_buffers := sets:set(buffer()), + in_progress := [progress_entry()], + in_progress_diagnostics := [diagnostic_entry()] + }. +-type buffer() :: uri(). +-type progress_entry() :: {uri(), pid()}. +-type diagnostic_entry() :: #{ + uri := uri(), + pending := [pid()], + diagnostics := [els_diagnostics:diagnostic()] +}. +-export_type([diagnostic_entry/0, state/0]). %%============================================================================== %% 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}. + {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}). + 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}). + gen_server:call(?SERVER, {set_io_device, IoDevice}). -spec send_notification(binary(), map()) -> ok. send_notification(Method, Params) -> - gen_server:cast(?SERVER, {notification, Method, Params}). + gen_server:cast(?SERVER, {notification, Method, Params}). -spec send_request(binary(), map()) -> ok. send_request(Method, Params) -> - gen_server:cast(?SERVER, {request, Method, Params}). + gen_server:cast(?SERVER, {request, Method, Params}). -spec send_response(pid(), any()) -> ok. send_response(Job, Result) -> - gen_server:cast(?SERVER, {response, Job, Result}). + gen_server:cast(?SERVER, {response, Job, Result}). + +-spec register_result(any()) -> ok. +register_result(Resp) -> + els_server ! {result, Resp, self()}, + ok. + +-spec register_diagonstics([els_diagnostics:diagnostic()], pid()) -> ok. +register_diagonstics(Diagnostics, Job) -> + els_server ! {diagnostics, Diagnostics, Job}, + ok. %%============================================================================== %% Testing %%============================================================================== --spec reset_internal_state() -> ok. -reset_internal_state() -> - gen_server:call(?MODULE, {reset_internal_state}). +-spec reset_state() -> ok. +reset_state() -> + gen_server:call(?MODULE, {reset_state}). %%============================================================================== %% gen_server callbacks %%============================================================================== -spec init([]) -> {ok, state()}. init([]) -> - ?LOG_INFO("Starting els_server..."), - State = #state{ request_id = 0 - , internal_state = #{} - , pending = [] - }, - {ok, State}. + %% Ensure the terminate function is called on shutdown, allowing the + %% job to clean up. + process_flag(trap_exit, true), + ?LOG_INFO("Starting els_server..."), + State = #{ + status => started, + io_device => standard_io, + request_id => 0, + pending => [], + open_buffers => sets:new(), + in_progress => [], + in_progress_diagnostics => [] + }, + {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 = #{}}}. + {reply, ok, State#{io_device := IoDevice}}; +handle_call({reset_state}, _From, State) -> + {reply, ok, State#{ + status => started, + open_buffers => sets:new(), + in_progress => [], + in_progress_diagnostics => [] + }}. -spec handle_cast(any(), state()) -> {noreply, state()}. handle_cast({process_requests, Requests}, State0) -> - State = lists:foldl(fun handle_request/2, State0, Requests), - {noreply, State}; + State = lists:foldl(fun handle_request/2, State0, Requests), + {noreply, State}; handle_cast({notification, Method, Params}, State) -> - do_send_notification(Method, Params, State), - {noreply, State}; + do_send_notification(Method, Params, State), + {noreply, State}; handle_cast({request, Method, Params}, State0) -> - State = do_send_request(Method, Params, State0), - {noreply, State}; + State = do_send_request(Method, Params, State0), + {noreply, State}; handle_cast({response, Job, Result}, State0) -> - State = do_send_response(Job, Result, State0), - {noreply, State}; + State = do_send_response(Job, Result, State0), + {noreply, State}; handle_cast(_, State) -> - {noreply, State}. + {noreply, State}. + +-spec handle_info(any(), els_server:state()) -> {noreply, els_server:state()}. +handle_info({result, Result, Job}, State0) -> + ?LOG_DEBUG("Received result [job=~p]", [Job]), + #{in_progress := InProgress} = State0, + State = do_send_response(Job, Result, State0), + NewState = State#{in_progress => lists:keydelete(Job, 2, InProgress)}, + {noreply, NewState}; +%% LSP 3.15 introduce versioning for diagnostics. Until all clients +%% support it, we need to keep track of old diagnostics and re-publish +%% them every time we get a new chunk. +handle_info({diagnostics, Diagnostics, Job}, State) -> + #{in_progress_diagnostics := InProgress} = State, + ?LOG_DEBUG("Received diagnostics [job=~p]", [Job]), + case find_entry(Job, InProgress) of + {ok, { + #{ + pending := Jobs, + diagnostics := OldDiagnostics, + uri := Uri + }, + Rest + }} -> + NewDiagnostics = Diagnostics ++ OldDiagnostics, + els_diagnostics_provider:publish(Uri, NewDiagnostics), + NewState = + case lists:delete(Job, Jobs) of + [] -> + State#{in_progress_diagnostics => Rest}; + Remaining -> + State#{ + in_progress_diagnostics => + [ + #{ + pending => Remaining, + diagnostics => NewDiagnostics, + uri => Uri + } + | Rest + ] + } + end, + {noreply, NewState}; + {error, not_found} -> + {noreply, State} + end; +handle_info(_Request, State) -> + {noreply, State}. + +-spec terminate(any(), els_server:state()) -> ok. +terminate(_Reason, #{in_progress := InProgress}) -> + [els_background_job:stop(Job) || {_Uri, Job} <- InProgress], + ok. %%============================================================================== %% Internal Functions %%============================================================================== -spec handle_request(map(), state()) -> state(). -handle_request(#{ <<"method">> := <<"$/cancelRequest">> - , <<"params">> := Params - }, State0) -> - #{<<"id">> := Id} = Params, - #state{pending = Pending} = State0, - case lists:keyfind(Id, 1, Pending) of - false -> - ?LOG_DEBUG("Trying to cancel not existing request [params=~p]", - [Params]), - State0; - {RequestId, Provider, Job} when RequestId =:= Id -> - ?LOG_DEBUG("[SERVER] Cancelling request [id=~p] [provider=~p] [job=~p]", - [Id, Provider, Job]), - els_provider:cancel_request(Provider, Job), - State0#state{pending = lists:keydelete(Id, 1, Pending)} - end; -handle_request(#{ <<"method">> := _ReqMethod } = Request - , #state{ internal_state = InternalState - , pending = Pending - } = State0) -> - Method = maps:get(<<"method">>, Request), - Params = maps:get(<<"params">>, Request, #{}), - Type = case maps:is_key(<<"id">>, Request) of - true -> request; - false -> notification - end, - case els_methods:dispatch(Method, Params, Type, InternalState) of - {response, Result, NewInternalState} -> - RequestId = maps:get(<<"id">>, Request), - Response = els_protocol:response(RequestId, Result), - ?LOG_DEBUG("[SERVER] Sending response [response=~s]", [Response]), - send(Response, State0), - State0#state{internal_state = NewInternalState}; - {error, Error, NewInternalState} -> - RequestId = maps:get(<<"id">>, Request, null), - ErrorResponse = els_protocol:error(RequestId, Error), - ?LOG_DEBUG( "[SERVER] Sending error response [response=~s]" - , [ErrorResponse] - ), - send(ErrorResponse, State0), - State0#state{internal_state = NewInternalState}; - {noresponse, NewInternalState} -> - ?LOG_DEBUG("[SERVER] No response", []), - State0#state{internal_state = NewInternalState}; - {noresponse, {Provider, BackgroundJob}, NewInternalState} -> - RequestId = maps:get(<<"id">>, Request), - ?LOG_DEBUG("[SERVER] Suspending response [background_job=~p]", - [BackgroundJob]), - NewPending = [{RequestId, Provider, BackgroundJob}| Pending], - State0#state{ internal_state = NewInternalState - , pending = NewPending - }; - {notification, M, P, NewInternalState} -> - do_send_notification(M, P, State0), - State0#state{internal_state = NewInternalState} - end; -handle_request(Response, State0) -> - ?LOG_DEBUG( "[SERVER] got request response [response=~p]" - , [Response] +handle_request( + #{ + <<"method">> := <<"$/cancelRequest">>, + <<"params">> := Params + }, + State0 +) -> + #{<<"id">> := Id} = Params, + #{pending := Pending, in_progress := InProgress} = State0, + case lists:keyfind(Id, 1, Pending) of + false -> + ?LOG_DEBUG( + "Trying to cancel not existing request [params=~p]", + [Params] + ), + State0; + {RequestId, Job} when RequestId =:= Id -> + ?LOG_DEBUG("[SERVER] Cancelling request [id=~p] [job=~p]", [Id, Job]), + els_background_job:stop(Job), + Error = #{ + code => ?ERR_REQUEST_CANCELLED, + message => <<"Request was cancelled">> + }, + ErrorResponse = els_protocol:error(RequestId, Error), + ?LOG_DEBUG( + "[SERVER] Sending error response [response=~p]", + [ErrorResponse] ), - State0. + send(ErrorResponse, State0), + State0#{ + pending => lists:keydelete(Id, 1, Pending), + in_progress => lists:keydelete(Job, 2, InProgress) + } + end; +handle_request( + #{<<"method">> := _ReqMethod} = Request, + #{ + pending := Pending, + in_progress := InProgress, + in_progress_diagnostics := InProgressDiagnostics + } = State0 +) -> + Method = maps:get(<<"method">>, Request), + Params = maps:get(<<"params">>, Request, #{}), + Type = + case maps:is_key(<<"id">>, Request) of + true -> request; + false -> notification + end, + case els_methods:dispatch(Method, Params, Type, State0) of + {response, Result, State} -> + RequestId = maps:get(<<"id">>, Request), + Response = els_protocol:response(RequestId, Result), + ?LOG_DEBUG("[SERVER] Sending response [response=~s]", [Response]), + send(Response, State0), + State; + {error, Error, State} -> + RequestId = maps:get(<<"id">>, Request, null), + ErrorResponse = els_protocol:error(RequestId, Error), + ?LOG_DEBUG( + "[SERVER] Sending error response [response=~s]", + [ErrorResponse] + ), + send(ErrorResponse, State0), + State; + {noresponse, State} -> + ?LOG_DEBUG("[SERVER] No response", []), + State; + {async, Uri, BackgroundJob, State} -> + RequestId = maps:get(<<"id">>, Request), + ?LOG_DEBUG( + "[SERVER] Suspending response [background_job=~p id=~p]", + [BackgroundJob, RequestId] + ), + NewPending = [{RequestId, BackgroundJob} | Pending], + State#{ + pending => NewPending, + in_progress => [{Uri, BackgroundJob} | InProgress] + }; + {diagnostics, Uri, Jobs, State} -> + Entry = #{uri => Uri, pending => Jobs, diagnostics => []}, + State#{in_progress_diagnostics => [Entry | InProgressDiagnostics]}; + {notification, M, P, State} -> + do_send_notification(M, P, State0), + State + end; +handle_request(Response, State0) -> + ?LOG_DEBUG( + "[SERVER] got request response [response=~p]", + [Response] + ), + State0. -spec do_send_notification(binary(), map(), state()) -> ok. +%% This notification is specifically filtered out to avoid recursive +%% calling of log notifications (see issue #1050) +do_send_notification(<<"window/logMessage">> = Method, Params, State) -> + Notification = els_protocol:notification(Method, Params), + send(Notification, State); do_send_notification(Method, Params, State) -> - Notification = els_protocol:notification(Method, Params), - ?LOG_DEBUG( "[SERVER] Sending notification [notification=~s]" - , [Notification] - ), - send(Notification, State). + Notification = els_protocol:notification(Method, Params), + ?LOG_DEBUG( + "[SERVER] Sending notification [notification=~s]", + [Notification] + ), + send(Notification, State). -spec do_send_request(binary(), map(), state()) -> state(). -do_send_request(Method, Params, #state{request_id = RequestId0} = State0) -> - RequestId = RequestId0 + 1, - Request = els_protocol:request(RequestId, Method, Params), - ?LOG_DEBUG( "[SERVER] Sending request [request=~p]" - , [Request] - ), - send(Request, State0), - State0#state{request_id = RequestId}. +do_send_request(Method, Params, #{request_id := RequestId0} = State0) -> + RequestId = RequestId0 + 1, + Request = els_protocol:request(RequestId, Method, Params), + ?LOG_DEBUG( + "[SERVER] Sending request [request=~p]", + [Request] + ), + send(Request, State0), + State0#{request_id => RequestId}. -spec do_send_response(pid(), any(), state()) -> state(). do_send_response(Job, Result, State0) -> - #state{pending = Pending0} = State0, - case lists:keyfind(Job, 3, Pending0) of - false -> - ?LOG_DEBUG( - "[SERVER] Sending delayed response, but no request found [job=~p]", - [Job]), - State0; - {RequestId, _Provider, J} when J =:= Job -> - Response = els_protocol:response(RequestId, Result), - ?LOG_DEBUG( "[SERVER] Sending delayed response [job=~p] [response=~p]" - , [Job, Response] - ), - send(Response, State0), - Pending = lists:keydelete(RequestId, 1, Pending0), - State0#state{pending = Pending} - end. + #{pending := Pending0} = State0, + case lists:keyfind(Job, 2, Pending0) of + false -> + ?LOG_DEBUG( + "[SERVER] Sending delayed response, but no request found [job=~p]", + [Job] + ), + State0; + {RequestId, J} when J =:= Job -> + Response = els_protocol:response(RequestId, Result), + ?LOG_DEBUG( + "[SERVER] Sending delayed response [job=~p] [response=~p]", + [Job, Response] + ), + send(Response, State0), + Pending = lists:keydelete(RequestId, 1, Pending0), + State0#{pending => Pending} + end. -spec send(binary(), state()) -> ok. -send(Payload, #state{io_device = IoDevice}) -> - els_stdio:send(IoDevice, Payload). +send(Payload, #{io_device := IoDevice}) -> + els_stdio:send(IoDevice, Payload). + +-spec find_entry(pid(), [els_server:diagnostic_entry()]) -> + {ok, {els_server:diagnostic_entry(), [els_server:diagnostic_entry()]}} + | {error, not_found}. +find_entry(Job, InProgress) -> + find_entry(Job, InProgress, []). + +-spec find_entry(pid(), [els_server:diagnostic_entry()], [els_server:diagnostic_entry()]) -> + {ok, {els_server:diagnostic_entry(), [els_server:diagnostic_entry()]}} + | {error, not_found}. +find_entry(_Job, [], []) -> + {error, not_found}; +find_entry(Job, [#{pending := Pending} = Entry | Rest], Acc) -> + case lists:member(Job, Pending) of + true -> + {ok, {Entry, Rest ++ Acc}}; + false -> + find_entry(Job, Rest, [Entry | Acc]) + end. diff --git a/apps/els_lsp/src/els_signature_help_provider.erl b/apps/els_lsp/src/els_signature_help_provider.erl new file mode 100644 index 000000000..74a1efc94 --- /dev/null +++ b/apps/els_lsp/src/els_signature_help_provider.erl @@ -0,0 +1,212 @@ +-module(els_signature_help_provider). + +-behaviour(els_provider). + +-include("els_lsp.hrl"). +-include_lib("kernel/include/logger.hrl"). + +-export([ + handle_request/1, + trigger_characters/0 +]). + +-type item() :: {remote, atom(), atom()} | {local, atom()}. +-type parameter_number() :: non_neg_integer(). +%% Parameter numbers are 0-indexed. + +-spec trigger_characters() -> [binary()]. +trigger_characters() -> + [<<"(">>, <<",">>, <<")">>]. + +%%============================================================================== +%% els_provider functions +%%============================================================================== +-spec handle_request(els_provider:provider_request()) -> + {response, signature_help() | null}. +handle_request({signature_help, Params}) -> + #{ + <<"position">> := #{ + <<"line">> := Line, + <<"character">> := Character + }, + <<"textDocument">> := #{<<"uri">> := Uri} + } = Params, + {ok, #{text := Text} = Document} = els_utils:lookup_document(Uri), + Prefix = els_text:line(Text, Line, Character), + Tokens = lists:reverse(els_text:tokens(Prefix)), + case find_signature(Tokens, Text, Line - 1) of + {ok, Item, ActiveParameter} -> + {response, signatures(Document, Item, ActiveParameter)}; + none -> + {response, null} + end. + +%%============================================================================== +%% Internal functions +%%============================================================================== +-spec find_signature( + Tokens :: [tuple()], + Text :: binary(), + Line :: non_neg_integer() +) -> + {ok, item(), parameter_number()} | none. +find_signature(Tokens, Text, Line) -> + find_signature(Tokens, [0], Text, Line). + +-spec find_signature( + Tokens :: [tuple()], + ParameterStack :: [parameter_number()], + Text :: binary(), + Line :: non_neg_integer() +) -> + {ok, item(), parameter_number()} | none. +%% An unmatched open parenthesis is the start of a signature. +find_signature([{'(', _} | Rest], [ActiveParameter], _Text, _Line) -> + case Rest of + %% Check for "[...] module:func(" + [{atom, _, Func}, {':', _}, {atom, _, Module} | _Rest] -> + {ok, {remote, Module, Func}, ActiveParameter}; + %% Check for "-attribute(" + [{atom, _, _Attribute}, {'-', _} | _Rest] -> + none; + %% Check for "[...] func(" + [{atom, _, Func} | _Rest] -> + {ok, {local, Func}, ActiveParameter}; + _Tokens -> + none + end; +%% A comma outside of any data structure (list, tuple, map, or binary) is a +%% separator between arguments, so we increment the active parameter count. +find_signature([{',', _} | Rest], [ActiveParameter | ParameterStack], Text, Line) -> + find_signature(Rest, [ActiveParameter + 1 | ParameterStack], Text, Line); +%% Calls may contain any sort of expression but not statements, so when we +%% see a '.', we know we've failed to find a signature. +find_signature([{dot, _} | _Rest], _ParameterStack, _Text, _Line) -> + none; +%% When closing a scope, push a new parameter counter onto the stack. +find_signature([{ScopeClose, _} | Rest], ParameterStack, Text, Line) when + ScopeClose =:= ')'; + ScopeClose =:= '}'; + ScopeClose =:= ']'; + ScopeClose =:= '>>' +-> + find_signature(Rest, [0 | ParameterStack], Text, Line); +%% When opening a scope, pop the extra parameter counter if it exists. +find_signature([{ScopeOpen, _} | Rest], ParameterStack, Text, Line) when + ScopeOpen =:= '('; + ScopeOpen =:= '{'; + ScopeOpen =:= '['; + ScopeOpen =:= '<<' +-> + ParameterStack1 = + case ParameterStack of + [_] -> [0]; + [_Head | Tail] -> Tail + end, + find_signature(Rest, ParameterStack1, Text, Line); +%% Discard any other tokens +find_signature([_ | Rest], ParameterStack, Text, Line) -> + find_signature(Rest, ParameterStack, Text, Line); +%% If there are no lines remaining in the file, then we failed to find any +%% signatures and are done. +find_signature([], _ParameterStack, _Text, 0) -> + none; +%% If we have exhausted the set of tokens on this line, scan backwards a line +%% (up in the document) since expressions may be split across multiple lines. +find_signature([], ParameterStack, Text, Line) -> + LineContents = els_text:line(Text, Line), + Tokens = lists:reverse(els_text:tokens(LineContents)), + find_signature(Tokens, ParameterStack, Text, Line - 1). + +-spec signatures(els_dt_document:item(), item(), parameter_number()) -> + signature_help() | null. +signatures(Document, Item, ActiveParameter) -> + {Module, Function, POIs} = + case Item of + {local, F} -> + #{uri := Uri} = Document, + M = els_uri:module(Uri), + LocalPOIs = els_scope:local_and_included_pois(Document, function), + {M, F, LocalPOIs}; + {remote, M, F} -> + {M, F, exported_function_pois(M)} + end, + SignaturePOIs = + lists:sort( + fun(#{id := {_, AArity}}, #{id := {_, BArity}}) -> AArity < BArity end, + [ + POI + || #{id := {POIFunc, _Arity}} = POI <- POIs, + POIFunc =:= Function + ] + ), + ?LOG_DEBUG( + "Signature Help. [item=~p] [pois=~p]", + [Item, SignaturePOIs] + ), + case SignaturePOIs of + [] -> + null; + [_ | _] -> + %% The active signature is the signature with the smallest arity + %% that is at least as large as the active parameter, defaulting + %% to the highest arity signature. The active signature is zero + %% indexed. + ActiveSignature = + index_where( + fun(#{id := {_, Arity}}) -> Arity > ActiveParameter end, + SignaturePOIs, + length(SignaturePOIs) - 1 + ), + #{ + activeParameter => ActiveParameter, + activeSignature => ActiveSignature, + signatures => [signature_item(Module, POI) || POI <- SignaturePOIs] + } + end. + +-spec signature_item(atom(), els_poi:poi()) -> signature_information(). +signature_item(Module, #{data := #{args := Args}, id := {Function, Arity}}) -> + DocEntries = els_docs:function_docs('remote', Module, Function, Arity), + #{ + documentation => els_markup_content:new(DocEntries), + label => label(Function, Args), + parameters => [#{label => els_utils:to_binary(els_arg:name(Arg))} || Arg <- Args] + }. + +-spec exported_function_pois(atom()) -> [els_poi:poi()]. +exported_function_pois(Module) -> + case els_utils:find_module(Module) of + {ok, Uri} -> + case els_utils:lookup_document(Uri) of + {ok, Document} -> + Exports = [ + FA + || #{id := FA} <- els_scope:local_and_included_pois(Document, export_entry) + ], + [ + POI + || #{id := FA} = POI <- els_scope:local_and_included_pois(Document, function), + lists:member(FA, Exports) + ]; + {error, _} -> + [] + end; + {error, _} -> + [] + end. + +-spec label(atom(), [els_arg:arg()]) -> binary(). +label(Function, Args0) -> + ArgList = ["(", string:join([els_arg:name(Arg) || Arg <- Args0], ", "), ")"], + els_utils:to_binary([atom_to_binary(Function, utf8) | ArgList]). + +-spec index_where(Predicate, list(), Default) -> non_neg_integer() | Default when + Predicate :: fun((term()) -> boolean()), + Default :: term(). +index_where(Predicate, List, Default) -> + {IndexedList, _Acc} = lists:mapfoldl(fun(Item, Acc) -> {{Acc, Item}, Acc + 1} end, 0, List), + case lists:search(fun({_Index, Item}) -> Predicate(Item) end, IndexedList) of + {value, {Index, _Item}} -> Index; + false -> Default + end. diff --git a/apps/els_lsp/src/els_snippets_server.erl b/apps/els_lsp/src/els_snippets_server.erl index d8637fd67..93af1aa91 100644 --- a/apps/els_lsp/src/els_snippets_server.erl +++ b/apps/els_lsp/src/els_snippets_server.erl @@ -7,21 +7,23 @@ %%============================================================================== %% API %%============================================================================== --export([ builtin_snippets_dir/0 - , custom_snippets_dir/0 - , snippets/0 - , start_link/0 - ]). +-export([ + builtin_snippets_dir/0, + custom_snippets_dir/0, + snippets/0, + start_link/0 +]). %%============================================================================== %% 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 +]). -type state() :: #{}. %%============================================================================== @@ -45,107 +47,113 @@ %%============================================================================== -spec start_link() -> {ok, pid()}. start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, unused, []). + gen_server:start_link({local, ?SERVER}, ?MODULE, unused, []). -spec snippets() -> [completion_item()]. snippets() -> - [build_snippet(Entry) || Entry <- ets:tab2list(?TABLE)]. + [build_snippet(Entry) || Entry <- ets:tab2list(?TABLE)]. -spec builtin_snippets_dir() -> file:filename_all(). builtin_snippets_dir() -> - filename:join(code:priv_dir(?APP), "snippets"). + filename:join(code:priv_dir(?APP), "snippets"). -spec custom_snippets_dir() -> file:filename_all(). custom_snippets_dir() -> - {ok, [[Home]]} = init:get_argument(home), - filename:join([Home, ".config", "erlang_ls", "snippets"]). + {ok, [[Home]]} = init:get_argument(home), + filename:join([Home, ".config", "erlang_ls", "snippets"]). %%============================================================================== %% Callbacks for the gen_server behaviour %%============================================================================== -spec init(unused) -> {ok, state()}. init(unused) -> - load_snippets(), - {ok, #{}}. + load_snippets(), + {ok, #{}}. -spec handle_call(any(), {pid(), any()}, state()) -> {reply, any(), state()}. handle_call(_Request, _From, State) -> - {reply, not_implemented, State}. + {reply, not_implemented, 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(_Request, State) -> - {noreply, State}. + {noreply, State}. %%============================================================================== %% Internal Functions %%============================================================================== -spec load_snippets() -> ok. load_snippets() -> - init_snippets_table(), - [insert_snippet(S) || S <- builtin_snippets() ++ custom_snippets()], - ok. + init_snippets_table(), + [insert_snippet(S) || S <- builtin_snippets() ++ custom_snippets()], + ok. -spec init_snippets_table() -> ok. init_snippets_table() -> - ?TABLE = ets:new(?TABLE, [public, named_table, {read_concurrency, true}]), - ok. + ?TABLE = ets:new(?TABLE, [public, named_table, {read_concurrency, true}]), + ok. -spec insert_snippet(snippet()) -> ok. insert_snippet({Name, Content}) -> - true = ets:insert(?TABLE, {unicode:characters_to_binary(Name), Content}), - ok. + true = ets:insert(?TABLE, {unicode:characters_to_binary(Name), Content}), + ok. -spec builtin_snippets() -> [snippet()]. builtin_snippets() -> - case filelib:is_dir(code:priv_dir(?APP)) of - false -> - %% Probably running within an escript, so we need to extract the - %% snippets from the archive - snippets_from_escript(); - true -> - snippets_from_dir(builtin_snippets_dir()) - end. + case filelib:is_dir(code:priv_dir(?APP)) of + false -> + %% Probably running within an escript, so we need to extract the + %% snippets from the archive + snippets_from_escript(); + true -> + snippets_from_dir(builtin_snippets_dir()) + end. -spec snippets_from_escript() -> [snippet()]. snippets_from_escript() -> - Name = escript:script_name(), - {ok, Sections} = escript:extract(Name, []), - Archive = proplists:get_value(archive, Sections), - Fun = fun("els_lsp/priv/snippets/" ++ N, _GetInfo, GetBin, Acc) -> - [{N, GetBin()}|Acc]; - (_Name, _GetInfo, _GetBin, Acc) -> + Name = escript:script_name(), + {ok, Sections} = escript:extract(Name, []), + Archive = proplists:get_value(archive, Sections), + Fun = fun + ("els_lsp/priv/snippets/" ++ N, _GetInfo, GetBin, Acc) -> + [{N, GetBin()} | Acc]; + (_Name, _GetInfo, _GetBin, Acc) -> Acc - end, - {ok, Snippets} = zip:foldl(Fun, [], {Name, Archive}), - Snippets. + end, + {ok, Snippets} = zip:foldl(Fun, [], {Name, Archive}), + Snippets. -spec custom_snippets() -> [snippet()]. custom_snippets() -> - Dir = custom_snippets_dir(), - ensure_dir(Dir), - snippets_from_dir(Dir). + Dir = custom_snippets_dir(), + case ensure_dir(Dir) of + ok -> + snippets_from_dir(Dir); + {error, _} -> + [] + end. -spec snippets_from_dir(file:filename_all()) -> [snippet()]. snippets_from_dir(Dir) -> - [snippet_from_file(Dir, Filename) || Filename <- filelib:wildcard("*", Dir)]. + [snippet_from_file(Dir, Filename) || Filename <- filelib:wildcard("*", Dir)]. -spec snippet_from_file(file:filename_all(), file:filename_all()) -> snippet(). snippet_from_file(Dir, Filename) -> - {ok, Content} = file:read_file(filename:join(Dir, Filename)), - {Filename, Content}. + {ok, Content} = file:read_file(filename:join(Dir, Filename)), + {Filename, Content}. --spec ensure_dir(file:filename_all()) -> ok. +-spec ensure_dir(file:filename_all()) -> 'ok' | {'error', _}. ensure_dir(Dir) -> - ok = filelib:ensure_dir(filename:join(Dir, "dummy")). + filelib:ensure_dir(filename:join(Dir, "dummy")). -spec build_snippet({binary(), binary()}) -> completion_item(). build_snippet({Name, Snippet}) -> - #{ label => <<"snippet_", Name/binary>> - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , insertText => Snippet - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - }. + #{ + label => <<"snippet_", Name/binary>>, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => Snippet, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + }. diff --git a/apps/els_lsp/src/els_sup.erl b/apps/els_lsp/src/els_sup.erl index 1a6d9efda..cbef27ec0 100644 --- a/apps/els_lsp/src/els_sup.erl +++ b/apps/els_lsp/src/els_sup.erl @@ -13,10 +13,10 @@ %%============================================================================== %% API --export([ start_link/0 ]). +-export([start_link/0]). %% Supervisor Callbacks --export([ init/1 ]). +-export([init/1]). %%============================================================================== %% Includes @@ -33,54 +33,52 @@ %%============================================================================== -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 => 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_db_server - , start => {els_db_server, start_link, []} - , shutdown => brutal_kill - } - , #{ id => els_providers_sup - , start => {els_providers_sup, start_link, []} - , type => supervisor - } - , #{ id => els_background_job_sup - , start => {els_background_job_sup, start_link, []} - , type => supervisor - } - , #{ id => els_distribution_sup - , start => {els_distribution_sup, start_link, []} - , type => supervisor - } - , #{ id => els_snippets_server - , start => {els_snippets_server, start_link, []} - } - , #{ id => els_bsp_client - , start => {els_bsp_client, start_link, []} - } - , #{ id => els_index_buffer - , start => {els_index_buffer, start, []} - } - , #{ id => els_server - , start => {els_server, start_link, []} - } - ], - {ok, {SupFlags, ChildSpecs}}. + 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_db_server, + start => {els_db_server, start_link, []}, + shutdown => brutal_kill + }, + #{ + id => els_background_job_sup, + start => {els_background_job_sup, start_link, []}, + type => supervisor + }, + #{ + id => els_distribution_sup, + start => {els_distribution_sup, start_link, []}, + type => supervisor + }, + #{ + id => els_snippets_server, + start => {els_snippets_server, start_link, []} + }, + #{ + id => els_server, + start => {els_server, start_link, []} + } + ], + {ok, {SupFlags, ChildSpecs}}. %% @doc Restrict access to standard I/O %% @@ -95,33 +93,34 @@ init([]) -> %% 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("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. + ?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} -> - %% We need to pass the underlying io opts, otherwise shell_docs does - %% not know which encoding to use. See #754 - From ! {io_reply, ReplyAs, io:getopts()}; - {io_request, From, ReplyAs, _} -> - From ! {io_reply, ReplyAs, ok}; - _ -> - ok - end, - noop_group_leader() - end. + receive + Message -> + ?LOG_INFO("noop_group_leader got [message=~p]", [Message]), + case Message of + {io_request, From, ReplyAs, getopts} -> + %% We need to pass the underlying io opts, otherwise shell_docs does + %% not know which encoding to use. See #754 + From ! {io_reply, ReplyAs, io:getopts()}; + {io_request, From, ReplyAs, _} -> + From ! {io_reply, ReplyAs, ok}; + _ -> + ok + end, + noop_group_leader() + end. diff --git a/apps/els_lsp/src/els_telemetry.erl b/apps/els_lsp/src/els_telemetry.erl new file mode 100644 index 000000000..62a137e66 --- /dev/null +++ b/apps/els_lsp/src/els_telemetry.erl @@ -0,0 +1,10 @@ +-module(els_telemetry). + +-export([send_notification/1]). + +-type event() :: map(). + +-spec send_notification(event()) -> ok. +send_notification(Event) -> + Method = <<"telemetry/event">>, + els_server:send_notification(Method, Event). diff --git a/apps/els_lsp/src/els_text_document_position_params.erl b/apps/els_lsp/src/els_text_document_position_params.erl index 288f661e0..321f77b33 100644 --- a/apps/els_lsp/src/els_text_document_position_params.erl +++ b/apps/els_lsp/src/els_text_document_position_params.erl @@ -1,10 +1,11 @@ -module(els_text_document_position_params). --export([ uri/1 - , line/1 - , character/1 - , uri_line_character/1 - ]). +-export([ + uri/1, + line/1, + character/1, + uri_line_character/1 +]). -include("els_lsp.hrl"). @@ -13,17 +14,17 @@ -spec uri(params()) -> uri(). uri(Params) -> - maps:get(<<"uri">>, maps:get(<<"textDocument">>, Params)). + maps:get(<<"uri">>, maps:get(<<"textDocument">>, Params)). -spec line(params()) -> non_neg_integer(). line(Params) -> - maps:get(<<"line">>, maps:get(<<"position">>, Params)). + maps:get(<<"line">>, maps:get(<<"position">>, Params)). -spec character(params()) -> non_neg_integer(). character(Params) -> - maps:get(<<"line">>, maps:get(<<"position">>, Params)). + maps:get(<<"line">>, maps:get(<<"position">>, Params)). -spec uri_line_character(params()) -> - {uri(), non_neg_integer(), non_neg_integer()}. + {uri(), non_neg_integer(), non_neg_integer()}. uri_line_character(Params) -> - {uri(Params), line(Params), character(Params)}. + {uri(Params), line(Params), character(Params)}. diff --git a/apps/els_lsp/src/els_text_edit.erl b/apps/els_lsp/src/els_text_edit.erl index 8a6536b26..c23fc226b 100644 --- a/apps/els_lsp/src/els_text_edit.erl +++ b/apps/els_lsp/src/els_text_edit.erl @@ -1,9 +1,10 @@ -module(els_text_edit). --export([ diff_files/2 - , edit_insert_text/3 - , edit_replace_text/4 - ]). +-export([ + diff_files/2, + edit_insert_text/3, + edit_replace_text/4 +]). -include("els_lsp.hrl"). @@ -41,49 +42,51 @@ make_text_edits(Diffs) -> make_text_edits(Diffs, 0, []). -spec make_text_edits([diff()], number(), [text_edit()]) -> [text_edit()]. -make_text_edits([{eq, Data}|T], Line, Acc) -> +make_text_edits([{eq, Data} | T], Line, Acc) -> make_text_edits(T, Line + length(Data), Acc); - -make_text_edits([{del, Del}, {ins, Ins}|T], Line, Acc) -> +make_text_edits([{del, Del}, {ins, Ins} | T], Line, Acc) -> Len = length(Del), - Pos1 = #{ line => Line, character => 0 }, - Pos2 = #{ line => Line + Len, character => 0 }, - Edit = #{ range => #{ start => Pos1, 'end' => Pos2 } - , newText => els_utils:to_binary(lists:concat(Ins)) - }, - make_text_edits(T, Line + Len, [Edit|Acc]); - -make_text_edits([{ins, Data}|T], Line, Acc) -> - Pos = #{ line => Line, character => 0 }, - Edit = #{ range => #{ start => Pos, 'end' => Pos } - , newText => els_utils:to_binary(lists:concat(Data)) - }, - make_text_edits(T, Line, [Edit|Acc]); - -make_text_edits([{del, Data}|T], Line, Acc) -> + Pos1 = #{line => Line, character => 0}, + Pos2 = #{line => Line + Len, character => 0}, + Edit = #{ + range => #{start => Pos1, 'end' => Pos2}, + newText => list_to_binary(Ins) + }, + make_text_edits(T, Line + Len, [Edit | Acc]); +make_text_edits([{ins, Data} | T], Line, Acc) -> + Pos = #{line => Line, character => 0}, + Edit = #{ + range => #{start => Pos, 'end' => Pos}, + newText => list_to_binary(Data) + }, + make_text_edits(T, Line, [Edit | Acc]); +make_text_edits([{del, Data} | T], Line, Acc) -> Len = length(Data), - Pos1 = #{ line => Line, character => 0 }, - Pos2 = #{ line => Line + Len, character => 0 }, - Edit = #{ range => #{ start => Pos1, 'end' => Pos2 } - , newText => <<"">> - }, - make_text_edits(T, Line + Len, [Edit|Acc]); - -make_text_edits([], _Line, Acc) -> lists:reverse(Acc). + Pos1 = #{line => Line, character => 0}, + Pos2 = #{line => Line + Len, character => 0}, + Edit = #{ + range => #{start => Pos1, 'end' => Pos2}, + newText => <<"">> + }, + make_text_edits(T, Line + Len, [Edit | Acc]); +make_text_edits([], _Line, Acc) -> + lists:reverse(Acc). -spec edit_insert_text(uri(), binary(), number()) -> map(). edit_insert_text(Uri, Data, Line) -> - Pos = #{ line => Line, character => 0 }, - Edit = #{ range => #{ start => Pos, 'end' => Pos } - , newText => els_utils:to_binary(Data) - }, - #{ changes => #{ Uri => [Edit] }}. + Pos = #{line => Line, character => 0}, + Edit = #{ + range => #{start => Pos, 'end' => Pos}, + newText => Data + }, + #{changes => #{Uri => [Edit]}}. -spec edit_replace_text(uri(), binary(), number(), number()) -> map(). edit_replace_text(Uri, Data, LineFrom, LineTo) -> - Pos1 = #{ line => LineFrom, character => 0 }, - Pos2 = #{ line => LineTo, character => 0 }, - Edit = #{ range => #{ start => Pos1, 'end' => Pos2 } - , newText => els_utils:to_binary(Data) - }, - #{ changes => #{ Uri => [Edit] }}. + Pos1 = #{line => LineFrom, character => 0}, + Pos2 = #{line => LineTo, character => 0}, + Edit = #{ + range => #{start => Pos1, 'end' => Pos2}, + newText => Data + }, + #{changes => #{Uri => [Edit]}}. diff --git a/apps/els_lsp/src/els_text_search.erl b/apps/els_lsp/src/els_text_search.erl new file mode 100644 index 000000000..f7bae1171 --- /dev/null +++ b/apps/els_lsp/src/els_text_search.erl @@ -0,0 +1,54 @@ +%%============================================================================== +%% Text-based search +%%============================================================================== +-module(els_text_search). + +%%============================================================================== +%% API +%%============================================================================== +-export([find_candidate_uris/1]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include("els_lsp.hrl"). + +%%============================================================================== +%% API +%%============================================================================== +-spec find_candidate_uris({els_dt_references:poi_category(), any()}) -> [uri()]. +find_candidate_uris(Id) -> + Pattern = extract_pattern(Id), + els_dt_document:find_candidates(Pattern). + +%%============================================================================== +%% Internal Functions +%%============================================================================== +-spec extract_pattern({els_dt_references:poi_category(), any()}) -> + atom() | binary(). +extract_pattern({function, {_M, F, _A}}) -> + F; +extract_pattern({type, {F, _A}}) -> + F; +extract_pattern({type, {_M, F, _A}}) -> + F; +extract_pattern({macro, {Name, _Arity}}) -> + Name; +extract_pattern({macro, Name}) -> + Name; +extract_pattern({include, Id}) -> + include_id(Id); +extract_pattern({include_lib, Id}) -> + include_id(Id); +extract_pattern({behaviour, Name}) -> + Name; +extract_pattern({atom, Name}) -> + Name; +extract_pattern({record_field, {_Record, Name}}) -> + Name; +extract_pattern({record, Name}) -> + Name. + +-spec include_id(string()) -> string(). +include_id(Id) -> + filename:rootname(filename:basename(Id)). diff --git a/apps/els_lsp/src/els_text_synchronization.erl b/apps/els_lsp/src/els_text_synchronization.erl index 0e07e7550..e09c6bf3b 100644 --- a/apps/els_lsp/src/els_text_synchronization.erl +++ b/apps/els_lsp/src/els_text_synchronization.erl @@ -3,74 +3,136 @@ -include_lib("kernel/include/logger.hrl"). -include("els_lsp.hrl"). --export([ sync_mode/0 - , did_change/1 - , did_open/1 - , did_save/1 - , did_close/1 - ]). +-export([ + sync_mode/0, + did_change/1, + did_open/1, + did_save/1, + did_close/1, + did_change_watched_files/1 +]). -spec sync_mode() -> text_document_sync_kind(). sync_mode() -> - case els_config:get(incremental_sync) of - true -> ?TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL; - false -> ?TEXT_DOCUMENT_SYNC_KIND_FULL - end. + case els_config:get(incremental_sync) of + true -> ?TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL; + false -> ?TEXT_DOCUMENT_SYNC_KIND_FULL + end. --spec did_change(map()) -> ok. +-spec did_change(map()) -> ok | {ok, pid()}. did_change(Params) -> - ContentChanges = maps:get(<<"contentChanges">>, Params), - TextDocument = maps:get(<<"textDocument">> , Params), - Uri = maps:get(<<"uri">> , TextDocument), - case ContentChanges of - [] -> - ok; - [Change] when not is_map_key(<<"range">>, Change) -> - %% Full text sync - #{<<"text">> := Text} = Change, - {Duration, ok} = - timer:tc(fun() -> els_indexing:index(Uri, Text, 'deep') end), - ?LOG_DEBUG("didChange FULLSYNC [size: ~p] [duration: ~pms]\n", - [size(Text), Duration div 1000]), - ok; - ContentChanges -> - %% Incremental sync - ?LOG_DEBUG("didChange INCREMENTAL [changes: ~p]", [ContentChanges]), - Edits = [to_edit(Change) || Change <- ContentChanges], - {Duration, ok} = - timer:tc(fun() -> els_index_buffer:apply_edits_async(Uri, Edits) end), - ?LOG_DEBUG("didChange INCREMENTAL [duration: ~pms]\n", - [Duration div 1000]), - ok - end. + ContentChanges = maps:get(<<"contentChanges">>, Params), + TextDocument = maps:get(<<"textDocument">>, Params), + Uri = maps:get(<<"uri">>, TextDocument), + Version = maps:get(<<"version">>, TextDocument), + case ContentChanges of + [] -> + ok; + [Change] when not is_map_key(<<"range">>, Change) -> + #{<<"text">> := Text} = Change, + {ok, Document} = els_utils:lookup_document(Uri), + NewDocument = Document#{text => Text, version => Version}, + els_dt_document:insert(NewDocument), + background_index(NewDocument); + _ -> + ?LOG_DEBUG("didChange INCREMENTAL [changes: ~p]", [ContentChanges]), + Edits = [to_edit(Change) || Change <- ContentChanges], + {ok, #{text := Text0} = Document} = els_utils:lookup_document(Uri), + Text = els_text:apply_edits(Text0, Edits), + NewDocument = Document#{text => Text, version => Version}, + els_dt_document:insert(NewDocument), + background_index(NewDocument) + end. -spec did_open(map()) -> ok. did_open(Params) -> - TextDocument = maps:get(<<"textDocument">>, Params), - Uri = maps:get(<<"uri">> , TextDocument), - Text = maps:get(<<"text">> , TextDocument), - ok = els_indexing:index(Uri, Text, 'deep'), - Provider = els_diagnostics_provider, - els_provider:handle_request(Provider, {run_diagnostics, Params}), - ok. + #{ + <<"textDocument">> := #{ + <<"uri">> := Uri, + <<"text">> := Text, + <<"version">> := Version + } + } = Params, + Document = els_dt_document:new(Uri, Text, _Source = app, Version), + els_dt_document:insert(Document), + els_indexing:deep_index(Document, _UpdateWords = false), + ok. -spec did_save(map()) -> ok. did_save(Params) -> - TextDocument = maps:get(<<"textDocument">>, Params), - Uri = maps:get(<<"uri">> , TextDocument), - ok = els_index_buffer:flush(Uri), - Provider = els_diagnostics_provider, - els_provider:handle_request(Provider, {run_diagnostics, Params}), - ok. + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + els_docs_memo:delete_by_uri(Uri), + ok. + +-spec did_change_watched_files(map()) -> ok. +did_change_watched_files(Params) -> + #{<<"changes">> := Changes} = Params, + [ + handle_file_change(Uri, Type) + || #{<<"uri">> := Uri, <<"type">> := Type} <- Changes + ], + ok. -spec did_close(map()) -> ok. did_close(_Params) -> ok. -spec to_edit(map()) -> els_text:edit(). to_edit(#{<<"text">> := Text, <<"range">> := Range}) -> - #{ <<"start">> := #{<<"character">> := FromC, <<"line">> := FromL} - , <<"end">> := #{<<"character">> := ToC, <<"line">> := ToL} - } = Range, - {#{ from => {FromL, FromC} - , to => {ToL, ToC} - }, els_utils:to_list(Text)}. + #{ + <<"start">> := #{<<"character">> := FromC, <<"line">> := FromL}, + <<"end">> := #{<<"character">> := ToC, <<"line">> := ToL} + } = Range, + { + #{ + from => {FromL, FromC}, + to => {ToL, ToC} + }, + els_utils:to_list(Text) + }. + +-spec handle_file_change(uri(), file_change_type()) -> ok. +handle_file_change(Uri, Type) when + Type =:= ?FILE_CHANGE_TYPE_CREATED; + Type =:= ?FILE_CHANGE_TYPE_CHANGED +-> + els_docs_memo:delete_by_uri(Uri), + reload_from_disk(Uri); +handle_file_change(Uri, Type) when Type =:= ?FILE_CHANGE_TYPE_DELETED -> + els_docs_memo:delete_by_uri(Uri), + els_indexing:remove(Uri). + +-spec reload_from_disk(uri()) -> ok. +reload_from_disk(Uri) -> + Path = els_uri:path(Uri), + case file:read_file(Path) of + {ok, Text} -> + els_indexing:shallow_index(Uri, Text, app); + {error, Error} -> + %% File is not accessible. This can happen, for example, + %% during a rebase operation, when the file "appears and + %% disappears" multiple times in a very short + %% timeframe. Just log the fact as a warning, but keep + %% going. It should be possible to buffer + %% 'didChangeWatchedFiles' requests, but it's not a big + %% deal. + ?LOG_WARNING( + "Error while reloading from disk. Ignoring. " + "[uri=~p] [error=~p]", + [ + Uri, Error + ] + ) + end, + ok. + +-spec background_index(els_dt_document:item()) -> {ok, pid()}. +background_index(#{uri := Uri} = Document) -> + Config = #{ + task => fun(Doc, _State) -> + els_indexing:deep_index(Doc, _UpdateWords = true), + ok + end, + entries => [Document], + title => <<"Indexing ", Uri/binary>> + }, + els_background_job:new(Config). diff --git a/apps/els_lsp/src/els_text_synchronization_provider.erl b/apps/els_lsp/src/els_text_synchronization_provider.erl new file mode 100644 index 000000000..67a06c144 --- /dev/null +++ b/apps/els_lsp/src/els_text_synchronization_provider.erl @@ -0,0 +1,48 @@ +-module(els_text_synchronization_provider). + +-behaviour(els_provider). +-export([ + handle_request/1, + options/0 +]). + +-include("els_lsp.hrl"). + +%%============================================================================== +%% els_provider functions +%%============================================================================== +-spec options() -> map(). +options() -> + #{ + openClose => true, + change => els_text_synchronization:sync_mode(), + save => #{includeText => false} + }. + +-spec handle_request(any()) -> + {diagnostics, uri(), [pid()]} + | noresponse + | {async, uri(), pid()}. +handle_request({did_open, Params}) -> + ok = els_text_synchronization:did_open(Params), + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + {diagnostics, Uri, els_diagnostics:run_diagnostics(Uri)}; +handle_request({did_change, Params}) -> + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + case els_text_synchronization:did_change(Params) of + ok -> + noresponse; + {ok, Job} -> + {async, Uri, Job} + end; +handle_request({did_save, Params}) -> + ok = els_text_synchronization:did_save(Params), + #{<<"textDocument">> := #{<<"uri">> := Uri}} = Params, + ok = els_code_reload:maybe_compile_and_load(Uri), + {diagnostics, Uri, els_diagnostics:run_diagnostics(Uri)}; +handle_request({did_close, Params}) -> + ok = els_text_synchronization:did_close(Params), + noresponse; +handle_request({did_change_watched_files, Params}) -> + ok = els_text_synchronization:did_change_watched_files(Params), + noresponse. diff --git a/apps/els_lsp/src/els_typer.erl b/apps/els_lsp/src/els_typer.erl index de269a821..b5b37e0ce 100644 --- a/apps/els_lsp/src/els_typer.erl +++ b/apps/els_lsp/src/els_typer.erl @@ -30,362 +30,432 @@ -module(els_typer). --export([ get_info/1, get_type_spec/3 ]). +-export([get_info/1, get_type_spec/3]). -include("els_lsp.hrl"). --type files() :: [file:filename()]. --type callgraph() :: dialyzer_callgraph:callgraph(). +-if(?OTP_RELEASE >= 26). +-define(DEFAULT_PLT_FILE, dialyzer_iplt:get_default_iplt_filename()). +-define(PLT_FROM_FILE(PltFile), dialyzer_iplt:from_file(PltFile)). +-else. +-define(DEFAULT_PLT_FILE, dialyzer_plt:get_default_plt()). +-define(PLT_FROM_FILE(PltFile), dialyzer_plt:from_file(PltFile)). +-endif. + +-type files() :: [file:filename()]. +-type callgraph() :: dialyzer_callgraph:callgraph(). -type codeserver() :: dialyzer_codeserver:codeserver(). --type plt() :: dialyzer_plt:plt(). - --record(analysis, - {mode = 'show', - macros = [] :: [{atom(), term()}], - includes = [] :: files(), - codeserver = dialyzer_codeserver:new():: codeserver(), - callgraph = dialyzer_callgraph:new() :: callgraph(), - files = [] :: files(), % absolute names - plt = none :: 'none' | file:filename(), - show_succ = false :: boolean(), - %% Files in 'fms' are compilable with option 'to_pp'; we keep them - %% as {FileName, ModuleName} in case the ModuleName is different - fms = [] :: [{file:filename(), module()}], - ex_func = map__new() :: map_dict(), - record = map__new() :: map_dict(), - func = map__new() :: map_dict(), - inc_func = map__new() :: map_dict(), - trust_plt = dialyzer_plt:new() :: plt()}). +-type plt() :: dialyzer_plt:plt(). + +-record(analysis, { + mode = 'show', + macros = [] :: [{atom(), term()}], + includes = [] :: files(), + codeserver = dialyzer_codeserver:new() :: codeserver(), + callgraph = dialyzer_callgraph:new() :: callgraph(), + % absolute names + files = [] :: files(), + plt = none :: 'none' | file:filename(), + show_succ = false :: boolean(), + %% Files in 'fms' are compilable with option 'to_pp'; we keep them + %% as {FileName, ModuleName} in case the ModuleName is different + fms = [] :: [{file:filename(), module()}], + ex_func = map__new() :: map_dict(), + record = map__new() :: map_dict(), + func = map__new() :: map_dict(), + inc_func = map__new() :: map_dict(), + trust_plt = dialyzer_plt:new() :: plt() +}). -type analysis() :: #analysis{}. --record(info, {records = maps:new() :: erl_types:type_table(), - functions = [] :: [func_info()], - types = map__new() :: map_dict()}). +-record(info, { + records = maps:new() :: erl_types:type_table(), + functions = [] :: [func_info()], + types = map__new() :: map_dict() +}). -type info() :: #info{}. --export_type([ info/0 ]). +-export_type([info/0]). -spec get_info(uri()) -> #info{}. get_info(Uri) -> - Path = binary_to_list(els_uri:path(Uri)), - Macros = els_compiler_diagnostics:macro_options(), - Includes = els_compiler_diagnostics:include_options(), - Analysis = #analysis{ macros = Macros - , includes = Includes - }, - TrustedFiles = [], - Analysis2 = extract(Analysis, TrustedFiles), - Analysis3 = Analysis2#analysis{files = [Path]}, - Analysis4 = collect_info(Analysis3), - Analysis5 = get_type_info(Analysis4), - [{File, Module}] = Analysis5#analysis.fms, - get_final_info(File, Module, Analysis5). + Path = binary_to_list(els_uri:path(Uri)), + Macros = els_compiler_diagnostics:macro_options(), + Includes = els_compiler_diagnostics:include_options(), + Analysis = #analysis{ + macros = Macros, + includes = Includes + }, + TrustedFiles = [], + Analysis2 = extract(Analysis, TrustedFiles), + Analysis3 = Analysis2#analysis{files = [Path]}, + Analysis4 = collect_info(Analysis3), + Analysis5 = get_type_info(Analysis4), + [{File, Module}] = Analysis5#analysis.fms, + get_final_info(File, Module, Analysis5). %%-------------------------------------------------------------------- -spec extract(analysis(), files()) -> analysis(). -extract(#analysis{macros = Macros, - includes = Includes, - trust_plt = TrustPLT} = Analysis, TrustedFiles) -> - CodeServer = dialyzer_codeserver:new(), - Fun = - fun(File, CS) -> - %% We include one more dir; the one above the one we are trusting - %% E.g, for /home/tests/typer_ann/test.ann.erl, we should include - %% /home/tests/ rather than /home/tests/typer_ann/ - CompOpts = dialyzer_utils:src_compiler_opts() ++ Includes ++ Macros, - {ok, Core} = dialyzer_utils:get_core_from_src(File, CompOpts), - {ok, RecDict} = dialyzer_utils:get_record_and_type_info(Core), - Mod = list_to_atom(filename:basename(File, ".erl")), - {ok, SpecDict, CbDict} = dialyzer_utils:get_spec_info(Mod, Core, RecDict), - CS1 = dialyzer_codeserver:store_temp_records(Mod, RecDict, CS), - dialyzer_codeserver:store_temp_contracts(Mod, SpecDict, CbDict, CS1) +extract( + #analysis{ + macros = Macros, + includes = Includes, + trust_plt = TrustPLT + } = Analysis, + TrustedFiles +) -> + CodeServer = dialyzer_codeserver:new(), + Fun = + fun(File, CS) -> + %% We include one more dir; the one above the one we are trusting + %% E.g, for /home/tests/typer_ann/test.ann.erl, we should include + %% /home/tests/ rather than /home/tests/typer_ann/ + CompOpts = dialyzer_utils:src_compiler_opts() ++ Includes ++ Macros, + {ok, Core} = dialyzer_utils:get_core_from_src(File, CompOpts), + {ok, RecDict} = dialyzer_utils:get_record_and_type_info(Core), + Mod = list_to_atom(filename:basename(File, ".erl")), + {ok, SpecDict, CbDict} = dialyzer_utils:get_spec_info(Mod, Core, RecDict), + CS1 = dialyzer_codeserver:store_temp_records(Mod, RecDict, CS), + dialyzer_codeserver:store_temp_contracts(Mod, SpecDict, CbDict, CS1) + end, + CodeServer1 = lists:foldl(Fun, CodeServer, TrustedFiles), + CodeServer2 = + dialyzer_utils:merge_types( + CodeServer1, + % XXX change to the PLT? + TrustPLT + ), + NewExpTypes = dialyzer_codeserver:get_temp_exported_types(CodeServer1), + case sets:size(NewExpTypes) of + 0 -> ok end, - CodeServer1 = lists:foldl(Fun, CodeServer, TrustedFiles), - CodeServer2 = - dialyzer_utils:merge_types(CodeServer1, - TrustPLT), % XXX change to the PLT? - NewExpTypes = dialyzer_codeserver:get_temp_exported_types(CodeServer1), - case sets:size(NewExpTypes) of 0 -> ok end, - CodeServer3 = dialyzer_codeserver:finalize_exported_types(NewExpTypes, CodeServer2), - CodeServer4 = dialyzer_utils:process_record_remote_types(CodeServer3), - NewCodeServer = dialyzer_contracts:process_contract_remote_types(CodeServer4), - ContractsDict = dialyzer_codeserver:get_contracts(NewCodeServer), - Contracts = orddict:from_list(dict:to_list(ContractsDict)), - NewTrustPLT = dialyzer_plt:insert_contract_list(TrustPLT, Contracts), - Analysis#analysis{trust_plt = NewTrustPLT}. + CodeServer3 = dialyzer_codeserver:finalize_exported_types(NewExpTypes, CodeServer2), + CodeServer4 = dialyzer_utils:process_record_remote_types(CodeServer3), + NewCodeServer = dialyzer_contracts:process_contract_remote_types(CodeServer4), + ContractsDict = dialyzer_codeserver:get_contracts(NewCodeServer), + Contracts = orddict:from_list(dict:to_list(ContractsDict)), + NewTrustPLT = dialyzer_plt:insert_contract_list(TrustPLT, Contracts), + Analysis#analysis{trust_plt = NewTrustPLT}. %%-------------------------------------------------------------------- -spec get_type_info(analysis()) -> analysis(). -get_type_info(#analysis{callgraph = CallGraph, - trust_plt = TrustPLT, - codeserver = CodeServer} = Analysis) -> - StrippedCallGraph = remove_external(CallGraph, TrustPLT), - NewPlt = dialyzer_succ_typings:analyze_callgraph(StrippedCallGraph, - TrustPLT, - CodeServer), - Analysis#analysis{callgraph = StrippedCallGraph, trust_plt = NewPlt}. +-if(?OTP_RELEASE >= 25). +get_type_info( + #analysis{ + callgraph = CallGraph, + trust_plt = TrustPLT, + codeserver = CodeServer + } = Analysis +) -> + StrippedCallGraph = remove_external(CallGraph, TrustPLT), + NewPlt = dialyzer_succ_typings:analyze_callgraph( + StrippedCallGraph, + TrustPLT, + CodeServer, + none, + [] + ), + Analysis#analysis{callgraph = StrippedCallGraph, trust_plt = NewPlt}. +-else. +get_type_info( + #analysis{ + callgraph = CallGraph, + trust_plt = TrustPLT, + codeserver = CodeServer + } = Analysis +) -> + StrippedCallGraph = remove_external(CallGraph, TrustPLT), + NewPlt = dialyzer_succ_typings:analyze_callgraph( + StrippedCallGraph, + TrustPLT, + CodeServer + ), + Analysis#analysis{callgraph = StrippedCallGraph, trust_plt = NewPlt}. +-endif. -spec remove_external(callgraph(), plt()) -> callgraph(). remove_external(CallGraph, PLT) -> - {StrippedCG0, Ext} = dialyzer_callgraph:remove_external(CallGraph), - case get_external(Ext, PLT) of - [] -> ok; - _Externals -> - rcv_ext_types() - end, - StrippedCG0. + {StrippedCG0, Ext} = dialyzer_callgraph:remove_external(CallGraph), + case get_external(Ext, PLT) of + [] -> ok; + _Externals -> rcv_ext_types() + end, + StrippedCG0. -spec get_external([{mfa(), mfa()}], plt()) -> [mfa()]. get_external(Exts, Plt) -> - Fun = fun ({_From, To = {M, F, A}}, Acc) -> - case dialyzer_plt:contains_mfa(Plt, To) of - false -> + Fun = fun({_From, To = {M, F, A}}, Acc) -> + case dialyzer_plt:contains_mfa(Plt, To) of + false -> case erl_bif_types:is_known(M, F, A) of - true -> Acc; - false -> [To|Acc] + true -> Acc; + false -> [To | Acc] end; - true -> Acc - end - end, - lists:foldl(Fun, [], Exts). + true -> + Acc + end + end, + lists:foldl(Fun, [], Exts). %%-------------------------------------------------------------------- %% Showing type information or annotating files with such information. %%-------------------------------------------------------------------- --type fa() :: {atom(), arity()}. +-type fa() :: {atom(), arity()}. -type func_info() :: {non_neg_integer(), atom(), arity()}. -spec get_final_info(string(), atom(), #analysis{}) -> #info{}. get_final_info(File, Module, Analysis) -> - Records = get_records(File, Analysis), - Types = get_types(Module, Analysis, Records), - Functions = get_functions(File, Analysis), - #info{records = Records, functions = Functions, types = Types}. + Records = get_records(File, Analysis), + Types = get_types(Module, Analysis, Records), + Functions = get_functions(File, Analysis), + #info{records = Records, functions = Functions, types = Types}. -spec get_records(string(), #analysis{}) -> any(). get_records(File, Analysis) -> - map__lookup(File, Analysis#analysis.record). + map__lookup(File, Analysis#analysis.record). -spec get_types(atom(), #analysis{}, any()) -> dict:dict(). get_types(Module, Analysis, Records) -> - TypeInfoPlt = Analysis#analysis.trust_plt, - TypeInfo = - case dialyzer_plt:lookup_module(TypeInfoPlt, Module) of - none -> []; - {value, List} -> List - end, - CodeServer = Analysis#analysis.codeserver, - TypeInfoList = - case Analysis#analysis.show_succ of - true -> - [convert_type_info(I) || I <- TypeInfo]; - false -> - [get_type(I, CodeServer, Records) || I <- TypeInfo] - end, - map__from_list(TypeInfoList). + TypeInfoPlt = Analysis#analysis.trust_plt, + TypeInfo = + case dialyzer_plt:lookup_module(TypeInfoPlt, Module) of + none -> []; + {value, List} -> List + end, + CodeServer = Analysis#analysis.codeserver, + TypeInfoList = + case Analysis#analysis.show_succ of + true -> + [convert_type_info(I) || I <- TypeInfo]; + false -> + [get_type(I, CodeServer, Records) || I <- TypeInfo] + end, + map__from_list(TypeInfoList). -spec convert_type_info({mfa(), any(), any()}) -> {fa(), {any(), any()}}. convert_type_info({{_M, F, A}, Range, Arg}) -> - {{F, A}, {Range, Arg}}. + {{F, A}, {Range, Arg}}. -spec get_type({mfa(), any(), any()}, any(), any()) -> {fa(), {any(), any()}}. get_type({{_M, F, A} = MFA, Range, Arg}, CodeServer, _Records) -> - case dialyzer_codeserver:lookup_mfa_contract(MFA, CodeServer) of - error -> - {{F, A}, {Range, Arg}}; - {ok, {_FileLine, Contract, _Xtra}} -> - Sig = erl_types:t_fun(Arg, Range), - case dialyzer_contracts:check_contract(Contract, Sig) of - ok -> {{F, A}, {contract, Contract}}; - {range_warnings, _} -> - {{F, A}, {contract, Contract}}; - {error, {overlapping_contract, []}} -> - {{F, A}, {contract, Contract}} - end - end. + case dialyzer_codeserver:lookup_mfa_contract(MFA, CodeServer) of + error -> + {{F, A}, {Range, Arg}}; + {ok, {_FileLine, Contract, _Xtra}} -> + Sig = erl_types:t_fun(Arg, Range), + case dialyzer_contracts:check_contract(Contract, Sig) of + ok -> {{F, A}, {contract, Contract}}; + {range_warnings, _} -> {{F, A}, {contract, Contract}}; + {error, {overlapping_contract, []}} -> {{F, A}, {contract, Contract}} + end + end. -spec get_functions(string(), #analysis{}) -> [any()]. get_functions(File, Analysis) -> - Funcs = map__lookup(File, Analysis#analysis.func), - Inc_Funcs = map__lookup(File, Analysis#analysis.inc_func), - remove_module_info(Funcs) ++ normalize_incFuncs(Inc_Funcs). + Funcs = map__lookup(File, Analysis#analysis.func), + Inc_Funcs = map__lookup(File, Analysis#analysis.inc_func), + remove_module_info(Funcs) ++ normalize_incFuncs(Inc_Funcs). -spec normalize_incFuncs([any()]) -> [any()]. normalize_incFuncs(Functions) -> - [FunInfo || {_FileName, FunInfo} <- Functions]. + [FunInfo || {_FileName, FunInfo} <- Functions]. -spec remove_module_info([func_info()]) -> [func_info()]. remove_module_info(FunInfoList) -> - F = fun ({_,module_info,0}) -> false; - ({_,module_info,1}) -> false; - ({Line,F,A}) when is_integer(Line), is_atom(F), is_integer(A) -> true - end, - lists:filter(F, FunInfoList). + F = fun + ({_, module_info, 0}) -> false; + ({_, module_info, 1}) -> false; + ({Line, F, A}) when is_integer(Line), is_atom(F), is_integer(A) -> true + end, + lists:filter(F, FunInfoList). -spec get_type_spec(atom(), arity(), #info{}) -> string(). get_type_spec(F, A, Info) -> - Type = get_type_info({F,A}, Info#info.types), - TypeStr = - case Type of - {contract, C} -> - dialyzer_contracts:contract_to_string(C); - {RetType, ArgType} -> - Sig = erl_types:t_fun(ArgType, RetType), - dialyzer_utils:format_sig(Sig, Info#info.records) - end, - Prefix = lists:concat(["-spec ", erl_types:atom_to_string(F)]), - lists:concat([Prefix, TypeStr, "."]). + Type = get_type_info({F, A}, Info#info.types), + TypeStr = + case Type of + {contract, C} -> + dialyzer_contracts:contract_to_string(C); + {RetType, ArgType} -> + Sig = erl_types:t_fun(ArgType, RetType), + dialyzer_utils:format_sig(Sig, Info#info.records) + end, + Prefix = lists:concat(["-spec ", erl_types:atom_to_string(F)]), + lists:concat([Prefix, TypeStr, "."]). -spec get_type_info({any(), any()}, dict:dict()) -> {any(), any()}. get_type_info(Func, Types) -> - case map__lookup(Func, Types) of - {contract, _Fun} = C -> C; - {_RetType, _ArgType} = RA -> RA - end. + case map__lookup(Func, Types) of + {contract, _Fun} = C -> C; + {_RetType, _ArgType} = RA -> RA + end. -type inc_file_info() :: {file:filename(), func_info()}. --record(tmpAcc, {file :: file:filename(), - module :: atom(), - funcAcc = [] :: [func_info()], - incFuncAcc = [] :: [inc_file_info()], - dialyzerObj = [] :: [{mfa(), {_, _}}]}). +-record(tmpAcc, { + file :: file:filename(), + module :: atom(), + funcAcc = [] :: [func_info()], + incFuncAcc = [] :: [inc_file_info()], + dialyzerObj = [] :: [{mfa(), {_, _}}] +}). -spec collect_info(analysis()) -> analysis(). collect_info(Analysis) -> - DialyzerPlt = get_dialyzer_plt(), - NewPlt = dialyzer_plt:merge_plts([Analysis#analysis.trust_plt, DialyzerPlt]), - NewAnalysis = lists:foldl(fun collect_one_file_info/2, - Analysis#analysis{trust_plt = NewPlt}, - Analysis#analysis.files), - TmpCServer = NewAnalysis#analysis.codeserver, - TmpCServer1 = dialyzer_utils:merge_types(TmpCServer, NewPlt), - NewExpTypes = dialyzer_codeserver:get_temp_exported_types(TmpCServer), - OldExpTypes = dialyzer_plt:get_exported_types(NewPlt), - MergedExpTypes = sets:union(NewExpTypes, OldExpTypes), - TmpCServer2 = - dialyzer_codeserver:finalize_exported_types(MergedExpTypes, TmpCServer1), - TmpCServer3 = dialyzer_utils:process_record_remote_types(TmpCServer2), - NewCServer = dialyzer_contracts:process_contract_remote_types(TmpCServer3), - NewAnalysis#analysis{codeserver = NewCServer}. + DialyzerPlt = get_dialyzer_plt(), + NewPlt = dialyzer_plt:merge_plts([Analysis#analysis.trust_plt, DialyzerPlt]), + NewAnalysis = lists:foldl( + fun collect_one_file_info/2, + Analysis#analysis{trust_plt = NewPlt}, + Analysis#analysis.files + ), + TmpCServer = NewAnalysis#analysis.codeserver, + TmpCServer1 = dialyzer_utils:merge_types(TmpCServer, NewPlt), + NewExpTypes = dialyzer_codeserver:get_temp_exported_types(TmpCServer), + OldExpTypes = dialyzer_plt:get_exported_types(NewPlt), + MergedExpTypes = sets:union(NewExpTypes, OldExpTypes), + TmpCServer2 = + dialyzer_codeserver:finalize_exported_types(MergedExpTypes, TmpCServer1), + TmpCServer3 = dialyzer_utils:process_record_remote_types(TmpCServer2), + NewCServer = dialyzer_contracts:process_contract_remote_types(TmpCServer3), + NewAnalysis#analysis{codeserver = NewCServer}. -spec collect_one_file_info(string(), #analysis{}) -> #analysis{}. collect_one_file_info(File, Analysis) -> - Macros = Analysis#analysis.macros, - Includes = Analysis#analysis.includes, - Options = dialyzer_utils:src_compiler_opts() ++ Includes ++ Macros, - {ok, Core} = dialyzer_utils:get_core_from_src(File, Options), - {ok, Records} = dialyzer_utils:get_record_and_type_info(Core), - Mod = cerl:concrete(cerl:module_name(Core)), - {ok, SpecInfo, CbInfo} = dialyzer_utils:get_spec_info(Mod, Core, Records), - ExpTypes = get_exported_types_from_core(Core), - analyze_core_tree(Core, Records, SpecInfo, CbInfo, - ExpTypes, Analysis, File). + Macros = Analysis#analysis.macros, + Includes = Analysis#analysis.includes, + Options = dialyzer_utils:src_compiler_opts() ++ Includes ++ Macros, + {ok, Core} = dialyzer_utils:get_core_from_src(File, Options), + {ok, Records} = dialyzer_utils:get_record_and_type_info(Core), + Mod = cerl:concrete(cerl:module_name(Core)), + {ok, SpecInfo, CbInfo} = dialyzer_utils:get_spec_info(Mod, Core, Records), + ExpTypes = get_exported_types_from_core(Core), + analyze_core_tree( + Core, + Records, + SpecInfo, + CbInfo, + ExpTypes, + Analysis, + File + ). -spec analyze_core_tree(any(), any(), any(), any(), sets:set(_), #analysis{}, string()) -> - #analysis{}. + #analysis{}. analyze_core_tree(Core, Records, SpecInfo, CbInfo, ExpTypes, Analysis, File) -> - Module = cerl:concrete(cerl:module_name(Core)), - TmpTree = cerl:from_records(Core), - CS1 = Analysis#analysis.codeserver, - NextLabel = dialyzer_codeserver:get_next_core_label(CS1), - {Tree, NewLabel} = cerl_trees:label(TmpTree, NextLabel), - CS2 = dialyzer_codeserver:insert(Module, Tree, CS1), - CS3 = dialyzer_codeserver:set_next_core_label(NewLabel, CS2), - CS4 = dialyzer_codeserver:store_temp_records(Module, Records, CS3), - CS5 = dialyzer_codeserver:store_temp_contracts(Module, SpecInfo, CbInfo, CS4), - OldExpTypes = dialyzer_codeserver:get_temp_exported_types(CS5), - MergedExpTypes = sets:union(ExpTypes, OldExpTypes), - CS6 = dialyzer_codeserver:insert_temp_exported_types(MergedExpTypes, CS5), - Ex_Funcs = [{0,F,A} || {_,_,{F,A}} <- cerl:module_exports(Tree)], - CG = Analysis#analysis.callgraph, - {V, E} = dialyzer_callgraph:scan_core_tree(Tree, CG), - dialyzer_callgraph:add_edges(E, V, CG), - Fun = fun analyze_one_function/2, - All_Defs = cerl:module_defs(Tree), - Acc = lists:foldl(Fun, #tmpAcc{file = File, module = Module}, All_Defs), - Exported_FuncMap = map__insert({File, Ex_Funcs}, Analysis#analysis.ex_func), - %% we must sort all functions in the file which - %% originate from this file by *numerical order* of lineNo - Sorted_Functions = lists:keysort(1, Acc#tmpAcc.funcAcc), - FuncMap = map__insert({File, Sorted_Functions}, Analysis#analysis.func), - %% we do not need to sort functions which are imported from included files - IncFuncMap = map__insert({File, Acc#tmpAcc.incFuncAcc}, - Analysis#analysis.inc_func), - FMs = Analysis#analysis.fms ++ [{File, Module}], - RecordMap = map__insert({File, Records}, Analysis#analysis.record), - Analysis#analysis{fms = FMs, - callgraph = CG, - codeserver = CS6, - ex_func = Exported_FuncMap, - inc_func = IncFuncMap, - record = RecordMap, - func = FuncMap}. + Module = cerl:concrete(cerl:module_name(Core)), + TmpTree = cerl:from_records(Core), + CS1 = Analysis#analysis.codeserver, + NextLabel = dialyzer_codeserver:get_next_core_label(CS1), + {Tree, NewLabel} = cerl_trees:label(TmpTree, NextLabel), + CS2 = dialyzer_codeserver:insert(Module, Tree, CS1), + CS3 = dialyzer_codeserver:set_next_core_label(NewLabel, CS2), + CS4 = dialyzer_codeserver:store_temp_records(Module, Records, CS3), + CS5 = dialyzer_codeserver:store_temp_contracts(Module, SpecInfo, CbInfo, CS4), + OldExpTypes = dialyzer_codeserver:get_temp_exported_types(CS5), + MergedExpTypes = sets:union(ExpTypes, OldExpTypes), + CS6 = dialyzer_codeserver:insert_temp_exported_types(MergedExpTypes, CS5), + Ex_Funcs = [{0, F, A} || {_, _, {F, A}} <- cerl:module_exports(Tree)], + CG = Analysis#analysis.callgraph, + {V, E} = dialyzer_callgraph:scan_core_tree(Tree, CG), + dialyzer_callgraph:add_edges(E, V, CG), + Fun = fun analyze_one_function/2, + All_Defs = cerl:module_defs(Tree), + Acc = lists:foldl(Fun, #tmpAcc{file = File, module = Module}, All_Defs), + Exported_FuncMap = map__insert({File, Ex_Funcs}, Analysis#analysis.ex_func), + %% we must sort all functions in the file which + %% originate from this file by *numerical order* of lineNo + Sorted_Functions = lists:keysort(1, Acc#tmpAcc.funcAcc), + FuncMap = map__insert({File, Sorted_Functions}, Analysis#analysis.func), + %% we do not need to sort functions which are imported from included files + IncFuncMap = map__insert( + {File, Acc#tmpAcc.incFuncAcc}, + Analysis#analysis.inc_func + ), + FMs = Analysis#analysis.fms ++ [{File, Module}], + RecordMap = map__insert({File, Records}, Analysis#analysis.record), + Analysis#analysis{ + fms = FMs, + callgraph = CG, + codeserver = CS6, + ex_func = Exported_FuncMap, + inc_func = IncFuncMap, + record = RecordMap, + func = FuncMap + }. -spec analyze_one_function({any(), any()}, #tmpAcc{}) -> #tmpAcc{}. analyze_one_function({Var, FunBody} = Function, Acc) -> - F = cerl:fname_id(Var), - A = cerl:fname_arity(Var), - TmpDialyzerObj = {{Acc#tmpAcc.module, F, A}, Function}, - NewDialyzerObj = Acc#tmpAcc.dialyzerObj ++ [TmpDialyzerObj], - Anno = cerl:get_ann(FunBody), - LineNo = get_line(Anno), - FileName = get_file(Anno), - BaseName = filename:basename(FileName), - FuncInfo = {LineNo, F, A}, - OriginalName = Acc#tmpAcc.file, - {FuncAcc, IncFuncAcc} = - case (FileName =:= OriginalName) orelse (BaseName =:= OriginalName) of - true -> %% Coming from original file - %% io:format("Added function ~tp\n", [{LineNo, F, A}]), - {Acc#tmpAcc.funcAcc ++ [FuncInfo], Acc#tmpAcc.incFuncAcc}; - false -> - %% Coming from other sourses, including: - %% -- .yrl (yecc-generated file) - %% -- yeccpre.hrl (yecc-generated file) - %% -- other cases - {Acc#tmpAcc.funcAcc, Acc#tmpAcc.incFuncAcc ++ [{FileName, FuncInfo}]} - end, - Acc#tmpAcc{funcAcc = FuncAcc, - incFuncAcc = IncFuncAcc, - dialyzerObj = NewDialyzerObj}. + F = cerl:fname_id(Var), + A = cerl:fname_arity(Var), + TmpDialyzerObj = {{Acc#tmpAcc.module, F, A}, Function}, + NewDialyzerObj = Acc#tmpAcc.dialyzerObj ++ [TmpDialyzerObj], + Anno = cerl:get_ann(FunBody), + LineNo = get_line(Anno), + FileName = get_file(Anno), + BaseName = filename:basename(FileName), + FuncInfo = {LineNo, F, A}, + OriginalName = Acc#tmpAcc.file, + {FuncAcc, IncFuncAcc} = + case (FileName =:= OriginalName) orelse (BaseName =:= OriginalName) of + %% Coming from original file + true -> + %% io:format("Added function ~tp\n", [{LineNo, F, A}]), + {Acc#tmpAcc.funcAcc ++ [FuncInfo], Acc#tmpAcc.incFuncAcc}; + false -> + %% Coming from other sourses, including: + %% -- .yrl (yecc-generated file) + %% -- yeccpre.hrl (yecc-generated file) + %% -- other cases + {Acc#tmpAcc.funcAcc, Acc#tmpAcc.incFuncAcc ++ [{FileName, FuncInfo}]} + end, + Acc#tmpAcc{ + funcAcc = FuncAcc, + incFuncAcc = IncFuncAcc, + dialyzerObj = NewDialyzerObj + }. -spec get_line([line()]) -> 'none' | integer(). -get_line([Line|_]) when is_integer(Line) -> Line; -get_line([_|T]) -> get_line(T); +get_line([Line | _]) when is_integer(Line) -> Line; +get_line([_ | T]) -> get_line(T); get_line([]) -> none. -spec get_file([any()]) -> any(). -get_file([_|T]) -> get_file(T); -get_file([]) -> "no_file". % should not happen +get_file([_ | T]) -> get_file(T); +% should not happen +get_file([]) -> "no_file". -spec get_dialyzer_plt() -> plt(). get_dialyzer_plt() -> - PltFile = case els_config:get(plt_path) of - undefined -> - dialyzer_plt:get_default_plt(); - PltPath -> + PltFile = + case els_config:get(plt_path) of + undefined -> + ?DEFAULT_PLT_FILE; + PltPath -> PltPath - end, - dialyzer_plt:from_file(PltFile). + end, + ?PLT_FROM_FILE(PltFile). %% Exported Types -spec get_exported_types_from_core(any()) -> sets:set(). get_exported_types_from_core(Core) -> - Attrs = cerl:module_attrs(Core), - ExpTypes1 = [cerl:concrete(L2) || {L1, L2} <- Attrs, - cerl:is_literal(L1), - cerl:is_literal(L2), - cerl:concrete(L1) =:= 'export_type'], - ExpTypes2 = lists:flatten(ExpTypes1), - M = cerl:atom_val(cerl:module_name(Core)), - sets:from_list([{M, F, A} || {F, A} <- ExpTypes2]). + Attrs = cerl:module_attrs(Core), + ExpTypes1 = [ + cerl:concrete(L2) + || {L1, L2} <- Attrs, + cerl:is_literal(L1), + cerl:is_literal(L2), + cerl:concrete(L1) =:= 'export_type' + ], + ExpTypes2 = lists:flatten(ExpTypes1), + M = cerl:atom_val(cerl:module_name(Core)), + sets:from_list([{M, F, A} || {F, A} <- ExpTypes2]). %%-------------------------------------------------------------------- %% Handle messages. @@ -393,18 +463,18 @@ get_exported_types_from_core(Core) -> -spec rcv_ext_types() -> [any()]. rcv_ext_types() -> - Self = self(), - Self ! {Self, done}, - rcv_ext_types(Self, []). + Self = self(), + Self ! {Self, done}, + rcv_ext_types(Self, []). -spec rcv_ext_types(pid(), [any()]) -> [any()]. rcv_ext_types(Self, ExtTypes) -> - receive - {Self, ext_types, ExtType} -> - rcv_ext_types(Self, [ExtType|ExtTypes]); - {Self, done} -> - lists:usort(ExtTypes) - end. + receive + {Self, ext_types, ExtType} -> + rcv_ext_types(Self, [ExtType | ExtTypes]); + {Self, done} -> + lists:usort(ExtTypes) + end. %%-------------------------------------------------------------------- %% A convenient abstraction of a Key-Value mapping data structure @@ -415,17 +485,21 @@ rcv_ext_types(Self, ExtTypes) -> -spec map__new() -> map_dict(). map__new() -> - dict:new(). + dict:new(). -spec map__insert({term(), term()}, map_dict()) -> map_dict(). map__insert(Object, Map) -> - {Key, Value} = Object, - dict:store(Key, Value, Map). + {Key, Value} = Object, + dict:store(Key, Value, Map). -spec map__lookup(term(), map_dict()) -> term(). map__lookup(Key, Map) -> - try dict:fetch(Key, Map) catch error:_ -> none end. + try + dict:fetch(Key, Map) + catch + error:_ -> none + end. -spec map__from_list([{fa(), term()}]) -> map_dict(). map__from_list(List) -> - dict:from_list(List). + dict:from_list(List). diff --git a/apps/els_lsp/src/els_unused_includes_diagnostics.erl b/apps/els_lsp/src/els_unused_includes_diagnostics.erl index 6c71b4318..75d088728 100644 --- a/apps/els_lsp/src/els_unused_includes_diagnostics.erl +++ b/apps/els_lsp/src/els_unused_includes_diagnostics.erl @@ -11,10 +11,11 @@ %%============================================================================== %% Exports %%============================================================================== --export([ is_default/0 - , run/1 - , source/0 - ]). +-export([ + is_default/0, + run/1, + source/0 +]). %%============================================================================== %% Includes @@ -27,125 +28,140 @@ -spec is_default() -> boolean(). is_default() -> - true. + true. -spec run(uri()) -> [els_diagnostics:diagnostic()]. run(Uri) -> - case els_utils:lookup_document(Uri) of - {error, _Error} -> - []; - {ok, Document} -> - UnusedIncludes = find_unused_includes(Document), - [ els_diagnostics:make_diagnostic( - els_protocol:range(inclusion_range(UI, Document)) - , <<"Unused file: ", (filename:basename(UI))/binary>> - , ?DIAGNOSTIC_WARNING - , source() - ) || UI <- UnusedIncludes ] - end. + %% hrl don't have to warning unuse + case + filename:extension(binary_to_list(Uri)) =/= ".hrl" andalso + els_utils:lookup_document(Uri) + of + false -> + []; + {error, _Error} -> + []; + {ok, Document} -> + UnusedIncludes = find_unused_includes(Document), + [ + els_diagnostics:make_diagnostic( + els_protocol:range(inclusion_range(UI, Document)), + <<"Unused file: ", (filename:basename(UI))/binary>>, + ?DIAGNOSTIC_WARNING, + source(), + %% Additional data with complete path + <<UI/binary>> + ) + || UI <- UnusedIncludes + ] + end. -spec source() -> binary(). source() -> - <<"UnusedIncludes">>. + <<"UnusedIncludes">>. %%============================================================================== %% Internal Functions %%============================================================================== -spec find_unused_includes(els_dt_document:item()) -> [uri()]. find_unused_includes(#{uri := Uri} = Document) -> - Graph = expand_includes(Document), - POIs = els_dt_document:pois(Document, - [ application - , implicit_fun - , import_entry - , macro - , record_expr - , record_field - , type_application - , export_type_entry - ]), - IncludedUris0 = els_diagnostics_utils:included_uris(Document), - IncludedUris1 = filter_includes_with_compiler_attributes(IncludedUris0), - ExcludeUnusedIncludes = - lists:filtermap( - fun(Include) -> - case els_utils:find_header(els_utils:filename_to_atom(Include)) of - {ok, File} -> {true, File}; - {error, _Error} -> - false - end - end, els_config:get(exclude_unused_includes)), - IncludedUris = IncludedUris1 -- ExcludeUnusedIncludes, - Fun = fun(POI, Acc) -> - update_unused(Graph, Uri, POI, Acc) - end, - UnusedIncludes = lists:foldl(Fun, IncludedUris, POIs), - digraph:delete(Graph), - UnusedIncludes. + Graph = expand_includes(Document), + POIs = els_dt_document:pois( + Document, + [ + application, + implicit_fun, + import_entry, + macro, + record_expr, + record_field, + type_application, + export_type_entry + ] + ), + IncludedUris0 = els_diagnostics_utils:included_uris(Document), + IncludedUris1 = filter_includes_with_compiler_attributes(IncludedUris0), + ExcludeUnusedIncludes = + lists:filtermap( + fun(Include) -> + case els_utils:find_header(els_utils:filename_to_atom(Include)) of + {ok, File} -> {true, File}; + {error, _Error} -> false + end + end, + els_config:get(exclude_unused_includes) + ), + IncludedUris = IncludedUris1 -- ExcludeUnusedIncludes, + UnusedIncludes = update_unused(IncludedUris, Graph, Uri, POIs), + digraph:delete(Graph), + UnusedIncludes. --spec update_unused(digraph:graph(), uri(), poi(), [uri()]) -> [uri()]. -update_unused(Graph, Uri, POI, Acc) -> - case els_code_navigation:goto_definition(Uri, POI) of - {ok, Uri, _DefinitionPOI} -> - Acc; - {ok, DefinitionUri, _DefinitionPOI} -> - case digraph:get_path(Graph, DefinitionUri, Uri) of - false -> - Acc; - Path -> - Acc -- Path - end; - {error, _Reason} -> - Acc - end. +-spec update_unused([uri()], digraph:graph(), uri(), [els_poi:poi()]) -> [uri()]. +update_unused(Acc = [], _Graph, _Uri, _POIs) -> + Acc; +update_unused(Acc, _Graph, _Uri, _POIs = []) -> + Acc; +update_unused(Acc, Graph, Uri, [POI | POIs]) -> + NewAcc = + case els_code_navigation:goto_definition(Uri, POI) of + {ok, [{DefinitionUri, _DefinitionPOI} | _]} when DefinitionUri =:= Uri -> + Acc; + {ok, [{DefinitionUri, _DefinitionPOI} | _]} -> + case digraph:get_path(Graph, DefinitionUri, Uri) of + false -> + Acc; + Path -> + Acc -- Path + end; + _ -> + Acc + end, + update_unused(NewAcc, Graph, Uri, POIs). -spec expand_includes(els_dt_document:item()) -> digraph:graph(). expand_includes(Document) -> - DG = digraph:new(), - AccFun = fun(#{uri := IncludedUri}, #{uri := IncluderUri}, _) -> - Dest = digraph:add_vertex(DG, IncluderUri), - Src = digraph:add_vertex(DG, IncludedUri), - _IncludedBy = digraph:add_edge(DG, Src, Dest), - DG - end, - els_diagnostics_utils:traverse_include_graph(AccFun, DG, Document). + DG = digraph:new(), + AccFun = fun(#{uri := IncludedUri}, #{uri := IncluderUri}, _) -> + Dest = digraph:add_vertex(DG, IncluderUri), + Src = digraph:add_vertex(DG, IncludedUri), + _IncludedBy = digraph:add_edge(DG, Src, Dest), + DG + end, + els_diagnostics_utils:traverse_include_graph(AccFun, DG, Document). --spec inclusion_range(uri(), els_dt_document:item()) -> poi_range(). +-spec inclusion_range(uri(), els_dt_document:item()) -> els_poi:poi_range(). inclusion_range(Uri, Document) -> - Path = binary_to_list(els_uri:path(Uri)), - case - els_compiler_diagnostics:inclusion_range(Path, Document, include) ++ - els_compiler_diagnostics:inclusion_range(Path, Document, include_lib) of - [Range|_] -> - Range; - _ -> - #{from => {1, 1}, to => {2, 1}} - end. + case els_range:inclusion_range(Uri, Document) of + {ok, Range} -> + Range; + _ -> + #{from => {1, 1}, to => {2, 1}} + end. %% @doc Given a list of included uris, filter out the ones containing %% compiler attributes. If the Uri cannot be found, keep it in the list. -spec filter_includes_with_compiler_attributes([uri()]) -> [uri()]. filter_includes_with_compiler_attributes(Uris) -> - Filter = fun(Uri) -> - case els_utils:lookup_document(Uri) of - {error, _Error} -> - {true, Uri}; - {ok, Document} -> - case contains_compiler_attributes(Document) of - true -> + Filter = fun(Uri) -> + case els_utils:lookup_document(Uri) of + {error, _Error} -> + {true, Uri}; + {ok, Document} -> + case contains_compiler_attributes(Document) of + true -> false; - false -> + false -> {true, Uri} - end - end - end, - lists:filtermap(Filter, Uris). + end + end + end, + lists:filtermap(Filter, Uris). %% @doc Return true if the Document contains a compiler attribute. -spec contains_compiler_attributes(els_dt_document:item()) -> boolean(). contains_compiler_attributes(Document) -> - compiler_attributes(Document) =/= []. + compiler_attributes(Document) =/= []. --spec compiler_attributes(els_dt_document:item()) -> [poi()]. +-spec compiler_attributes(els_dt_document:item()) -> [els_poi:poi()]. compiler_attributes(Document) -> - els_dt_document:pois(Document, [compile]). + els_dt_document:pois(Document, [compile]). diff --git a/apps/els_lsp/src/els_unused_macros_diagnostics.erl b/apps/els_lsp/src/els_unused_macros_diagnostics.erl index 55dcb177c..5ef831506 100644 --- a/apps/els_lsp/src/els_unused_macros_diagnostics.erl +++ b/apps/els_lsp/src/els_unused_macros_diagnostics.erl @@ -11,10 +11,11 @@ %%============================================================================== %% Exports %%============================================================================== --export([ is_default/0 - , run/1 - , source/0 - ]). +-export([ + is_default/0, + run/1, + source/0 +]). %%============================================================================== %% Includes @@ -27,47 +28,50 @@ -spec is_default() -> boolean(). is_default() -> - true. + true. -spec run(uri()) -> [els_diagnostics:diagnostic()]. run(Uri) -> - case filename:extension(Uri) of - <<".erl">> -> - case els_utils:lookup_document(Uri) of - {error, _Error} -> - []; - {ok, Document} -> - UnusedMacros = find_unused_macros(Document), - [make_diagnostic(POI) || POI <- UnusedMacros ] - end; - _ -> - [] - end. + case filename:extension(Uri) of + <<".erl">> -> + case els_utils:lookup_document(Uri) of + {error, _Error} -> + []; + {ok, Document} -> + UnusedMacros = find_unused_macros(Document), + [make_diagnostic(POI) || POI <- UnusedMacros] + end; + _ -> + [] + end. -spec source() -> binary(). source() -> - <<"UnusedMacros">>. + <<"UnusedMacros">>. %%============================================================================== %% Internal Functions %%============================================================================== --spec find_unused_macros(els_dt_document:item()) -> [poi()]. +-spec find_unused_macros(els_dt_document:item()) -> [els_poi:poi()]. find_unused_macros(Document) -> - Defines = els_dt_document:pois(Document, [define]), - Macros = els_dt_document:pois(Document, [macro]), - MacroIds = [Id || #{id := Id} <- Macros], - [POI || #{id := Id} = POI <- Defines, not lists:member(Id, MacroIds)]. + Defines = els_dt_document:pois(Document, [define]), + Macros = els_dt_document:pois(Document, [macro]), + MacroIds = [Id || #{id := Id} <- Macros], + [POI || #{id := Id} = POI <- Defines, not lists:member(Id, MacroIds)]. --spec make_diagnostic(poi()) -> els_diagnostics:diagnostic(). +-spec make_diagnostic(els_poi:poi()) -> els_diagnostics:diagnostic(). make_diagnostic(#{id := POIId, range := POIRange}) -> - Range = els_protocol:range(POIRange), - MacroName = case POIId of - {Id, Arity} -> - els_utils:to_binary( - lists:flatten(io_lib:format("~s/~p", [Id, Arity]))); - Id -> atom_to_binary(Id, utf8) - end, - Message = <<"Unused macro: ", MacroName/binary>>, - Severity = ?DIAGNOSTIC_WARNING, - Source = source(), - els_diagnostics:make_diagnostic(Range, Message, Severity, Source). + Range = els_protocol:range(POIRange), + MacroName = + case POIId of + {Id, Arity} -> + els_utils:to_binary( + lists:flatten(io_lib:format("~s/~p", [Id, Arity])) + ); + Id -> + atom_to_binary(Id, utf8) + end, + Message = <<"Unused macro: ", MacroName/binary>>, + Severity = ?DIAGNOSTIC_WARNING, + Source = source(), + els_diagnostics:make_diagnostic(Range, Message, Severity, Source). diff --git a/apps/els_lsp/src/els_unused_record_fields_diagnostics.erl b/apps/els_lsp/src/els_unused_record_fields_diagnostics.erl index 084ce1a47..4e92e46da 100644 --- a/apps/els_lsp/src/els_unused_record_fields_diagnostics.erl +++ b/apps/els_lsp/src/els_unused_record_fields_diagnostics.erl @@ -11,10 +11,11 @@ %%============================================================================== %% Exports %%============================================================================== --export([ is_default/0 - , run/1 - , source/0 - ]). +-export([ + is_default/0, + run/1, + source/0 +]). %%============================================================================== %% Includes @@ -27,42 +28,42 @@ -spec is_default() -> boolean(). is_default() -> - true. + true. -spec run(uri()) -> [els_diagnostics:diagnostic()]. run(Uri) -> - case filename:extension(Uri) of - <<".erl">> -> - case els_utils:lookup_document(Uri) of - {error, _Error} -> - []; - {ok, Document} -> - UnusedRecordFields = find_unused_record_fields(Document), - [make_diagnostic(POI) || POI <- UnusedRecordFields ] - end; - _ -> - [] - end. + case filename:extension(Uri) of + <<".erl">> -> + case els_utils:lookup_document(Uri) of + {error, _Error} -> + []; + {ok, Document} -> + UnusedRecordFields = find_unused_record_fields(Document), + [make_diagnostic(POI) || POI <- UnusedRecordFields] + end; + _ -> + [] + end. -spec source() -> binary(). source() -> - <<"UnusedRecordFields">>. + <<"UnusedRecordFields">>. %%============================================================================== %% Internal Functions %%============================================================================== --spec find_unused_record_fields(els_dt_document:item()) -> [poi()]. +-spec find_unused_record_fields(els_dt_document:item()) -> [els_poi:poi()]. find_unused_record_fields(Document) -> - Definitions = els_dt_document:pois(Document, [record_def_field]), - Usages = els_dt_document:pois(Document, [record_field]), - UsagesIds = lists:usort([Id || #{id := Id} <- Usages]), - [POI || #{id := Id} = POI <- Definitions, not lists:member(Id, UsagesIds)]. + Definitions = els_dt_document:pois(Document, [record_def_field]), + Usages = els_dt_document:pois(Document, [record_field]), + UsagesIds = lists:usort([Id || #{id := Id} <- Usages]), + [POI || #{id := Id} = POI <- Definitions, not lists:member(Id, UsagesIds)]. --spec make_diagnostic(poi()) -> els_diagnostics:diagnostic(). +-spec make_diagnostic(els_poi:poi()) -> els_diagnostics:diagnostic(). make_diagnostic(#{id := {RecName, RecField}, range := POIRange}) -> - Range = els_protocol:range(POIRange), - FullName = els_utils:to_binary(io_lib:format("#~p.~p", [RecName, RecField])), - Message = <<"Unused record field: ", FullName/binary>>, - Severity = ?DIAGNOSTIC_WARNING, - Source = source(), - els_diagnostics:make_diagnostic(Range, Message, Severity, Source). + Range = els_protocol:range(POIRange), + FullName = els_utils:to_binary(io_lib:format("#~p.~p", [RecName, RecField])), + Message = <<"Unused record field: ", FullName/binary>>, + Severity = ?DIAGNOSTIC_WARNING, + Source = source(), + els_diagnostics:make_diagnostic(Range, Message, Severity, Source). diff --git a/apps/els_lsp/src/els_work_done_progress.erl b/apps/els_lsp/src/els_work_done_progress.erl index eb777e120..1e5184998 100644 --- a/apps/els_lsp/src/els_work_done_progress.erl +++ b/apps/els_lsp/src/els_work_done_progress.erl @@ -6,48 +6,52 @@ %%============================================================================== %% Includes %%============================================================================== --include("els_lsp.hrl"). - %%============================================================================== %% Types %%============================================================================== -type percentage() :: 0..100. --type value_begin() :: #{ kind := 'begin' - , title := binary() - , cancellable => boolean() - , message => binary() - , percentage => percentage() - }. --type value_report() :: #{ kind := 'report' - , cancellable => boolean() - , message => binary() - , percentage => percentage() - }. --type value_end() :: #{ kind := 'end' - , message => binary() - }. --type value() :: value_begin() - | value_report() - | value_end(). +-type value_begin() :: #{ + kind := 'begin', + title := binary(), + cancellable => boolean(), + message => binary(), + percentage => percentage() +}. +-type value_report() :: #{ + kind := 'report', + cancellable => boolean(), + message => binary(), + percentage => percentage() +}. +-type value_end() :: #{ + kind := 'end', + message => binary() +}. +-type value() :: + value_begin() + | value_report() + | value_end(). --export_type([ value_begin/0 - , value_report/0 - , value_end/0 - , value/0 - ]). +-export_type([ + value_begin/0, + value_report/0, + value_end/0, + value/0 +]). %%============================================================================== %% Exports %%============================================================================== --export([ is_supported/0 - , send_create_request/0 - , value_begin/2 - , value_begin/3 - , value_report/1 - , value_report/2 - , value_end/1 - ]). +-export([ + is_supported/0, + send_create_request/0, + value_begin/2, + value_begin/3, + value_report/1, + value_report/2, + value_end/1 +]). %%============================================================================== %% API @@ -55,56 +59,62 @@ -spec is_supported() -> boolean(). is_supported() -> - case els_config:get(capabilities) of - #{<<"window">> := #{<<"workDoneProgress">> := WorkDoneProgress }} - when is_boolean(WorkDoneProgress) -> - WorkDoneProgress; - _ -> - false - end. + case els_config:get(capabilities) of + #{<<"window">> := #{<<"workDoneProgress">> := WorkDoneProgress}} when + is_boolean(WorkDoneProgress) + -> + WorkDoneProgress; + _ -> + false + end. -spec send_create_request() -> els_progress:token(). send_create_request() -> - Token = els_progress:token(), - Method = <<"window/workDoneProgress/create">>, - Params = #{token => Token}, - ok = els_server:send_request(Method, Params), - Token. + Token = els_progress:token(), + Method = <<"window/workDoneProgress/create">>, + Params = #{token => Token}, + ok = els_server:send_request(Method, Params), + Token. -spec value_begin(binary(), binary()) -> value_begin(). value_begin(Title, Message) -> - #{ kind => 'begin' - , title => Title - , cancellable => false - , message => Message - }. + #{ + kind => 'begin', + title => Title, + cancellable => false, + message => Message + }. -spec value_begin(binary(), binary(), percentage()) -> value_begin(). value_begin(Title, Message, Percentage) -> - #{ kind => 'begin' - , title => Title - , cancellable => false - , message => Message - , percentage => Percentage - }. + #{ + kind => 'begin', + title => Title, + cancellable => false, + message => Message, + percentage => Percentage + }. -spec value_report(binary()) -> value_report(). value_report(Message) -> - #{ kind => 'report' - , cancellable => false - , message => Message - }. + #{ + kind => 'report', + cancellable => false, + message => Message + }. -spec value_report(binary(), percentage()) -> value_report(). value_report(Message, Percentage) -> - #{ kind => 'report' - , cancellable => false - , message => Message - , percentage => Percentage - }. + #{ + kind => 'report', + cancellable => false, + message => Message, + percentage => Percentage + }. -spec value_end(binary()) -> value_end(). value_end(Message) -> - #{ kind => 'end' - , message => Message - }. + #{ + kind => 'end', + message => Message + }. diff --git a/apps/els_lsp/src/els_workspace_symbol_provider.erl b/apps/els_lsp/src/els_workspace_symbol_provider.erl index 17bfaf708..034984614 100644 --- a/apps/els_lsp/src/els_workspace_symbol_provider.erl +++ b/apps/els_lsp/src/els_workspace_symbol_provider.erl @@ -2,31 +2,24 @@ -behaviour(els_provider). --export([ handle_request/2 - , is_enabled/0 - ]). +-export([ + handle_request/1 +]). -include("els_lsp.hrl"). -define(LIMIT, 100). --type state() :: any(). - %%============================================================================== %% els_provider functions %%============================================================================== - --spec is_enabled() -> boolean(). -is_enabled() -> - true. - --spec handle_request(any(), state()) -> {any(), state()}. -handle_request({symbol, Params}, State) -> - %% TODO: Version 3.15 of the protocol introduces a much nicer way of - %% specifying queries, allowing clients to send the symbol kind. - #{<<"query">> := Query} = Params, - TrimmedQuery = string:trim(Query), - {modules(TrimmedQuery), State}. +-spec handle_request(any()) -> {response, any()}. +handle_request({symbol, Params}) -> + %% TODO: Version 3.15 of the protocol introduces a much nicer way of + %% specifying queries, allowing clients to send the symbol kind. + #{<<"query">> := Query} = Params, + TrimmedQuery = string:trim(Query), + {response, modules(TrimmedQuery)}. %%============================================================================== %% Internal Functions @@ -34,44 +27,48 @@ handle_request({symbol, Params}, State) -> -spec modules(binary()) -> [symbol_information()]. modules(<<>>) -> - []; + []; modules(Query) -> - {ok, All} = els_dt_document_index:find_by_kind(module), - Compare = fun(#{id := X}, #{id := Y}) -> X < Y end, - AllSorted = lists:sort(Compare, All), - {ok, RePattern} = re:compile(Query), - ReOpts = [{capture, none}], - F = fun(Name) -> - re:run(Name, RePattern, ReOpts) =:= match - end, - Filtered = filter_with_limit(AllSorted, F, ?LIMIT, []), - [symbol_information(X) || X <- Filtered]. + {ok, All} = els_dt_document_index:find_by_kind(module), + Compare = fun(#{id := X}, #{id := Y}) -> X < Y end, + AllSorted = lists:sort(Compare, All), + {ok, RePattern} = re:compile(Query), + ReOpts = [{capture, none}], + F = fun(Name) -> + re:run(Name, RePattern, ReOpts) =:= match + end, + Filtered = filter_with_limit(AllSorted, F, ?LIMIT, []), + [symbol_information(X) || X <- Filtered]. -spec symbol_information({binary(), uri()}) -> symbol_information(). symbol_information({Name, Uri}) -> - Range = #{from => {1, 1}, to => {1, 1}}, - #{ name => Name - , kind => ?SYMBOLKIND_MODULE - , location => #{ uri => Uri - , range => els_protocol:range(Range) - } - }. + Range = #{from => {1, 1}, to => {1, 1}}, + #{ + name => Name, + kind => ?SYMBOLKIND_MODULE, + location => #{ + uri => Uri, + range => els_protocol:range(Range) + } + }. --spec filter_with_limit( [els_dt_document_index:item()] - , function() - , integer() - , [{binary(), uri()}]) -> - [{binary(), uri()}]. +-spec filter_with_limit( + [els_dt_document_index:item()], + function(), + integer(), + [{binary(), uri()}] +) -> + [{binary(), uri()}]. filter_with_limit(_Modules, _Filter, 0, Result) -> - lists:reverse(Result); + lists:reverse(Result); filter_with_limit([], _Filter, _Limit, Result) -> - lists:reverse(Result); + lists:reverse(Result); filter_with_limit([Item | Items], Filter, Limit, Result) -> - #{kind := module, id := Module, uri := Uri} = Item, - Name = atom_to_binary(Module, utf8), - case Filter(Name) of - true -> - filter_with_limit(Items, Filter, Limit - 1, [{Name, Uri} | Result]); - false -> - filter_with_limit(Items, Filter, Limit, Result) - end. + #{kind := module, id := Module, uri := Uri} = Item, + Name = atom_to_binary(Module, utf8), + case Filter(Name) of + true -> + filter_with_limit(Items, Filter, Limit - 1, [{Name, Uri} | Result]); + false -> + filter_with_limit(Items, Filter, Limit, Result) + end. diff --git a/apps/els_lsp/src/erlang_ls.erl b/apps/els_lsp/src/erlang_ls.erl index 2db185375..72e7297e5 100644 --- a/apps/els_lsp/src/erlang_ls.erl +++ b/apps/els_lsp/src/erlang_ls.erl @@ -1,11 +1,12 @@ -module(erlang_ls). --export([ main/1 ]). +-export([main/1]). --export([ parse_args/1 - , log_root/0 - , cache_root/0 - ]). +-export([ + parse_args/1, + log_root/0, + cache_root/0 +]). %%============================================================================== %% Includes @@ -14,26 +15,30 @@ -include_lib("els_lsp/include/els_lsp.hrl"). -define(DEFAULT_LOGGING_LEVEL, "info"). +-define(LOG_MAX_NO_BYTES, 10 * 1000 * 1000). +-define(LOG_MAX_NO_FILES, 5). -spec main([any()]) -> ok. main(Args) -> - application:load(getopt), - application:load(els_core), - application:load(?APP), - ok = parse_args(Args), - application:set_env(els_core, server, els_server), - configure_logging(), - {ok, _} = application:ensure_all_started(?APP), - patch_logging(), - configure_client_logging(), - ?LOG_INFO("Started erlang_ls server", []), - receive _ -> ok end. + application:load(getopt), + application:load(els_core), + application:load(?APP), + ok = parse_args(Args), + application:set_env(els_core, server, els_server), + configure_logging(), + {ok, _} = application:ensure_all_started(?APP, permanent), + patch_logging(), + configure_client_logging(), + ?LOG_INFO("Started erlang_ls server", []), + receive + _ -> ok + end. -spec print_version() -> ok. print_version() -> - {ok, Vsn} = application:get_key(?APP, vsn), - io:format("Version: ~s~n", [Vsn]), - ok. + {ok, Vsn} = application:get_key(?APP, vsn), + io:format("Version: ~s~n", [Vsn]), + ok. %%============================================================================== %% Argument parsing @@ -41,62 +46,48 @@ print_version() -> -spec parse_args([string()]) -> ok. parse_args(Args) -> - case getopt:parse(opt_spec_list(), Args) of - {ok, {[version | _], _BadArgs}} -> - print_version(), - halt(0); - {ok, {ParsedArgs, _BadArgs}} -> - set_args(ParsedArgs); - {error, {invalid_option, _}} -> - getopt:usage(opt_spec_list(), "Erlang LS"), - halt(1) - end. + case getopt:parse(opt_spec_list(), Args) of + {ok, {[version | _], _BadArgs}} -> + print_version(), + halt(0); + {ok, {ParsedArgs, _BadArgs}} -> + set_args(ParsedArgs); + {error, {invalid_option, _}} -> + getopt:usage(opt_spec_list(), "Erlang LS"), + halt(1) + end. -spec opt_spec_list() -> [getopt:option_spec()]. opt_spec_list() -> - [ { version - , $v - , "version" - , undefined - , "Print the current version of Erlang LS" - } - , { transport - , $t - , "transport" - , {string, "stdio"} - , "DEPRECATED. Only the \"stdio\" transport is currently supported." - } - , { log_dir - , $d - , "log-dir" - , {string, filename:basedir(user_log, "erlang_ls")} - , "Directory where logs will be written." - } - , { log_level - , $l - , "log-level" - , {string, ?DEFAULT_LOGGING_LEVEL} - , "The log level that should be used." - } - ]. + [ + {version, $v, "version", undefined, "Print the current version of Erlang LS"}, + {transport, $t, "transport", {string, "stdio"}, + "DEPRECATED. Only the \"stdio\" transport is currently supported."}, + {log_dir, $d, "log-dir", {string, filename:basedir(user_log, "erlang_ls")}, + "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([]) -> + ok; +set_args([version | Rest]) -> + set_args(Rest); set_args([{Arg, Val} | Rest]) -> - set(Arg, Val), - set_args(Rest). + set(Arg, Val), + set_args(Rest). -spec set(atom(), getopt:arg_value()) -> ok. set(transport, _Transport) -> - %% Deprecated option, only kept for backward compatibility. - ok; + %% Deprecated option, only kept for backward compatibility. + ok; set(log_dir, Dir) -> - application:set_env(els_core, 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)); + application:set_env(els_core, log_level, list_to_atom(Level)); set(port_old, Port) -> - application:set_env(els_core, port, Port). + application:set_env(els_core, port, Port). %%============================================================================== %% Logger configuration @@ -104,43 +95,51 @@ set(port_old, Port) -> -spec configure_logging() -> ok. configure_logging() -> - LogFile = filename:join([log_root(), "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 => ?LSP_LOG_FORMAT } - } - }, - [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. + LogFile = filename:join([log_root(), "server.log"]), + {ok, LoggingLevel} = application:get_env(els_core, log_level), + ok = filelib:ensure_dir(LogFile), + [logger:remove_handler(H) || H <- logger:get_handler_ids()], + Handler = #{ + config => #{ + file => LogFile, max_no_bytes => ?LOG_MAX_NO_BYTES, max_no_files => ?LOG_MAX_NO_FILES + }, + level => LoggingLevel, + formatter => {logger_formatter, #{template => ?LSP_LOG_FORMAT}} + }, + StdErrHandler = #{ + config => #{type => standard_error}, + level => error, + formatter => {logger_formatter, #{template => ?LSP_LOG_FORMAT}} + }, + logger:add_handler(els_core_handler, logger_std_h, Handler), + logger:add_handler(els_stderr_handler, logger_std_h, StdErrHandler), + 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. + %% 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]). + {ok, LogDir} = application:get_env(els_core, log_dir), + {ok, CurrentDir} = file:get_cwd(), + Dirname = filename:basename(CurrentDir), + filename:join([LogDir, Dirname]). -spec cache_root() -> file:name(). cache_root() -> - {ok, CurrentDir} = file:get_cwd(), - Dirname = filename:basename(CurrentDir), - filename:join(filename:basedir(user_cache, "erlang_ls"), Dirname). + {ok, CurrentDir} = file:get_cwd(), + Dirname = filename:basename(CurrentDir), + filename:join(filename:basedir(user_cache, "erlang_ls"), Dirname). -spec configure_client_logging() -> ok. configure_client_logging() -> LoggingLevel = application:get_env(els_core, log_level, notice), - ok = logger:add_handler( els_log_notification - , els_log_notification - , #{ level => LoggingLevel } - ). + ok = logger:add_handler( + els_log_notification, + els_log_notification, + #{level => LoggingLevel} + ). diff --git a/apps/els_lsp/src/wrangler_handler.erl b/apps/els_lsp/src/wrangler_handler.erl new file mode 100644 index 000000000..44cfc13d8 --- /dev/null +++ b/apps/els_lsp/src/wrangler_handler.erl @@ -0,0 +1,168 @@ +%! Wrangler refactor form. +%! Choose some of the highlighted refactoring candidates then exit the form. +%! Before exiting, do not edit the file manually. +%%============================================================================== +%% A module to call Wrangler functions. +%% If wrangler is not configured, neutral elements will be returned. +%% +%% Using apply to circumvent tests resulting in 'unknown function' errors. +%%============================================================================== + +-module(wrangler_handler). +-export([ + is_enabled/0, + wrangler_config/0, + get_code_actions/2, + get_code_lenses/1, + enabled_commands/0, + execute_command/2, + get_highlights/3, + semantic_token_types/0, + semantic_token_modifiers/0, + get_semantic_tokens/1 +]). + +-include("els_lsp.hrl"). +-include_lib("kernel/include/logger.hrl"). + +%%============================================================================== +%% Configuration related functions. +%%============================================================================== + +%% Check if Wrangler is enabled in the config file. +-spec is_enabled() -> boolean(). +is_enabled() -> + case els_config:get(wrangler) of + notconfigured -> false; + Config -> maps:get("enabled", Config, false) + end. + +%% Returns Wrangler`s config from the config file. +%% Used by Wrangler. +-spec wrangler_config() -> map(). +wrangler_config() -> + els_config:get(wrangler). + +%% Returns the semantic token types defined by Wrangler. +%% Used to register server capabilities. +-spec semantic_token_types() -> any(). +semantic_token_types() -> + case is_enabled() of + true -> apply(wls_semantic_tokens, token_types, []); + false -> [] + end. + +%% Returns the semantic token modifiers defined by Wrangler. +%% Used to register server capabilities. +-spec semantic_token_modifiers() -> any(). +semantic_token_modifiers() -> + case is_enabled() of + true -> apply(wls_semantic_tokens, token_modifiers, []); + false -> [] + end. + +%% Returns the enabled Wrangler commands. +%% Used to register server capabilities. +-spec enabled_commands() -> [els_command:command_id()]. +enabled_commands() -> + case is_enabled() of + true -> + Commands = apply(wls_execute_command_provider, enabled_commands, []), + ?LOG_INFO("Wrangler Enabled Commands: ~p", [Commands]), + Commands; + false -> + [] + end. + +%%============================================================================== +%% Getters for the language features provided by Wrangler. +%%============================================================================== + +-spec get_code_actions(uri(), range()) -> [map()]. +get_code_actions(Uri, Range) -> + case is_enabled() of + true -> + case apply(wls_code_actions, get_actions, [Uri, Range]) of + [] -> + []; + Actions -> + ?LOG_INFO("Wrangler Code Actions: ~p", [Actions]), + Actions + end; + false -> + [] + end. + +-spec get_code_lenses(els_dt_document:item()) -> [els_code_lens:lens()]. +get_code_lenses(Document) -> + case is_enabled() of + true -> + case + lists:flatten([ + apply(wls_code_lens, lenses, [Id, Document]) + || Id <- apply(wls_code_lens, enabled_lenses, []) + ]) + of + [] -> + []; + Lenses -> + ?LOG_INFO("Wrangler Code Lenses: ~p", [Lenses]), + Lenses + end; + false -> + [] + end. + +-spec get_highlights(uri(), integer(), integer()) -> 'null' | [map()]. +get_highlights(Uri, Line, Character) -> + case is_enabled() of + true -> + case apply(wls_highlight, get_highlights, [Uri, {Line, Character}]) of + null -> + null; + Highlights -> + ?LOG_INFO("Wrangler Highlights: ~p", [Highlights]), + Highlights + end; + false -> + null + end. + +-spec get_semantic_tokens(uri()) -> [integer()]. +get_semantic_tokens(Uri) -> + case is_enabled() of + true -> + case apply(wls_semantic_tokens, semantic_tokens, [Uri]) of + [] -> + []; + SemanticTokens -> + ?LOG_INFO("Wrangler Semantic Tokens: ~p", [SemanticTokens]), + SemanticTokens + end; + false -> + [] + end. + +%%============================================================================== +%% Passing commands to Wrangler. +%%============================================================================== + +-spec execute_command(els_command:command_id(), [any()]) -> boolean(). +execute_command(Command, Arguments) -> + case is_enabled() of + true -> + case + lists:member( + Command, + apply(wls_execute_command_provider, enabled_commands, []) + ) + of + true -> + apply(wls_execute_command_provider, execute_command, [Command, Arguments]), + true; + false -> + false + end; + false -> + false + end. diff --git a/apps/els_lsp/test/els_call_hierarchy_SUITE.erl b/apps/els_lsp/test/els_call_hierarchy_SUITE.erl index 2242ff508..2db640db3 100644 --- a/apps/els_lsp/test/els_call_hierarchy_SUITE.erl +++ b/apps/els_lsp/test/els_call_hierarchy_SUITE.erl @@ -3,18 +3,20 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ incoming_calls/1 - , outgoing_calls/1 - ]). +-export([ + incoming_calls/1, + outgoing_calls/1 +]). %%============================================================================== %% Includes @@ -32,220 +34,411 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec incoming_calls(config()) -> ok. incoming_calls(Config) -> - UriA = ?config(call_hierarchy_a_uri, Config), - UriB = ?config(call_hierarchy_b_uri, Config), - #{result := PrepareResult} = - els_client:preparecallhierarchy(UriA, _Line = 11, _Char = 6), - Data = els_utils:base64_encode_term( - #{ poi => - #{ data => - #{ args => [{1, "Arg1"}] - , wrapping_range => #{ from => {7, 1} - , to => {17, 0} - } - } - , id => {function_a, 1} - , kind => function - , range => #{ from => {7, 1} - , to => {7, 11} - } - } - }), - Item = #{ data => Data - , detail => <<"call_hierarchy_a [L7]">> - , kind => ?SYMBOLKIND_FUNCTION - , name => <<"function_a/1">> - , range => - #{ 'end' => #{character => 10, line => 6} - , start => #{character => 0, line => 6}} - , selectionRange => - #{ 'end' => #{character => 10, line => 6} - , start => #{character => 0, line => 6}} - , uri => UriA}, - ?assertEqual([Item], PrepareResult), - #{result := Result} = els_client:callhierarchy_incomingcalls(Item), - Calls = [#{ from => - #{ data => - els_utils:base64_encode_term( - #{ poi => - #{ data => - #{ args => [{1, "Arg1"}] - , wrapping_range => - #{ from => {7, 1} - , to => {17, 0} - } - } - , id => {function_a, 1} - , kind => function - , range => #{ from => {7, 1} - , to => {7, 11} - } - } - }) - , detail => <<"call_hierarchy_a [L16]">> - , kind => 12 - , name => <<"function_a/1">> - , range => - #{ 'end' => #{ character => 12 - , line => 15 - } - , start => #{ character => 2 - , line => 15 - } - } - , selectionRange => - #{ 'end' => #{ character => 12 - , line => 15 - } - , start => #{ character => 2 - , line => 15 - } - } - , uri => UriA - } - , fromRanges => - [#{ 'end' => #{character => 12, line => 15} - , start => #{character => 2, line => 15} - }]} - , #{ from => - #{ data => - els_utils:base64_encode_term( - #{ poi => - #{ data => - #{ args => [{1, "Arg1"}] - , wrapping_range => - #{ from => {7, 1} - , to => {14, 0}}} - , id => {function_a, 1} - , kind => function - , range => #{from => {7, 1}, to => {7, 11}}}}) - , detail => <<"call_hierarchy_b [L11]">> - , kind => 12 - , name => <<"function_a/1">> - , range => - #{ 'end' => #{character => 29, line => 10} - , start => #{character => 2, line => 10} - } - , selectionRange => - #{ 'end' => #{character => 29, line => 10} - , start => #{character => 2, line => 10} - } - , uri => UriB} - , fromRanges => - [#{ 'end' => #{character => 29, line => 10} - , start => #{character => 2, line => 10} - }] - } - ], - ?assertEqual(Calls, Result). + UriA = ?config(call_hierarchy_a_uri, Config), + UriB = ?config(call_hierarchy_b_uri, Config), + #{result := PrepareResult} = + els_client:preparecallhierarchy(UriA, _Line = 11, _Char = 6), + Data = els_utils:base64_encode_term( + #{ + poi => + #{ + data => + #{ + args => [ + #{ + index => 1, + name => "N", + range => #{ + from => {9, 12}, + to => {9, 13} + } + } + ], + + wrapping_range => #{ + from => {7, 1}, + to => {17, 0} + }, + symbol_range => #{from => {7, 1}, to => {16, 19}}, + folding_range => #{ + from => {7, ?END_OF_LINE}, + to => {16, ?END_OF_LINE} + } + }, + id => {function_a, 1}, + kind => function, + range => #{ + from => {7, 1}, + to => {7, 11} + } + } + } + ), + Item = #{ + data => Data, + detail => <<"call_hierarchy_a [L7]">>, + kind => ?SYMBOLKIND_FUNCTION, + name => <<"function_a/1">>, + range => + #{ + 'end' => #{character => 10, line => 6}, + start => #{character => 0, line => 6} + }, + selectionRange => + #{ + 'end' => #{character => 10, line => 6}, + start => #{character => 0, line => 6} + }, + uri => UriA + }, + ?assertEqual([Item], PrepareResult), + #{result := Result} = els_client:callhierarchy_incomingcalls(Item), + Calls = [ + #{ + from => + #{ + data => + els_utils:base64_encode_term( + #{ + poi => + #{ + data => + #{ + args => [ + #{ + index => 1, + name => "N", + range => #{ + from => {9, 12}, + to => {9, 13} + } + } + ], + wrapping_range => + #{ + from => {7, 1}, + to => {14, 0} + }, + + symbol_range => #{from => {7, 1}, to => {13, 19}}, + folding_range => + #{ + from => {7, ?END_OF_LINE}, + to => {13, ?END_OF_LINE} + } + }, + id => {function_a, 1}, + kind => function, + range => #{from => {7, 1}, to => {7, 11}} + } + } + ), + detail => <<"call_hierarchy_b [L11]">>, + kind => 12, + name => <<"function_a/1">>, + range => + #{ + 'end' => #{character => 29, line => 10}, + start => #{character => 2, line => 10} + }, + selectionRange => + #{ + 'end' => #{character => 29, line => 10}, + start => #{character => 2, line => 10} + }, + uri => UriB + }, + fromRanges => + [ + #{ + 'end' => #{character => 29, line => 10}, + start => #{character => 2, line => 10} + } + ] + }, + #{ + from => + #{ + data => + els_utils:base64_encode_term( + #{ + poi => + #{ + data => + #{ + args => [ + #{ + index => 1, + name => "N", + range => #{ + from => {9, 12}, + to => {9, 13} + } + } + ], + wrapping_range => + #{ + from => {7, 1}, + to => {17, 0} + }, + symbol_range => #{from => {7, 1}, to => {16, 19}}, + folding_range => + #{ + from => {7, ?END_OF_LINE}, + to => {16, ?END_OF_LINE} + } + }, + id => {function_a, 1}, + kind => function, + range => #{ + from => {7, 1}, + to => {7, 11} + } + } + } + ), + detail => <<"call_hierarchy_a [L16]">>, + kind => 12, + name => <<"function_a/1">>, + range => + #{ + 'end' => #{ + character => 12, + line => 15 + }, + start => #{ + character => 2, + line => 15 + } + }, + selectionRange => + #{ + 'end' => #{ + character => 12, + line => 15 + }, + start => #{ + character => 2, + line => 15 + } + }, + uri => UriA + }, + fromRanges => + [ + #{ + 'end' => #{character => 12, line => 15}, + start => #{character => 2, line => 15} + } + ] + } + ], + lists:map( + fun(Call) -> + case lists:member(Call, Result) of + true -> + ct:comment("Call found: ~p", [Call]); + false -> + ct:fail("Call not found: ~p", [Call]) + end + end, + Calls + ), + ?assertEqual(length(Calls), length(Result)). -spec outgoing_calls(config()) -> ok. outgoing_calls(Config) -> - UriA = ?config(call_hierarchy_a_uri, Config), - #{result := PrepareResult} = - els_client:preparecallhierarchy(UriA, _Line = 9, _Char = 6), - Data = els_utils:base64_encode_term( - #{ poi => - #{ data => #{ args => [{1, "Arg1"}] - , wrapping_range => #{ from => {7, 1} - , to => {17, 0} - } + UriA = ?config(call_hierarchy_a_uri, Config), + #{result := PrepareResult} = + els_client:preparecallhierarchy(UriA, _Line = 9, _Char = 6), + Data = els_utils:base64_encode_term( + #{ + poi => + #{ + data => #{ + args => [ + #{ + index => 1, + name => "N", + range => #{from => {9, 12}, to => {9, 13}} } - , id => {function_a, 1} - , kind => function - , range => #{ from => {7, 1} - , to => {7, 11} - } - } - }), - Item = #{ data => Data - , detail => <<"call_hierarchy_a [L7]">> - , kind => ?SYMBOLKIND_FUNCTION - , name => <<"function_a/1">> - , range => - #{ 'end' => #{character => 10, line => 6} - , start => #{character => 0, line => 6}} - , selectionRange => - #{ 'end' => #{character => 10, line => 6} - , start => #{character => 0, line => 6}} - , uri => UriA}, - ?assertEqual([Item], PrepareResult), - #{result := Result} = els_client:callhierarchy_outgoingcalls(Item), - POIs = [els_utils:base64_decode_term(D) || #{to := #{data := D}} <- Result], - ?assertEqual([#{ poi => - #{ data => - #{ args => [{1, "Arg1"}] - , wrapping_range => #{ from => {7, 1} - , to => {17, 0} - }} - , id => {function_a, 1} - , kind => function - , range => #{ from => {7, 1} - , to => {7, 11} - } - }} - , #{ poi => - #{ data => - #{ args => [] - , wrapping_range => #{ from => {18, 1} - , to => {20, 0} - }} - , id => {function_b, 0} - , kind => function - , range => #{ from => {18, 1} - , to => {18, 11} - } - }} - , #{ poi => - #{ data => - #{ args => [{1, "Arg1"}] - , wrapping_range => #{ from => {7, 1} - , to => {14, 0} - }} - , id => {function_a, 1} - , kind => function - , range => #{ from => {7, 1} - , to => {7, 11} - } - }} - , #{ poi => - #{ data => - #{ args => [] - , wrapping_range => #{ from => {18, 1} - , to => {20, 0} - }} - , id => {function_b, 0} - , kind => function - , range => #{ from => {18, 1} - , to => {18, 11} - } - }} - ] - , POIs). + ], + wrapping_range => #{ + from => {7, 1}, + to => {17, 0} + }, + symbol_range => #{from => {7, 1}, to => {16, 19}}, + folding_range => #{ + from => {7, ?END_OF_LINE}, + to => {16, ?END_OF_LINE} + } + }, + id => {function_a, 1}, + kind => function, + range => #{ + from => {7, 1}, + to => {7, 11} + } + } + } + ), + Item = #{ + data => Data, + detail => <<"call_hierarchy_a [L7]">>, + kind => ?SYMBOLKIND_FUNCTION, + name => <<"function_a/1">>, + range => + #{ + 'end' => #{character => 10, line => 6}, + start => #{character => 0, line => 6} + }, + selectionRange => + #{ + 'end' => #{character => 10, line => 6}, + start => #{character => 0, line => 6} + }, + uri => UriA + }, + ?assertEqual([Item], PrepareResult), + #{result := Result} = els_client:callhierarchy_outgoingcalls(Item), + POIs = [els_utils:base64_decode_term(D) || #{to := #{data := D}} <- Result], + ?assertEqual( + [ + #{ + poi => + #{ + data => + #{ + args => [ + #{ + index => 1, + name => "N", + range => #{from => {9, 12}, to => {9, 13}} + } + ], + wrapping_range => #{ + from => {7, 1}, + to => {17, 0} + }, + symbol_range => #{from => {7, 1}, to => {16, 19}}, + folding_range => #{ + from => {7, ?END_OF_LINE}, + to => {16, ?END_OF_LINE} + } + }, + id => {function_a, 1}, + kind => function, + range => #{ + from => {7, 1}, + to => {7, 11} + } + } + }, + #{ + poi => + #{ + data => + #{ + args => [], + wrapping_range => #{ + from => {18, 1}, + to => {20, 0} + }, + symbol_range => #{from => {18, 1}, to => {19, 6}}, + folding_range => #{ + from => {18, ?END_OF_LINE}, + to => {19, ?END_OF_LINE} + } + }, + id => {function_b, 0}, + kind => function, + range => #{ + from => {18, 1}, + to => {18, 11} + } + } + }, + #{ + poi => + #{ + data => + #{ + args => [ + #{ + index => 1, + name => "N", + range => #{from => {9, 12}, to => {9, 13}} + } + ], + wrapping_range => #{ + from => {7, 1}, + to => {14, 0} + }, + symbol_range => #{from => {7, 1}, to => {13, 19}}, + folding_range => #{ + from => {7, ?END_OF_LINE}, + to => {13, ?END_OF_LINE} + } + }, + id => {function_a, 1}, + kind => function, + range => #{ + from => {7, 1}, + to => {7, 11} + } + } + }, + #{ + poi => + #{ + data => + #{ + args => [], + wrapping_range => #{ + from => {18, 1}, + to => {20, 0} + }, + symbol_range => #{from => {18, 1}, to => {19, 6}}, + folding_range => #{ + from => {18, ?END_OF_LINE}, + to => {19, ?END_OF_LINE} + } + }, + id => {function_b, 0}, + kind => function, + range => #{ + from => {18, 1}, + to => {18, 11} + } + } + } + ], + POIs + ). diff --git a/apps/els_lsp/test/els_code_action_SUITE.erl b/apps/els_lsp/test/els_code_action_SUITE.erl index 7dca29dfd..cb7c85cfa 100644 --- a/apps/els_lsp/test/els_code_action_SUITE.erl +++ b/apps/els_lsp/test/els_code_action_SUITE.erl @@ -1,17 +1,38 @@ -module(els_code_action_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ add_underscore_to_unused_var/1 - ]). +-export([ + add_underscore_to_unused_var/1, + export_unused_function/1, + suggest_variable/1, + fix_module_name/1, + remove_unused_macro/1, + remove_unused_import/1, + create_undefined_function/1, + create_undefined_function_arity/1, + create_undefined_function_variable_names/1, + fix_callbacks/1, + extract_function/1, + add_include_file_macro/1, + define_macro/1, + define_macro_with_args/1, + suggest_macro/1, + undefined_record/1, + undefined_record_suggest/1, + browse_docs/1, + browse_error_compiler/1, + browse_error_elvis/1 +]). %%============================================================================== %% Includes @@ -29,57 +50,1161 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) when + TestCase == browse_docs +-> + meck:new(els_utils, [passthrough]), + meck:expect(els_utils, find_module, fun + (my_dep_mod) -> + {ok, <<"file:///home/me/proj/lib/my_dep/src/my_dep_mod.erl">>}; + (M) -> + meck:passthrough([M]) + end), + meck:new(els_config, [passthrough]), + meck:expect(els_config, is_dep, fun + ("/home/me/proj/lib/my_dep/src/my_dep_mod.erl") -> true; + (_) -> false + end), + + els_test_utils:init_per_testcase(TestCase, Config); init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(TestCase, Config) when + TestCase == browse_docs +-> + meck:unload([els_config, els_utils]), + els_test_utils:end_per_testcase(TestCase, Config); end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). +%%============================================================================== +%% Const +%%============================================================================== +-define(COMMENTS_LINES, 2). %%============================================================================== %% Testcases %%============================================================================== -spec add_underscore_to_unused_var(config()) -> ok. add_underscore_to_unused_var(Config) -> - Uri = ?config(code_navigation_uri, Config), - Diag = #{ message => <<"variable 'A' is unused">> - , range => #{ 'end' => #{character => 0, line => 80} - , start => #{character => 0, line => 79} - } - , severity => 2 - , source => <<"Compiler">> - }, - Range = els_protocol:range(#{from => {80, 1}, to => {81, 1}}), - PrefixedCommand = els_command:with_prefix(<<"replace-lines">>), - #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), - Expected = - [ #{ command => #{ arguments => [ #{ from => 79 - , lines => <<" _A = X,\n">> - , to => 80 - , uri => Uri - } - ] - , command => PrefixedCommand - , title => <<"Add '_' to 'A'">> - } - , kind => <<"quickfix">> - , title => <<"Add '_' to 'A'">> - } + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 6, 3}, + to => {?COMMENTS_LINES + 6, 4} + }), + Diag = #{ + message => <<"variable 'A' is unused">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + [ + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => Range, + newText => <<"_A">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Add '_' to 'A'">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec export_unused_function(config()) -> ok. +export_unused_function(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 12, 1}, + to => {?COMMENTS_LINES + 12, 10} + }), + Diag = #{ + message => <<"function function_c/0 is unused">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + [ + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => + #{ + 'end' => #{ + character => 0, + line => ?COMMENTS_LINES + 3 + }, + start => #{ + character => 0, + line => ?COMMENTS_LINES + 3 + } + }, + newText => <<"-export([function_c/0]).\n">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Export function_c/0">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec suggest_variable(config()) -> ok. +suggest_variable(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 15, 9}, + to => {?COMMENTS_LINES + 15, 13} + }), + Diag = #{ + message => <<"variable 'Barf' is unbound">>, + range => Range, + severity => 3, + source => <<"Compiler">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => Range, + newText => <<"Bar">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Did you mean 'Bar'?">> + }, + ?assert(lists:member(Expected, Result)), + ok. + +-spec fix_module_name(config()) -> ok. +fix_module_name(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 1, 9}, + to => {?COMMENTS_LINES + 1, 25} + }), + Diag = #{ + message => << + "Module name 'code_action_oops' does not " + "match file name 'code_action'" + >>, + range => Range, + severity => 3, + source => <<"Compiler">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + [ + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => Range, + newText => <<"code_action">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Change to -module(code_action).">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec remove_unused_macro(config()) -> ok. +remove_unused_macro(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 17, 9}, + to => {?COMMENTS_LINES + 17, 15} + }), + LineRange = els_range:line(#{ + from => {?COMMENTS_LINES + 17, 9}, + to => {?COMMENTS_LINES + 17, 15} + }), + Diag = #{ + message => <<"Unused macro: TIMEOUT">>, + range => Range, + severity => 2, + source => <<"UnusedMacros">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + [ + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => els_protocol:range(LineRange), + newText => <<"">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Remove unused macro TIMEOUT.">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec remove_unused_import(config()) -> ok. +remove_unused_import(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 19, 15}, + to => {?COMMENTS_LINES + 19, 40} + }), + LineRange = els_range:line(#{ + from => {?COMMENTS_LINES + 19, 15}, + to => {?COMMENTS_LINES + 19, 40} + }), + {ok, FileName} = els_utils:find_header( + els_utils:filename_to_atom("stdlib/include/assert.hrl") + ), + Diag = #{ + message => <<"Unused file: assert.hrl">>, + range => Range, + severity => 2, + source => <<"UnusedIncludes">>, + data => FileName + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + [ + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => els_protocol:range(LineRange), + newText => <<>> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Remove unused -include_lib(assert.hrl).">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec create_undefined_function((config())) -> ok. +create_undefined_function(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {23, 3}, + to => {23, 9} + }), + Diag = #{ + message => <<"function foobar/0 undefined">>, + range => Range, + severity => 2, + source => <<"">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => + els_protocol:range(#{ + from => {28, 1}, + to => {28, 1} + }), + newText => + <<"foobar() ->\n ok.\n\n">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Create function foobar/0">> + }, + ?assert(lists:member(Expected, Result)), + ok. + +-spec create_undefined_function_arity((config())) -> ok. +create_undefined_function_arity(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {24, 3}, + to => {24, 9} + }), + Diag = #{ + message => <<"function foobar/3 undefined">>, + range => Range, + severity => 2, + source => <<"">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => + els_protocol:range(#{ + from => {28, 1}, + to => {28, 1} + }), + newText => + <<"foobar(Arg1, Arg2, Arg3) ->\n ok.\n\n">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Create function foobar/3">> + }, + ?assert(lists:member(Expected, Result)), + ok. + +-spec create_undefined_function_variable_names((config())) -> ok. +create_undefined_function_variable_names(Config) -> + Uri = ?config(code_action_uri, Config), + Range = els_protocol:range(#{ + from => {25, 3}, + to => {25, 9} + }), + Diag = #{ + message => <<"function foobar/5 undefined">>, + range => Range, + severity => 2, + source => <<"">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Expected = + #{ + edit => #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => + els_protocol:range(#{ + from => {28, 1}, + to => {28, 1} + }), + newText => + <<"foobar(Foo, FooBar, Bar, FooBar) ->\n ok.\n\n">> + } + ] + } + }, + kind => <<"quickfix">>, + title => <<"Create function foobar/5">> + }, + ?assert(lists:member(Expected, Result)), + ok. + +-spec fix_callbacks(config()) -> ok. +fix_callbacks(Config) -> + Uri = ?config(code_action_uri, Config), + % Ignored + Range = els_protocol:range(#{from => {4, 1}, to => {4, 15}}), + Diag = #{ + message => <<"undefined callback function init/1 \(behaviour 'gen_server'\)">>, + range => Range, + severity => 3, + source => <<"Compiler">> + }, + #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), + Title = <<"Add missing callbacks for: gen_server">>, + Command = els_command:with_prefix(<<"add-behaviour-callbacks">>), + ?assertMatch( + [ + #{ + command := #{ + title := Title, + command := Command, + arguments := + [ + #{ + uri := Uri, + behaviour := <<"gen_server">> + } + ] + }, + kind := <<"quickfix">>, + title := Title + } + ], + Result + ), + ok. + +-spec extract_function(config()) -> ok. +extract_function(Config) -> + Uri = ?config(extract_function_uri, Config), + %% These shouldn't return any code actions + %% -export([f/2]). + %% ^^^^^ + #{result := []} = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {2, 1}, to => {2, 5}}), + [] + ), + %% <empty line> + #{result := []} = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {3, 1}, to => {3, 5}}), + [] + ), + %% f(A, B) -> + %% ^^^^^ + #{result := []} = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {4, 1}, to => {4, 5}}), + [] + ), + %% C = 1, + %% ^ + #{result := []} = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {5, 8}, to => {5, 9}}), + [] + ), + %% other_function() + %% ^^^^^^^^^^^^^^^^ + #{result := []} = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {13, 4}, to => {13, 20}}), + [] + ), + %% This should return a code action + %% F = A + B + C, + %% ^^^^^^^^^ + #{ + result := [ + #{ + command := #{ + title := <<"Extract function">>, + arguments := [#{uri := Uri}] + }, + kind := <<"refactor.extract">>, + title := <<"Extract function">> + } + ] + } = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {6, 8}, to => {6, 17}}), + [] + ), + %% This should return a code action + %% G = case A of + %% ^^^^ + #{ + result := [ + #{ + command := #{ + title := <<"Extract function">>, + arguments := [#{uri := Uri}] + }, + kind := <<"refactor.extract">>, + title := <<"Extract function">> + } + ] + } = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {7, 9}, to => {7, 13}}), + [] + ), + %% This should return a code action + %% H = [X || X <- [A, B, C], X > 1], + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + #{ + result := [ + #{ + command := #{ + title := <<"Extract function">>, + arguments := [#{uri := Uri}] + }, + kind := <<"refactor.extract">>, + title := <<"Extract function">> + } + ] + } = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {11, 8}, to => {11, 36}}), + [] + ), + %% This should return a code action + %% I = {A, B, A}, + %% ^^^^^^^^^ + #{ + result := [ + #{ + command := #{ + title := <<"Extract function">>, + arguments := [#{uri := Uri}] + }, + kind := <<"refactor.extract">>, + title := <<"Extract function">> + } + ] + } = els_client:document_codeaction( + Uri, + els_protocol:range(#{from => {12, 8}, to => {12, 17}}), + [] + ), + ok. + +-spec add_include_file_macro(config()) -> ok. +add_include_file_macro(Config) -> + Uri = ?config(code_action_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 17, 9}, + to => {?COMMENTS_LINES + 17, 15} + }), + DestRange = els_protocol:range(#{ + from => {4, 1}, + to => {4, 1} + }), + Diag = #{ + message => <<"undefined macro 'INCLUDED_MACRO_A'">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + Path = <<"code_navigation/include/code_navigation.hrl">>, + Changes = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-include_lib(\"", Path/binary, "\").\n">> + } + ] + }, + Expected = + #{ + edit => #{changes => Changes}, + kind => <<"quickfix">>, + title => <<"Add -include_lib(\"", Path/binary, "\")">> + }, + ?assert(lists:member(Expected, Result)), + ok. + +-spec define_macro(config()) -> ok. +define_macro(Config) -> + Uri = ?config(code_action_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 17, 9}, + to => {?COMMENTS_LINES + 17, 15} + }), + DestRange = els_protocol:range(#{ + from => {4, 1}, + to => {4, 1} + }), + Diag = #{ + message => <<"undefined macro 'MAGIC_MACRO'">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + Changes = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-define(MAGIC_MACRO, undefined).\n">> + } + ] + }, + Expected = [ + #{ + edit => #{changes => Changes}, + kind => <<"quickfix">>, + title => <<"Define MAGIC_MACRO">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec suggest_macro(config()) -> ok. +suggest_macro(Config) -> + Uri = ?config(code_action_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 17, 9}, + to => {?COMMENTS_LINES + 17, 15} + }), + Diag = #{ + message => <<"undefined macro 'assertEql/2'">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + Changes = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => Range, + newText => <<"?assertEqual">> + } + ] + }, + Expected = + #{ + edit => #{changes => Changes}, + kind => <<"quickfix">>, + title => <<"Did you mean 'assertEqual'?">> + }, + ?assert(lists:member(Expected, Result)), + ok. + +-spec define_macro_with_args(config()) -> ok. +define_macro_with_args(Config) -> + Uri = ?config(code_action_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 17, 9}, + to => {?COMMENTS_LINES + 17, 15} + }), + DestRange = els_protocol:range(#{ + from => {4, 1}, + to => {4, 1} + }), + Diag = #{ + message => <<"undefined macro 'MAGIC_MACRO/2'">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + Changes = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-define(MAGIC_MACRO(_, _), undefined).\n">> + } + ] + }, + Expected = [ + #{ + edit => #{changes => Changes}, + kind => <<"quickfix">>, + title => <<"Define MAGIC_MACRO/2">> + } ], - ?assertEqual(Expected, Result), - ok. + ?assertEqual(Expected, Result), + ok. + +-spec undefined_record(config()) -> ok. +undefined_record(Config) -> + Uri = ?config(code_action_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {?COMMENTS_LINES + 17, 9}, + to => {?COMMENTS_LINES + 17, 15} + }), + DestRange = els_protocol:range(#{ + from => {4, 1}, + to => {4, 1} + }), + Diag = #{ + message => <<"record included_record_a undefined">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + Path1 = <<"code_navigation/include/code_navigation.hrl">>, + Path2 = <<"code_navigation/include/hover_record.hrl">>, + Changes1 = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-include_lib(\"", Path1/binary, "\").\n">> + } + ] + }, + Changes2 = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-include_lib(\"", Path2/binary, "\").\n">> + } + ] + }, + Changes3 = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-record(included_record_a, {}).\n">> + } + ] + }, + Expected = [ + #{ + edit => #{changes => Changes1}, + kind => <<"quickfix">>, + title => <<"Add -include_lib(\"", Path1/binary, "\")">> + }, + #{ + edit => #{changes => Changes2}, + kind => <<"quickfix">>, + title => <<"Add -include_lib(\"", Path2/binary, "\")">> + }, + #{ + edit => #{changes => Changes3}, + kind => <<"quickfix">>, + title => <<"Define record included_record_a">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec undefined_record_suggest(config()) -> ok. +undefined_record_suggest(Config) -> + Uri = ?config(undefined_record_suggest_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {5, 4}, + to => {5, 11} + }), + DestRange = els_protocol:range(#{ + from => {4, 1}, + to => {4, 1} + }), + Diag = #{ + message => <<"record foo_bar undefined">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + Changes1 = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => Range, + newText => <<"#foobar">> + } + ] + }, + Changes2 = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-record(foo_bar, {}).\n">> + } + ] + }, + Expected = [ + #{ + edit => #{changes => Changes1}, + kind => <<"quickfix">>, + title => <<"Did you mean #foobar{}?">> + }, + #{ + edit => #{changes => Changes2}, + kind => <<"quickfix">>, + title => <<"Define record foo_bar">> + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec browse_docs(config()) -> ok. +browse_docs(Config) -> + Uri = ?config(code_action_browse_docs_uri, Config), + %% -spec function_a(file:filename()) -> pid(). + %% ^ + Range1 = els_protocol:range(#{ + from => {3, 38}, + to => {3, 39} + }), + #{result := Result1} = + els_client:document_codeaction(Uri, Range1, []), + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: OTP docs: erlang:pid/0">>, + arguments := + [ + #{ + arity := 0, + function := <<"pid">>, + module := <<"erlang">>, + source := <<"otp">>, + app := <<"erts">>, + kind := <<"type">> + } + ] + }, + title := <<"Browse: OTP docs: erlang:pid/0">>, + kind := <<"browse">> + } + ], + Result1 + ), + %% -spec function_a(file:filename()) -> pid(). + %% ^ + Range2 = els_protocol:range(#{ + from => {3, 38}, + to => {3, 39} + }), + #{result := Result2} = + els_client:document_codeaction(Uri, Range2, []), + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: OTP docs: erlang:pid/0">>, + arguments := + [ + #{ + arity := 0, + function := <<"pid">>, + module := <<"erlang">>, + source := <<"otp">>, + app := <<"erts">>, + kind := <<"type">> + } + ] + }, + title := <<"Browse: OTP docs: erlang:pid/0">>, + kind := <<"browse">> + } + ], + Result2 + ), + %% -spec function_a(file:filename()) -> pid(). + %% ^ + Range3 = els_protocol:range(#{ + from => {3, 18}, + to => {3, 19} + }), + #{result := Result3} = + els_client:document_codeaction(Uri, Range3, []), + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: OTP docs: file:filename/0">>, + arguments := + [ + #{ + arity := 0, + function := <<"filename">>, + module := <<"file">>, + source := <<"otp">>, + app := <<"kernel">>, + kind := <<"type">> + } + ] + }, + title := <<"Browse: OTP docs: file:filename/0">>, + kind := <<"browse">> + } + ], + Result3 + ), + %% lists:sort(L), + %% ^ + Range4 = els_protocol:range(#{ + from => {5, 5}, + to => {5, 6} + }), + #{result := Result4} = + els_client:document_codeaction(Uri, Range4, []), + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: OTP docs: lists:sort/1">>, + arguments := + [ + #{ + arity := 1, + function := <<"sort">>, + module := <<"lists">>, + source := <<"otp">>, + app := <<"stdlib">>, + kind := <<"function">> + } + ] + }, + title := <<"Browse: OTP docs: lists:sort/1">>, + kind := <<"browse">> + } + ], + Result4 + ), + %% self(), + %% ^ + Range5 = els_protocol:range(#{ + from => {6, 5}, + to => {6, 6} + }), + #{result := Result5} = + els_client:document_codeaction(Uri, Range5, []), + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: OTP docs: erlang:self/0">>, + arguments := + [ + #{ + arity := 0, + function := <<"self">>, + module := <<"erlang">>, + source := <<"otp">>, + app := <<"erts">>, + kind := <<"function">> + } + ] + }, + title := <<"Browse: OTP docs: erlang:self/0">>, + kind := <<"browse">> + } + ], + Result5 + ), + %% -spec function_b() -> my_dep_mod:my_type(). + %% ^ + Range6 = els_protocol:range(#{ + from => {8, 23}, + to => {8, 24} + }), + #{result := Result6} = + els_client:document_codeaction(Uri, Range6, []), + + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: Hex docs: my_dep_mod:my_type/0">>, + arguments := + [ + #{ + arity := 0, + function := <<"my_type">>, + module := <<"my_dep_mod">>, + source := <<"hex">>, + app := <<"my_dep">>, + kind := <<"type">> + } + ] + }, + title := <<"Browse: Hex docs: my_dep_mod:my_type/0">>, + kind := <<"browse">> + } + ], + Result6 + ), + %% my_dep_mod:my_function(). + %% ^ + Range7 = els_protocol:range(#{ + from => {10, 5}, + to => {10, 6} + }), + #{result := Result7} = + els_client:document_codeaction(Uri, Range7, []), + + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: Hex docs: my_dep_mod:my_function/0">>, + arguments := + [ + #{ + arity := 0, + function := <<"my_function">>, + module := <<"my_dep_mod">>, + source := <<"hex">>, + app := <<"my_dep">>, + kind := <<"function">> + } + ] + }, + title := <<"Browse: Hex docs: my_dep_mod:my_function/0">>, + kind := <<"browse">> + } + ], + Result7 + ), + ok. + +-spec browse_error_compiler(config()) -> ok. +browse_error_compiler(Config) -> + Uri = ?config(code_action_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {5, 4}, + to => {5, 11} + }), + Diag = #{ + message => <<"Some cool compiler error">>, + code => <<"L1337">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: Erlang Error Index: L1337">>, + arguments := + [ + #{ + source := <<"Compiler">>, + code := <<"L1337">> + } + ] + }, + title := <<"Browse: Erlang Error Index: L1337">>, + kind := <<"browse">> + } + ], + Result + ), + ok. + +-spec browse_error_elvis(config()) -> ok. +browse_error_elvis(Config) -> + Uri = ?config(code_action_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {5, 4}, + to => {5, 11} + }), + Diag = #{ + message => <<"Elvis cool error">>, + code => <<"cool_error">>, + range => Range, + severity => 2, + source => <<"Elvis">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + ?assertMatch( + [ + #{ + command := + #{ + command := _, + title := <<"Browse: Elvis rules: cool_error">>, + arguments := + [ + #{ + source := <<"Elvis">>, + code := <<"cool_error">> + } + ] + }, + title := <<"Browse: Elvis rules: cool_error">>, + kind := <<"browse">> + } + ], + Result + ), + ok. diff --git a/apps/els_lsp/test/els_code_lens_SUITE.erl b/apps/els_lsp/test/els_code_lens_SUITE.erl index 8c4f5b461..b8ab846f5 100644 --- a/apps/els_lsp/test/els_code_lens_SUITE.erl +++ b/apps/els_lsp/test/els_code_lens_SUITE.erl @@ -1,21 +1,23 @@ -module(els_code_lens_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ default_lenses/1 - , server_info/1 - , ct_run_test/1 - , show_behaviour_usages/1 - , function_references/1 - ]). +-export([ + default_lenses/1, + server_info/1, + ct_run_test/1, + show_behaviour_usages/1, + function_references/1 +]). %%============================================================================== %% Includes @@ -33,47 +35,43 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(server_info, Config) -> - meck:new(els_code_lens_server_info, [passthrough, no_link]), - meck:expect(els_code_lens_server_info, is_default, 0, true), - %% Let's disable the suggest_spec lens to avoid noise - meck:new(els_code_lens_suggest_spec, [passthrough, no_link]), - meck:expect(els_code_lens_suggest_spec, is_default, 0, false), - els_test_utils:init_per_testcase(server_info, Config); + meck:new(els_code_lens_server_info, [passthrough, no_link]), + meck:expect(els_code_lens_server_info, is_default, 0, true), + els_test_utils:init_per_testcase(server_info, Config); init_per_testcase(ct_run_test, Config) -> - meck:new(els_code_lens_ct_run_test, [passthrough, no_link]), - meck:expect(els_code_lens_ct_run_test, is_default, 0, true), - els_test_utils:init_per_testcase(server_info, Config); + meck:new(els_code_lens_ct_run_test, [passthrough, no_link]), + meck:expect(els_code_lens_ct_run_test, is_default, 0, true), + els_test_utils:init_per_testcase(server_info, Config); init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(server_info, Config) -> - els_test_utils:end_per_testcase(server_info, Config), - meck:unload(els_code_lens_server_info), - meck:unload(els_code_lens_suggest_spec), - ok; + els_test_utils:end_per_testcase(server_info, Config), + meck:unload(els_code_lens_server_info), + ok; end_per_testcase(ct_run_test, Config) -> - els_test_utils:end_per_testcase(ct_run_test, Config), - meck:unload(els_code_lens_ct_run_test), - ok; + els_test_utils:end_per_testcase(ct_run_test, Config), + meck:unload(els_code_lens_ct_run_test), + ok; end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases @@ -81,106 +79,146 @@ end_per_testcase(TestCase, Config) -> -spec default_lenses(config()) -> ok. default_lenses(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Result} = els_client:document_codelens(Uri), - Commands = [els_command:without_prefix(Command) || - #{command := #{command := Command }} <- Result], - ?assertEqual([ <<"function-references">> - , <<"suggest-spec">> - ] - , lists:usort(Commands)), - ?assertEqual(40, length(Commands)), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Result} = els_client:document_codelens(Uri), + Commands = [ + els_command:without_prefix(Command) + || #{command := #{command := Command}} <- Result + ], + ?assertEqual( + [ + <<"function-references">> + ], + lists:usort(Commands) + ), + ?assertEqual(27, length(Commands)), + ok. -spec server_info(config()) -> ok. server_info(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Result} = els_client:document_codelens(Uri), - PrefixedCommand = els_command:with_prefix(<<"server-info">>), - FilteredResult = [L || #{ command := #{ command := Command }} = L - <- Result, Command =:= PrefixedCommand], - Title = <<"Erlang LS (in code_navigation) info">>, - Expected = - [ #{ command => #{ arguments => [] - , command => PrefixedCommand - , title => Title - } - , data => [] - , range => - #{'end' => #{character => 0, line => 1}, - start => #{character => 0, line => 0}} - } + Uri = ?config(code_navigation_uri, Config), + #{result := Result} = els_client:document_codelens(Uri), + PrefixedCommand = els_command:with_prefix(<<"server-info">>), + FilteredResult = [ + L + || #{command := #{command := Command}} = L <- + Result, + Command =:= PrefixedCommand ], - ?assertEqual(Expected, FilteredResult), - ok. + Title = <<"Erlang LS (in code_navigation) info">>, + Expected = + [ + #{ + command => #{ + arguments => [], + command => PrefixedCommand, + title => Title + }, + data => [], + range => + #{ + 'end' => #{character => 0, line => 1}, + start => #{character => 0, line => 0} + } + } + ], + ?assertEqual(Expected, FilteredResult), + ok. -spec ct_run_test(config()) -> ok. ct_run_test(Config) -> - Uri = ?config(sample_SUITE_uri, Config), - PrefixedCommand = els_command:with_prefix(<<"ct-run-test">>), - #{result := Result} = els_client:document_codelens(Uri), - FilteredResult = [L || #{ command := #{ command := Command }} = L - <- Result, Command =:= PrefixedCommand], - Expected = [ #{ command => #{ arguments => [ #{ arity => 1 - , function => <<"one">> - , line => 58 - , module => <<"sample_SUITE">> - , uri => Uri - }] - , command => PrefixedCommand - , title => <<"Run test">> - } - , data => [] - , range => #{ 'end' => #{ character => 3 - , line => 57 - } - , start => #{ character => 0 - , line => 57 - }}}], - ?assertEqual(Expected, FilteredResult), - ok. + Uri = ?config(sample_SUITE_uri, Config), + PrefixedCommand = els_command:with_prefix(<<"ct-run-test">>), + #{result := Result} = els_client:document_codelens(Uri), + FilteredResult = [ + L + || #{command := #{command := Command}} = L <- + Result, + Command =:= PrefixedCommand + ], + Expected = [ + #{ + command => #{ + arguments => [ + #{ + arity => 1, + function => <<"one">>, + line => 58, + module => <<"sample_SUITE">>, + uri => Uri + } + ], + command => PrefixedCommand, + title => <<"Run test">> + }, + data => [], + range => #{ + 'end' => #{ + character => 3, + line => 57 + }, + start => #{ + character => 0, + line => 57 + } + } + } + ], + ?assertEqual(Expected, FilteredResult), + ok. -spec show_behaviour_usages(config()) -> ok. show_behaviour_usages(Config) -> - Uri = ?config(behaviour_a_uri, Config), - PrefixedCommand = els_command:with_prefix(<<"show-behaviour-usages">>), - #{result := Result} = els_client:document_codelens(Uri), - Expected = [#{ command => - #{ arguments => [] - , command => PrefixedCommand - , title => <<"Behaviour used in 1 place(s)">> - } - , data => [] - , range => - #{ 'end' => #{character => 19, line => 0} - , start => #{character => 8, line => 0} - }}], - ?assertEqual(Expected, Result), - ok. + Uri = ?config(behaviour_a_uri, Config), + PrefixedCommand = els_command:with_prefix(<<"show-behaviour-usages">>), + #{result := Result} = els_client:document_codelens(Uri), + Expected = [ + #{ + command => + #{ + arguments => [], + command => PrefixedCommand, + title => <<"Behaviour used in 1 place(s)">> + }, + data => [], + range => + #{ + 'end' => #{character => 19, line => 0}, + start => #{character => 8, line => 0} + } + } + ], + ?assertEqual(Expected, Result), + ok. -spec function_references(config()) -> ok. function_references(Config) -> - Uri = ?config(code_lens_function_references_uri, Config), - #{result := Result} = els_client:document_codelens(Uri), - Expected = [ lens(5, 0) - , lens(10, 1) - , lens(14, 2) - ], - ?assertEqual(Expected, Result), - ok. + Uri = ?config(code_lens_function_references_uri, Config), + #{result := Result} = els_client:document_codelens(Uri), + Expected = [ + lens(5, 0), + lens(10, 1), + lens(14, 2) + ], + ?assertEqual(Expected, Result), + ok. -spec lens(number(), number()) -> map(). lens(Line, Usages) -> - Title = unicode:characters_to_binary( - io_lib:format("Used ~p times", [Usages])), - #{ command => - #{ arguments => [] - , command => els_command:with_prefix(<<"function-references">>) - , title => Title - } - , data => [] - , range => - #{ 'end' => #{character => 1, line => Line} - , start => #{character => 0, line => Line} - } - }. + Title = unicode:characters_to_binary( + io_lib:format("Used ~p times", [Usages]) + ), + #{ + command => + #{ + arguments => [], + command => els_command:with_prefix(<<"function-references">>), + title => Title + }, + data => [], + range => + #{ + 'end' => #{character => 1, line => Line}, + start => #{character => 0, line => Line} + } + }. diff --git a/apps/els_lsp/test/els_code_reload_SUITE.erl b/apps/els_lsp/test/els_code_reload_SUITE.erl new file mode 100644 index 000000000..b8af730e8 --- /dev/null +++ b/apps/els_lsp/test/els_code_reload_SUITE.erl @@ -0,0 +1,133 @@ +-module(els_code_reload_SUITE). + +%% CT Callbacks +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). + +%% Test cases +-export([ + code_reload/1, + code_reload_sticky_mod/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 init_per_suite(config()) -> config(). +init_per_suite(Config) -> + els_test_utils:init_per_suite(Config). + +-spec end_per_suite(config()) -> ok. +end_per_suite(Config) -> + els_test_utils:end_per_suite(Config). + +-spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) -> + mock_rpc(), + mock_code_reload_enabled(), + els_test_utils:init_per_testcase(TestCase, Config). + +-spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(TestCase, Config) -> + unmock_rpc(), + unmock_code_reload_enabled(), + els_test_utils:end_per_testcase(TestCase, Config). + +%%============================================================================== +%% Testcases +%%============================================================================== + +-spec code_reload(config()) -> ok. +code_reload(Config) -> + Uri = ?config(diagnostics_uri, Config), + Module = els_uri:module(Uri), + ok = els_code_reload:maybe_compile_and_load(Uri), + {ok, HostName} = inet:gethostname(), + NodeName = list_to_atom("fakenode@" ++ HostName), + ?assert(meck:called(rpc, call, [NodeName, c, c, [Module, []]])), + ok. + +-spec code_reload_sticky_mod(config()) -> ok. +code_reload_sticky_mod(Config) -> + Uri = ?config(diagnostics_uri, Config), + Module = els_uri:module(Uri), + {ok, HostName} = inet:gethostname(), + NodeName = list_to_atom("fakenode@" ++ HostName), + meck:expect( + rpc, + call, + fun + (PNode, code, is_sticky, [_]) when PNode =:= NodeName -> + true; + (Node, Mod, Fun, Args) -> + meck:passthrough([Node, Mod, Fun, Args]) + end + ), + ok = els_code_reload:maybe_compile_and_load(Uri), + ?assert(meck:called(rpc, call, [NodeName, code, is_sticky, [Module]])), + ?assertNot(meck:called(rpc, call, [NodeName, c, c, [Module, []]])), + ok. + +%%============================================================================== +%% Internal Functions +%%============================================================================== + +mock_rpc() -> + meck:new(rpc, [passthrough, no_link, unstick]), + {ok, HostName} = inet:gethostname(), + NodeName = list_to_atom("fakenode@" ++ HostName), + meck:expect( + rpc, + call, + fun + (PNode, c, c, [Module, '_']) when PNode =:= NodeName -> + {ok, Module}; + (Node, Mod, Fun, Args) -> + meck:passthrough([Node, Mod, Fun, Args]) + end + ). + +unmock_rpc() -> + meck:unload(rpc). + +mock_code_reload_enabled() -> + meck:new(els_config, [passthrough, no_link]), + meck:expect( + els_config, + get, + fun + (code_reload) -> + {ok, HostName} = inet:gethostname(), + #{"node" => "fakenode@" ++ HostName}; + (Key) -> + meck:passthrough([Key]) + end + ). + +unmock_code_reload_enabled() -> + meck:unload(els_config). diff --git a/apps/els_lsp/test/els_completion_SUITE.erl b/apps/els_lsp/test/els_completion_SUITE.erl index 8cc071322..b610b1d4f 100644 --- a/apps/els_lsp/test/els_completion_SUITE.erl +++ b/apps/els_lsp/test/els_completion_SUITE.erl @@ -1,51 +1,61 @@ -module(els_completion_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ attributes/1 - , attribute_behaviour/1 - , attribute_include/1 - , attribute_include_lib/1 - , attribute_export/1 - , attribute_export_incomplete/1 - , attribute_export_type/1 - , default_completions/1 - , empty_completions/1 - , exported_functions/1 - , exported_functions_arity/1 - , exported_types/1 - , functions_arity/1 - , functions_export_list/1 - , handle_empty_lines/1 - , handle_colon_inside_string/1 - , macros/1 - , only_exported_functions_after_colon/1 - , records/1 - , record_fields/1 - , types/1 - , types_export_list/1 - , variables/1 - , remote_fun/1 - , snippets/1 - , resolve_application_local/1 - , resolve_opaque_application_local/1 - , resolve_application_unexported_local/1 - , resolve_application_remote_self/1 - , resolve_application_remote_external/1 - , resolve_application_remote_otp/1 - , resolve_type_application_local/1 - , resolve_opaque_application_remote_self/1 - , resolve_type_application_remote_external/1 - , resolve_opaque_application_remote_external/1 - , resolve_type_application_remote_otp/1 - ]). +-export([ + attributes/1, + attribute_behaviour/1, + attribute_include/1, + attribute_include_lib/1, + attribute_export/1, + attribute_export_incomplete/1, + attribute_export_type/1, + default_completions/1, + empty_completions/1, + exported_functions/1, + exported_functions_arity/1, + exported_types/1, + functions_arity/1, + functions_export_list/1, + functions_no_args/1, + handle_empty_lines/1, + handle_colon_inside_string/1, + macros/1, + only_exported_functions_after_colon/1, + records/1, + record_fields/1, + record_fields_inside_record/1, + record_fields_no_completion/1, + types/1, + types_export_list/1, + types_context/1, + types_no_args/1, + variables/1, + remote_fun/1, + snippets/1, + resolve_application_local/1, + resolve_opaque_application_local/1, + resolve_application_unexported_local/1, + resolve_application_remote_self/1, + resolve_application_remote_external/1, + resolve_application_remote_otp/1, + resolve_type_application_local/1, + resolve_opaque_application_remote_self/1, + resolve_type_application_remote_external/1, + resolve_opaque_application_remote_external/1, + resolve_type_application_remote_otp/1, + completion_request_fails/1, + list_comprehension/1, + map_comprehension/1 +]). %%============================================================================== %% Includes @@ -64,1205 +74,2077 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec attributes(config()) -> ok. attributes(Config) -> - Uri = ?config(completion_attributes_uri, Config), - TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, - Expected = [ #{ insertText => <<"behaviour(${1:Behaviour}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-behaviour().">> - } - , #{ insertText => <<"define(${1:MACRO}, ${2:Value}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-define().">> - } - , #{ insertText => <<"export([${1:}]).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-export().">> - } - , #{ insertText => <<"export_type([${1:}]).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-export_type().">> - } - , #{ insertText => <<"if(${1:Pred}).\n${2:}\n-endif.">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-if().">> - } - , #{ insertText => <<"ifdef(${1:VAR}).\n${2:}\n-endif.">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-ifdef().">> - } - , #{ insertText => <<"ifndef(${1:VAR}).\n${2:}\n-endif.">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-ifndef().">> - } - , #{ insertText => <<"include(${1:}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-include().">> - } - , #{ insertText => <<"include_lib(${1:}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-include_lib().">> - } - , #{ insertText => <<"opaque ${1:name}() :: ${2:definition}.">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-opaque name() :: definition.">> - } - , #{ insertText => <<"record(${1:name}, {${2:field} = ${3:Value} " - ":: ${4:Type}()}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-record().">> - } - , #{ insertText => <<"type ${1:name}() :: ${2:definition}.">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-type name() :: definition.">> - } - , #{ insertText => <<"dialyzer(${1:}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-dialyzer().">> - } - , #{ insertText => <<"compile(${1:}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-compile().">> - } - , #{ insertText => <<"import(${1:Module}, [${2:}]).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-import().">> - } - , #{ insertText => - <<"callback ${1:name}(${2:Args}) -> ${3:return()}.">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-callback name(Args) -> return().">> - } - , #{ insertText => <<"on_load(${1:Function}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-on_load().">> - } - , #{ insertText => <<"vsn(${1:Version}).">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_SNIPPET - , label => <<"-vsn(Version).">>} - ], - #{result := Completions} = - els_client:completion(Uri, 5, 2, TriggerKindChar, <<"-">>), - ?assertEqual([], Completions -- Expected), - ?assertEqual([], Expected -- Completions), - ok. + Uri = ?config(completion_attributes_uri, Config), + TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Expected = + [ + #{ + insertText => <<"behaviour(${1:Behaviour}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-behaviour().">> + }, + #{ + insertText => <<"define(${1:MACRO}, ${2:Value}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-define().">> + }, + #{ + insertText => <<"export([${1:}]).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-export().">> + }, + #{ + insertText => <<"export_type([${1:}]).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-export_type().">> + }, + #{ + insertText => <<"feature(${1:Feature}, ${2:enable}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-feature().">> + }, + #{ + insertText => <<"if(${1:Pred}).\n${2:}\n-endif.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-if().">> + }, + #{ + insertText => <<"ifdef(${1:VAR}).\n${2:}\n-endif.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-ifdef().">> + }, + #{ + insertText => <<"ifndef(${1:VAR}).\n${2:}\n-endif.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-ifndef().">> + }, + #{ + insertText => <<"include(${1:}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-include().">> + }, + #{ + insertText => <<"include_lib(${1:}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-include_lib().">> + }, + #{ + insertText => <<"opaque ${1:name}() :: ${2:definition}.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-opaque name() :: definition.">> + }, + #{ + insertText => << + "record(${1:name}, {${2:field} = ${3:Value} " + ":: ${4:Type}()})." + >>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-record().">> + }, + #{ + insertText => <<"type ${1:name}() :: ${2:definition}.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-type name() :: definition.">> + }, + #{ + insertText => <<"dialyzer(${1:}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-dialyzer().">> + }, + #{ + insertText => <<"compile(${1:}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-compile().">> + }, + #{ + insertText => <<"import(${1:Module}, [${2:}]).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-import().">> + }, + #{ + insertText => + <<"callback ${1:name}(${2:Args}) -> ${3:return()}.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-callback name(Args) -> return().">> + }, + #{ + insertText => <<"on_load(${1:Function}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-on_load().">> + }, + #{ + insertText => <<"vsn(${1:Version}).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-vsn(Version).">> + }, + #{ + insertText => <<"module(completion_attributes).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-module(completion_attributes).">> + }, + #{ + insertText => <<"spec exported_function() -> ${1:_}.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-spec">> + }, + #{ + insertText => <<"nifs([${1:}]).">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + label => <<"-nifs().">> + } + ] ++ docs_attributes(), + #{result := Completions} = + els_client:completion(Uri, 5, 2, TriggerKindChar, <<"-">>), + ?assertEqual([], Completions -- Expected), + ?assertEqual([], Expected -- Completions), + ok. + +-spec docs_attributes() -> [completion_item()]. +-if(?OTP_RELEASE >= 27). +docs_attributes() -> + [ + #{ + label => <<"-moduledoc \"\"\"Text\"\"\".">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"moduledoc \"\"\"\n${1:Text}\n\"\"\".">> + }, + #{ + label => <<"-doc \"\"\"Text\"\"\".">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"doc \"\"\"\n${1:Text}\n\"\"\".">> + }, + #{ + label => <<"-moduledoc false.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"moduledoc false.">> + }, + #{ + label => <<"-doc false.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"doc false.">> + }, + #{ + label => <<"-moduledoc #{}.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"moduledoc #{${1:}}.">> + }, + #{ + label => <<"-doc #{}.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"doc #{${1:}}.">> + }, + #{ + label => <<"-moduledoc File.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"moduledoc {file,\"${1:File}\"}.">> + }, + #{ + label => <<"-doc File.">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_SNIPPET, + insertText => <<"doc {file,\"${1:File}\"}.">> + } + ]. +-else. +docs_attributes() -> + []. +-endif. -spec attribute_behaviour(config()) -> ok. attribute_behaviour(Config) -> - Uri = ?config(completion_attributes_uri, Config), - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, - Expected = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"gen_event">>} - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"gen_server">>} - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"gen_statem">>} - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"supervisor">>} - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"behaviour_a">>} - ], - #{result := Completions} = - els_client:completion(Uri, 2, 12, TriggerKindInvoked, <<"">>), - [?assert(lists:member(E, Completions)) || E <- Expected], - ok. + Uri = ?config(completion_attributes_uri, Config), + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"behaviour_a">> + } + ], + #{result := Completions} = + els_client:completion(Uri, 2, 12, TriggerKindInvoked, <<"">>), + [?assert(lists:member(E, Completions)) || E <- Expected], + ok. -spec attribute_include(config()) -> ok. attribute_include(Config) -> - Uri = ?config(completion_attributes_uri, Config), - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_CHARACTER, - Expected = [#{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FILE - , label => <<"code_navigation.hrl">> - } - ], - #{result := Completions} = - els_client:completion(Uri, 3, 11, TriggerKindInvoked, <<"\"">>), - [?assert(lists:member(E, Completions)) || E <- Expected], - ok. + Uri = ?config(completion_attributes_uri, Config), + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FILE, + label => <<"code_navigation.hrl">> + } + ], + #{result := Completions} = + els_client:completion(Uri, 3, 11, TriggerKindInvoked, <<"\"">>), + [?assert(lists:member(E, Completions)) || E <- Expected], + ok. -spec attribute_include_lib(config()) -> ok. attribute_include_lib(Config) -> - Uri = ?config(completion_attributes_uri, Config), - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_CHARACTER, - Expected = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FILE - , label => <<"code_navigation/include/rename.hrl">> - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FILE - , label => <<"code_navigation/include/code_navigation.hrl">> - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FILE - , label => <<"code_navigation/include/diagnostics.hrl">> - } - ], - #{result := Completions} = - els_client:completion(Uri, 4, 15, TriggerKindInvoked, <<"\"">>), - [?assert(lists:member(E, Completions)) || E <- Expected], - ok. + Uri = ?config(completion_attributes_uri, Config), + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FILE, + label => <<"code_navigation/include/rename.hrl">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FILE, + label => <<"code_navigation/include/code_navigation.hrl">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FILE, + label => <<"code_navigation/include/diagnostics.hrl">> + } + ], + #{result := Completions} = + els_client:completion(Uri, 4, 15, TriggerKindInvoked, <<"\"">>), + [?assert(lists:member(E, Completions)) || E <- Expected], + ok. -spec attribute_export(config()) -> ok. attribute_export(Config) -> - Uri = ?config(completion_attributes_uri, Config), - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, - Expected = [#{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"unexported_function/0">> - , data => - #{ arity => 0 - , function => <<"unexported_function">> - , module => <<"completion_attributes">> - } - } - ], - NotExpected = [#{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"exported_function/0">> - , data => - #{ arity => 0 - , function => <<"exported_function">> - , module => <<"completion_attributes">> - } - } - ], - #{result := Completions} = - els_client:completion(Uri, 5, 10, TriggerKindInvoked, <<"">>), - [?assert(lists:member(E, Completions)) || E <- Expected], - [?assertNot(lists:member(E, Completions)) || E <- NotExpected], - ok. + Uri = ?config(completion_attributes_uri, Config), + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"unexported_function/0">>, + data => + #{ + arity => 0, + function => <<"unexported_function">>, + module => <<"completion_attributes">> + } + } + ], + NotExpected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"exported_function/0">>, + data => + #{ + arity => 0, + function => <<"exported_function">>, + module => <<"completion_attributes">> + } + } + ], + #{result := Completions} = + els_client:completion(Uri, 5, 10, TriggerKindInvoked, <<"">>), + [?assert(lists:member(E, Completions)) || E <- Expected], + [?assertNot(lists:member(E, Completions)) || E <- NotExpected], + ok. -spec attribute_export_incomplete(config()) -> ok. attribute_export_incomplete(Config) -> - Uri = ?config(completion_incomplete_uri, Config), - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, - Expected = [#{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"function_unexported/0">> - , data => - #{ arity => 0 - , function => <<"function_unexported">> - , module => <<"completion_incomplete">> - } - } - ], - NotExpected = [#{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"function_exported/0">> - , data => - #{ arity => 0 - , function => <<"function_exported">> - , module => <<"completion_incomplete">> - } - } - ], - #{result := Completions} = - els_client:completion(Uri, 4, 18, TriggerKindInvoked, <<"">>), - [?assert(lists:member(E, Completions)) || E <- Expected], - [?assertNot(lists:member(E, Completions)) || E <- NotExpected], - ok. + Uri = ?config(completion_incomplete_uri, Config), + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"function_unexported/0">>, + data => + #{ + arity => 0, + function => <<"function_unexported">>, + module => <<"completion_incomplete">> + } + } + ], + NotExpected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"function_exported/0">>, + data => + #{ + arity => 0, + function => <<"function_exported">>, + module => <<"completion_incomplete">> + } + } + ], + #{result := Completions} = + els_client:completion(Uri, 4, 18, TriggerKindInvoked, <<"">>), + [?assert(lists:member(E, Completions)) || E <- Expected], + [?assertNot(lists:member(E, Completions)) || E <- NotExpected], + ok. -spec attribute_export_type(config()) -> ok. attribute_export_type(Config) -> - Uri = ?config(completion_attributes_uri, Config), - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, - Expected = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"unexported_type/0">> - , data => #{ - module => <<"completion_attributes">> - , type => <<"unexported_type">> - , arity => 0 - } - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"unexported_opaque/0">> - , data => #{ - module => <<"completion_attributes">> - , type => <<"unexported_opaque">> - , arity => 0 - } - } - ], - NotExpected = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"exported_type/0">> - , data => #{} - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"exported_opaque/0">> - , data => #{} - } - ], - #{result := Completions} = - els_client:completion(Uri, 6, 15, TriggerKindInvoked, <<"">>), - [?assert(lists:member(E, Completions)) || E <- Expected], - [?assertNot(lists:member(E, Completions)) || E <- NotExpected], - ok. + Uri = ?config(completion_attributes_uri, Config), + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"unexported_type/0">>, + data => #{ + module => <<"completion_attributes">>, + type => <<"unexported_type">>, + arity => 0 + } + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"unexported_opaque/0">>, + data => #{ + module => <<"completion_attributes">>, + type => <<"unexported_opaque">>, + arity => 0 + } + } + ], + NotExpected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"exported_type/0">>, + data => #{} + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"exported_opaque/0">>, + data => #{} + } + ], + #{result := Completions} = + els_client:completion(Uri, 6, 15, TriggerKindInvoked, <<"">>), + [?assert(lists:member(E, Completions)) || E <- Expected], + [?assertNot(lists:member(E, Completions)) || E <- NotExpected], + ok. -spec default_completions(config()) -> ok. default_completions(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_extra_uri, Config), - Functions = [ #{ insertText => <<"do_3(${1:Arg1}, ${2:Arg2})">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"do_3/2">> - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do_3">> - , arity => 2 - } - } - , #{ insertText => <<"do_2()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"do_2/0">> - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do_2">> - , arity => 0 - } - } - , #{ insertText => <<"do(${1:_Config})">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"do/1">> - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do">> - , arity => 1 - } - } - , #{ insertText => <<"do_4(${1:Arg1}, ${2:Arg2})">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"do_4/2">> - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do_4">> - , arity => 2 - } - } - , #{ insertText => <<"'DO_LOUDER'()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"'DO_LOUDER'/0">> - , data => #{ module => <<"code_navigation_extra">> - , function => <<"DO_LOUDER">> - , arity => 0 - } - } - ], - - Expected1 = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"code_lens_function_references">> - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"code_navigation_extra">> - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"code_navigation">> - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"code_navigation_types">> - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_MODULE - , label => <<"code_navigation_undefined">> - } - | Functions ], - - DefaultCompletion = els_completion_provider:keywords() - ++ els_completion_provider:bifs(function, false) - ++ els_snippets_server:snippets(), - #{ result := Completion1 - } = els_client:completion(Uri, 9, 6, TriggerKind, <<"">>), - ?assertEqual( - lists:sort(Expected1), - filter_completion(Completion1, DefaultCompletion)), - - Expected2 = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_CONSTANT - , label => <<"foo">> - } - | Functions ], - - #{ result := Completion2 - } = els_client:completion(Uri, 6, 14, TriggerKind, <<"">>), - ?assertEqual( - lists:sort(Expected2), - filter_completion(Completion2, DefaultCompletion)), - - ok. + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_extra_uri, Config), + Functions = [ + #{ + insertText => <<"do_3(${1:Nat}, ${2:Wot})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"do_3/2">>, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do_3">>, + arity => 2 + } + }, + #{ + insertText => <<"do_2()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"do_2/0">>, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do_2">>, + arity => 0 + } + }, + #{ + insertText => <<"do(${1:_Config})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"do/1">>, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do">>, + arity => 1 + } + }, + #{ + insertText => <<"do_4(${1:Nat}, ${2:OpaqueLocal})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"do_4/2">>, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do_4">>, + arity => 2 + } + }, + #{ + insertText => <<"'DO_LOUDER'()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"'DO_LOUDER'/0">>, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"DO_LOUDER">>, + arity => 0 + } + }, + #{ + insertText => <<"function_a(${1:Arg1}, ${2:Arg2})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"function_a/2">>, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"function_a">>, + arity => 2 + } + } + ], + + Expected1 = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_lens_function_references">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_navigation_extra">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_navigation">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"code_navigation">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_navigation_types">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_navigation_undefined">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_navigation_broken">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_action">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_completion_fail">> + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_MODULE, + label => <<"code_action_browse_docs">> + } + | Functions + ], + DefaultCompletion = + keywords() ++ + els_completion_provider:bifs(function, args) ++ + els_snippets_server:snippets(), + #{result := Completion1} = els_client:completion(Uri, 9, 6, TriggerKind, <<"">>), + ?assertEqual( + [], + filter_completion(Completion1, DefaultCompletion) -- Expected1 + ), + ?assertEqual( + [], + Expected1 -- filter_completion(Completion1, DefaultCompletion) + ), + + Expected2 = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"foo">> + } + | Functions + ], + + #{result := Completion2} = els_client:completion(Uri, 6, 14, TriggerKind, <<"">>), + ?assertEqual( + lists:sort(Expected2), + filter_completion(Completion2, DefaultCompletion) + ), + + ok. -spec empty_completions(config()) -> ok. empty_completions(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_extra_uri, Config), + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_extra_uri, Config), - #{ result := Completion - } = els_client:completion(Uri, 5, 1, TriggerKind, <<"">>), - ?assertEqual([], Completion), - ok. + #{result := Completion} = els_client:completion(Uri, 5, 1, TriggerKind, <<"">>), + ?assertEqual([], Completion), + ok. -spec exported_functions(config()) -> ok. exported_functions(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, - Uri = ?config(code_navigation_uri, Config), - ExpectedCompletion1 = expected_exported_functions(), - - #{result := Completion1} = - els_client:completion(Uri, 32, 25, TriggerKind, <<":">>), - ?assertEqual(lists:sort(ExpectedCompletion1), lists:sort(Completion1)), - - #{result := Completion2} = - els_client:completion(Uri, 52, 34, TriggerKind, <<":">>), - ExpectedCompletionArity = expected_exported_functions_arity_only(), - ?assertEqual(lists:sort(ExpectedCompletionArity), lists:sort(Completion2)), - - ExpectedCompletionQuoted = - [ #{ label => <<"do/1">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => - #{ module => <<"Code.Navigation.Elixirish">> - , function => <<"do">> - , arity => 1 + TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Uri = ?config(code_navigation_uri, Config), + ExpectedCompletion1 = expected_exported_functions(), + + #{result := Completion1} = + els_client:completion(Uri, 32, 25, TriggerKind, <<":">>), + ?assertEqual(lists:sort(ExpectedCompletion1), lists:sort(Completion1)), + + #{result := Completion2} = + els_client:completion(Uri, 52, 34, TriggerKind, <<":">>), + ExpectedCompletionArity = expected_exported_functions_arity_only(), + ?assertEqual(lists:sort(ExpectedCompletionArity), lists:sort(Completion2)), + + ExpectedCompletionQuoted = + [ + #{ + label => <<"do/1">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => + #{ + module => <<"Code.Navigation.Elixirish">>, + function => <<"do">>, + arity => 1 + } } - } - ], - #{result := Completion3} = - els_client:completion(Uri, 100, 39, TriggerKind, <<":">>), - ?assertEqual(lists:sort(ExpectedCompletionQuoted), lists:sort(Completion3)), + ], + #{result := Completion3} = + els_client:completion(Uri, 100, 39, TriggerKind, <<":">>), + ?assertEqual(lists:sort(ExpectedCompletionQuoted), lists:sort(Completion3)), - ok. + ok. %% [#200] Complete only with arity for named funs -spec exported_functions_arity(config()) -> ok. exported_functions_arity(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_uri, Config), - ExpectedCompletion = expected_exported_functions_arity_only(), - #{result := Completion} = - els_client:completion(Uri, 52, 35, TriggerKind, <<"">>), - ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_uri, Config), + ExpectedCompletion = expected_exported_functions_arity_only(), + #{result := Completion} = + els_client:completion(Uri, 52, 35, TriggerKind, <<"">>), + ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), - ok. + ok. -spec exported_types(config()) -> ok. exported_types(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, - Uri = ?config(code_navigation_uri, Config), - Types = [ <<"date_time">>, <<"fd">>, <<"file_info">>, <<"filename">> - , <<"filename_all">>, <<"io_device">>, <<"mode">>, <<"name">> - , <<"name_all">>, <<"posix">> - ], - Expected = [ #{ insertText => <<T/binary, "()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<T/binary, "/0">> - , data => #{ module => <<"file">> - , type => T - , arity => 0 - } - } - || T <- Types - ], + TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Uri = ?config(code_navigation_uri, Config), + + OtpRelease = list_to_integer(erlang:system_info(otp_release)), + + Types = + [ + <<"date_time">>, + <<"fd">>, + <<"file_info">>, + <<"filename">>, + <<"filename_all">>, + <<"io_device">> + ] ++ + [<<"location">> || OtpRelease >= 26] ++ + [ + <<"mode">>, + <<"name">>, + <<"name_all">>, + <<"posix">> + ], + Expected = [ + #{ + insertText => <<T/binary, "()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<T/binary, "/0">>, + data => #{ + module => <<"file">>, + type => T, + arity => 0 + } + } + || T <- Types + ], - ct:comment("Exported types from module are returned in a spec context"), - #{result := Completion1} = - els_client:completion(Uri, 55, 60, TriggerKind, <<":">>), - ?assertEqual(lists:sort(Expected), lists:sort(Completion1)), + ct:comment("Exported types from module are returned in a spec context"), + #{result := Completion1} = + els_client:completion(Uri, 55, 60, TriggerKind, <<":">>), + ?assertEqual(lists:sort(Expected), lists:sort(Completion1)), - ok. + ok. %% [#200] Complete only with arity for named funs -spec functions_arity(config()) -> ok. functions_arity(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_uri, Config), - ExportedFunctions = [ {<<"callback_a">>, 0} - , {<<"function_a">>, 0} - , {<<"function_b">>, 0} - , {<<"function_c">>, 0} - , {<<"function_d">>, 0} - , {<<"function_e">>, 0} - , {<<"function_f">>, 0} - , {<<"function_g">>, 1} - , {<<"function_h">>, 0} - , {<<"function_i">>, 0} - , {<<"function_j">>, 0} - , {<<"function_k">>, 0} - , {<<"function_l">>, 2} - , {<<"function_m">>, 1} - , {<<"function_n">>, 0} - , {<<"function_o">>, 0} - , {<<"'PascalCaseFunction'">>, 1} - , {<<"function_p">>, 1} - , {<<"function_q">>, 0} - , {<<"macro_b">>, 2} - , {<<"function_mb">>, 0} - ], - ExpectedCompletion = - [ #{ label => - <<FunName/binary, "/", (integer_to_binary(Arity))/binary>> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => #{ module => <<"code_navigation">> - , function => string:trim(FunName, both, [$']) - , arity => Arity - } - } - || {FunName, Arity} <- ExportedFunctions - ] ++ els_completion_provider:bifs(function, true), - #{result := Completion} = - els_client:completion(Uri, 51, 17, TriggerKind, <<"">>), - ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), - - ok. + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_uri, Config), + ExportedFunctions = [ + {<<"callback_a">>, 0}, + {<<"function_a">>, 0}, + {<<"function_b">>, 0}, + {<<"function_c">>, 0}, + {<<"function_d">>, 0}, + {<<"function_e">>, 0}, + {<<"function_f">>, 0}, + {<<"function_g">>, 1}, + {<<"function_h">>, 0}, + {<<"function_i">>, 0}, + {<<"function_j">>, 0}, + {<<"function_k">>, 0}, + {<<"function_l">>, 2}, + {<<"function_m">>, 1}, + {<<"function_n">>, 0}, + {<<"function_o">>, 0}, + {<<"'PascalCaseFunction'">>, 1}, + {<<"function_p">>, 1}, + {<<"function_q">>, 0}, + {<<"macro_b">>, 2}, + {<<"function_mb">>, 0}, + {<<"code_navigation">>, 0}, + {<<"code_navigation">>, 1}, + {<<"multiple_instances_same_file">>, 0}, + {<<"code_navigation_extra">>, 3}, + {<<"multiple_instances_diff_file">>, 0} + ], + ExpectedCompletion = + [ + #{ + label => + <<FunName/binary, "/", (integer_to_binary(Arity))/binary>>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{ + module => <<"code_navigation">>, + function => string:trim(FunName, both, [$']), + arity => Arity + } + } + || {FunName, Arity} <- ExportedFunctions + ] ++ els_completion_provider:bifs(function, arity_only), + #{result := Completion} = + els_client:completion(Uri, 51, 17, TriggerKind, <<"">>), + ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), + + ok. %% [#196] Complete only with arity for funs inside the export list -spec functions_export_list(config()) -> ok. functions_export_list(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_extra_uri, Config), - Expected = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"do_3/2">> - , data => - #{ module => <<"code_navigation_extra">> - , function => <<"do_3">> - , arity => 2 - } + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_extra_uri, Config), + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"do_3/2">>, + data => + #{ + module => <<"code_navigation_extra">>, + function => <<"do_3">>, + arity => 2 } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , label => <<"do_4/2">> - , data => - #{ module => <<"code_navigation_extra">> - , function => <<"do_4">> - , arity => 2 - } + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + label => <<"do_4/2">>, + data => + #{ + module => <<"code_navigation_extra">>, + function => <<"do_4">>, + arity => 2 } - ], - #{result := Completion} = - els_client:completion(Uri, 3, 13, TriggerKind, <<"">>), - ?assertEqual(Expected, Completion). + } + ], + #{result := Completion} = + els_client:completion(Uri, 3, 13, TriggerKind, <<"">>), + ?assertEqual(Expected, Completion). + +-spec functions_no_args(config()) -> ok. +functions_no_args(Config) -> + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_extra_uri, Config), + ct:comment("Completing function at func|t(), should include args"), + #{result := Completion1} = + els_client:completion(Uri, 26, 7, TriggerKind, <<>>), + ?assertEqual( + [<<"function_a(${1:Arg1}, ${2:Arg2})">>], + [ + InsertText + || #{ + kind := ?COMPLETION_ITEM_KIND_FUNCTION, + label := <<"function_a/2">>, + insertText := InsertText + } <- Completion1 + ] + ), + ct:comment("Completing function at funct|(), shouldn't include args"), + #{result := Completion2} = + els_client:completion(Uri, 26, 8, TriggerKind, <<>>), + ?assertEqual( + [<<"function_a">>], + [ + InsertText + || #{ + kind := ?COMPLETION_ITEM_KIND_FUNCTION, + label := <<"function_a/2">>, + insertText := InsertText + } <- Completion2 + ] + ), + ct:comment("Completing function at code_navigation:|(), shouldn't include args"), + #{result := Completion3} = + els_client:completion(Uri, 27, 19, ?COMPLETION_TRIGGER_KIND_CHARACTER, <<":">>), + ?assertEqual( + [<<"function_a">>], + [ + InsertText + || #{ + kind := ?COMPLETION_ITEM_KIND_FUNCTION, + label := <<"function_a/0">>, + insertText := InsertText + } <- Completion3 + ] + ), + ct:comment("Completing function at code_navigation:funct|(), shouldn't include args"), + #{result := Completion4} = + els_client:completion(Uri, 28, 24, TriggerKind, <<>>), + ?assertEqual( + [<<"function_a">>], + [ + InsertText + || #{ + kind := ?COMPLETION_ITEM_KIND_FUNCTION, + insertText := InsertText, + label := <<"function_a/0">> + } <- Completion4 + ] + ). -spec handle_empty_lines(config()) -> ok. handle_empty_lines(Config) -> - Uri = ?config(code_navigation_uri, Config), - TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Uri = ?config(code_navigation_uri, Config), + TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, - #{ result := Completion1 - } = els_client:completion(Uri, 32, 1, TriggerKind, <<"">>), - ?assertEqual([], Completion1), + #{result := Completion1} = els_client:completion(Uri, 32, 1, TriggerKind, <<"">>), + ?assertEqual([], Completion1), - #{ result := Completion2 - } = els_client:completion(Uri, 32, 2, TriggerKind, <<":">>), - ?assertEqual([], Completion2), + #{result := Completion2} = els_client:completion(Uri, 32, 2, TriggerKind, <<":">>), + ?assertEqual([], Completion2), - ok. + ok. -spec handle_colon_inside_string(config()) -> ok. handle_colon_inside_string(Config) -> - Uri = ?config(code_navigation_uri, Config), - TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Uri = ?config(code_navigation_uri, Config), + TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, - #{ result := Completion - } = els_client:completion(Uri, 76, 10, TriggerKind, <<":">>), - ?assertEqual([], Completion), + #{result := Completion} = els_client:completion(Uri, 76, 10, TriggerKind, <<":">>), + ?assertEqual([], Completion), - ok. + ok. -spec macros(config()) -> ok. macros(Config) -> - Uri = ?config(code_navigation_uri, Config), - TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, - Expected = [ #{ kind => ?COMPLETION_ITEM_KIND_CONSTANT - , label => <<"INCLUDED_MACRO_A">> - , insertText => <<"INCLUDED_MACRO_A">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{} - } - , #{ kind => ?COMPLETION_ITEM_KIND_CONSTANT - , label => <<"MACRO_A">> - , insertText => <<"MACRO_A">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{} - } - , #{ kind => ?COMPLETION_ITEM_KIND_CONSTANT - , label => <<"MACRO_A/1">> - , insertText => <<"MACRO_A(${1:X})">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{} - } - , #{ kind => ?COMPLETION_ITEM_KIND_CONSTANT - , label => <<"macro_A">> - , insertText => <<"macro_A">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{} - } - , #{ kind => ?COMPLETION_ITEM_KIND_CONSTANT - , label => <<"MACRO_FOR_TRANSITIVE_INCLUSION">> - , insertText => <<"MACRO_FOR_TRANSITIVE_INCLUSION">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{} - } - ], + Uri = ?config(code_navigation_uri, Config), + TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected = [ + #{ + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"INCLUDED_MACRO_A">>, + insertText => <<"INCLUDED_MACRO_A">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"MACRO_A">>, + insertText => <<"MACRO_A">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"'MACRO A'">>, + insertText => <<"'MACRO A'">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"MACRO_A/1">>, + insertText => <<"MACRO_A(${1:X})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"macro_A">>, + insertText => <<"macro_A">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_CONSTANT, + label => <<"MACRO_FOR_TRANSITIVE_INCLUSION">>, + insertText => <<"MACRO_FOR_TRANSITIVE_INCLUSION">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{} + } + ], - #{result := Completion1} = - els_client:completion(Uri, 24, 1, TriggerKindChar, <<"?">>), - [?assert(lists:member(E, Completion1)) || E <- Expected], + #{result := Completion1} = + els_client:completion(Uri, 24, 1, TriggerKindChar, <<"?">>), + [?assert(lists:member(E, Completion1)) || E <- Expected], - #{result := Completion2} = - els_client:completion(Uri, 40, 5, TriggerKindInvoked, <<"">>), - [?assert(lists:member(E, Completion2)) || E <- Expected], + #{result := Completion2} = + els_client:completion(Uri, 40, 5, TriggerKindInvoked, <<"">>), + [?assert(lists:member(E, Completion2)) || E <- Expected], - ok. + ok. -spec only_exported_functions_after_colon(config()) -> ok. only_exported_functions_after_colon(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_uri, Config), - ExpectedCompletion = expected_exported_functions(), + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_uri, Config), + ExpectedCompletion = expected_exported_functions(), - #{result := Completion} = - els_client:completion(Uri, 32, 26, TriggerKind, <<"d">>), - ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), + #{result := Completion} = + els_client:completion(Uri, 32, 26, TriggerKind, <<"d">>), + ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), - ok. + ok. -spec records(config()) -> ok. records(Config) -> - Uri = ?config(code_navigation_uri, Config), - TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, - Expected = [ #{ kind => ?COMPLETION_ITEM_KIND_STRUCT - , label => <<"'?MODULE'">> - , data => #{} - } - , #{ kind => ?COMPLETION_ITEM_KIND_STRUCT - , label => <<"included_record_a">> - , data => #{} - } - , #{ kind => ?COMPLETION_ITEM_KIND_STRUCT - , label => <<"record_a">> - , data => #{} - } - , #{ kind => ?COMPLETION_ITEM_KIND_STRUCT - , label => <<"'PascalCaseRecord'">> - , data => #{} - } - ], + Uri = ?config(code_navigation_uri, Config), + TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected = [ + #{ + kind => ?COMPLETION_ITEM_KIND_STRUCT, + label => <<"'?MODULE'">>, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_STRUCT, + label => <<"included_record_a">>, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_STRUCT, + label => <<"record_a">>, + data => #{} + }, + #{ + kind => ?COMPLETION_ITEM_KIND_STRUCT, + label => <<"'PascalCaseRecord'">>, + data => #{} + } + ], - #{result := Completion1} = - els_client:completion(Uri, 24, 1, TriggerKindChar, <<"#">>), - ?assertEqual(lists:sort(Expected), lists:sort(Completion1)), + #{result := Completion1} = + els_client:completion(Uri, 24, 1, TriggerKindChar, <<"#">>), + ?assertEqual(lists:sort(Expected), lists:sort(Completion1)), - #{result := Completion2} = - els_client:completion(Uri, 23, 6, TriggerKindInvoked, <<"">>), - ?assertEqual(lists:sort(Expected), lists:sort(Completion2)), + #{result := Completion2} = + els_client:completion(Uri, 23, 6, TriggerKindInvoked, <<"">>), + ?assertEqual(lists:sort(Expected), lists:sort(Completion2)), - ok. + ok. -spec record_fields(config()) -> ok. record_fields(Config) -> - Uri = ?config(code_navigation_uri, Config), - TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, - TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, - Expected1 = [ #{ kind => ?COMPLETION_ITEM_KIND_FIELD - , label => <<"field_a">> - } - , #{ kind => ?COMPLETION_ITEM_KIND_FIELD - , label => <<"field_b">> - } - , #{ kind => ?COMPLETION_ITEM_KIND_FIELD - , label => <<"'Field C'">> - } - ], - #{result := Completion1} = - els_client:completion(Uri, 34, 19, TriggerKindChar, <<".">>), - ?assertEqual(lists:sort(Expected1), lists:sort(Completion1)), - - #{result := Completion2} = - els_client:completion(Uri, 34, 22, TriggerKindInvoked, <<"">>), - ?assertEqual(lists:sort(Expected1), lists:sort(Completion2)), - - Expected2 = [ #{ kind => ?COMPLETION_ITEM_KIND_FIELD - , label => <<"included_field_a">> - } - , #{ kind => ?COMPLETION_ITEM_KIND_FIELD - , label => <<"included_field_b">> - } - ], - #{result := Completion3} = - els_client:completion(Uri, 52, 60, TriggerKindChar, <<".">>), - ?assertEqual(lists:sort(Expected2), lists:sort(Completion3)), - - #{result := Completion4} = - els_client:completion(Uri, 52, 63, TriggerKindInvoked, <<"">>), - ?assertEqual(lists:sort(Expected2), lists:sort(Completion4)), - - ExpectedQuoted = [ #{ kind => ?COMPLETION_ITEM_KIND_FIELD - , label => <<"'Field #1'">> - } - , #{ kind => ?COMPLETION_ITEM_KIND_FIELD - , label => <<"'Field #2'">> - } - ], - #{result := Completion5} = - els_client:completion(Uri, 52, 90, TriggerKindChar, <<".">>), - ?assertEqual(lists:sort(ExpectedQuoted), lists:sort(Completion5)), - - ok. + Uri = ?config(code_navigation_uri, Config), + TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected1 = [ + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"field_a">> + }, + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"field_b">> + }, + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"'Field C'">> + } + ], + #{result := Completion1} = + els_client:completion(Uri, 34, 19, TriggerKindChar, <<".">>), + ?assertEqual(lists:sort(Expected1), lists:sort(Completion1)), + + #{result := Completion2} = + els_client:completion(Uri, 34, 22, TriggerKindInvoked, <<"">>), + ?assertEqual(lists:sort(Expected1), lists:sort(Completion2)), + + Expected2 = [ + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"included_field_a">> + }, + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"included_field_b">> + } + ], + #{result := Completion3} = + els_client:completion(Uri, 52, 60, TriggerKindChar, <<".">>), + ?assertEqual(lists:sort(Expected2), lists:sort(Completion3)), + + #{result := Completion4} = + els_client:completion(Uri, 52, 63, TriggerKindInvoked, <<"">>), + ?assertEqual(lists:sort(Expected2), lists:sort(Completion4)), + + ExpectedQuoted = [ + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"'Field #1'">> + }, + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"'Field #2'">> + } + ], + #{result := Completion5} = + els_client:completion(Uri, 52, 90, TriggerKindChar, <<".">>), + ?assertEqual(lists:sort(ExpectedQuoted), lists:sort(Completion5)), + + ok. + +-spec record_fields_inside_record(config()) -> ok. +record_fields_inside_record(Config) -> + Uri = ?config(completion_records_uri, Config), + TriggerKindChar = ?COMPLETION_TRIGGER_KIND_CHARACTER, + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + Expected1 = [ + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"field_a">>, + insertText => <<"field_a = ${1:FieldA}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + }, + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"field_b">>, + insertText => <<"field_b = ${1:FieldB}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + }, + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"'Field C'">>, + insertText => <<"'Field C' = ${1:Field C}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + } + ], + Expected2 = [ + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"field_x">>, + insertText => <<"field_x = ${1:FieldX}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + }, + #{ + kind => ?COMPLETION_ITEM_KIND_FIELD, + label => <<"field_y">>, + insertText => <<"field_y = ${1:FieldY}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + } + ], + %% Completion of #record_a in function head + %% #record_a{| + #{result := Completion1} = + els_client:completion(Uri, 6, 22, TriggerKindChar, <<"{">>), + + %% Completion of #record_a in function head + %% #record_a{field_a = a, | + #{result := Completion1} = + els_client:completion(Uri, 6, 35, TriggerKindChar, <<" ">>), + + %% Completion of #record_a in function head + %% #record_a{| + #{result := Completion1} = + els_client:completion(Uri, 6, 22, TriggerKindInvoked, <<>>), + + %% Completion of #record_a in function head + %% #record_a{fiel| + #{result := Completion1} = + els_client:completion(Uri, 6, 26, TriggerKindInvoked, <<>>), + + %% Completion of #record_b in function body + %% #record_b{fi| + #{result := Completion2} = + els_client:completion(Uri, 7, 16, TriggerKindInvoked, <<>>), + + %% Completion of #record_a inside #record_b + %% #record_b{field_x = #record_a{| + #{result := Completion1} = + els_client:completion(Uri, 7, 35, TriggerKindInvoked, <<>>), + + %% Completion of #record_a in function head + %% #record_b{field_x = #record_a{| + #{result := Completion1} = + els_client:completion(Uri, 7, 35, TriggerKindChar, <<"{">>), + + %% Completion of #record_b + %% #record_b{field_x = #record_a{}, | + #{result := Completion2} = + els_client:completion(Uri, 7, 38, TriggerKindChar, <<" ">>), + + %% Records in comments are ignored + #{result := Completion2} = + els_client:completion(Uri, 9, 16, TriggerKindInvoked, <<>>), + + %% No completion when trying to complete inside a tuple + #{result := []} = + els_client:completion(Uri, 10, 6, TriggerKindChar, <<"{">>), + #{result := []} = + els_client:completion(Uri, 10, 6, TriggerKindInvoked, <<>>), + + %% #record_b{field_y = y|} invoke completion + %% shouldn't trigger record fields completion + #{result := Completion3} = + els_client:completion(Uri, 9, 26, TriggerKindInvoked, <<>>), + ?assertNotEqual(lists:sort(Expected1), Completion3), + ?assertNotEqual(lists:sort(Expected2), Completion3), + ?assertEqual(lists:sort(Expected1), lists:sort(Completion1)), + ?assertEqual(lists:sort(Expected2), lists:sort(Completion2)), + ok. + +-spec record_fields_no_completion(config()) -> ok. +record_fields_no_completion(Config) -> + %% These should NOT trigger record fields completion + %% Instead expected behaviour is to trigger regular atom completion + Uri = ?config(completion_records_uri, Config), + TriggerKindInvoked = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := Result} = + els_client:completion(Uri, 14, 14, TriggerKindInvoked, <<>>), + + #{result := Result} = + els_client:completion(Uri, 18, 14, TriggerKindInvoked, <<>>), + + Labels = [Label || #{label := Label} <- Result], + ?assert(lists:member(<<"function_a/1">>, Labels)), + ?assert(lists:member(<<"function_b/1">>, Labels)), + ok. -spec types(config()) -> ok. types(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_uri, Config), - Expected = [ #{ insertText => <<"type_a()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"type_a/0">> - , data => #{ module => <<"code_navigation">> - , type => <<"type_a">> - , arity => 0 - } - } - , #{ insertText => <<"included_type_a()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"included_type_a/0">> - , data => #{ module => <<"code_navigation">> - , type => <<"included_type_a">> - , arity => 0 - } - } - , #{ insertText => <<"'INCLUDED_TYPE'(${1:T})">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"'INCLUDED_TYPE'/1">> - , data => #{ module => <<"code_navigation">> - , type => <<"INCLUDED_TYPE">> - , arity => 1 - } - } - , #{ insertText => <<"type_b()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"type_b/0">> - , data => #{ module => <<"code_navigation">> - , type => <<"type_b">> - , arity => 0 - } - } - ], - - DefaultCompletion = els_completion_provider:keywords() - ++ els_completion_provider:bifs(type_definition, false) - ++ els_snippets_server:snippets(), - - ct:comment("Types defined both in the current file and in includes"), - #{result := Completion1} = - els_client:completion(Uri, 55, 27, TriggerKind, <<"">>), - ?assertEqual( - lists:sort(Expected), - filter_completion(Completion1, DefaultCompletion)), + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_uri, Config), + Expected = [ + #{ + insertText => <<"type_a()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"type_a/0">>, + data => #{ + module => <<"code_navigation">>, + type => <<"type_a">>, + arity => 0 + } + }, + #{ + insertText => <<"included_type_a()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"included_type_a/0">>, + data => #{ + module => <<"code_navigation">>, + type => <<"included_type_a">>, + arity => 0 + } + }, + #{ + insertText => <<"'INCLUDED_TYPE'(${1:T})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"'INCLUDED_TYPE'/1">>, + data => #{ + module => <<"code_navigation">>, + type => <<"INCLUDED_TYPE">>, + arity => 1 + } + }, + #{ + insertText => <<"type_b()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"type_b/0">>, + data => #{ + module => <<"code_navigation">>, + type => <<"type_b">>, + arity => 0 + } + } + ], - ok. + DefaultCompletion = + els_completion_provider:bifs(type_definition, args) ++ + els_snippets_server:snippets(), + + ct:comment("Types defined both in the current file and in includes"), + #{result := Completion1} = + els_client:completion(Uri, 55, 27, TriggerKind, <<"">>), + ?assertEqual( + [], + Expected -- (Completion1 -- DefaultCompletion) + ), + ?assertEqual( + [], + (Completion1 -- DefaultCompletion) -- Expected + ), + ok. -spec types_export_list(config()) -> ok. types_export_list(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_types_uri, Config), - Expected = [ #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"type_b/0">> - , data => #{ module => <<"code_navigation_types">> - , type => <<"type_b">> - , arity => 0 - } - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"user_type_a/0">> - , data => #{ module => <<"code_navigation_types">> - , type => <<"user_type_a">> - , arity => 0 - } - } - , #{ insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM - , label => <<"user_type_b/0">> - , data => #{ module => <<"code_navigation_types">> - , type => <<"user_type_b">> - , arity => 0 - } - } - ], - ct:comment("Types in an export_type section is provided with arity"), - #{result := Completion} = - els_client:completion(Uri, 5, 19, TriggerKind, <<"">>), - ?assertEqual(Expected, Completion), - ok. + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_types_uri, Config), + Expected = [ + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"user_type_a/0">>, + data => #{ + module => <<"code_navigation_types">>, + type => <<"user_type_a">>, + arity => 0 + } + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"user_type_b/0">>, + data => #{ + module => <<"code_navigation_types">>, + type => <<"user_type_b">>, + arity => 0 + } + }, + #{ + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"user_type_d/0">>, + data => #{ + module => <<"code_navigation_types">>, + type => <<"user_type_d">>, + arity => 0 + } + } + ], + ct:comment("Types in an export_type section is provided with arity"), + #{result := Completion} = + els_client:completion(Uri, 5, 19, TriggerKind, <<"">>), + ?assertEqual(Expected, Completion), + ok. + +-spec types_context(config()) -> ok. +types_context(Config) -> + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_types_uri, Config), + UserTypes = + [ + #{ + data => + #{ + arity => 0, + module => <<"code_navigation_types">>, + type => <<"opaque_type_a">> + }, + insertText => <<"opaque_type_a()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"opaque_type_a/0">> + }, + #{ + data => + #{ + arity => 0, + module => <<"code_navigation_types">>, + type => <<"type_a">> + }, + insertText => <<"type_a()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"type_a/0">> + }, + #{ + data => + #{ + arity => 0, + module => <<"code_navigation_types">>, + type => <<"type_b">> + }, + insertText => <<"type_b()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"type_b/0">> + }, + #{ + data => + #{ + arity => 0, + module => <<"code_navigation_types">>, + type => <<"user_type_a">> + }, + insertText => <<"user_type_a()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"user_type_a/0">> + }, + #{ + data => + #{ + arity => 0, + module => <<"code_navigation_types">>, + type => <<"user_type_b">> + }, + insertText => <<"user_type_b()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"user_type_b/0">> + }, + #{ + data => + #{ + arity => 0, + module => <<"code_navigation_types">>, + type => <<"user_type_c">> + }, + insertText => <<"user_type_c()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"user_type_c/0">> + }, + #{ + data => + #{ + arity => 0, + module => <<"code_navigation_types">>, + type => <<"user_type_d">> + }, + insertText => <<"user_type_d()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + kind => ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label => <<"user_type_d/0">> + } + ], + Bifs = els_completion_provider:bifs(type_definition, args), + Expected1 = UserTypes ++ Bifs, + + ct:comment("Completing a type inside a -type declaration should return types"), + #{result := Completion1} = + els_client:completion(Uri, 16, 27, TriggerKind, <<>>), + ?assertEqual([], Completion1 -- Expected1), + ?assertEqual([], Expected1 -- Completion1), + + ct:comment("Completing a map value inside a -type should return types"), + #{result := Completion2} = + els_client:completion(Uri, 21, 43, TriggerKind, <<>>), + ?assertEqual([], Completion2 -- Expected1), + ?assertEqual([], Expected1 -- Completion2), + + ct:comment("Completing a type in a record definition should return types"), + #{result := Completion1} = + els_client:completion(Uri, 18, 38, TriggerKind, <<>>), + + ct:comment("Completing a record value should not return types"), + #{result := Completion4} = + els_client:completion(Uri, 18, 23, TriggerKind, <<>>), + ?assertNotEqual([], Completion4), + ?assertEqual(UserTypes, UserTypes -- Completion4), + + ct:comment("Completing a type in a spec return types"), + #{result := Completion5} = + els_client:completion(Uri, 24, 31, TriggerKind, <<>>), + #{result := Completion5} = + els_client:completion(Uri, 24, 54, TriggerKind, <<>>), + ?assertEqual([], Completion5 -- Expected1), + ?assertEqual([], Expected1 -- Completion5), + + ct:comment("Completing a value in function head should not return types"), + #{result := Completion6} = + els_client:completion(Uri, 25, 15, TriggerKind, <<>>), + ?assertNotEqual([], Completion6), + ?assertEqual(UserTypes, UserTypes -- Completion6), + + ct:comment("Completing a value in function body should not return types"), + #{result := Completion6} = + els_client:completion(Uri, 26, 8, TriggerKind, <<>>), + ?assertNotEqual([], Completion6), + ?assertEqual(UserTypes, UserTypes -- Completion6), + ok. + +-spec types_no_args(config()) -> ok. +types_no_args(Config) -> + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_types_uri, Config), + ct:comment("Completing function at typ|e(), should include args"), + #{result := Completion1} = + els_client:completion(Uri, 28, 27, TriggerKind, <<>>), + ?assertEqual( + [<<"type_a()">>], + [ + InsertText + || #{ + kind := ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label := <<"type_a/0">>, + insertText := InsertText + } <- Completion1 + ] + ), + ct:comment("Completing function at type|(), shouldn't include args"), + #{result := Completion2} = + els_client:completion(Uri, 28, 28, TriggerKind, <<>>), + ?assertEqual( + [<<"type_a">>], + [ + InsertText + || #{ + kind := ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label := <<"type_a/0">>, + insertText := InsertText + } <- Completion2 + ] + ), + ct:comment("Completing function at code_navigation:|(), shouldn't include args"), + #{result := Completion3} = + els_client:completion(Uri, 28, 49, ?COMPLETION_TRIGGER_KIND_CHARACTER, <<":">>), + ?assertEqual( + [<<"type_a">>], + [ + InsertText + || #{ + kind := ?COMPLETION_ITEM_KIND_TYPE_PARAM, + label := <<"type_a/0">>, + insertText := InsertText + } <- Completion3 + ] + ). -spec variables(config()) -> ok. variables(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(code_navigation_extra_uri, Config), - Expected = [ #{ kind => ?COMPLETION_ITEM_KIND_VARIABLE - , label => <<"_Config">> - }], + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(code_navigation_extra_uri, Config), + Expected = [ + #{ + kind => ?COMPLETION_ITEM_KIND_VARIABLE, + label => <<"_Config">> + }, + #{ + kind => ?COMPLETION_ITEM_KIND_VARIABLE, + label => <<"Arg1">> + }, + #{ + kind => ?COMPLETION_ITEM_KIND_VARIABLE, + label => <<"Arg2">> + } + ], - #{result := Completion} = - els_client:completion(Uri, 5, 8, TriggerKind, <<"">>), - ?assertEqual(lists:sort(Expected), lists:sort(Completion)), + #{result := Completion} = + els_client:completion(Uri, 5, 8, TriggerKind, <<"">>), + ?assertEqual(lists:sort(Expected), lists:sort(Completion)), - ok. + ok. expected_exported_functions() -> - [ #{ label => <<"do/1">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertText => <<"do(${1:_Config})">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do">> - , arity => 1 - } - } - , #{ label => <<"do_2/0">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertText => <<"do_2()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do_2">> - , arity => 0 - } - } - , #{ label => <<"'DO_LOUDER'/0">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertText => <<"'DO_LOUDER'()">> - , insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET - , data => #{ module => <<"code_navigation_extra">> - , function => <<"DO_LOUDER">> - , arity => 0 - } - } - ]. + [ + #{ + label => <<"do/1">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertText => <<"do(${1:_Config})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do">>, + arity => 1 + } + }, + #{ + label => <<"do_2/0">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertText => <<"do_2()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do_2">>, + arity => 0 + } + }, + #{ + label => <<"'DO_LOUDER'/0">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertText => <<"'DO_LOUDER'()">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"DO_LOUDER">>, + arity => 0 + } + }, + #{ + label => <<"function_a/2">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertText => <<"function_a(${1:Arg1}, ${2:Arg2})">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"function_a">>, + arity => 2 + } + } + ]. expected_exported_functions_arity_only() -> - [ #{ label => <<"do/1">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do">> - , arity => 1 - } - } - , #{ label => <<"do_2/0">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => #{ module => <<"code_navigation_extra">> - , function => <<"do_2">> - , arity => 0 - } - } - , #{ label => <<"'DO_LOUDER'/0">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => #{ module => <<"code_navigation_extra">> - , function => <<"DO_LOUDER">> - , arity => 0 - } - } - ]. + [ + #{ + label => <<"do/1">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do">>, + arity => 1 + } + }, + #{ + label => <<"do_2/0">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"do_2">>, + arity => 0 + } + }, + #{ + label => <<"'DO_LOUDER'/0">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"DO_LOUDER">>, + arity => 0 + } + }, + #{ + label => <<"function_a/2">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{ + module => <<"code_navigation_extra">>, + function => <<"function_a">>, + arity => 2 + } + } + ]. %% [#790] Complete only with arity for remote applications -spec remote_fun(config()) -> ok. remote_fun(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, - Uri = ?config(completion_caller_uri, Config), - ExpectedCompletion = [ #{ label => <<"complete_1/0">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => #{ module => <<"completion">> - , function => <<"complete_1">> - , arity => 0 - } - } - , #{ label => <<"complete_2/0">> - , kind => ?COMPLETION_ITEM_KIND_FUNCTION - , insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT - , data => #{ module => <<"completion">> - , function => <<"complete_2">> - , arity => 0 - } - } - ], - #{result := Completion} = - els_client:completion(Uri, 6, 19, TriggerKind, <<":">>), - ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), - - ok. + TriggerKind = ?COMPLETION_TRIGGER_KIND_CHARACTER, + Uri = ?config(completion_caller_uri, Config), + ExpectedCompletion = [ + #{ + label => <<"complete_1/0">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{ + module => <<"completion">>, + function => <<"complete_1">>, + arity => 0 + } + }, + #{ + label => <<"complete_2/0">>, + kind => ?COMPLETION_ITEM_KIND_FUNCTION, + insertTextFormat => ?INSERT_TEXT_FORMAT_PLAIN_TEXT, + data => #{ + module => <<"completion">>, + function => <<"complete_2">>, + arity => 0 + } + } + ], + #{result := Completion} = + els_client:completion(Uri, 6, 19, TriggerKind, <<":">>), + ?assertEqual(lists:sort(ExpectedCompletion), lists:sort(Completion)), + + ok. -spec snippets(config()) -> ok. snippets(Config) -> - TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - Uri = ?config(completion_snippets_uri, Config), - #{result := Result} = els_client:completion(Uri, 3, 6, TriggerKind, <<"">>), - Completions = [C || #{kind := ?COMPLETION_ITEM_KIND_SNIPPET} = C <- Result], - SnippetsDir = els_snippets_server:builtin_snippets_dir(), - Snippets = filelib:wildcard("*", SnippetsDir), - CustomSnippetsDir = els_snippets_server:custom_snippets_dir(), - CustomSnippets = filelib:wildcard("*", CustomSnippetsDir), - Expected = lists:usort(Snippets ++ CustomSnippets), - ?assertEqual(length(Expected), length(Completions)). + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + Uri = ?config(completion_snippets_uri, Config), + #{result := Result} = els_client:completion(Uri, 3, 6, TriggerKind, <<"">>), + Completions = [C || #{kind := ?COMPLETION_ITEM_KIND_SNIPPET} = C <- Result], + SnippetsDir = els_snippets_server:builtin_snippets_dir(), + Snippets = filelib:wildcard("*", SnippetsDir), + CustomSnippetsDir = els_snippets_server:custom_snippets_dir(), + CustomSnippets = filelib:wildcard("*", CustomSnippetsDir), + Expected = lists:usort(Snippets ++ CustomSnippets), + ?assertEqual(length(Expected), length(Completions)). filter_completion(Completion, ToFilter) -> - CompletionSet = ordsets:from_list(Completion), - FilterSet = ordsets:from_list(ToFilter), - ?assertEqual(FilterSet, ordsets:intersection(CompletionSet, FilterSet)), - ordsets:to_list(ordsets:subtract(CompletionSet, FilterSet)). + CompletionSet = ordsets:from_list(Completion), + FilterSet = ordsets:from_list(ToFilter), + ?assertEqual(FilterSet, ordsets:intersection(CompletionSet, FilterSet)), + ordsets:to_list(ordsets:subtract(CompletionSet, FilterSet)). -spec resolve_application_local(config()) -> ok. resolve_application_local(Config) -> - Uri = ?config(completion_resolve_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 14, 5, CompletionKind, <<"">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_FUNCTION, <<"call_1/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => call_markdown( - <<"call_1">>, - <<"Call me maybe">>) - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 14, 5, CompletionKind, <<"">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_FUNCTION, + <<"call_1/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => call_markdown( + <<"call_1">>, + <<"Call me maybe">> + ) + } + }, + ?assertEqual(Expected, Result). -spec resolve_application_unexported_local(config()) -> ok. resolve_application_unexported_local(Config) -> - Uri = ?config(completion_resolve_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 14, 5, CompletionKind, <<"">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_FUNCTION, <<"call_2/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => call_markdown( - <<"call_2">>, - <<"Call me sometime">>) - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 14, 5, CompletionKind, <<"">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_FUNCTION, + <<"call_2/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => call_markdown( + <<"call_2">>, + <<"Call me sometime">> + ) + } + }, + ?assertEqual(Expected, Result). -spec resolve_application_remote_self(config()) -> ok. resolve_application_remote_self(Config) -> - Uri = ?config(completion_resolve_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 13, 23, CompletionKind, <<":">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_FUNCTION, <<"call_1/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => call_markdown( - <<"call_1">>, - <<"Call me maybe">>) - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 13, 23, CompletionKind, <<":">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_FUNCTION, + <<"call_1/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => call_markdown( + <<"call_1">>, + <<"Call me maybe">> + ) + } + }, + ?assertEqual(Expected, Result). -spec resolve_application_remote_external(config()) -> ok. resolve_application_remote_external(Config) -> - Uri = ?config(completion_resolve_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 15, 25, CompletionKind, <<":">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_FUNCTION, <<"call_1/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => call_markdown( - <<"completion_resolve_2">>, - <<"call_1">>, - <<"I just met you">>) - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 15, 25, CompletionKind, <<":">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_FUNCTION, + <<"call_1/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => call_markdown( + <<"call_1">>, + <<"I just met you">> + ) + } + }, + ?assertEqual(Expected, Result). -spec resolve_application_remote_otp(config()) -> ok. resolve_application_remote_otp(Config) -> - Uri = ?config(completion_resolve_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 16, 8, CompletionKind, <<":">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_FUNCTION, <<"write/2">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Value = case has_eep48(file) of - true -> <<"```erlang\nwrite(IoDevice, Bytes) -> ok | {error, " - "Reason}\nwhen\n IoDevice :: io_device() | atom(),\n Bytes ::" - " iodata(),\n Reason :: posix() | badarg | terminated.\n```\n\n" - "---\n\nWrites `Bytes` to the file referenced by `IoDevice`\\. " - "This function is the only way to write to a file opened in `raw`" - " mode \\(although it works for normally opened files too\\)\\. " - "Returns `ok` if successful, and `{error, Reason}` otherwise\\." - "\n\nIf the file is opened with `encoding` set to something else " - "than `latin1`, each byte written can result in many bytes being " - "written to the file, as the byte range 0\\.\\.255 can represent " - "anything between one and four bytes depending on value and UTF " - "encoding type\\.\n\nTypical error reasons:\n\n* **`ebadf`** \n" - " The file is not opened for writing\\.\n\n* **`enospc`** \n" - " No space is left on the device\\.\n">>; - false -> <<"## file:write/2\n\n---\n\n```erlang\n\n write(File, " - "Bytes) when is_pid(File) orelse is_atom(File)\n\n write(#file_" - "descriptor{module = Module} = Handle, Bytes) \n\n write(_, _) " - "\n\n```\n\n```erlang\n-spec write(IoDevice, Bytes) -> ok | " - "{error, Reason} when\n IoDevice :: io_device() | atom()," - "\n Bytes :: iodata(),\n Reason :: posix() | " - "badarg | terminated.\n```">> - end, - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => Value - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 16, 8, CompletionKind, <<":">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_FUNCTION, + <<"write/2">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + OtpRelease = list_to_integer(erlang:system_info(otp_release)), + Value = + case has_eep48(file) of + true when OtpRelease >= 27 -> + << + "```erlang\n" + "write(IoDevice, Bytes) -> ok | {error, Reason} when\n" + " IoDevice :: io_device() | io:device(),\n" + " Bytes :: iodata(),\n" + " Reason :: posix() | badarg | terminated.\n" + "```\n\n---\n\n```" + "erlang\nWrites `Bytes` to the file referenced by `IoDevice`." + " This function is the only\nway to write to a file opened in" + " `raw` mode (although it works for normally\nopened files" + " too). Returns `ok` if successful, and `{error, Reason}`" + " otherwise.\n\nIf the file is opened with `encoding` set to" + " something else than `latin1`, each\nbyte written can result" + " in many bytes being written to the file, as the byte\nrange" + " 0..255 can represent anything between one and four bytes" + " depending on\nvalue and UTF encoding type. If you want to" + " write `t:unicode:chardata/0` to the\n`IoDevice` you should" + " use `io:put_chars/2` instead.\n\nTypical error reasons:\n\n" + "- **`ebadf`** - The file is not opened for writing.\n\n" + "- **`enospc`** - No space is left on the device.\n```\n" + >>; + true when OtpRelease == 26 -> + << + "```erlang\nwrite(IoDevice, Bytes) -> ok | {error, " + "Reason}\nwhen\n IoDevice :: io_device() | io:device(),\n" + " Bytes :: iodata()," + "\n Reason :: posix() | badarg | terminated.\n```\n\n" + "---\n\n" + "Writes `Bytes` to the file referenced by `IoDevice`\\." + " This function is the only way to write to a file opened in" + " `raw` mode \\(although it works for normally opened files" + " too\\)\\. Returns `ok` if successful, and" + " `{error, Reason}` otherwise\\.\n\nIf the file is opened" + " with `encoding` set to something else than `latin1`," + " each byte written can result in many bytes being written" + " to the file, as the byte range 0\\.\\.255 can represent" + " anything between one and four bytes depending on value" + " and UTF encoding type\\. If you want to write" + " [`unicode:chardata()`](https://erlang.org/doc/man/unicode" + ".html#type-chardata) to the `IoDevice` you should use" + " [`io:put_chars/2`](https://erlang.org/doc/man/io.html" + "#put_chars-2) instead\\.\n\nTypical error reasons:\n\n" + "* **`ebadf`** \n The file is not opened for writing\\.\n\n" + "* **`enospc`** \n No space is left on the device\\.\n" + >>; + true -> + << + "```erlang\nwrite(IoDevice, Bytes) -> ok | {error, " + "Reason}\nwhen\n IoDevice :: io_device() | atom(),\n Bytes ::" + " iodata(),\n Reason :: posix() | badarg | terminated.\n```\n\n" + "---\n\nWrites `Bytes` to the file referenced by `IoDevice`\\. " + "This function is the only way to write to a file opened in `raw`" + " mode \\(although it works for normally opened files too\\)\\. " + "Returns `ok` if successful, and `{error, Reason}` otherwise\\." + "\n\nIf the file is opened with `encoding` set to something else " + "than `latin1`, each byte written can result in many bytes being " + "written to the file, as the byte range 0\\.\\.255 can represent " + "anything between one and four bytes depending on value and UTF " + "encoding type\\.\n\nTypical error reasons:\n\n* **`ebadf`** \n" + " The file is not opened for writing\\.\n\n* **`enospc`** \n" + " No space is left on the device\\.\n" + >>; + false -> + << + "## file:write/2\n\n---\n\n```erlang\n\n write(File, " + "Bytes) when is_pid(File) orelse is_atom(File)\n\n write(#file_" + "descriptor{module = Module} = Handle, Bytes) \n\n write(_, _) " + "\n\n```\n\n```erlang\n-spec write(IoDevice, Bytes) -> ok | " + "{error, Reason} when\n IoDevice :: io_device() | atom()," + "\n Bytes :: iodata(),\n Reason :: posix() | " + "badarg | terminated.\n```" + >> + end, + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result). call_markdown(F, Doc) -> - call_markdown(<<"completion_resolve">>, F, Doc). -call_markdown(M, F, Doc) -> - case has_eep48_edoc() of - true -> - <<"```erlang\n", - F/binary, "() -> ok.\n" + <<"```erlang\n", F/binary, + "() -> ok.\n" "```\n\n" - "---\n\n", - Doc/binary, "\n">>; - false -> - <<"## ", M/binary, ":", F/binary, "/0\n\n" - "---\n\n" - "```erlang\n" - "-spec ", F/binary, "() -> 'ok'.\n" - "```\n\n", - Doc/binary, "\n\n">> - end. + "---\n\n", Doc/binary, "\n">>. -spec resolve_type_application_local(config()) -> ok. resolve_type_application_local(Config) -> - Uri = ?config(completion_resolve_type_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 14, 16, CompletionKind, <<"">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_TYPE_PARAM, <<"mytype/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-type mytype() :: " - "completion_resolve_type:myopaque().\n```" - "\n\n---\n\nThis is my type\n">>; - false -> <<"```erlang\n-type mytype() :: " - "completion_resolve_type:myopaque().\n```">> - end, - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => Value - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_type_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 14, 16, CompletionKind, <<"">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_TYPE_PARAM, + <<"mytype/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + Value = + << + "```erlang\n-type mytype() :: " + "completion_resolve_type:myopaque().\n```" + "\n\n---\n\nThis is my type\n" + >>, + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result). -spec resolve_opaque_application_local(config()) -> ok. resolve_opaque_application_local(Config) -> - Uri = ?config(completion_resolve_type_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 14, 17, CompletionKind, <<"">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_TYPE_PARAM, <<"myopaque/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-opaque myopaque() \n```\n\n---\n\n" - "This is my opaque\n">>; - false -> <<"```erlang\n" - "-opaque myopaque() :: term().\n```">> - end, - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => Value - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_type_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 14, 17, CompletionKind, <<"">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_TYPE_PARAM, + <<"myopaque/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + Value = + << + "```erlang\n-opaque myopaque() \n```\n\n---\n\n" + "This is my opaque\n" + >>, + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result). -spec resolve_opaque_application_remote_self(config()) -> ok. resolve_opaque_application_remote_self(Config) -> - Uri = ?config(completion_resolve_type_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 14, 48, CompletionKind, <<":">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_TYPE_PARAM, <<"myopaque/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-opaque myopaque() \n```\n\n---\n\n" - "This is my opaque\n">>; - false -> <<"```erlang\n" - "-opaque myopaque() :: term().\n" - "```">> - end, - - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => Value - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_type_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 14, 48, CompletionKind, <<":">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_TYPE_PARAM, + <<"myopaque/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + + Value = + << + "```erlang\n-opaque myopaque() \n```\n\n---\n\n" + "This is my opaque\n" + >>, + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result). -spec resolve_type_application_remote_external(config()) -> ok. resolve_type_application_remote_external(Config) -> - Uri = ?config(completion_resolve_type_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 15, 40, CompletionKind, <<":">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_TYPE_PARAM, <<"mytype/1">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-type mytype(T) :: [T].\n```\n\n---\n\n" - "Hello\n">>; - false -> <<"```erlang\n-type mytype(T) :: [T].\n```">> - end, - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => Value - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_type_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 15, 40, CompletionKind, <<":">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_TYPE_PARAM, + <<"mytype/1">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + Value = + << + "```erlang\n-type mytype(T) :: [T].\n```\n\n---\n\n" + "Hello\n" + >>, + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result). -spec resolve_opaque_application_remote_external(config()) -> ok. resolve_opaque_application_remote_external(Config) -> - Uri = ?config(completion_resolve_type_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 15, 40, CompletionKind, <<":">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_TYPE_PARAM, <<"myopaque/1">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-opaque myopaque(T) \n```\n\n---\n\n" - "Is there anybody in there\n">>; - false -> <<"```erlang\n-opaque myopaque(T) :: [T].\n```">> - end, - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => Value - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_type_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 15, 40, CompletionKind, <<":">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_TYPE_PARAM, + <<"myopaque/1">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + Value = + << + "```erlang\n-opaque myopaque(T) \n```\n\n---\n\n" + "Is there anybody in there\n" + >>, + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result). -spec resolve_type_application_remote_otp(config()) -> ok. resolve_type_application_remote_otp(Config) -> - Uri = ?config(completion_resolve_type_uri, Config), - CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, - #{result := CompletionItems} = - els_client:completion(Uri, 17, 8, CompletionKind, <<":">>), - [Selected] = select_completionitems(CompletionItems, - ?COMPLETION_ITEM_KIND_TYPE_PARAM, <<"name_all/0">>), - #{result := Result} = els_client:completionitem_resolve(Selected), - Value = case has_eep48(file) of - true -> <<"```erlang\n-type name_all() ::\n string() |" - " atom() | deep_list() | (RawFilename :: binary()).\n" - "```\n\n---\n\nIf VM is in Unicode filename mode, " - "characters are allowed to be \\> 255\\. `RawFilename`" - " is a filename not subject to Unicode translation, " - "meaning that it can contain characters not conforming" - " to the Unicode encoding expected from the file system" - " \\(that is, non\\-UTF\\-8 characters although the VM is" - " started in Unicode filename mode\\)\\. Null characters " - "\\(integer value zero\\) are *not* allowed in filenames " - "\\(not even at the end\\)\\.\n">>; - false -> <<"```erlang\n-type name_all() :: " - "string() | atom() | deep_list() | " - "(RawFilename :: binary()).\n```">> - end, - Expected = Selected#{ documentation => - #{ kind => <<"markdown">> - , value => Value - } - }, - ?assertEqual(Expected, Result). + Uri = ?config(completion_resolve_type_uri, Config), + CompletionKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + #{result := CompletionItems} = + els_client:completion(Uri, 17, 8, CompletionKind, <<":">>), + [Selected] = select_completionitems( + CompletionItems, + ?COMPLETION_ITEM_KIND_TYPE_PARAM, + <<"name_all/0">> + ), + #{result := Result} = els_client:completionitem_resolve(Selected), + OtpRelease = list_to_integer(erlang:system_info(otp_release)), + Value = + case has_eep48(file) of + true when OtpRelease >= 27 -> + << + "```erlang\nname_all()\n```\n\n---\n\n" + "```erlang\nA file name used as input into `m:file` API" + " functions.\n\nIf VM is in Unicode filename mode," + " characters are allowed to be > 255.\n`RawFilename`" + " is a filename not subject to Unicode translation," + " meaning that it\ncan contain characters not conforming" + " to the Unicode encoding expected from the\nfile system" + " (that is, non-UTF-8 characters although the VM is" + " started in Unicode\nfilename mode). Null characters" + " (integer value zero) are _not_ allowed in\nfilenames" + " (not even at the end).\n```\n" + >>; + true -> + << + "```erlang\n-type name_all() ::\n string() |" + " atom() | deep_list() | (RawFilename :: binary()).\n" + "```\n\n---\n\nIf VM is in Unicode filename mode, " + "characters are allowed to be \\> 255\\. `RawFilename`" + " is a filename not subject to Unicode translation, " + "meaning that it can contain characters not conforming" + " to the Unicode encoding expected from the file system" + " \\(that is, non\\-UTF\\-8 characters although the VM is" + " started in Unicode filename mode\\)\\. Null characters " + "\\(integer value zero\\) are *not* allowed in filenames " + "\\(not even at the end\\)\\.\n" + >>; + false -> + << + "```erlang\n-type name_all() :: " + "string() | atom() | deep_list() | " + "(RawFilename :: binary()).\n```" + >> + end, + Expected = Selected#{ + documentation => + #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result). + +%% Issue #1387 +completion_request_fails(Config) -> + Uri = ?config(code_completion_fail_uri, Config), + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + %% Complete at -module(s|). + #{result := Result1} = els_client:completion(Uri, 1, 10, TriggerKind, <<>>), + ?assertNotEqual(null, Result1), + %% Complete at a|, file doesn't end with a newline! + #{result := Result2} = els_client:completion(Uri, 2, 2, TriggerKind, <<>>), + ?assertNotEqual(null, Result2), + ok. select_completionitems(CompletionItems, Kind, Label) -> - [CI || #{ kind := K , label := L - } = CI <- CompletionItems, L =:= Label, K =:= Kind]. + [CI || #{kind := K, label := L} = CI <- CompletionItems, L =:= Label, K =:= Kind]. -has_eep48_edoc() -> - list_to_integer(erlang:system_info(otp_release)) >= 24. has_eep48(Module) -> - case catch code:get_doc(Module) of - {ok, _} -> true; - _ -> false - end. \ No newline at end of file + case catch code:get_doc(Module) of + {ok, {docs_v1, _, erlang, _, _, _, Docs}} -> + lists:any( + fun + ({_, _, _, Doc, _}) when is_map(Doc) -> true; + ({_, _, _, _, _}) -> false + end, + Docs + ); + _ -> + false + end. + +keywords() -> + els_completion_provider:keywords(test, test). + +list_comprehension(Config) -> + Uri = ?config(completion_more_uri, Config), + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + %% Complete at [| + #{result := Result1} = els_client:completion(Uri, 4, 6, TriggerKind, <<>>), + ?assertEqual( + [ + #{ + label => <<"[Expr || Elem <- List]">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertText => <<"${3:Expr} || ${2:Elem} <- ${1:List}]">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + } + ], + Result1 + ), + %% Complete at [|] + #{result := Result2} = + els_client:completion(Uri, 5, 6, TriggerKind, <<>>), + ?assertEqual( + [ + #{ + label => <<"[Expr || Elem <- List]">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertText => <<"${3:Expr} || ${2:Elem} <- ${1:List}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + } + ], + Result2 + ). + +map_comprehension(Config) -> + Uri = ?config(completion_more_uri, Config), + TriggerKind = ?COMPLETION_TRIGGER_KIND_INVOKED, + %% Complete at #{| + #{result := Result1} = els_client:completion(Uri, 8, 7, TriggerKind, <<>>), + ?assertEqual( + [ + #{ + label => <<"#{K => V || K := V <- Map}">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertText => + <<"${4:K} => ${5:V} || ${2:K} => ${3:V} <- ${1:Map}}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + } + ], + Result1 + ), + %% Complete at #{|} + #{result := Result2} = + els_client:completion(Uri, 9, 7, TriggerKind, <<>>), + ?assertEqual( + [ + #{ + label => <<"#{K => V || K := V <- Map}">>, + kind => ?COMPLETION_ITEM_KIND_KEYWORD, + insertText => + <<"${4:K} => ${5:V} || ${2:K} => ${3:V} <- ${1:Map}">>, + insertTextFormat => ?INSERT_TEXT_FORMAT_SNIPPET + } + ], + Result2 + ). diff --git a/apps/els_lsp/test/els_definition_SUITE.erl b/apps/els_lsp/test/els_definition_SUITE.erl index a498b8176..022a33170 100644 --- a/apps/els_lsp/test/els_definition_SUITE.erl +++ b/apps/els_lsp/test/els_definition_SUITE.erl @@ -4,52 +4,60 @@ -module(els_definition_SUITE). %% CT Callbacks --export([ all/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , suite/0 - ]). +-export([ + all/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + suite/0 +]). %% Test cases --export([ application_local/1 - , application_remote/1 - , atom/1 - , behaviour/1 - , definition_after_closing/1 - , duplicate_definition/1 - , export_entry/1 - , fun_local/1 - , fun_remote/1 - , import_entry/1 - , module_import_entry/1 - , include/1 - , include_lib/1 - , macro/1 - , macro_lowercase/1 - , macro_included/1 - , macro_with_args/1 - , macro_with_args_included/1 - , macro_with_implicit_args/1 - , parse_transform/1 - , record_access/1 - , record_access_included/1 - , record_access_macro_name/1 - , record_expr/1 - , record_expr_included/1 - , record_expr_macro_name/1 - , record_field/1 - , record_field_included/1 - , record_type_macro_name/1 - , type_application_remote/1 - , type_application_undefined/1 - , type_application_user/1 - , type_export_entry/1 - , variable/1 - , opaque_application_remote/1 - , opaque_application_user/1 - ]). +-export([ + application_local/1, + application_remote/1, + atom/1, + behaviour/1, + behaviour_callback_definition/1, + definition_after_closing/1, + duplicate_definition/1, + export_entry/1, + fun_local/1, + fun_remote/1, + import_entry/1, + module_import_entry/1, + include/1, + include_lib/1, + macro/1, + macro_lowercase/1, + macro_included/1, + macro_with_args/1, + macro_with_args_included/1, + macro_with_implicit_args/1, + multiple_atom_instances_same_mod/1, + multiple_atom_instances_diff_mod/1, + parse_transform/1, + record_access/1, + record_access_included/1, + record_access_macro_name/1, + record_expr/1, + record_expr_included/1, + record_expr_macro_name/1, + record_field/1, + record_field_included/1, + record_type_macro_name/1, + testcase/1, + type_application_remote/1, + type_application_undefined/1, + type_application_user/1, + type_export_entry/1, + variable/1, + variable_list_comp/1, + opaque_application_remote/1, + opaque_application_user/1, + parse_incomplete/1 +]). %%============================================================================== %% Includes @@ -67,436 +75,692 @@ %%============================================================================== -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. %%============================================================================== %% Testcases %%============================================================================== -spec application_local(config()) -> ok. application_local(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 22, 5), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {25, 1}, to => {25, 11}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 22, 5), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {25, 1}, to => {25, 11}}), + Range + ), + ok. -spec application_remote(config()) -> ok. application_remote(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 32, 13), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {5, 1}, to => {5, 3}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 32, 13), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {5, 1}, to => {5, 3}}), + Range + ), + ok. -spec atom(config()) -> ok. atom(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def0 = els_client:definition(Uri, 84, 20), - Def1 = els_client:definition(Uri, 85, 20), - Def2 = els_client:definition(Uri, 86, 20), - Def3 = els_client:definition(Uri, 85, 27), - #{result := #{range := Range0, uri := DefUri0}} = Def0, - #{result := #{range := Range1, uri := DefUri1}} = Def1, - #{result := #{range := Range2, uri := DefUri2}} = Def2, - #{result := #{range := Range3, uri := DefUri3}} = Def3, - ?assertEqual(?config(code_navigation_types_uri, Config), DefUri0), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 30}}) - , Range0), - ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri1), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 30}}) - , Range1), - ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri2), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 30}}) - , Range2), - ?assertEqual(?config('Code.Navigation.Elixirish_uri', Config), DefUri3), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 36}}) - , Range3), - ok. + Uri = ?config(code_navigation_uri, Config), + Def0 = els_client:definition(Uri, 84, 20), + Def1 = els_client:definition(Uri, 85, 20), + Def2 = els_client:definition(Uri, 86, 20), + Def3 = els_client:definition(Uri, 85, 27), + #{result := [#{range := Range0, uri := DefUri0}]} = Def0, + #{result := [#{range := Range1, uri := DefUri1}, #{range := Range12, uri := DefUri12}]} = + Def1, + #{result := [#{range := Range2, uri := DefUri2}, #{range := Range22, uri := DefUri22}]} = + Def2, + #{result := [#{range := Range3, uri := DefUri3}]} = Def3, + ?assertEqual(?config(code_navigation_types_uri, Config), DefUri0), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 30}}), + Range0 + ), + ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri12), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 30}}), + Range12 + ), + ?assertEqual(Uri, DefUri1), + ?assertEqual( + els_protocol:range(#{from => {132, 1}, to => {132, 22}}), + Range1 + ), + ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri22), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 30}}), + Range22 + ), + ?assertEqual(Uri, DefUri2), + ?assertEqual( + els_protocol:range(#{from => {132, 1}, to => {132, 22}}), + Range2 + ), + ?assertEqual(?config('Code.Navigation.Elixirish_uri', Config), DefUri3), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 36}}), + Range3 + ), + ok. -spec behaviour(config()) -> ok. behaviour(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 3, 16), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(behaviour_a_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 20}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 3, 16), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(behaviour_a_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 20}}), + Range + ), + ok. + +-spec behaviour_callback_definition(config()) -> ok. +behaviour_callback_definition(Config) -> + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 28, 5), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(behaviour_a_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {3, 1}, to => {3, 30}}), + Range + ), + ok. + +-spec testcase(config()) -> ok. +testcase(Config) -> + Uri = ?config(sample_SUITE_uri, Config), + Def = els_client:definition(Uri, 35, 6), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {58, 1}, to => {58, 4}}), + Range + ), + ok. + +-spec multiple_atom_instances_same_mod(config()) -> ok. +multiple_atom_instances_same_mod(Config) -> + Uri = ?config(code_navigation_uri, Config), + Defs = els_client:definition(Uri, 130, 36), + #{result := Results} = Defs, + ?assertEqual(3, length(Results)), + ExpectedRanges = [ + els_protocol:range(#{from => {1, 9}, to => {1, 24}}), + els_protocol:range(#{from => {126, 1}, to => {126, 16}}), + els_protocol:range(#{from => {128, 1}, to => {128, 16}}) + ], + lists:foreach( + fun(Def) -> + #{range := Range, uri := DefUri} = Def, + ?assertEqual(Uri, DefUri), + ?assert(lists:member(Range, ExpectedRanges)) + end, + Results + ), + ok. + +-spec multiple_atom_instances_diff_mod(config()) -> ok. +multiple_atom_instances_diff_mod(Config) -> + Uri = ?config(code_navigation_uri, Config), + Defs = els_client:definition(Uri, 134, 35), + #{result := Results} = Defs, + ?assertEqual(2, length(Results)), + RangeDef1 = els_protocol:range(#{from => {132, 1}, to => {132, 22}}), + RangeDef2 = els_protocol:range(#{from => {1, 9}, to => {1, 30}}), + Uri2 = ?config(code_navigation_extra_uri, Config), + ?assertMatch( + [ + #{ + range := RangeDef1, + uri := Uri + }, + #{ + range := RangeDef2, + uri := Uri2 + } + ], + Results + ), + ok. %% Issue #191: Definition not found after document is closed -spec definition_after_closing(config()) -> ok. definition_after_closing(Config) -> - Uri = ?config(code_navigation_uri, Config), - ExtraUri = ?config(code_navigation_extra_uri, Config), - Def = els_client:definition(Uri, 32, 13), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(ExtraUri, DefUri), - ?assertEqual( els_protocol:range(#{from => {5, 1}, to => {5, 3}}) - , Range), - - %% Close file, get definition - ok = els_client:did_close(ExtraUri), - Def1 = els_client:definition(Uri, 32, 13), - #{result := #{range := Range, uri := DefUri}} = Def1, - ok. + Uri = ?config(code_navigation_uri, Config), + ExtraUri = ?config(code_navigation_extra_uri, Config), + Def = els_client:definition(Uri, 32, 13), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(ExtraUri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {5, 1}, to => {5, 3}}), + Range + ), + + %% Close file, get definition + ok = els_client:did_close(ExtraUri), + Def1 = els_client:definition(Uri, 32, 13), + #{result := [#{range := Range, uri := DefUri}]} = Def1, + ok. -spec duplicate_definition(config()) -> ok. duplicate_definition(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 57, 5), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {60, 1}, to => {60, 11}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 57, 5), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {60, 1}, to => {60, 11}}), + Range + ), + ok. -spec export_entry(config()) -> ok. export_entry(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 8, 15), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {28, 1}, to => {28, 11}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 8, 15), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {28, 1}, to => {28, 11}}), + Range + ), + ok. -spec fun_local(config()) -> ok. fun_local(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 51, 16), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {25, 1}, to => {25, 11}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 51, 16), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {25, 1}, to => {25, 11}}), + Range + ), + ok. -spec fun_remote(config()) -> ok. fun_remote(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 52, 14), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {5, 1}, to => {5, 3}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 52, 14), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {5, 1}, to => {5, 3}}), + Range + ), + ok. -spec import_entry(config()) -> ok. import_entry(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 10, 34), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {5, 1}, to => {5, 3}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 10, 34), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {5, 1}, to => {5, 3}}), + Range + ), + ok. -spec module_import_entry(config()) -> ok. module_import_entry(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 90, 3), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {5, 1}, to => {5, 3}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 90, 3), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_extra_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {5, 1}, to => {5, 3}}), + Range + ), + ok. -spec include(config()) -> ok. include(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 12, 20), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {1, 1}, to => {1, 1}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 12, 20), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {1, 1}, to => {1, 1}}), + Range + ), + ok. -spec include_lib(config()) -> ok. include_lib(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 13, 22), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {1, 1}, to => {1, 1}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 13, 22), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {1, 1}, to => {1, 1}}), + Range + ), + ok. -spec macro(config()) -> ok. macro(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 26, 5), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {18, 9}, to => {18, 16}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 26, 5), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {18, 9}, to => {18, 16}}), + Range + ), + ok. -spec macro_lowercase(config()) -> ok. macro_lowercase(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 48, 3), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {45, 9}, to => {45, 16}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 48, 3), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {45, 9}, to => {45, 16}}), + Range + ), + ok. -spec macro_included(config()) -> ok. macro_included(Config) -> - Uri = ?config(code_navigation_uri, Config), - UriHeader = ?config(code_navigation_h_uri, Config), - #{result := #{range := Range1, uri := DefUri1}} = - els_client:definition(Uri, 53, 19), - ?assertEqual(UriHeader, DefUri1), - ?assertEqual( els_protocol:range(#{from => {3, 9}, to => {3, 25}}) - , Range1), - #{result := #{range := RangeQuoted, uri := DefUri2}} = - els_client:definition(Uri, 52, 75), - ?assertEqual(UriHeader, DefUri2), - ?assertEqual( els_protocol:range(#{from => {7, 9}, to => {7, 27}}) - , RangeQuoted), - ok. + Uri = ?config(code_navigation_uri, Config), + UriHeader = ?config(code_navigation_h_uri, Config), + #{result := [#{range := Range1, uri := DefUri1}]} = + els_client:definition(Uri, 53, 19), + ?assertEqual(UriHeader, DefUri1), + ?assertEqual( + els_protocol:range(#{from => {3, 9}, to => {3, 25}}), + Range1 + ), + #{result := [#{range := RangeQuoted, uri := DefUri2}]} = + els_client:definition(Uri, 52, 75), + ?assertEqual(UriHeader, DefUri2), + ?assertEqual( + els_protocol:range(#{from => {7, 9}, to => {7, 27}}), + RangeQuoted + ), + ok. -spec macro_with_args(config()) -> ok. macro_with_args(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 40, 9), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {19, 9}, to => {19, 16}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 40, 9), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {19, 9}, to => {19, 16}}), + Range + ), + ok. -spec macro_with_args_included(config()) -> ok. macro_with_args_included(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 43, 9), - #{result := #{uri := DefUri}} = Def, - ?assertEqual( <<"assert.hrl">> - , filename:basename(els_uri:path(DefUri))), - %% Do not assert on line number to avoid binding to a specific OTP version - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 43, 9), + #{result := [#{uri := DefUri}]} = Def, + ?assertEqual( + <<"assert.hrl">>, + filename:basename(els_uri:path(DefUri)) + ), + %% Do not assert on line number to avoid binding to a specific OTP version + ok. -spec macro_with_implicit_args(config()) -> ok. macro_with_implicit_args(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 124, 5), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {118, 9}, to => {118, 16}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 124, 5), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {118, 9}, to => {118, 16}}), + Range + ), + ok. -spec parse_transform(config()) -> ok. parse_transform(Config) -> - Uri = ?config(diagnostics_parse_transform_usage_uri, Config), - Def = els_client:definition(Uri, 5, 45), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(diagnostics_parse_transform_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 36}}) - , Range), - ok. + Uri = ?config(diagnostics_parse_transform_usage_uri, Config), + Def = els_client:definition(Uri, 5, 45), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(diagnostics_parse_transform_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 36}}), + Range + ), + ok. -spec record_access(config()) -> ok. record_access(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 34, 13), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {16, 9}, to => {16, 17}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 34, 13), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {16, 9}, to => {16, 17}}), + Range + ), + ok. -spec record_access_included(config()) -> ok. record_access_included(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 52, 43), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 26}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 52, 43), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 26}}), + Range + ), + ok. -spec record_access_macro_name(config()) -> ok. record_access_macro_name(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 116, 33), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {111, 9}, to => {111, 16}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 116, 33), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {111, 9}, to => {111, 16}}), + Range + ), + ok. %% TODO: Additional constructors for POI %% TODO: Navigation should return POI, not range -spec record_expr(config()) -> ok. record_expr(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 33, 11), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {16, 9}, to => {16, 17}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 33, 11), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {16, 9}, to => {16, 17}}), + Range + ), + ok. -spec record_expr_included(config()) -> ok. record_expr_included(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 53, 30), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {1, 9}, to => {1, 26}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 53, 30), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {1, 9}, to => {1, 26}}), + Range + ), + ok. -spec record_expr_macro_name(config()) -> ok. record_expr_macro_name(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 115, 11), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {111, 9}, to => {111, 16}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 115, 11), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {111, 9}, to => {111, 16}}), + Range + ), + ok. -spec record_field(config()) -> ok. record_field(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 33, 20), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {16, 20}, to => {16, 27}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 33, 20), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {16, 20}, to => {16, 27}}), + Range + ), + ok. -spec record_field_included(config()) -> ok. record_field_included(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 53, 45), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), - ?assertEqual( els_protocol:range(#{from => {1, 29}, to => {1, 45}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 53, 45), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(?config(code_navigation_h_uri, Config), DefUri), + ?assertEqual( + els_protocol:range(#{from => {1, 29}, to => {1, 45}}), + Range + ), + ok. -spec record_type_macro_name(config()) -> ok. record_type_macro_name(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 113, 28), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {111, 9}, to => {111, 16}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 113, 28), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {111, 9}, to => {111, 16}}), + Range + ), + ok. -spec type_application_remote(config()) -> ok. type_application_remote(Config) -> - ExtraUri = ?config(code_navigation_extra_uri, Config), - TypesUri = ?config(code_navigation_types_uri, Config), - Def = els_client:definition(ExtraUri, 11, 38), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(TypesUri, DefUri), - ?assertEqual( els_protocol:range(#{from => {3, 1}, to => {3, 26}}) - , Range), - ok. + ExtraUri = ?config(code_navigation_extra_uri, Config), + TypesUri = ?config(code_navigation_types_uri, Config), + Def = els_client:definition(ExtraUri, 11, 38), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(TypesUri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {3, 1}, to => {3, 26}}), + Range + ), + ok. -spec type_application_undefined(config()) -> ok. type_application_undefined(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 55, 42), - #{result := Result} = Def, - Expected = [ - #{ range => #{'end' => #{character => 49, line => 54}, - start => #{character => 33, line => 54}} - , uri => Uri} - ], - ?assertEqual(Expected, Result), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 55, 42), + #{result := Result} = Def, + Expected = [ + #{ + range => #{ + 'end' => #{character => 49, line => 54}, + start => #{character => 33, line => 54} + }, + uri => Uri + } + ], + ?assertEqual(Expected, Result), + ok. -spec type_application_user(config()) -> ok. type_application_user(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 55, 25), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {37, 1}, to => {37, 25}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 55, 25), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {37, 1}, to => {37, 25}}), + Range + ), + ok. -spec type_export_entry(config()) -> ok. type_export_entry(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def = els_client:definition(Uri, 9, 17), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(Uri, DefUri), - ?assertEqual( els_protocol:range(#{from => {37, 1}, to => {37, 25}}) - , Range), - ok. + Uri = ?config(code_navigation_uri, Config), + Def = els_client:definition(Uri, 9, 17), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(Uri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {37, 1}, to => {37, 25}}), + Range + ), + ok. -spec variable(config()) -> ok. variable(Config) -> - Uri = ?config(code_navigation_uri, Config), - Def0 = els_client:definition(Uri, 104, 9), - Def1 = els_client:definition(Uri, 105, 10), - Def2 = els_client:definition(Uri, 107, 10), - Def3 = els_client:definition(Uri, 108, 10), - #{result := #{range := Range0, uri := DefUri0}} = Def0, - #{result := #{range := Range1, uri := DefUri0}} = Def1, - #{result := #{range := Range2, uri := DefUri0}} = Def2, - #{result := #{range := Range3, uri := DefUri0}} = Def3, - - ?assertEqual(?config(code_navigation_uri, Config), DefUri0), - ?assertEqual( els_protocol:range(#{from => {103, 12}, to => {103, 15}}) - , Range0), - ?assertEqual( els_protocol:range(#{from => {104, 3}, to => {104, 6}}) - , Range1), - ?assertEqual( els_protocol:range(#{from => {106, 12}, to => {106, 15}}) - , Range2), - ?assertEqual( els_protocol:range(#{from => {106, 12}, to => {106, 15}}) - , Range3), - ok. - + Uri = ?config(code_navigation_uri, Config), + ?assertEqual( + {#{from => {103, 12}, to => {103, 15}}, Uri}, + definition(Uri, 104, 9) + ), + ?assertEqual( + {#{from => {104, 3}, to => {104, 6}}, Uri}, + definition(Uri, 105, 10) + ), + ?assertEqual( + {#{from => {106, 12}, to => {106, 15}}, Uri}, + definition(Uri, 107, 10) + ), + ?assertEqual( + {#{from => {106, 12}, to => {106, 15}}, Uri}, + definition(Uri, 108, 10) + ), + %% Inside macro + ?assertEqual( + {#{from => {19, 17}, to => {19, 18}}, Uri}, + definition(Uri, 19, 36) + ), + ok. + +-spec variable_list_comp(config()) -> ok. +variable_list_comp(Config) -> + Uri = ?config(variable_list_comp_uri, Config), + + %% one: Should skip LC + ?assertEqual( + {#{from => {4, 5}, to => {4, 8}}, Uri}, + definition(Uri, 6, 5) + ), + %% one: Should go to LC generator pattern + ?assertEqual( + {#{from => {5, 14}, to => {5, 17}}, Uri}, + definition(Uri, 5, 7) + ), + %% two: Should go to first LC generator pattern + ?assertEqual( + {#{from => {9, 14}, to => {9, 17}}, Uri}, + definition(Uri, 9, 7) + ), + %% two: Should go to second LC generator pattern + ?assertEqual( + {#{from => {10, 14}, to => {10, 17}}, Uri}, + definition(Uri, 10, 7) + ), + %% three: Should go to variable definition + ?assertEqual( + {#{from => {13, 5}, to => {13, 8}}, Uri}, + definition(Uri, 14, 7) + ), + %% three: Should go to variable definition (same) + ?assertEqual( + {#{from => {13, 5}, to => {13, 8}}, Uri}, + definition(Uri, 15, 5) + ), + %% four: Should go to first LC generator pattern + ?assertEqual( + {#{from => {18, 22}, to => {18, 25}}, Uri}, + definition(Uri, 18, 8) + ), + %% four: Should go to second LC generator pattern + ?assertEqual( + {#{from => {19, 22}, to => {19, 26}}, Uri}, + definition(Uri, 18, 13) + ), + %% four: Should go to third LC generator pattern + ?assertEqual( + {#{from => {19, 39}, to => {19, 42}}, Uri}, + definition(Uri, 19, 32) + ), + ok. -spec opaque_application_remote(config()) -> ok. opaque_application_remote(Config) -> - ExtraUri = ?config(code_navigation_extra_uri, Config), - TypesUri = ?config(code_navigation_types_uri, Config), - Def = els_client:definition(ExtraUri, 16, 61), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(TypesUri, DefUri), - ?assertEqual( els_protocol:range(#{from => {7, 1}, to => {7, 35}}) - , Range), - ok. + ExtraUri = ?config(code_navigation_extra_uri, Config), + TypesUri = ?config(code_navigation_types_uri, Config), + Def = els_client:definition(ExtraUri, 16, 61), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(TypesUri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {7, 1}, to => {7, 35}}), + Range + ), + ok. -spec opaque_application_user(config()) -> ok. opaque_application_user(Config) -> - ExtraUri = ?config(code_navigation_extra_uri, Config), - Def = els_client:definition(ExtraUri, 16, 24), - #{result := #{range := Range, uri := DefUri}} = Def, - ?assertEqual(ExtraUri, DefUri), - ?assertEqual( els_protocol:range(#{from => {20, 1}, to => {20, 34}}) - , Range), - ok. + ExtraUri = ?config(code_navigation_extra_uri, Config), + Def = els_client:definition(ExtraUri, 16, 24), + #{result := [#{range := Range, uri := DefUri}]} = Def, + ?assertEqual(ExtraUri, DefUri), + ?assertEqual( + els_protocol:range(#{from => {20, 1}, to => {20, 34}}), + Range + ), + ok. + +-spec parse_incomplete(config()) -> ok. +parse_incomplete(Config) -> + Uri = ?config(code_navigation_broken_uri, Config), + Range = els_protocol:range(#{from => {3, 1}, to => {3, 11}}), + ?assertMatch( + #{result := [#{range := Range, uri := Uri}]}, + els_client:definition(Uri, 7, 3) + ), + ?assertMatch( + #{result := [#{range := Range, uri := Uri}]}, + els_client:definition(Uri, 8, 3) + ), + ?assertMatch( + #{result := [#{range := Range, uri := Uri}]}, + els_client:definition(Uri, 9, 8) + ), + ?assertMatch( + #{result := [#{range := Range, uri := Uri}]}, + els_client:definition(Uri, 11, 7) + ), + ?assertMatch( + #{result := [#{range := Range, uri := Uri}]}, + els_client:definition(Uri, 12, 12) + ), + ?assertMatch( + #{result := [#{range := Range, uri := Uri}]}, + els_client:definition(Uri, 17, 3) + ), + ?assertMatch( + #{result := [#{range := Range, uri := Uri}]}, + els_client:definition(Uri, 19, 3) + ), + ok. + +definition(Uri, Line, Char) -> + case els_client:definition(Uri, Line, Char) of + #{result := [#{range := Range, uri := DefUri}]} -> + {els_range:to_poi_range(Range), DefUri}; + Res -> + {error, Res} + end. diff --git a/apps/els_lsp/test/els_diagnostics_SUITE.erl b/apps/els_lsp/test/els_diagnostics_SUITE.erl index 928c82205..64f44925b 100644 --- a/apps/els_lsp/test/els_diagnostics_SUITE.erl +++ b/apps/els_lsp/test/els_diagnostics_SUITE.erl @@ -1,47 +1,62 @@ -module(els_diagnostics_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ bound_var_in_pattern/1 - , compiler/1 - , compiler_with_behaviour/1 - , compiler_with_broken_behaviour/1 - , compiler_with_custom_macros/1 - , compiler_with_parse_transform/1 - , compiler_with_parse_transform_list/1 - , compiler_with_parse_transform_included/1 - , compiler_with_parse_transform_broken/1 - , compiler_with_parse_transform_deps/1 - , compiler_with_parse_transform_error/1 - , compiler_telemetry/1 - , code_path_extra_dirs/1 - , use_long_names/1 - , epp_with_nonexistent_macro/1 - , code_reload/1 - , code_reload_sticky_mod/1 - , elvis/1 - , escript/1 - , escript_warnings/1 - , escript_errors/1 - , crossref/1 - , crossref_autoimport/1 - , crossref_autoimport_disabled/1 - , crossref_pseudo_functions/1 - , unused_includes/1 - , unused_includes_compiler_attribute/1 - , exclude_unused_includes/1 - , unused_macros/1 - , unused_record_fields/1 - , gradualizer/1 - ]). +-export([ + atom_typo/1, + bound_var_in_pattern/1, + bound_var_in_pattern_cannot_parse/1, + compiler/1, + compiler_with_behaviour/1, + compiler_with_behaviour_recursive/1, + compiler_with_broken_behaviour/1, + compiler_with_custom_macros/1, + compiler_with_parse_transform/1, + compiler_with_parse_transform_list/1, + compiler_with_parse_transform_included/1, + compiler_with_parse_transform_broken/1, + compiler_with_parse_transform_deps/1, + compiler_with_parse_transform_error/1, + compiler_telemetry/1, + code_path_extra_dirs/1, + use_long_names/1, + use_long_names_no_domain/1, + use_long_names_custom_hostname/1, + epp_with_nonexistent_macro/1, + epp_with_builtin_macro/1, + elvis/1, + escript/1, + escript_warnings/1, + escript_errors/1, + crossref/1, + crossref_compiler_enabled/1, + crossref_autoimport/1, + crossref_autoimport_disabled/1, + crossref_pseudo_functions/1, + unused_includes/1, + unused_includes_compiler_attribute/1, + unused_includes_broken/1, + exclude_unused_includes/1, + unused_macros/1, + unused_macros_refactorerl/1, + unused_record_fields/1, + gradualizer/1, + eqwalizer/1, + module_name_check/1, + module_name_check_whitespace/1, + edoc_main/1, + edoc_skip_app_src/1, + edoc_custom_tags/1 +]). %%============================================================================== %% Includes @@ -60,670 +75,1107 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). -init_per_testcase(TestCase, Config) when TestCase =:= code_reload orelse - TestCase =:= code_reload_sticky_mod -> - mock_rpc(), - mock_code_reload_enabled(), - els_test_utils:init_per_testcase(TestCase, Config); -init_per_testcase(TestCase, Config) - when TestCase =:= crossref orelse - TestCase =:= crossref_pseudo_functions orelse - TestCase =:= crossref_autoimport orelse - TestCase =:= crossref_autoimport_disabled -> - meck:new(els_crossref_diagnostics, [passthrough, no_link]), - meck:expect(els_crossref_diagnostics, is_default, 0, true), - els_mock_diagnostics:setup(), - els_test_utils:init_per_testcase(TestCase, Config); +init_per_testcase(TestCase, Config) when + TestCase =:= atom_typo +-> + init_with_diagnostics(TestCase, [<<"atom_typo">>], Config); +init_per_testcase(TestCase, Config) when + TestCase =:= crossref orelse + TestCase =:= crossref_pseudo_functions orelse + TestCase =:= crossref_autoimport orelse + TestCase =:= crossref_autoimport_disabled +-> + init_with_diagnostics(TestCase, [<<"crossref">>], Config); +init_per_testcase(TestCase, Config) when + TestCase =:= elvis +-> + init_with_diagnostics(TestCase, [<<"elvis">>], Config); +init_per_testcase(TestCase, Config) when + TestCase =:= crossref_compiler_enabled +-> + init_with_diagnostics(TestCase, [<<"crossref">>, <<"compiler">>], Config); init_per_testcase(code_path_extra_dirs, Config) -> - meck:new(yamerl, [passthrough, no_link]), - Content = <<"code_path_extra_dirs:\n", - " - \"../code_navigation/*/\"\n">>, - meck:expect(yamerl, decode_file, 2, fun(_, Opts) -> - yamerl:decode(Content, Opts) - end), - els_mock_diagnostics:setup(), - els_test_utils:init_per_testcase(code_path_extra_dirs, Config); + meck:new(yamerl, [passthrough, no_link]), + Content = <<"code_path_extra_dirs:\n", " - \"../code_navigation/*/\"\n">>, + meck:expect(yamerl, decode, 2, fun(_, Opts) -> + meck:passthrough([Content, Opts]) + end), + els_mock_diagnostics:setup(), + els_test_utils:init_per_testcase(code_path_extra_dirs, Config); init_per_testcase(use_long_names, Config) -> - meck:new(yamerl, [passthrough, no_link]), - Content = <<"runtime:\n", - " use_long_names: true\n", - " cookie: mycookie\n", - " node_name: my_node\n">>, - meck:expect(yamerl, decode_file, 2, fun(_, Opts) -> - yamerl:decode(Content, Opts) - end), - els_mock_diagnostics:setup(), - els_test_utils:init_per_testcase(code_path_extra_dirs, Config); -init_per_testcase(exclude_unused_includes = TestCase, Config) -> - els_mock_diagnostics:setup(), - NewConfig = els_test_utils:init_per_testcase(TestCase, Config), - els_config:set(exclude_unused_includes, ["et/include/et.hrl"]), - NewConfig; + Content = + <<"runtime:\n", " use_long_names: true\n", " cookie: mycookie\n", + " node_name: my_node\n", " domain: test.local">>, + init_long_names_config(Content, Config); +init_per_testcase(use_long_names_no_domain, Config) -> + Content = + <<"runtime:\n", " use_long_names: true\n", " cookie: mycookie\n", + " node_name: my_node\n">>, + init_long_names_config(Content, Config); +init_per_testcase(use_long_names_custom_hostname, Config) -> + Content = + <<"runtime:\n", " use_long_names: true\n", " cookie: mycookie\n", + " node_name: my_node\n", " hostname: 127.0.0.1">>, + init_long_names_config(Content, Config); +init_per_testcase(exclude_unused_includes = TestCase, Config0) -> + Config = init_with_diagnostics(TestCase, [<<"unused_includes">>], Config0), + els_config:set(exclude_unused_includes, ["et/include/et.hrl"]), + Config; init_per_testcase(TestCase, Config) when TestCase =:= compiler_telemetry -> - els_mock_diagnostics:setup(), - mock_compiler_telemetry_enabled(), - els_test_utils:init_per_testcase(TestCase, Config); + els_mock_diagnostics:setup(), + mock_compiler_telemetry_enabled(), + els_test_utils:init_per_testcase(TestCase, Config); init_per_testcase(TestCase, Config) when TestCase =:= gradualizer -> - meck:new(els_gradualizer_diagnostics, [passthrough, no_link]), - meck:expect(els_gradualizer_diagnostics, is_default, 0, true), - els_mock_diagnostics:setup(), - els_test_utils:init_per_testcase(TestCase, Config); + meck:new(els_gradualizer_diagnostics, [passthrough, no_link]), + meck:expect(els_gradualizer_diagnostics, is_default, 0, true), + els_mock_diagnostics:setup(), + els_test_utils:init_per_testcase(TestCase, Config); +init_per_testcase(TestCase, Config) when TestCase =:= eqwalizer -> + meck:new(els_eqwalizer_diagnostics, [passthrough, no_link]), + meck:expect(els_eqwalizer_diagnostics, is_default, 0, true), + Diagnostics = [ + els_utils:to_list( + json:encode(#{ + <<"diagnostic">> => + #{ + <<"code">> => <<"eqwalizer">>, + <<"message">> => + <<"Expected: 'ok'\nGot : 'not_ok'\n">>, + <<"range">> => + #{ + <<"end">> => + #{ + <<"character">> => 10, + <<"line">> => 6 + }, + <<"start">> => + #{ + <<"character">> => 4, + <<"line">> => 6 + } + }, + <<"severity">> => 2, + <<"source">> => <<"elp">> + }, + <<"relative_path">> => + <<"src/diagnostics_eqwalizer.erl">> + }) + ) + ], + meck:expect(els_eqwalizer_diagnostics, eqwalize, 2, Diagnostics), + els_mock_diagnostics:setup(), + els_test_utils:init_per_testcase(TestCase, Config); +init_per_testcase(TestCase, Config) when + TestCase =:= edoc_main; + TestCase =:= edoc_skip_app_src; + TestCase =:= edoc_custom_tags +-> + meck:new(els_edoc_diagnostics, [passthrough, no_link]), + meck:expect(els_edoc_diagnostics, is_default, 0, true), + els_mock_diagnostics:setup(), + els_test_utils:init_per_testcase(TestCase, Config); +% RefactorErl +init_per_testcase(TestCase, Config) when + TestCase =:= unused_macros_refactorerl +-> + mock_refactorerl(), + els_test_utils:init_per_testcase(TestCase, Config); +init_per_testcase(TestCase, Config) when + TestCase =:= unused_includes; + TestCase =:= unused_includes_broken; + TestCase =:= unused_includes_compiler_attribute +-> + init_with_diagnostics(TestCase, [<<"unused_includes">>], Config); init_per_testcase(TestCase, Config) -> - els_mock_diagnostics:setup(), - els_test_utils:init_per_testcase(TestCase, Config). + els_mock_diagnostics:setup(), + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. -end_per_testcase(TestCase, Config) when TestCase =:= code_reload orelse - TestCase =:= code_reload_sticky_mod -> - unmock_rpc(), - unmock_code_reload_enabled(), - els_test_utils:end_per_testcase(TestCase, Config); -end_per_testcase(TestCase, Config) - when TestCase =:= crossref orelse - TestCase =:= crossref_pseudo_functions orelse - TestCase =:= crossref_autoimport orelse - TestCase =:= crossref_autoimport_disabled -> - meck:unload(els_crossref_diagnostics), - els_test_utils:end_per_testcase(TestCase, Config), - els_mock_diagnostics:teardown(), - ok; -end_per_testcase(TestCase, Config) - when TestCase =:= code_path_extra_dirs orelse - TestCase =:= use_long_names -> - meck:unload(yamerl), - els_test_utils:end_per_testcase(code_path_extra_dirs, Config), - els_mock_diagnostics:teardown(), - ok; +end_per_testcase(TestCase, Config) when + TestCase =:= code_path_extra_dirs orelse + TestCase =:= use_long_names orelse + TestCase =:= use_long_names_no_domain orelse + TestCase =:= use_long_names_custom_hostname +-> + meck:unload(yamerl), + els_test_utils:end_per_testcase(code_path_extra_dirs, Config), + els_mock_diagnostics:teardown(), + ok; end_per_testcase(exclude_unused_includes = TestCase, Config) -> - els_config:set(exclude_unused_includes, []), - els_test_utils:end_per_testcase(TestCase, Config), - els_mock_diagnostics:teardown(), - ok; + reset_diagnostics_config(Config), + els_config:set(exclude_unused_includes, []), + els_test_utils:end_per_testcase(TestCase, Config), + els_mock_diagnostics:teardown(), + ok; end_per_testcase(TestCase, Config) when TestCase =:= compiler_telemetry -> - unmock_compiler_telemetry_enabled(), - els_test_utils:end_per_testcase(TestCase, Config), - els_mock_diagnostics:teardown(), - ok; + unmock_compiler_telemetry_enabled(), + els_test_utils:end_per_testcase(TestCase, Config), + els_mock_diagnostics:teardown(), + ok; end_per_testcase(TestCase, Config) when TestCase =:= gradualizer -> - meck:unload(els_gradualizer_diagnostics), - els_test_utils:end_per_testcase(TestCase, Config), - els_mock_diagnostics:teardown(), - ok; + meck:unload(els_gradualizer_diagnostics), + els_test_utils:end_per_testcase(TestCase, Config), + els_mock_diagnostics:teardown(), + ok; +end_per_testcase(TestCase, Config) when TestCase =:= eqwalizer -> + meck:unload(els_eqwalizer_diagnostics), + els_test_utils:end_per_testcase(TestCase, Config), + els_mock_diagnostics:teardown(), + ok; +end_per_testcase(TestCase, Config) when + TestCase =:= edoc_main; + TestCase =:= edoc_skip_app_src; + TestCase =:= edoc_custom_tags +-> + meck:unload(els_edoc_diagnostics), + els_test_utils:end_per_testcase(TestCase, Config), + els_mock_diagnostics:teardown(), + ok; +end_per_testcase(TestCase, Config) when + TestCase =:= unused_macros_refactorerl +-> + unmock_refactoerl(), + els_test_utils:end_per_testcase(TestCase, Config), + els_mock_diagnostics:teardown(), + ok; end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config), - els_mock_diagnostics:teardown(), - ok. + reset_diagnostics_config(Config), + els_test_utils:end_per_testcase(TestCase, Config), + els_mock_diagnostics:teardown(), + ok. + +-spec init_long_names_config(binary(), config()) -> config(). +init_long_names_config(Content, Config) -> + meck:new(yamerl, [passthrough, no_link]), + meck:expect(yamerl, decode, 2, fun(_, Opts) -> + meck:passthrough([Content, Opts]) + end), + els_mock_diagnostics:setup(), + els_test_utils:init_per_testcase(code_path_extra_dirs, Config). + +% RefactorErl %%============================================================================== %% Testcases %%============================================================================== +-spec atom_typo(config()) -> ok. +atom_typo(_Config) -> + Path = src_path("atom_typo.erl"), + Source = <<"AtomTypo">>, + Errors = [], + Warnings = [ + #{ + message => <<"Atom typo? Did you mean: true">>, + range => {{5, 2}, {5, 6}} + }, + #{ + message => <<"Atom typo? Did you mean: false">>, + range => {{6, 2}, {6, 8}} + }, + #{ + message => <<"Atom typo? Did you mean: false">>, + range => {{7, 2}, {7, 7}} + }, + #{ + message => <<"Atom typo? Did you mean: undefined">>, + range => {{8, 2}, {8, 11}} + }, + #{ + message => <<"Atom typo? Did you mean: undefined">>, + range => {{9, 2}, {9, 10}} + }, + #{ + message => <<"Atom typo? Did you mean: error">>, + range => {{10, 2}, {10, 8}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + -spec bound_var_in_pattern(config()) -> ok. bound_var_in_pattern(_Config) -> - Path = src_path("diagnostics_bound_var_in_pattern.erl"), - Source = <<"BoundVarInPattern">>, - Errors = [], - Warnings = [], - Hints = [ #{ message => <<"Bound variable in pattern: Var1">> - , range => {{5, 2}, {5, 6}}} - , #{ message => <<"Bound variable in pattern: Var2">> - , range => {{9, 9}, {9, 13}}} - , #{ message => <<"Bound variable in pattern: Var4">> - , range => {{17, 8}, {17, 12}}} - , #{ message => <<"Bound variable in pattern: Var3">> - , range => {{15, 10}, {15, 14}}} - , #{ message => <<"Bound variable in pattern: Var5">> - , range => {{23, 6}, {23, 10}}} - %% erl_syntax_lib:annotate_bindings does not handle named funs - %% correctly - %% , #{ message => <<"Bound variable in pattern: New">> - %% , range => {{28, 6}, {28, 9}}} - %% , #{ message => <<"Bound variable in pattern: F">> - %% , range => {{29, 6}, {29, 9}}} - ], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_bound_var_in_pattern.erl"), + Source = <<"BoundVarInPattern">>, + Errors = [], + Warnings = [], + Hints0 = [ + #{ + message => <<"Bound variable in pattern: Var1">>, + range => {{5, 2}, {5, 6}} + }, + #{ + message => <<"Bound variable in pattern: Var2">>, + range => {{9, 9}, {9, 13}} + }, + #{ + message => <<"Bound variable in pattern: Var4">>, + range => {{17, 8}, {17, 12}} + }, + #{ + message => <<"Bound variable in pattern: Var3">>, + range => {{15, 10}, {15, 14}} + }, + #{ + message => <<"Bound variable in pattern: Var5">>, + range => {{23, 6}, {23, 10}} + } + ], + Hints = + case list_to_integer(erlang:system_info(otp_release)) of + Version when Version < 25 -> + Hints0; + _ -> + lists:append( + Hints0, + [ + #{ + message => <<"Bound variable in pattern: New">>, + range => {{28, 6}, {28, 9}} + }, + #{ + message => <<"Bound variable in pattern: F">>, + range => {{29, 6}, {29, 7}} + } + ] + ) + end, + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec bound_var_in_pattern_cannot_parse(config()) -> ok. +bound_var_in_pattern_cannot_parse(_Config) -> + Path = src_path("diagnostics_bound_var_in_pattern_cannot_parse.erl"), + Source = <<"BoundVarInPattern">>, + Errors = [], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler(config()) -> ok. compiler(_Config) -> - Path = src_path("diagnostics.erl"), - Source = <<"Compiler">>, - Errors = [ #{ code => <<"L0000">> - , message => <<"Issue in included file (1): bad attribute">> - , range => {{3, 0}, {3, 35}}} - , #{ code => <<"L0000">> - , message => <<"Issue in included file (3): bad attribute">> - , range => {{3, 0}, {3, 35}}} - , #{ code => <<"L1295">> - , message => <<"type undefined_type() undefined">> - , range => {{5, 30}, {5, 44}}} - ], - Warnings = [ #{ code => <<"L1230">> - , message => <<"function main/1 is unused">> - , range => {{6, 0}, {6, 4}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics.erl"), + Source = <<"Compiler">>, + Errors = [ + #{ + code => <<"L0000">>, + message => <<"Issue in included file (1): bad attribute">>, + range => {{3, 0}, {3, 35}} + }, + #{ + code => <<"L0000">>, + message => <<"Issue in included file (3): bad attribute">>, + range => {{3, 0}, {3, 35}} + }, + #{ + code => <<"L1295">>, + message => <<"type undefined_type() undefined">>, + range => {{5, 30}, {5, 44}} + } + ], + Warnings = [ + #{ + code => <<"L1230">>, + message => <<"function main/1 is unused">>, + range => {{6, 0}, {6, 4}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec compiler_with_behaviour_recursive(config()) -> ok. +compiler_with_behaviour_recursive(_Config) -> + %% Test that recursive deps are handled for behaviours + Path = src_path("diagnostics_behaviour_recursive_impl.erl"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler_with_behaviour(config()) -> ok. compiler_with_behaviour(_Config) -> - Path = src_path("diagnostics_behaviour_impl.erl"), - Source = <<"Compiler">>, - Errors = [], - Warnings = [ #{ code => <<"L1284">> - , message => - <<"undefined callback function one/0 " - "(behaviour 'diagnostics_behaviour')">> - , range => {{2, 0}, {2, 34}}}, - #{ code => <<"L1284">> - , message => - <<"undefined callback function two/0 " - "(behaviour 'diagnostics_behaviour')">> - , range => {{2, 0}, {2, 34}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_behaviour_impl.erl"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [ + #{ + code => <<"L1284">>, + message => + << + "undefined callback function one/0 " + "(behaviour 'diagnostics_behaviour')" + >>, + range => {{2, 0}, {2, 34}} + }, + #{ + code => <<"L1284">>, + message => + << + "undefined callback function two/0 " + "(behaviour 'diagnostics_behaviour')" + >>, + range => {{2, 0}, {2, 34}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). %% Testing #614 -spec compiler_with_broken_behaviour(config()) -> ok. compiler_with_broken_behaviour(_Config) -> - Path = src_path("code_navigation.erl"), - {ok, Session} = els_test:start_session(Path), - Diagnostics = els_test:wait_for_diagnostics(Session, <<"Compiler">>), - els_test:assert_contains( - #{ code => <<"L0000">> - , message => <<"Issue in included file (5): syntax error before: ">> - , range => {{2, 0}, {2, 24}}}, Diagnostics). + Path = src_path("code_navigation.erl"), + {ok, Session} = els_test:start_session(Path), + Diagnostics = els_test:wait_for_diagnostics(Session, <<"Compiler">>), + els_test:assert_contains( + #{ + code => <<"L0000">>, + message => <<"Issue in included file (5): syntax error before: ">>, + range => {{2, 0}, {2, 24}} + }, + Diagnostics + ). -spec compiler_with_custom_macros(config()) -> ok. compiler_with_custom_macros(_Config) -> - %% This test uses priv/code_navigation/erlang_ls.config to define - %% some macros. - Path = src_path("diagnostics_macros.erl"), - Source = <<"Compiler">>, - Errors = case els_test:compiler_returns_column_numbers() of - true -> - %% diagnostic_macro has a spec with no '.' at the end - %% which causes the poi for the spec to becomes the - %% entire spec + function. So this range here is 8 - %% lines long. - [ #{ code => <<"E1507">> - , message => <<"undefined macro 'UNDEFINED'">> - , range => {{2, 0}, {10, 6}} - } - ]; - false -> - [ #{ code => <<"E1507">> - , message => <<"undefined macro 'UNDEFINED'">> - , range => {{8, 0}, {9, 0}} - } - ] - end, - Warnings = [], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + %% This test uses priv/code_navigation/erlang_ls.config to define + %% some macros. + Path = src_path("diagnostics_macros.erl"), + Source = <<"Compiler">>, + Errors = + case els_test:compiler_returns_column_numbers() of + true -> + %% diagnostic_macro has a spec with no '.' at the end + %% which causes the poi for the spec to becomes the + %% entire spec + function. So this range here is 8 + %% lines long. + [ + #{ + code => <<"E1507">>, + message => <<"undefined macro 'UNDEFINED'">>, + range => {{2, 0}, {10, 6}} + } + ]; + false -> + [ + #{ + code => <<"E1507">>, + message => <<"undefined macro 'UNDEFINED'">>, + range => {{8, 0}, {9, 0}} + } + ] + end, + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler_with_parse_transform(config()) -> ok. compiler_with_parse_transform(_Config) -> - _ = code:delete(diagnostics_parse_transform), - _ = code:purge(diagnostics_parse_transform), - Path = src_path("diagnostics_parse_transform_usage.erl"), - Source = <<"Compiler">>, - Errors = [], - Warnings = [ #{ code => <<"L1268">> - , message => <<"variable 'Args' is unused">> - , range => {{6, 5}, {6, 9}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + _ = code:delete(diagnostics_parse_transform), + _ = code:purge(diagnostics_parse_transform), + Path = src_path("diagnostics_parse_transform_usage.erl"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [ + #{ + code => <<"L1268">>, + message => <<"variable 'Args' is unused">>, + range => {{6, 5}, {6, 9}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler_with_parse_transform_list(config()) -> ok. compiler_with_parse_transform_list(_Config) -> - _ = code:delete(diagnostics_parse_transform), - _ = code:purge(diagnostics_parse_transform), - Path = src_path("diagnostics_parse_transform_usage_list.erl"), - Source = <<"Compiler">>, - Errors = [], - Warnings = [ #{ code => <<"L1268">> - , message => <<"variable 'Args' is unused">> - , range => {{6, 5}, {6, 9}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + _ = code:delete(diagnostics_parse_transform), + _ = code:purge(diagnostics_parse_transform), + Path = src_path("diagnostics_parse_transform_usage_list.erl"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [ + #{ + code => <<"L1268">>, + message => <<"variable 'Args' is unused">>, + range => {{6, 5}, {6, 9}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler_with_parse_transform_included(config()) -> ok. compiler_with_parse_transform_included(_Config) -> - _ = code:delete(diagnostics_parse_transform), - _ = code:purge(diagnostics_parse_transform), - Path = src_path("diagnostics_parse_transform_usage_included.erl"), - Source = <<"Compiler">>, - Errors = [], - Warnings = [ #{ code => <<"L1268">> - , message => <<"variable 'Args' is unused">> - , range => {{6, 5}, {6, 9}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + _ = code:delete(diagnostics_parse_transform), + _ = code:purge(diagnostics_parse_transform), + Path = src_path("diagnostics_parse_transform_usage_included.erl"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [ + #{ + code => <<"L1268">>, + message => <<"variable 'Args' is unused">>, + range => {{6, 5}, {6, 9}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler_with_parse_transform_broken(config()) -> ok. compiler_with_parse_transform_broken(_Config) -> - Path = src_path("diagnostics_parse_transform_usage_broken.erl"), - Source = <<"Compiler">>, - Errors = - [ #{ code => <<"L0000">> - , message => <<"Issue in included file (10): syntax error before: ">> - , range => {{4, 27}, {4, 61}} - } - , #{ code => <<"C1008">> - , message => <<"undefined parse transform " - "'diagnostics_parse_transform_broken'">> - , range => {{0, 0}, {1, 0}} - } - ], - Warnings = [], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_parse_transform_usage_broken.erl"), + Source = <<"Compiler">>, + Errors = + [ + #{ + code => <<"L0000">>, + message => <<"Issue in included file (10): syntax error before: ">>, + range => {{4, 27}, {4, 61}} + }, + #{ + code => <<"C1008">>, + message => << + "undefined parse transform " + "'diagnostics_parse_transform_broken'" + >>, + range => {{0, 0}, {1, 0}} + } + ], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler_with_parse_transform_deps(config()) -> ok. compiler_with_parse_transform_deps(_Config) -> - Path = src_path("diagnostics_parse_transform_deps_a.erl"), - Source = <<"Compiler">>, - Errors = [], - Warnings = [ #{ code => <<"L1230">> - , message => <<"function unused/0 is unused">> - , range => {{4, 0}, {4, 6}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_parse_transform_deps_a.erl"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [ + #{ + code => <<"L1230">>, + message => <<"function unused/0 is unused">>, + range => {{4, 0}, {4, 6}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). %% Issue 1140 -spec compiler_with_parse_transform_error(config()) -> ok. compiler_with_parse_transform_error(_Config) -> - Path = src_path("diagnostics_parse_transform_error.erl"), - Source = <<"Compiler">>, - Errors = [#{ code => <<"my_parse_transform">> - , message => <<"custom_description">> - , range => {{41, 0}, {42, 0}} - } - ], - Warnings = [], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_parse_transform_error.erl"), + Source = <<"Compiler">>, + Errors = [ + #{ + code => <<"my_parse_transform">>, + message => <<"custom_description">>, + range => {{41, 0}, {42, 0}} + } + ], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec compiler_telemetry(config()) -> ok. compiler_telemetry(Config) -> - Path = src_path("diagnostics.erl"), - Source = <<"Compiler">>, - Errors = [ #{ code => <<"L0000">> - , message => <<"Issue in included file (1): bad attribute">> - , range => {{3, 0}, {3, 35}} - } - , #{ code => <<"L0000">> - , message => <<"Issue in included file (3): bad attribute">> - , range => {{3, 0}, {3, 35}} - } - , #{ code => <<"L1295">> - , message => <<"type undefined_type() undefined">> - , range => {{5, 30}, {5, 44}} - } - ], - Warnings = [ #{ code => <<"L1230">> - , message => <<"function main/1 is unused">> - , range => {{6, 0}, {6, 4}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints), - Telemetry = wait_for_compiler_telemetry(), - #{ type := Type - , uri := UriT - , diagnostics := DiagnosticsCodes } = Telemetry, - ?assertEqual(<<"erlang-diagnostic-codes">>, Type), - Uri = ?config(diagnostics_uri, Config), - ?assertEqual(Uri, UriT), - ?assertEqual([ <<"L1230">>, <<"L0000">>, <<"L0000">>, <<"L1295">>] - , DiagnosticsCodes), - ok. + Path = src_path("diagnostics.erl"), + Source = <<"Compiler">>, + Errors = [ + #{ + code => <<"L0000">>, + message => <<"Issue in included file (1): bad attribute">>, + range => {{3, 0}, {3, 35}} + }, + #{ + code => <<"L0000">>, + message => <<"Issue in included file (3): bad attribute">>, + range => {{3, 0}, {3, 35}} + }, + #{ + code => <<"L1295">>, + message => <<"type undefined_type() undefined">>, + range => {{5, 30}, {5, 44}} + } + ], + Warnings = [ + #{ + code => <<"L1230">>, + message => <<"function main/1 is unused">>, + range => {{6, 0}, {6, 4}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints), + Telemetry = wait_for_compiler_telemetry(), + #{ + type := Type, + uri := UriT, + diagnostics := DiagnosticsCodes + } = Telemetry, + ?assertEqual(<<"erlang-diagnostic-codes">>, Type), + Uri = ?config(diagnostics_uri, Config), + ?assertEqual(Uri, UriT), + ?assertEqual( + [<<"L1230">>, <<"L0000">>, <<"L0000">>, <<"L1295">>], + DiagnosticsCodes + ), + ok. -spec code_path_extra_dirs(config()) -> ok. code_path_extra_dirs(_Config) -> - RootPath = binary_to_list(els_test_utils:root_path()), - Dirs = [ AbsDir - || Dir <- filelib:wildcard("*", RootPath), - filelib:is_dir(AbsDir = filename:absname(Dir, RootPath))], - ?assertMatch(true, lists:all(fun(Elem) -> code:del_path(Elem) end, Dirs)), - ok. + RootPath = binary_to_list(els_test_utils:root_path()), + Dirs = [ + AbsDir + || Dir <- filelib:wildcard("*", RootPath), + filelib:is_dir(AbsDir = filename:absname(Dir, RootPath)) + ], + ?assertMatch(true, lists:all(fun(Elem) -> code:del_path(Elem) end, Dirs)), + ok. -spec use_long_names(config()) -> ok. use_long_names(_Config) -> - {ok, HostName} = inet:gethostname(), - NodeName = "my_node@" ++ - HostName ++ "." ++ - proplists:get_value(domain, inet:get_rc(), ""), - Node = list_to_atom(NodeName), - ?assertMatch(Node, els_config_runtime:get_node_name()), - ok. + HostName = els_config_runtime:get_hostname(), + NodeName = + "my_node@" ++ + HostName ++ "." ++ + els_config_runtime:get_domain(), + Node = list_to_atom(NodeName), + ?assertMatch(Node, els_config_runtime:get_node_name()), + ok. + +-spec use_long_names_no_domain(config()) -> ok. +use_long_names_no_domain(_Config) -> + HostName = els_config_runtime:get_hostname(), + NodeName = + "my_node@" ++ HostName, + Node = list_to_atom(NodeName), + ?assertMatch(Node, strip_local(els_config_runtime:get_node_name())), + ok. + +-spec use_long_names_custom_hostname(config()) -> ok. +use_long_names_custom_hostname(_Config) -> + HostName = els_config_runtime:get_hostname(), + NodeName = "my_node@127.0.0.1", + Node = list_to_atom(NodeName), + ?assertMatch(HostName, "127.0.0.1"), + ?assertMatch(Node, strip_local(els_config_runtime:get_node_name())), + ok. -spec epp_with_nonexistent_macro(config()) -> ok. epp_with_nonexistent_macro(_Config) -> - Path = include_path("nonexistent_macro.hrl"), - Source = <<"Compiler">>, - Errors = [ #{ code => <<"E1516">> - , message => <<"can't find include file \"nonexisten-file.hrl\"">> - , range => {{2, 0}, {3, 0}} - } - , #{ code => <<"E1507">> - , message => <<"undefined macro 'MODULE'">> - , range => {{4, 0}, {5, 0}} - } - , #{ code => <<"E1522">> - , message => <<"-error(\"including nonexistent_macro.hrl " - "is not allowed\").">> - , range => {{6, 0}, {7, 0}}} - ], - Warnings = [], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = include_path("nonexistent_macro.hrl"), + Source = <<"Compiler">>, + Errors = [ + #{ + code => <<"E1516">>, + message => <<"can't find include file \"nonexisten-file.hrl\"">>, + range => {{2, 0}, {3, 0}} + }, + #{ + code => <<"E1522">>, + message => << + "-error(\"including nonexistent_macro.hrl " + "is not allowed\")." + >>, + range => {{6, 0}, {7, 0}} + } + ], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec epp_with_builtin_macro(config()) -> ok. +epp_with_builtin_macro(_Config) -> + %% This should NOT trigger a diagnostic + Path = include_path("builtin_macros.hrl"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec elvis(config()) -> ok. elvis(_Config) -> - {ok, Cwd} = file:get_cwd(), - RootPath = els_test_utils:root_path(), - try - file:set_cwd(RootPath), - Path = src_path("elvis_diagnostics.erl"), - Source = <<"Elvis">>, - Errors = [], - Warnings = [ #{ code => operator_spaces - , message => <<"Missing space right \",\" on line 6">> - , range => {{5, 0}, {6, 0}} - , relatedInformation => [] - } - , #{ code => operator_spaces - , message => <<"Missing space right \",\" on line 7">> - , range => {{6, 0}, {7, 0}} - , relatedInformation => [] - } - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints) - catch _Err -> - file:set_cwd(Cwd) - end, - ok. + {ok, Cwd} = file:get_cwd(), + RootPath = els_test_utils:root_path(), + try + file:set_cwd(RootPath), + Path = src_path("elvis_diagnostics.erl"), + Source = <<"Elvis">>, + Errors = [], + Warnings = [ + #{ + code => operator_spaces, + message => <<"Missing space to the right of \",\" on line 6">>, + range => {{5, 0}, {6, 0}}, + relatedInformation => [] + }, + #{ + code => operator_spaces, + message => <<"Missing space to the right of \",\" on line 7">>, + range => {{6, 0}, {7, 0}}, + relatedInformation => [] + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints) + catch + _Err -> + file:set_cwd(Cwd) + end, + ok. -spec escript(config()) -> ok. escript(_Config) -> - Path = src_path("diagnostics.escript"), - Source = <<"Compiler">>, - els_test:run_diagnostics_test(Path, Source, [], [], []). + Path = src_path("diagnostics.escript"), + Source = <<"Compiler">>, + els_test:run_diagnostics_test(Path, Source, [], [], []). -spec escript_warnings(config()) -> ok. escript_warnings(_Config) -> - Path = src_path("diagnostics_warnings.escript"), - Source = <<"Compiler">>, - Errors = [], - Warnings = [ #{ code => <<"L1230">> - , message => <<"function unused/0 is unused">> - , range => {{23, 0}, {24, 0}} - } - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_warnings.escript"), + Source = <<"Compiler">>, + Errors = [], + Warnings = [ + #{ + code => <<"L1230">>, + message => <<"function unused/0 is unused">>, + range => {{23, 0}, {24, 0}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec escript_errors(config()) -> ok. escript_errors(_Config) -> - Path = src_path("diagnostics_errors.escript"), - Source = <<"Compiler">>, - Errors = [ #{ code => <<"P1711">> - , message => <<"syntax error before: tion_with_error">> - , range => {{23, 0}, {24, 0}} - } - ], - Warnings = [], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). - --spec code_reload(config()) -> ok. -code_reload(Config) -> - Uri = ?config(diagnostics_uri, Config), - Module = els_uri:module(Uri), - ok = els_compiler_diagnostics:on_complete(Uri, []), - {ok, HostName} = inet:gethostname(), - NodeName = list_to_atom("fakenode@" ++ HostName), - ?assert(meck:called(rpc, call, [NodeName, c, c, [Module]])), - ok. - --spec code_reload_sticky_mod(config()) -> ok. -code_reload_sticky_mod(Config) -> - Uri = ?config(diagnostics_uri, Config), - Module = els_uri:module(Uri), - {ok, HostName} = inet:gethostname(), - NodeName = list_to_atom("fakenode@" ++ HostName), - meck:expect( rpc - , call - , fun(PNode, code, is_sticky, [_]) when PNode =:= NodeName -> - true; - (Node, Mod, Fun, Args) -> - meck:passthrough([Node, Mod, Fun, Args]) - end - ), - ok = els_compiler_diagnostics:on_complete(Uri, []), - ?assert(meck:called(rpc, call, [NodeName, code, is_sticky, [Module]])), - ?assertNot(meck:called(rpc, call, [NodeName, c, c, [Module]])), - ok. + Path = src_path("diagnostics_errors.escript"), + Source = <<"Compiler">>, + Errors = [ + #{ + code => <<"P1711">>, + message => <<"syntax error before: tion_with_error">>, + range => {{23, 0}, {24, 0}} + } + ], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec crossref(config()) -> ok. crossref(_Config) -> - Path = src_path("diagnostics_xref.erl"), - Source = <<"CrossRef">>, - Errors = - [ #{ message => <<"Cannot find definition for function non_existing/0">> - , range => {{6, 2}, {6, 14}} - } - , #{ message => <<"Cannot find definition for function lists:map/3">> - , range => {{5, 2}, {5, 11}} - } - ], - Warnings = [], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_xref.erl"), + Source = <<"CrossRef">>, + Errors = + [ + #{ + message => <<"Cannot find definition for function non_existing/0">>, + range => {{6, 2}, {6, 14}} + }, + #{ + message => <<"Cannot find definition for function lists:map/3">>, + range => {{5, 8}, {5, 11}} + } + ], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec crossref_compiler_enabled(config()) -> ok. +crossref_compiler_enabled(_Config) -> + Path = src_path("diagnostics_xref.erl"), + Source = <<"CrossRef">>, + %% Don't expect diagnostics for missing local functions if compiler is enabled + Errors = + [ + #{ + message => <<"Cannot find definition for function lists:map/3">>, + range => {{5, 8}, {5, 11}} + } + ], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). %% #641 -spec crossref_pseudo_functions(config()) -> ok. crossref_pseudo_functions(_Config) -> - Path = src_path("diagnostics_xref_pseudo.erl"), - Errors = - [ #{ message => - <<"Cannot find definition for function " - "unknown_module:nonexistent/0">> - , range => {{34, 2}, {34, 28}} - } - , #{ message => - <<"Cannot find definition for function " - "unknown_module:module_info/1">> - , range => {{13, 2}, {13, 28}} - } - , #{ message => - <<"Cannot find definition for function " - "unknown_module:module_info/0">> - , range => {{12, 2}, {12, 28}} - } - ], - els_test:run_diagnostics_test(Path, <<"CrossRef">>, Errors, [], []). + Path = src_path("diagnostics_xref_pseudo.erl"), + Errors = + [ + #{ + message => <<"Cannot find module unknown_module">>, + range => {{36, 2}, {36, 16}} + }, + #{ + message => <<"Cannot find module unknown_module">>, + range => {{13, 2}, {13, 16}} + }, + #{ + message => <<"Cannot find module unknown_module">>, + range => {{12, 2}, {12, 16}} + } + ], + els_test:run_diagnostics_test(Path, <<"CrossRef">>, Errors, [], []). %% #860 -spec crossref_autoimport(config()) -> ok. crossref_autoimport(_Config) -> - %% This testcase cannot be run from an Erlang source tree version, - %% it needs a released version. - Path = src_path("diagnostics_autoimport.erl"), - els_test:run_diagnostics_test(Path, <<"CrossRef">>, [], [], []). + %% This testcase cannot be run from an Erlang source tree version, + %% it needs a released version. + Path = src_path("diagnostics_autoimport.erl"), + els_test:run_diagnostics_test(Path, <<"CrossRef">>, [], [], []). %% #860 -spec crossref_autoimport_disabled(config()) -> ok. crossref_autoimport_disabled(_Config) -> - %% This testcase cannot be run from an Erlang source tree version, - %% it needs a released version. - Path = src_path("diagnostics_autoimport_disabled.erl"), - els_test:run_diagnostics_test(Path, <<"CrossRef">>, [], [], []). + %% This testcase cannot be run from an Erlang source tree version, + %% it needs a released version. + Path = src_path("diagnostics_autoimport_disabled.erl"), + els_test:run_diagnostics_test(Path, <<"Quick CrossRef">>, [], [], []). -spec unused_includes(config()) -> ok. unused_includes(_Config) -> - Path = src_path("diagnostics_unused_includes.erl"), - Source = <<"UnusedIncludes">>, - Errors = [], - Warnings = [#{ message => <<"Unused file: et.hrl">> - , range => {{3, 0}, {3, 34}} - } - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_unused_includes.erl"), + Source = <<"UnusedIncludes">>, + Errors = [], + {ok, FileName} = els_utils:find_header( + els_utils:filename_to_atom("et/include/et.hrl") + ), + Warnings = [ + #{ + message => <<"Unused file: et.hrl">>, + range => {{3, 0}, {3, 34}}, + data => FileName + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec unused_includes_compiler_attribute(config()) -> ok. unused_includes_compiler_attribute(_Config) -> - Path = src_path("diagnostics_unused_includes_compiler_attribute.erl"), - Source = <<"UnusedIncludes">>, - Errors = [], - Warnings = [ #{ message => <<"Unused file: file.hrl">> - , range => {{3, 0}, {3, 40}} - } - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_unused_includes_compiler_attribute.erl"), + Source = <<"UnusedIncludes">>, + Errors = [], + {ok, FileName} = els_utils:find_header( + els_utils:filename_to_atom("kernel/include/file.hrl") + ), + Warnings = [ + #{ + message => <<"Unused file: file.hrl">>, + range => {{3, 0}, {3, 40}}, + data => FileName + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec unused_includes_broken(config()) -> ok. +unused_includes_broken(_Config) -> + Path = src_path("diagnostics_unused_includes_broken.erl"), + Source = <<"UnusedIncludes">>, + Errors = [], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec exclude_unused_includes(config()) -> ok. exclude_unused_includes(_Config) -> - Path = src_path("diagnostics_unused_includes.erl"), - Source = <<"UnusedIncludes">>, - Errors = [], - Warnings = [], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_unused_includes.erl"), + Source = <<"UnusedIncludes">>, + Errors = [], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec unused_macros(config()) -> ok. unused_macros(_Config) -> - Path = src_path("diagnostics_unused_macros.erl"), - Source = <<"UnusedMacros">>, - Errors = [], - Warnings = [ #{ message => <<"Unused macro: UNUSED_MACRO">> - , range => {{5, 8}, {5, 20}} - }, - #{ message => <<"Unused macro: UNUSED_MACRO_WITH_ARG/1">> - , range => {{6, 8}, {6, 29}} - } - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_unused_macros.erl"), + Source = <<"UnusedMacros">>, + Errors = [], + Warnings = [ + #{ + message => <<"Unused macro: UNUSED_MACRO">>, + range => {{5, 8}, {5, 20}} + }, + #{ + message => <<"Unused macro: UNUSED_MACRO_WITH_ARG/1">>, + range => {{6, 8}, {6, 29}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec unused_record_fields(config()) -> ok. unused_record_fields(_Config) -> - Path = src_path("diagnostics_unused_record_fields.erl"), - Source = <<"UnusedRecordFields">>, - Errors = [], - Warnings = - [ #{ message => <<"Unused record field: #unused_field.field_d">> - , range => {{5, 32}, {5, 39}} - } - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_unused_record_fields.erl"), + Source = <<"UnusedRecordFields">>, + Errors = [], + Warnings = + [ + #{ + message => <<"Unused record field: #unused_field.field_d">>, + range => {{5, 32}, {5, 39}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). -spec gradualizer(config()) -> ok. gradualizer(_Config) -> - Path = src_path("diagnostics_gradualizer.erl"), - Source = <<"Gradualizer">>, - Errors = [], - Warnings = [ #{ message => - <<"The variable N is expected to have type integer() " - "but it has type false | true\n">> - , range => {{10, 0}, {11, 0}}} - ], - Hints = [], - els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + Path = src_path("diagnostics_gradualizer.erl"), + Source = <<"Gradualizer">>, + Errors = [], + Warnings = [ + #{ + message => + << + "The variable N is expected to have type integer() " + "but it has type false | true\n" + >>, + range => {{10, 0}, {11, 0}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec eqwalizer(config()) -> ok. +eqwalizer(_Config) -> + Path = src_path("diagnostics_eqwalizer.erl"), + Source = <<"EqWAlizer">>, + Errors = [], + Warnings = [ + #{ + message => <<"Expected: 'ok'\nGot : 'not_ok'\n">>, + range => {{6, 4}, {6, 10}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec module_name_check(config()) -> ok. +module_name_check(_Config) -> + Path = src_path("diagnostics_module_name_check.erl"), + Source = <<"Compiler (via Erlang LS)">>, + Errors = [ + #{ + message => + << + "Module name 'module_name_check' does not match " + "file name 'diagnostics_module_name_check'" + >>, + range => {{0, 8}, {0, 25}} + } + ], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec module_name_check_whitespace(config()) -> ok. +module_name_check_whitespace(_Config) -> + Path = src_path("diagnostics module name check.erl"), + Source = <<"Compiler (via Erlang LS)">>, + Errors = [], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec edoc_main(config()) -> ok. +edoc_main(_Config) -> + Path = src_path("edoc_diagnostics.erl"), + Source = <<"Edoc">>, + Errors = [ + #{ + message => <<"`-quote ended unexpectedly at line 13">>, + range => {{12, 0}, {13, 0}} + } + ], + Warnings = [ + #{ + message => + <<"tag @mydoc not recognized.">>, + range => {{4, 0}, {5, 0}} + }, + #{ + message => + <<"tag @docc not recognized.">>, + range => {{8, 0}, {9, 0}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec edoc_skip_app_src(config()) -> ok. +edoc_skip_app_src(_Config) -> + Path = src_path("code_navigation.app.src"), + Source = <<"Edoc">>, + Errors = [], + Warnings = [], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +-spec edoc_custom_tags(config()) -> ok. +edoc_custom_tags(_Config) -> + %% Custom tags are defined in priv/code_navigation/erlang_ls.config + Path = src_path("edoc_diagnostics_custom_tags.erl"), + Source = <<"Edoc">>, + Errors = [], + Warnings = [ + #{ + message => + <<"tag @docc not recognized.">>, + range => {{9, 0}, {10, 0}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). + +% RefactorErl test cases +-spec unused_macros_refactorerl(config()) -> ok. +unused_macros_refactorerl(_Config) -> + Path = src_path("diagnostics_unused_macros.erl"), + Source = <<"RefactorErl">>, + Errors = [], + Warnings = [ + #{ + message => <<"Unused macro: UNUSED_MACRO">>, + range => {{5, 0}, {5, 35}} + }, + #{ + message => <<"Unused macro: UNUSED_MACRO_WITH_ARG">>, + range => {{6, 0}, {6, 36}} + } + ], + Hints = [], + els_test:run_diagnostics_test(Path, Source, Errors, Warnings, Hints). %%============================================================================== %% Internal Functions %%============================================================================== +strip_local(Node) -> + list_to_atom(strip_local(atom_to_list(Node), [])). -mock_rpc() -> - meck:new(rpc, [passthrough, no_link, unstick]), - {ok, HostName} = inet:gethostname(), - NodeName = list_to_atom("fakenode@" ++ HostName), - meck:expect( rpc - , call - , fun(PNode, c, c, [Module]) when PNode =:= NodeName -> - {ok, Module}; - (Node, Mod, Fun, Args) -> - meck:passthrough([Node, Mod, Fun, Args]) - end - ). - -unmock_rpc() -> - meck:unload(rpc). - -mock_code_reload_enabled() -> - meck:new(els_config, [passthrough, no_link]), - meck:expect( els_config - , get - , fun(code_reload) -> - {ok, HostName} = inet:gethostname(), - #{"node" => "fakenode@" ++ HostName}; - (Key) -> - meck:passthrough([Key]) - end - ). - -unmock_code_reload_enabled() -> - meck:unload(els_config). +strip_local([], Acc) -> + lists:reverse(Acc); +strip_local(".local", Acc) -> + lists:reverse(Acc); +strip_local([H | T], Acc) -> + strip_local(T, [H | Acc]). mock_compiler_telemetry_enabled() -> - meck:new(els_config, [passthrough, no_link]), - meck:expect( els_config - , get - , fun(compiler_telemetry_enabled) -> - true; - (Key) -> - meck:passthrough([Key]) - end - ), - Self = self(), - meck:expect( els_server - , send_notification - , fun(<<"telemetry/event">> = Method, Params) -> - Self ! {on_complete_telemetry, Params}, - meck:passthrough([Method, Params]); - (M, P) -> - meck:passthrough([M, P]) - end - ), - ok. + meck:new(els_config, [passthrough, no_link]), + meck:expect( + els_config, + get, + fun + (compiler_telemetry_enabled) -> + true; + (Key) -> + meck:passthrough([Key]) + end + ), + Self = self(), + meck:expect( + els_server, + send_notification, + fun + (<<"telemetry/event">> = Method, Params) -> + Self ! {on_complete_telemetry, Params}, + meck:passthrough([Method, Params]); + (M, P) -> + meck:passthrough([M, P]) + end + ), + ok. -spec wait_for_compiler_telemetry() -> {uri(), [els_diagnostics:diagnostic()]}. wait_for_compiler_telemetry() -> - receive - {on_complete_telemetry, Params} -> - Params - end. + receive + {on_complete_telemetry, #{type := <<"erlang-diagnostic-codes">>} = Params} -> + Params + end. unmock_compiler_telemetry_enabled() -> - meck:unload(els_config), - meck:unload(els_server). + meck:unload(els_config), + meck:unload(els_server). src_path(Module) -> - filename:join(["code_navigation", "src", Module]). + filename:join(["code_navigation", "src", Module]). include_path(Header) -> - filename:join(["code_navigation", "include", Header]). + filename:join(["code_navigation", "include", Header]). + +% Mock RefactorErl utils +mock_refactorerl() -> + {ok, HostName} = inet:gethostname(), + NodeName = list_to_atom("referl_fake@" ++ HostName), + + meck:new(els_refactorerl_utils, [passthrough, no_link, unstick]), + meck:expect( + els_refactorerl_utils, + run_diagnostics, + 2, + [ + { + #{ + 'end' => #{character => 35, line => 5}, + start => #{character => 0, line => 5} + }, + <<"Unused macro: UNUSED_MACRO">> + }, + { + #{ + 'end' => #{character => 36, line => 6}, + start => #{character => 0, line => 6} + }, + <<"Unused macro: UNUSED_MACRO_WITH_ARG">> + } + ] + ), + meck:expect(els_refactorerl_utils, referl_node, 0, {ok, NodeName}), + meck:expect(els_refactorerl_utils, add, 1, ok), + meck:expect(els_refactorerl_utils, source_name, 0, <<"RefactorErl">>), + + meck:new(els_refactorerl_diagnostics, [passthrough, no_link, unstick]), + meck:expect(els_refactorerl_diagnostics, is_default, 0, true). + +unmock_refactoerl() -> + meck:unload(els_refactorerl_diagnostics), + meck:unload(els_refactorerl_utils). + +-spec init_with_diagnostics(atom(), [binary()], config()) -> config(). +init_with_diagnostics(TestCase, Diags, Config0) -> + els_mock_diagnostics:setup(), + Config = els_test_utils:init_per_testcase(TestCase, Config0), + enable_diagnostics(Diags, Config). + +%% Enable given diagnostics and disable the rest +-spec enable_diagnostics([binary()], config()) -> config(). +enable_diagnostics(Diags, Config) -> + Available = els_diagnostics:available_diagnostics(), + OldDiagnosticsConfig = els_config:get(diagnostics), + Disabled = Available -- Diags, + DiagConfig = #{"enabled" => Diags, "disabled" => Disabled}, + els_config:set(diagnostics, DiagConfig), + [{old_diagnostics_config, OldDiagnosticsConfig} | Config]. + +-spec reset_diagnostics_config(config()) -> config(). +reset_diagnostics_config(Config) -> + case proplists:get_value(old_diagnostics_config, Config, undefined) of + undefined -> + Config; + DiagnosticsConfig -> + ok = els_config:set(diagnostics, DiagnosticsConfig), + Config + end. diff --git a/apps/els_lsp/test/els_docs_SUITE.erl b/apps/els_lsp/test/els_docs_SUITE.erl new file mode 100644 index 000000000..1097341ac --- /dev/null +++ b/apps/els_lsp/test/els_docs_SUITE.erl @@ -0,0 +1,130 @@ +-module(els_docs_SUITE). + +-include("els_lsp.hrl"). + +%% CT Callbacks +-export([ + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). + +%% Test cases +-export([ + memo_docs_true/1, + memo_docs_false/1, + invalidate/1 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +%%============================================================================== +%% Types +%%============================================================================== +-type config() :: [{atom(), any()}]. + +%%============================================================================== +%% CT Callbacks +%%============================================================================== +-spec all() -> [atom()]. +all() -> + []. + +%% FIXME: Suite disabled until we have figured out why it fails on CI +%% all() -> +%% els_test_utils:all(?MODULE). + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + els_test_utils:init_per_suite(Config). + +-spec end_per_suite(config()) -> ok. +end_per_suite(Config) -> + els_test_utils:end_per_suite(Config). + +-spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) -> + els_test_utils:init_per_testcase(TestCase, Config). + +-spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(TestCase, Config) -> + els_test_utils:end_per_testcase(TestCase, Config). + +%%============================================================================== +%% Testcases +%%============================================================================== +-spec memo_docs_true(config()) -> ok. +memo_docs_true(Config) -> + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "docs_memo_true.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + + %% Function + MFACT1 = {M1 = docs_memo, F1 = function, A1 = 0, 'local', function}, + Expected1 = els_docs:function_docs('local', M1, F1, A1), + {ok, [#{entries := Result1}]} = els_docs_memo:lookup(MFACT1), + ?assertEqual(Expected1, Result1), + + %% Type + MFACT2 = {M2 = docs_memo, F2 = type, A2 = 0, 'local', type}, + Expected2 = els_docs:type_docs('local', M2, F2, A2), + {ok, [#{entries := Result2}]} = els_docs_memo:lookup(MFACT2), + ?assertEqual(Expected2, Result2), + + ok. + +-spec memo_docs_false(config()) -> ok. +memo_docs_false(Config) -> + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "docs_memo_false.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + + %% Function + MFACT1 = {M1 = docs_memo, F1 = function, A1 = 0, 'local', function}, + els_docs:function_docs('local', M1, F1, A1), + {ok, []} = els_docs_memo:lookup(MFACT1), + + %% Type + MFACT2 = {M2 = docs_memo, F2 = type, A2 = 0, 'local', type}, + els_docs:type_docs('local', M2, F2, A2), + {ok, []} = els_docs_memo:lookup(MFACT2), + + ok. + +-spec invalidate(config()) -> ok. +invalidate(Config) -> + Uri = ?config(docs_memo_uri, Config), + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "docs_memo_true.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + meck:new(els_text_synchronization, [passthrough]), + + MFACT = {M = docs_memo, F = function, A = 0, 'local', function}, + + %% Did save + els_docs:function_docs('local', M, F, A), + {ok, [_]} = els_docs_memo:lookup(MFACT), + ok = els_client:did_save(Uri), + els_test_utils:wait_until_mock_called(els_text_synchronization, did_save), + {ok, []} = els_docs_memo:lookup(MFACT), + + %% Did change watched files + els_docs:function_docs('local', M, F, A), + {ok, [_]} = els_docs_memo:lookup(MFACT), + ok = els_client:did_change_watched_files([{Uri, ?FILE_CHANGE_TYPE_CHANGED}]), + els_test_utils:wait_until_mock_called(els_text_synchronization, did_change_watched_files), + {ok, []} = els_docs_memo:lookup(MFACT), + + ok. diff --git a/apps/els_lsp/test/els_docs_SUITE_data/docs_memo_false.config b/apps/els_lsp/test/els_docs_SUITE_data/docs_memo_false.config new file mode 100644 index 000000000..d7ebd33cb --- /dev/null +++ b/apps/els_lsp/test/els_docs_SUITE_data/docs_memo_false.config @@ -0,0 +1 @@ +docs_memo: false diff --git a/apps/els_lsp/test/els_docs_SUITE_data/docs_memo_true.config b/apps/els_lsp/test/els_docs_SUITE_data/docs_memo_true.config new file mode 100644 index 000000000..dfb7b5388 --- /dev/null +++ b/apps/els_lsp/test/els_docs_SUITE_data/docs_memo_true.config @@ -0,0 +1 @@ +docs_memo: true diff --git a/apps/els_lsp/test/els_document_highlight_SUITE.erl b/apps/els_lsp/test/els_document_highlight_SUITE.erl index 29a5e44c0..dc3328e82 100644 --- a/apps/els_lsp/test/els_document_highlight_SUITE.erl +++ b/apps/els_lsp/test/els_document_highlight_SUITE.erl @@ -1,41 +1,43 @@ -module(els_document_highlight_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ application_local/1 - , application_remote/1 - , application_imported/1 - , function_definition/1 - , fun_local/1 - , fun_remote/1 - , atom/1 - , quoted_atom/1 - , record_def/1 - , record_expr/1 - , record_access/1 - , record_field/1 - , export/1 - , export_entry/1 - , export_type_entry/1 - , import/1 - , import_entry/1 - , type/1 - , type_application/1 - , opaque/1 - , macro_define/1 - , macro/1 - , spec/1 - , behaviour/1 - , callback/1 - ]). +-export([ + application_local/1, + application_remote/1, + application_imported/1, + function_definition/1, + fun_local/1, + fun_remote/1, + atom/1, + quoted_atom/1, + record_def/1, + record_expr/1, + record_access/1, + record_field/1, + export/1, + export_entry/1, + export_type_entry/1, + import/1, + import_entry/1, + type/1, + type_application/1, + opaque/1, + macro_define/1, + macro/1, + spec/1, + behaviour/1, + callback/1 +]). %%============================================================================== %% Includes @@ -53,288 +55,287 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec application_local(config()) -> ok. application_local(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 22, 5), - ExpectedLocations = expected_definitions(), - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 22, 5), + ExpectedLocations = expected_definitions(), + assert_locations(ExpectedLocations, Locations), + ok. -spec application_remote(config()) -> ok. application_remote(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 32, 13), - ExpectedLocations = [ #{range => #{from => {32, 3}, to => {32, 27}}} - , #{range => #{from => {52, 8}, to => {52, 38}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 32, 13), + ExpectedLocations = [ + #{range => #{from => {32, 3}, to => {32, 27}}}, + #{range => #{from => {52, 8}, to => {52, 38}}} + ], + assert_locations(ExpectedLocations, Locations), + ok. -spec application_imported(config()) -> ok. application_imported(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 35, 4), - ExpectedLocations = [ #{range => #{from => {35, 3}, to => {35, 9}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 35, 4), + ExpectedLocations = [#{range => #{from => {35, 3}, to => {35, 9}}}], + assert_locations(ExpectedLocations, Locations), + ok. -spec function_definition(config()) -> ok. function_definition(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 25, 1), - ExpectedLocations = expected_definitions(), - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 25, 1), + ExpectedLocations = expected_definitions(), + assert_locations(ExpectedLocations, Locations), + ok. -spec fun_local(config()) -> ok. fun_local(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 51, 16), - ExpectedLocations = expected_definitions(), - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 51, 16), + ExpectedLocations = expected_definitions(), + assert_locations(ExpectedLocations, Locations), + ok. -spec fun_remote(config()) -> ok. fun_remote(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 52, 14), - ExpectedLocations = [ #{range => #{from => {32, 3}, to => {32, 27}}} - , #{range => #{from => {52, 8}, to => {52, 38}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 52, 14), + ExpectedLocations = [ + #{range => #{from => {32, 3}, to => {32, 27}}}, + #{range => #{from => {52, 8}, to => {52, 38}}} + ], + assert_locations(ExpectedLocations, Locations), + ok. -spec atom(config()) -> ok. atom(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 94, 5), - ExpectedLocations = [ #{range => #{from => {94, 4}, to => {94, 11}}} - , #{range => #{from => {33, 18}, to => {33, 25}}} - , #{range => #{from => {34, 19}, to => {34, 26}}} - , #{range => #{from => {16, 20}, to => {16, 27}}} - , #{range => #{from => {34, 44}, to => {34, 51}}} - , #{range => #{from => {111, 19}, to => {111, 26}}} - , #{range => #{from => {113, 33}, to => {113, 40}}} - , #{range => #{from => {116, 14}, to => {116, 21}}} - , #{range => #{from => {116, 39}, to => {116, 46}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 94, 5), + ExpectedLocations = [ + #{range => #{from => {94, 4}, to => {94, 11}}}, + #{range => #{from => {33, 18}, to => {33, 25}}}, + #{range => #{from => {34, 19}, to => {34, 26}}}, + #{range => #{from => {16, 20}, to => {16, 27}}}, + #{range => #{from => {34, 44}, to => {34, 51}}}, + #{range => #{from => {111, 19}, to => {111, 26}}}, + #{range => #{from => {113, 33}, to => {113, 40}}}, + #{range => #{from => {116, 14}, to => {116, 21}}}, + #{range => #{from => {116, 39}, to => {116, 46}}} + ], + assert_locations(ExpectedLocations, Locations), + ok. -spec quoted_atom(config()) -> ok. quoted_atom(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations1} = els_client:document_highlight(Uri, 98, 1), - ExpectedLocations1 = [ #{range => #{from => {98, 1}, to => {98, 21}}} - , #{range => #{from => {5, 67}, to => {5, 89}}} - ], - assert_locations(ExpectedLocations1, Locations1), - #{result := Locations2} = els_client:document_highlight(Uri, 101, 20), - ExpectedLocations2 = [ #{range => #{from => {101, 5}, to => {101, 77}}} - ], - assert_locations(ExpectedLocations2, Locations2), - #{result := Locations3} = els_client:document_highlight(Uri, 99, 18), - ExpectedLocations3 = [ #{range => #{from => {99, 18}, to => {99, 27}}} - , #{range => #{from => {16, 38}, to => {16, 47}}} - ], - assert_locations(ExpectedLocations3, Locations3), - #{result := Locations4} = els_client:document_highlight(Uri, 100, 12), - ExpectedLocations4 = [ #{range => #{from => {100, 7}, to => {100, 43}}} - ], - assert_locations(ExpectedLocations4, Locations4), - #{result := Locations5} = els_client:document_highlight(Uri, 97, 48), - ExpectedLocations5 = [ #{range => #{from => {97, 34}, to => {97, 68}}} - ], - assert_locations(ExpectedLocations5, Locations5), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations1} = els_client:document_highlight(Uri, 98, 1), + ExpectedLocations1 = [ + #{range => #{from => {98, 1}, to => {98, 21}}}, + #{range => #{from => {5, 67}, to => {5, 89}}} + ], + assert_locations(ExpectedLocations1, Locations1), + #{result := Locations2} = els_client:document_highlight(Uri, 101, 20), + ExpectedLocations2 = [#{range => #{from => {101, 5}, to => {101, 77}}}], + assert_locations(ExpectedLocations2, Locations2), + #{result := Locations3} = els_client:document_highlight(Uri, 99, 18), + ExpectedLocations3 = [ + #{range => #{from => {99, 18}, to => {99, 27}}}, + #{range => #{from => {16, 38}, to => {16, 47}}} + ], + assert_locations(ExpectedLocations3, Locations3), + #{result := Locations4} = els_client:document_highlight(Uri, 100, 12), + ExpectedLocations4 = [#{range => #{from => {100, 7}, to => {100, 43}}}], + assert_locations(ExpectedLocations4, Locations4), + #{result := Locations5} = els_client:document_highlight(Uri, 97, 48), + ExpectedLocations5 = [#{range => #{from => {97, 34}, to => {97, 68}}}], + assert_locations(ExpectedLocations5, Locations5), + ok. -spec record_def(config()) -> ok. record_def(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 16, 10), - ExpectedLocations = record_uses(), - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 16, 10), + ExpectedLocations = record_uses(), + assert_locations(ExpectedLocations, Locations), + ok. -spec record_expr(config()) -> ok. record_expr(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 23, 4), - ExpectedLocations = record_uses(), - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 23, 4), + ExpectedLocations = record_uses(), + assert_locations(ExpectedLocations, Locations), + ok. -spec record_access(config()) -> ok. record_access(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 34, 10), - ExpectedLocations = record_uses(), - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 34, 10), + ExpectedLocations = record_uses(), + assert_locations(ExpectedLocations, Locations), + ok. -spec record_field(config()) -> ok. record_field(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 16, 23), - ExpectedLocations = [ #{range => #{from => {33, 18}, to => {33, 25}}} - , #{range => #{from => {16, 20}, to => {16, 27}}} - , #{range => #{from => {34, 19}, to => {34, 26}}} - , #{range => #{from => {34, 44}, to => {34, 51}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 16, 23), + ExpectedLocations = [ + #{range => #{from => {33, 18}, to => {33, 25}}}, + #{range => #{from => {16, 20}, to => {16, 27}}}, + #{range => #{from => {34, 19}, to => {34, 26}}}, + #{range => #{from => {34, 44}, to => {34, 51}}} + ], + assert_locations(ExpectedLocations, Locations), + ok. -spec export(config()) -> ok. export(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 5, 5), - ExpectedLocations = [ #{range => #{from => {5, 1}, to => {5, 108}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 5, 5), + ExpectedLocations = [#{range => #{from => {5, 1}, to => {5, 108}}}], + assert_locations(ExpectedLocations, Locations), + ok. -spec export_entry(config()) -> ok. export_entry(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 5, 25), - ExpectedLocations = expected_definitions(), - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 5, 25), + ExpectedLocations = expected_definitions(), + assert_locations(ExpectedLocations, Locations), + ok. -spec export_type_entry(config()) -> ok. export_type_entry(Config) -> - Uri = ?config(code_navigation_types_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 5, 16), - ExpectedLocations = [ #{range => #{from => {5, 16}, to => {5, 24}}} - %% Should also include the definition, but does not - %%, #{range => #{from => {3, 7}, to => {3, 13}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_types_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 5, 16), + ExpectedLocations = [ + #{range => #{from => {5, 16}, to => {5, 24}}} + %% Should also include the definition, but does not + %%, #{range => #{from => {3, 7}, to => {3, 13}}} + ], + assert_locations(ExpectedLocations, Locations), + ok. -spec import(config()) -> ok. import(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 10, 3), - ExpectedLocations = null, - %% Should include this range - %% ExpectedLocations = [ #{range => #{from => {10, 1}, to => {10, 51}}} - %% ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 10, 3), + ExpectedLocations = null, + %% Should include this range + %% ExpectedLocations = [ #{range => #{from => {10, 1}, to => {10, 51}}} + %% ], + assert_locations(ExpectedLocations, Locations), + ok. -spec import_entry(config()) -> ok. import_entry(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 10, 34), - ExpectedLocations = [ #{range => #{from => {10, 34}, to => {10, 38}}} - %% Should include uses of function but does not - %% , #{range => #{from => {90, 3}, to => {90, 5}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 10, 34), + ExpectedLocations = [ + #{range => #{from => {10, 34}, to => {10, 38}}} + %% Should include uses of function but does not + %% , #{range => #{from => {90, 3}, to => {90, 5}}} + ], + assert_locations(ExpectedLocations, Locations), + ok. -spec type(config()) -> ok. type(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 37, 9), - ExpectedLocations = [ #{range => #{from => {37, 1}, to => {37, 25}}} - %% Should also include the usage, but does not - %%, #{range => #{from => {55, 23}, to => {55, 29}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 37, 9), + ExpectedLocations = [ + #{range => #{from => {37, 1}, to => {37, 25}}} + %% Should also include the usage, but does not + %%, #{range => #{from => {55, 23}, to => {55, 29}}} + ], + assert_locations(ExpectedLocations, Locations), + ok. -spec type_application(config()) -> ok. type_application(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 55, 57), - ExpectedLocations = [ #{range => #{from => {55, 55}, to => {55, 62}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 55, 57), + ExpectedLocations = [#{range => #{from => {55, 55}, to => {55, 62}}}], + assert_locations(ExpectedLocations, Locations), + ok. -spec opaque(config()) -> ok. opaque(Config) -> - Uri = ?config(code_navigation_types_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 7, 9), - ExpectedLocations = [ #{range => #{from => {7, 1}, to => {7, 35}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_types_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 7, 9), + ExpectedLocations = [#{range => #{from => {7, 1}, to => {7, 35}}}], + assert_locations(ExpectedLocations, Locations), + ok. -spec macro_define(config()) -> ok. macro_define(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 18, 10), + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 18, 10), - ExpectedLocations = macro_uses(), - assert_locations(ExpectedLocations, Locations), - ok. + ExpectedLocations = macro_uses(), + assert_locations(ExpectedLocations, Locations), + ok. -spec macro(config()) -> ok. macro(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 26, 6), + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 26, 6), - ExpectedLocations = macro_uses(), - assert_locations(ExpectedLocations, Locations), - ok. + ExpectedLocations = macro_uses(), + assert_locations(ExpectedLocations, Locations), + ok. -spec spec(config()) -> ok. spec(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 55, 11), - %% The entire "-spec ... ." is part of the poi range - ExpectedLocations = [ #{range => #{from => {55, 1}, to => {55, 65}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 55, 11), + %% The entire "-spec ... ." is part of the poi range + ExpectedLocations = [#{range => #{from => {55, 1}, to => {55, 65}}}], + assert_locations(ExpectedLocations, Locations), + ok. -spec behaviour(config()) -> ok. behaviour(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 3, 1), - ExpectedLocations = [ #{range => #{from => {3, 1}, to => {3, 25}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 3, 1), + ExpectedLocations = [#{range => #{from => {3, 1}, to => {3, 25}}}], + assert_locations(ExpectedLocations, Locations), + ok. -spec callback(config()) -> ok. callback(Config) -> - Uri = ?config(rename_uri, Config), - #{result := Locations} = els_client:document_highlight(Uri, 3, 10), - ExpectedLocations = [ #{range => #{from => {3, 1}, to => {3, 34}}} - ], - assert_locations(ExpectedLocations, Locations), - ok. + Uri = ?config(rename_uri, Config), + #{result := Locations} = els_client:document_highlight(Uri, 3, 10), + ExpectedLocations = [#{range => #{from => {3, 1}, to => {3, 34}}}], + assert_locations(ExpectedLocations, Locations), + ok. %%============================================================================== %% Internal functions @@ -342,58 +343,81 @@ callback(Config) -> -spec assert_locations([map()] | null, [map()] | null) -> ok. assert_locations(null, null) -> - ok; + ok; assert_locations(ExpectedLocations, Locations) -> - SortFun = fun(#{ range := #{ start := #{ line := StartLineA, - character := StartCharA }, - 'end' := #{ line := EndLineA, - character := EndCharA } } }, - #{ range := #{ start := #{ line := StartLineB, - character := StartCharB }, - 'end' := #{ line := EndLineB, - character := EndCharB } } }) -> - {{StartLineA, StartCharA}, {EndLineA, EndCharA}} - =< - {{StartLineB, StartCharB}, {EndLineB, EndCharB}} - end, - ExpectedProtoLocs = lists:sort(SortFun, protocol_ranges(ExpectedLocations)), - SortedLocations = lists:sort(SortFun, Locations), - ?assertEqual(length(ExpectedLocations), length(Locations)), - Pairs = lists:zip(SortedLocations, ExpectedProtoLocs), - [ begin - #{range := Range} = Location, - #{range := ExpectedRange} = Expected, - ?assertEqual(ExpectedRange, Range) - end - || {Location, Expected} <- Pairs - ], - ok. + SortFun = fun( + #{ + range := #{ + start := #{ + line := StartLineA, + character := StartCharA + }, + 'end' := #{ + line := EndLineA, + character := EndCharA + } + } + }, + #{ + range := #{ + start := #{ + line := StartLineB, + character := StartCharB + }, + 'end' := #{ + line := EndLineB, + character := EndCharB + } + } + } + ) -> + {{StartLineA, StartCharA}, {EndLineA, EndCharA}} =< + {{StartLineB, StartCharB}, {EndLineB, EndCharB}} + end, + ExpectedProtoLocs = lists:sort(SortFun, protocol_ranges(ExpectedLocations)), + SortedLocations = lists:sort(SortFun, Locations), + ?assertEqual(length(ExpectedLocations), length(Locations)), + Pairs = lists:zip(SortedLocations, ExpectedProtoLocs), + [ + begin + #{range := Range} = Location, + #{range := ExpectedRange} = Expected, + ?assertEqual(ExpectedRange, Range) + end + || {Location, Expected} <- Pairs + ], + ok. protocol_ranges(Locations) -> - [ L#{range => els_protocol:range(R)} - || L = #{range := R} <- Locations ]. + [ + L#{range => els_protocol:range(R)} + || L = #{range := R} <- Locations + ]. -spec expected_definitions() -> [map()]. expected_definitions() -> - [ #{range => #{from => {25, 1}, to => {25, 11}}} - , #{range => #{from => {22, 3}, to => {22, 13}}} - , #{range => #{from => {51, 7}, to => {51, 23}}} - , #{range => #{from => {5, 25}, to => {5, 37}}} - ]. + [ + #{range => #{from => {25, 1}, to => {25, 11}}}, + #{range => #{from => {22, 3}, to => {22, 13}}}, + #{range => #{from => {51, 7}, to => {51, 23}}}, + #{range => #{from => {5, 25}, to => {5, 37}}} + ]. -spec record_uses() -> [map()]. record_uses() -> - [ #{range => #{from => {16, 9}, to => {16, 17}}} - , #{range => #{from => {23, 3}, to => {23, 12}}} - , #{range => #{from => {33, 7}, to => {33, 16}}} - , #{range => #{from => {34, 9}, to => {34, 18}}} - , #{range => #{from => {34, 34}, to => {34, 43}}} - , #{range => #{from => {99, 8}, to => {99, 17}}} - ]. + [ + #{range => #{from => {16, 9}, to => {16, 17}}}, + #{range => #{from => {23, 3}, to => {23, 12}}}, + #{range => #{from => {33, 7}, to => {33, 16}}}, + #{range => #{from => {34, 9}, to => {34, 18}}}, + #{range => #{from => {34, 34}, to => {34, 43}}}, + #{range => #{from => {99, 8}, to => {99, 17}}} + ]. -spec macro_uses() -> [map()]. macro_uses() -> - [ #{range => #{from => {18, 9}, to => {18, 16}}} - , #{range => #{from => {26, 3}, to => {26, 11}}} - , #{range => #{from => {75, 23}, to => {75, 31}}} - ]. + [ + #{range => #{from => {18, 9}, to => {18, 16}}}, + #{range => #{from => {26, 3}, to => {26, 11}}}, + #{range => #{from => {75, 23}, to => {75, 31}}} + ]. diff --git a/apps/els_lsp/test/els_document_symbol_SUITE.erl b/apps/els_lsp/test/els_document_symbol_SUITE.erl index dbf7028e8..4a0c27bab 100644 --- a/apps/els_lsp/test/els_document_symbol_SUITE.erl +++ b/apps/els_lsp/test/els_document_symbol_SUITE.erl @@ -1,18 +1,17 @@ -module(els_document_symbol_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ functions/1 - ]). - +-export([symbols/1]). -include("els_lsp.hrl"). @@ -32,75 +31,166 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== --spec functions(config()) -> ok. -functions(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Symbols} = els_client:document_symbol(Uri), - Expected = [ #{ kind => ?SYMBOLKIND_FUNCTION, - location => - #{ range => - #{ 'end' => #{character => ToC, line => ToL}, - start => #{character => FromC, line => FromL} - }, - uri => Uri - }, - name => Name - } || {Name, {FromL, FromC}, {ToL, ToC}} - <- lists:append([functions()])], - ?assertEqual(length(Expected), length(Symbols)), - Pairs = lists:zip(lists:sort(Expected), lists:sort(Symbols)), - [?assertEqual(E, S) || {E, S} <- Pairs], - ok. +-spec symbols(config()) -> ok. +symbols(Config) -> + Uri = ?config(code_navigation_uri, Config), + #{result := Symbols} = els_client:document_symbol(Uri), + Expected = lists:append([ + expected_functions(Uri), + expected_macros(Uri), + expected_records(Uri), + expected_types(Uri) + ]), + ?assertEqual(length(Expected), length(Symbols)), + Pairs = lists:zip(lists:sort(Expected), lists:sort(Symbols)), + [?assertEqual(E, S) || {E, S} <- Pairs], + ok. %%============================================================================== %% Internal Functions %%============================================================================== +expected_functions(Uri) -> + [ + #{ + kind => ?SYMBOLKIND_FUNCTION, + location => + #{ + range => + #{ + 'end' => #{character => ToC, line => ToL}, + start => #{character => FromC, line => FromL} + }, + uri => Uri + }, + name => Name + } + || {Name, {FromL, FromC}, {ToL, ToC}} <- lists:append([functions()]) + ]. + +expected_macros(Uri) -> + [ + #{ + kind => ?SYMBOLKIND_CONSTANT, + location => + #{ + range => + #{ + 'end' => #{character => ToC, line => ToL}, + start => #{character => FromC, line => FromL} + }, + uri => Uri + }, + name => Name + } + || {Name, {FromL, FromC}, {ToL, ToC}} <- lists:append([macros()]) + ]. + +expected_records(Uri) -> + [ + #{ + kind => ?SYMBOLKIND_STRUCT, + location => + #{ + range => + #{ + 'end' => #{character => ToC, line => ToL}, + start => #{character => FromC, line => FromL} + }, + uri => Uri + }, + name => Name + } + || {Name, {FromL, FromC}, {ToL, ToC}} <- lists:append([records()]) + ]. + +expected_types(Uri) -> + [ + #{ + kind => ?SYMBOLKIND_TYPE_PARAMETER, + location => + #{ + range => + #{ + 'end' => #{character => ToC, line => ToL}, + start => #{character => FromC, line => FromL} + }, + uri => Uri + }, + name => Name + } + || {Name, {FromL, FromC}, {ToL, ToC}} <- lists:append([types()]) + ]. + functions() -> - [ {<<"function_a/0">>, {20, 0}, {20, 10}} - , {<<"function_b/0">>, {24, 0}, {24, 10}} - , {<<"callback_a/0">>, {27, 0}, {27, 10}} - , {<<"function_c/0">>, {30, 0}, {30, 10}} - , {<<"function_d/0">>, {38, 0}, {38, 10}} - , {<<"function_e/0">>, {41, 0}, {41, 10}} - , {<<"function_f/0">>, {46, 0}, {46, 10}} - , {<<"function_g/1">>, {49, 0}, {49, 10}} - , {<<"function_h/0">>, {55, 0}, {55, 10}} - , {<<"function_i/0">>, {59, 0}, {59, 10}} - , {<<"function_i/0">>, {61, 0}, {61, 10}} - , {<<"function_j/0">>, {66, 0}, {66, 10}} - , {<<"function_k/0">>, {73, 0}, {73, 10}} - , {<<"function_l/2">>, {78, 0}, {78, 10}} - , {<<"function_m/1">>, {83, 0}, {83, 10}} - , {<<"function_n/0">>, {88, 0}, {88, 10}} - , {<<"function_o/0">>, {92, 0}, {92, 10}} - , {<<"'PascalCaseFunction'/1">>, {97, 0}, {97, 20}} - , {<<"function_p/1">>, {102, 0}, {102, 10}} - , {<<"function_q/0">>, {113, 0}, {113, 10}} - , {<<"macro_b/2">>, {119, 0}, {119, 7}} - , {<<"function_mb/0">>, {122, 0}, {122, 11}} - ]. + [ + {<<"function_a/0">>, {20, 0}, {22, 14}}, + {<<"function_b/0">>, {24, 0}, {25, 11}}, + {<<"callback_a/0">>, {27, 0}, {28, 5}}, + {<<"function_c/0">>, {30, 0}, {34, 20}}, + {<<"function_d/0">>, {38, 0}, {39, 14}}, + {<<"function_e/0">>, {41, 0}, {42, 25}}, + {<<"function_f/0">>, {46, 0}, {47, 11}}, + {<<"function_g/1">>, {49, 0}, {52, 70}}, + {<<"function_h/0">>, {55, 0}, {56, 15}}, + {<<"function_i/0">>, {59, 0}, {59, 20}}, + {<<"function_i/0">>, {61, 0}, {61, 20}}, + {<<"function_j/0">>, {66, 0}, {67, 5}}, + {<<"function_k/0">>, {73, 0}, {75, 13}}, + {<<"function_l/2">>, {78, 0}, {80, 6}}, + {<<"function_m/1">>, {83, 0}, {85, 36}}, + {<<"function_n/0">>, {88, 0}, {89, 8}}, + {<<"function_o/0">>, {92, 0}, {93, 18}}, + {<<"PascalCaseFunction/1">>, {97, 0}, {100, 78}}, + {<<"function_p/1">>, {102, 0}, {107, 13}}, + {<<"function_q/0">>, {113, 0}, {115, 47}}, + {<<"macro_b/2">>, {119, 0}, {120, 5}}, + {<<"function_mb/0">>, {122, 0}, {123, 17}}, + {<<"code_navigation/0">>, {125, 0}, {125, 37}}, + {<<"code_navigation/1">>, {127, 0}, {127, 24}}, + {<<"multiple_instances_same_file/0">>, {129, 0}, {129, 74}}, + {<<"code_navigation_extra/3">>, {131, 0}, {131, 67}}, + {<<"multiple_instances_diff_file/0">>, {133, 0}, {133, 56}} + ]. + +macros() -> + [ + {<<"macro_A">>, {44, 8}, {44, 15}}, + {<<"MACRO_B">>, {117, 8}, {117, 15}}, + {<<"MACRO_A">>, {17, 8}, {17, 15}}, + {<<"MACRO_A/1">>, {18, 8}, {18, 15}}, + {<<"MACRO A">>, {19, 8}, {19, 17}} + ]. + +records() -> + [ + {<<"record_a">>, {15, 8}, {15, 16}}, + {<<"?MODULE">>, {110, 8}, {110, 15}} + ]. + +types() -> + [{<<"type_a/0">>, {36, 0}, {36, 24}}]. diff --git a/apps/els_lsp/test/els_execute_command_SUITE.erl b/apps/els_lsp/test/els_execute_command_SUITE.erl index 13eb112bf..31d6ae9c2 100644 --- a/apps/els_lsp/test/els_execute_command_SUITE.erl +++ b/apps/els_lsp/test/els_execute_command_SUITE.erl @@ -1,26 +1,33 @@ -module(els_execute_command_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ els_lsp_info/1 - , ct_run_test/1 - , strip_server_prefix/1 - , suggest_spec/1 - ]). +-export([ + els_lsp_info/1, + ct_run_test/1, + strip_server_prefix/1, + suggest_spec/1, + extract_function/1, + extract_function_case/1, + extract_function_tuple/1, + extract_function_list_comp/1 +]). %%============================================================================== %% Includes %%============================================================================== -include_lib("common_test/include/ct.hrl"). -include_lib("stdlib/include/assert.hrl"). +-include_lib("els_lsp/include/els_lsp.hrl"). %%============================================================================== %% Types @@ -32,45 +39,61 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). -init_per_testcase(ct_run_test, Config0) -> - Config = els_test_utils:init_per_testcase(ct_run_test, Config0), - setup_mocks(), - Config; +init_per_testcase(TestCase, Config0) when + TestCase =:= ct_run_test; + TestCase =:= extract_function; + TestCase =:= extract_function_case; + TestCase =:= extract_function_tuple; + TestCase =:= extract_function_list_comp +-> + Config = els_test_utils:init_per_testcase(TestCase, Config0), + setup_mocks(), + Config; init_per_testcase(suggest_spec, Config0) -> - Config = els_test_utils:init_per_testcase(suggest_spec, Config0), - meck:new(els_protocol, [passthrough, no_link]), - meck:expect( els_protocol, request, 3 - , fun(RequestId, Method, Params) -> - meck:passthrough([RequestId, Method, Params]) - end), - Config; + Config = els_test_utils:init_per_testcase(suggest_spec, Config0), + meck:new(els_protocol, [passthrough, no_link]), + meck:expect( + els_protocol, + request, + 3, + fun(RequestId, Method, Params) -> + meck:passthrough([RequestId, Method, Params]) + end + ), + Config; init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. -end_per_testcase(ct_run_test, Config) -> - teardown_mocks(), - els_test_utils:end_per_testcase(ct_run_test, Config); +end_per_testcase(TestCase, Config) when + TestCase =:= ct_run_test; + TestCase =:= extract_function; + TestCase =:= extract_function_case; + TestCase =:= extract_function_tuple; + TestCase =:= extract_function_list_comp +-> + teardown_mocks(), + els_test_utils:end_per_testcase(TestCase, Config); end_per_testcase(suggest_spec, Config) -> - meck:unload(els_protocol), - els_test_utils:end_per_testcase(ct_run_test, Config); + meck:unload(els_protocol), + els_test_utils:end_per_testcase(suggest_spec, Config); end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases @@ -78,137 +101,328 @@ end_per_testcase(TestCase, Config) -> -spec els_lsp_info(config()) -> ok. els_lsp_info(Config) -> - Uri = ?config(code_navigation_uri, Config), - PrefixedCommand = els_command:with_prefix(<<"server-info">>), - #{result := Result} - = els_client:workspace_executecommand(PrefixedCommand, [#{uri => Uri}]), - Expected = [], - ?assertEqual(Expected, Result), - Notifications = wait_for_notifications(2), - [ begin - ?assertEqual(maps:get(method, Notification), <<"window/showMessage">>), - Params = maps:get(params, Notification), - ?assertEqual(<<"Erlang LS (in code_navigation), ">> - , binary:part(maps:get(message, Params), 0, 32)) - end || Notification <- Notifications ], - ok. + Uri = ?config(code_navigation_uri, Config), + PrefixedCommand = els_command:with_prefix(<<"server-info">>), + #{result := Result} = + els_client:workspace_executecommand(PrefixedCommand, [#{uri => Uri}]), + Expected = [], + ?assertEqual(Expected, Result), + Notifications = wait_for_notifications(2), + [ + begin + ?assertEqual(maps:get(method, Notification), <<"window/showMessage">>), + Params = maps:get(params, Notification), + ?assertEqual( + <<"Erlang LS (in code_navigation), ">>, + binary:part(maps:get(message, Params), 0, 32) + ) + end + || Notification <- Notifications + ], + ok. -spec ct_run_test(config()) -> ok. ct_run_test(Config) -> - Uri = ?config(sample_SUITE_uri, Config), - PrefixedCommand = els_command:with_prefix(<<"ct-run-test">>), - #{result := Result} - = els_client:workspace_executecommand( PrefixedCommand - , [#{ uri => Uri - , module => sample_SUITE - , function => one - , arity => 1 - , line => 58 - }]), - Expected = [], - ?assertEqual(Expected, Result), - els_test_utils:wait_until_mock_called(els_protocol, notification), - ?assertEqual(1, meck:num_calls(els_distribution_server, rpc_call, '_')), - Notifications = [{Method, Args} || - { _Pid - , { els_protocol - , notification - , [<<"textDocument/publishDiagnostics">> = Method, Args]} - , _Result - } <- meck:history(els_protocol)], - ?assertEqual([{ <<"textDocument/publishDiagnostics">> - , #{diagnostics => - [ #{ message => <<"Test passed!">> - , range => - #{ 'end' => #{character => 0, line => 58} - , start => #{character => 0, line => 57}} - , severity => 3 - , source => <<"Common Test">>}], - uri => Uri} - }] - , Notifications), - ok. + Uri = ?config(sample_SUITE_uri, Config), + PrefixedCommand = els_command:with_prefix(<<"ct-run-test">>), + #{result := Result} = + els_client:workspace_executecommand( + PrefixedCommand, + [ + #{ + uri => Uri, + module => sample_SUITE, + function => one, + arity => 1, + line => 58 + } + ] + ), + Expected = [], + ?assertEqual(Expected, Result), + els_test_utils:wait_until_mock_called(els_protocol, notification), + ?assertEqual(1, meck:num_calls(els_distribution_server, rpc_call, '_')), + Notifications = [ + {Method, Args} + || {_Pid, {els_protocol, notification, [<<"textDocument/publishDiagnostics">> = Method, Args]}, + _Result} <- meck:history(els_protocol) + ], + ?assertEqual( + [ + {<<"textDocument/publishDiagnostics">>, #{ + diagnostics => + [ + #{ + message => <<"Test passed!">>, + range => + #{ + 'end' => #{character => 0, line => 58}, + start => #{character => 0, line => 57} + }, + severity => 3, + source => <<"Common Test">> + } + ], + uri => Uri + }} + ], + Notifications + ), + ok. -spec suggest_spec(config()) -> ok. suggest_spec(Config) -> - Uri = ?config(execute_command_suggest_spec_uri, Config), - PrefixedCommand = els_command:with_prefix(<<"suggest-spec">>), - #{result := Result} - = els_client:workspace_executecommand( - PrefixedCommand - , [#{ uri => Uri - , line => 12 - , spec => <<"-spec without_spec(number(),binary()) -> " - "{number(),binary()}.">> - }]), - Expected = [], - ?assertEqual(Expected, Result), - Pattern = ['_', <<"workspace/applyEdit">>, '_'], - ok = meck:wait(1, els_protocol, request, Pattern, 5000), - History = meck:history(els_protocol), - [Edit] = [Params || { _Pid, { els_protocol - , request - , [_RequestId, <<"workspace/applyEdit">>, Params]} - , _Binary - } <- History], - #{edit := #{changes := #{Uri := [#{ newText := NewText - , range := Range}]}}} = Edit, - ?assertEqual(<<"-spec without_spec(number(),binary()) -> " - "{number(),binary()}.\n" - "without_spec(A, B) when is_binary(B) ->\n">> - , NewText), - ?assertEqual(#{ 'end' => #{ character => 0 - , line => 12 - } - , start => #{ character => 0 - , line => 11 - }}, Range), - ok. + Uri = ?config(execute_command_suggest_spec_uri, Config), + PrefixedCommand = els_command:with_prefix(<<"suggest-spec">>), + #{result := Result} = + els_client:workspace_executecommand( + PrefixedCommand, + [ + #{ + uri => Uri, + line => 12, + spec => << + "-spec without_spec(number(),binary()) -> " + "{number(),binary()}." + >> + } + ] + ), + Expected = [], + ?assertEqual(Expected, Result), + [Edit] = get_edits_from_meck_history(), + #{ + edit := #{ + changes := #{ + Uri := [ + #{ + newText := NewText, + range := Range + } + ] + } + } + } = Edit, + ?assertEqual( + << + "-spec without_spec(number(),binary()) -> " + "{number(),binary()}.\n" + "without_spec(A, B) when is_binary(B) ->\n" + >>, + NewText + ), + ?assertEqual( + #{ + 'end' => #{ + character => 0, + line => 12 + }, + start => #{ + character => 0, + line => 11 + } + }, + Range + ), + ok. -spec strip_server_prefix(config()) -> ok. strip_server_prefix(_Config) -> - PrefixedCommand = els_command:with_prefix(<<"server-info">>), - ?assertEqual( <<"server-info">> - , els_command:without_prefix(PrefixedCommand)), + PrefixedCommand = els_command:with_prefix(<<"server-info">>), + ?assertEqual( + <<"server-info">>, + els_command:without_prefix(PrefixedCommand) + ), - ?assertEqual( <<"server-info">> - , els_command:without_prefix(<<"123:server-info">>)), + ?assertEqual( + <<"server-info">>, + els_command:without_prefix(<<"123:server-info">>) + ), - ?assertEqual( <<"server-info">> - , els_command:without_prefix(<<"server-info">>)), + ?assertEqual( + <<"server-info">>, + els_command:without_prefix(<<"server-info">>) + ), - ?assertEqual( <<"server-info:f">> - , els_command:without_prefix(<<"13:server-info:f">>)), - ok. + ?assertEqual( + <<"server-info:f">>, + els_command:without_prefix(<<"13:server-info:f">>) + ), + ok. -spec setup_mocks() -> ok. setup_mocks() -> - meck:new(els_protocol, [passthrough, no_link]), - meck:expect( els_distribution_server, rpc_call, 4 - , fun(_, _, _, _) -> {ok, <<"Test passed!">>} end), - meck:expect( els_protocol, notification, 2 - , fun(Method, Params) -> - meck:passthrough([Method, Params]) - end), - ok. + meck:new(els_protocol, [passthrough, no_link]), + meck:expect( + els_distribution_server, + rpc_call, + 4, + fun(_, _, _, _) -> {ok, <<"Test passed!">>} end + ), + meck:expect( + els_protocol, + notification, + 2, + fun(Method, Params) -> + meck:passthrough([Method, Params]) + end + ), + ok. + +-spec extract_function(config()) -> ok. +extract_function(Config) -> + Uri = ?config(extract_function_uri, Config), + execute_command_refactor_extract(Uri, {5, 8}, {5, 17}), + [#{edit := #{changes := #{Uri := Changes}}}] = get_edits_from_meck_history(), + [ + #{ + newText := <<"new_function(A, B, C)">>, + range := #{ + start := #{character := 8, line := 5}, + 'end' := #{character := 17, line := 5} + } + }, + #{ + newText := << + "new_function(A, B, C) ->\n" + " A + B + C.\n\n" + >>, + range := #{ + 'end' := #{character := 0, line := 15}, + start := #{character := 0, line := 15} + } + } + ] = Changes. + +-spec extract_function_case(config()) -> ok. +extract_function_case(Config) -> + Uri = ?config(extract_function_uri, Config), + execute_command_refactor_extract(Uri, {6, 8}, {6, 12}), + [#{edit := #{changes := #{Uri := Changes}}}] = get_edits_from_meck_history(), + [ + #{ + newText := <<"new_function(A)">>, + range := #{ + start := #{character := 8, line := 6}, + 'end' := #{character := 11, line := 9} + } + }, + #{ + newText := + << + "new_function(A) ->\n" + " case A of\n" + " 1 -> one;\n" + " _ -> other\n" + " end.\n\n" + >>, + range := + #{ + 'end' := #{character := 0, line := 15}, + start := #{character := 0, line := 15} + } + } + ] = Changes. + +-spec extract_function_list_comp(config()) -> ok. +extract_function_list_comp(Config) -> + Uri = ?config(extract_function_uri, Config), + execute_command_refactor_extract(Uri, {13, 4}, {13, 32}), + [#{edit := #{changes := #{Uri := Changes}}}] = get_edits_from_meck_history(), + [ + #{ + range := + #{ + start := #{line := 13, character := 4}, + 'end' := #{line := 13, character := 32} + }, + newText := <<"new_function(A, B, C)">> + }, + #{ + range := + #{ + start := #{line := 15, character := 0}, + 'end' := #{line := 15, character := 0} + }, + newText := + << + "new_function(A, B, C) ->\n" + " [X || X <- [A, B, C], X > 1].\n\n" + >> + } + ] = Changes. + +-spec extract_function_tuple(config()) -> ok. +extract_function_tuple(Config) -> + Uri = ?config(extract_function_uri, Config), + execute_command_refactor_extract(Uri, {11, 8}, {11, 18}), + [#{edit := #{changes := #{Uri := Changes}}}] = get_edits_from_meck_history(), + [ + #{ + newText := <<"new_function(A, B),">>, + range := #{ + start := #{character := 8, line := 11}, + 'end' := #{character := 18, line := 11} + } + }, + #{ + newText := + << + "new_function(A, B) ->" + "\n {A, B, A}." + "\n\n" + >>, + range := + #{ + 'end' := #{character := 0, line := 15}, + start := #{character := 0, line := 15} + } + } + ] = Changes. + +-spec execute_command_refactor_extract(uri(), pos(), pos()) -> ok. +execute_command_refactor_extract(Uri, {FromL, FromC}, {ToL, ToC}) -> + PrefixedCommand = els_command:with_prefix(<<"refactor.extract">>), + #{result := []} = + els_client:workspace_executecommand( + PrefixedCommand, + [ + #{ + uri => Uri, + range => #{ + start => #{character => FromC, line => FromL}, + 'end' => #{character => ToC, line => ToL} + } + } + ] + ), + ok. + +-spec get_edits_from_meck_history() -> [map()]. +get_edits_from_meck_history() -> + Pattern = ['_', <<"workspace/applyEdit">>, '_'], + ok = meck:wait(1, els_protocol, request, Pattern, 5000), + History = meck:history(els_protocol), + [Edit || {_, {_, _, [1, <<"workspace/applyEdit">>, Edit]}, _} <- History]. -spec teardown_mocks() -> ok. teardown_mocks() -> - meck:unload(els_protocol), - ok. + meck:unload(els_protocol), + ok. -spec wait_for_notifications(pos_integer()) -> [map()]. wait_for_notifications(Num) -> - wait_for_notifications(Num, []). + wait_for_notifications(Num, []). -spec wait_for_notifications(integer(), [map()]) -> [map()]. wait_for_notifications(Num, Acc) when Num =< 0 -> - Acc; + Acc; wait_for_notifications(Num, Acc) -> - CheckFun = fun() -> case els_client:get_notifications() of - [] -> false; - Notifications -> {true, Notifications} - end - end, - {ok, Notifications} = els_test_utils:wait_for_fun(CheckFun, 10, 3), - wait_for_notifications(Num - length(Notifications), Acc). + CheckFun = fun() -> + case els_client:get_notifications() of + [] -> false; + Notifications -> {true, Notifications} + end + end, + {ok, Notifications} = els_test_utils:wait_for_fun(CheckFun, 10, 3), + wait_for_notifications(Num - length(Notifications), Acc). diff --git a/apps/els_lsp/test/els_foldingrange_SUITE.erl b/apps/els_lsp/test/els_foldingrange_SUITE.erl index cd0b8bce4..3b67929c2 100644 --- a/apps/els_lsp/test/els_foldingrange_SUITE.erl +++ b/apps/els_lsp/test/els_foldingrange_SUITE.erl @@ -3,17 +3,17 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ folding_range/1 - ]). +-export([folding_range/1]). %%============================================================================== %% Includes @@ -31,27 +31,27 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases @@ -59,108 +59,21 @@ end_per_testcase(TestCase, Config) -> -spec folding_range(config()) -> ok. folding_range(Config) -> - #{result := Result} = - els_client:folding_range(?config(code_navigation_uri, Config)), - Expected = [ #{ endCharacter => ?END_OF_LINE - , endLine => 22 - , startCharacter => ?END_OF_LINE - , startLine => 20 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 25 - , startCharacter => ?END_OF_LINE - , startLine => 24 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 28 - , startCharacter => ?END_OF_LINE - , startLine => 27 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 34 - , startCharacter => ?END_OF_LINE - , startLine => 30 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 39 - , startCharacter => ?END_OF_LINE - , startLine => 38 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 42 - , startCharacter => ?END_OF_LINE - , startLine => 41 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 47 - , startCharacter => ?END_OF_LINE - , startLine => 46 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 52 - , startCharacter => ?END_OF_LINE - , startLine => 49 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 56 - , startCharacter => ?END_OF_LINE - , startLine => 55 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 67 - , startCharacter => ?END_OF_LINE - , startLine => 66 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 75 - , startCharacter => ?END_OF_LINE - , startLine => 73 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 80 - , startCharacter => ?END_OF_LINE - , startLine => 78 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 85 - , startCharacter => ?END_OF_LINE - , startLine => 83 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 89 - , startCharacter => ?END_OF_LINE - , startLine => 88 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 93 - , startCharacter => ?END_OF_LINE - , startLine => 92 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 100 - , startCharacter => ?END_OF_LINE - , startLine => 97 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 107 - , startCharacter => ?END_OF_LINE - , startLine => 102 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 115 - , startCharacter => ?END_OF_LINE - , startLine => 113 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 120 - , startCharacter => ?END_OF_LINE - , startLine => 119 - } - , #{ endCharacter => ?END_OF_LINE - , endLine => 123 - , startCharacter => ?END_OF_LINE - , startLine => 122 - } - ], - ?assertEqual(Expected, Result), - ok. + #{result := Result} = + els_client:folding_range(?config(folding_ranges_uri, Config)), + Expected = [ + #{ + endCharacter => ?END_OF_LINE, + endLine => 3, + startCharacter => ?END_OF_LINE, + startLine => 2 + }, + #{ + endCharacter => ?END_OF_LINE, + endLine => 10, + startCharacter => ?END_OF_LINE, + startLine => 7 + } + ], + ?assertEqual(Expected, Result), + ok. diff --git a/apps/els_lsp/test/els_formatter_SUITE.erl b/apps/els_lsp/test/els_formatter_SUITE.erl index 35f7b7cd8..7b641a4ed 100644 --- a/apps/els_lsp/test/els_formatter_SUITE.erl +++ b/apps/els_lsp/test/els_formatter_SUITE.erl @@ -3,17 +3,17 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ format_doc/1 - ]). +-export([format_doc/1]). %%============================================================================== %% Includes @@ -31,51 +31,72 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) when + TestCase == format_doc +-> + case els_utils:is_windows() of + true -> + %% TODO: Testcase fails on windows since OTP 24, fix! + {skip, "Testcase not supported on Windows."}; + false -> + els_test_utils:init_per_testcase(TestCase, Config) + end; init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec format_doc(config()) -> ok. format_doc(Config) -> - {ok, Cwd} = file:get_cwd(), - RootPath = els_test_utils:root_path(), - try - file:set_cwd(RootPath), - Uri = ?config(format_input_uri, Config), - #{result := Result} = els_client:document_formatting(Uri, 8, true), - ?assertEqual( - [#{newText => <<"-spec main(any()) -> any().\n">>, - range => - #{'end' => #{character => 0, line => 5}, - start => #{character => 0, line => 4}}}, - #{newText => <<" X.\n">>, - range => - #{'end' => #{character => 0, line => 9}, - start => #{character => 0, line => 6}}} - ] - , Result) - after - file:set_cwd(Cwd) - end, - ok. + {ok, Cwd} = file:get_cwd(), + RootPath = els_test_utils:root_path(), + try + file:set_cwd(RootPath), + Uri = ?config(format_input_uri, Config), + ok = els_config:set(formatting, #{}), + #{result := Result} = els_client:document_formatting(Uri, 8, true), + ?assertEqual( + [ + #{ + newText => <<"-spec main(any()) -> any().\n">>, + range => + #{ + 'end' => #{character => 0, line => 5}, + start => #{character => 0, line => 4} + } + }, + #{ + newText => <<" X.\n">>, + range => + #{ + 'end' => #{character => 0, line => 9}, + start => #{character => 0, line => 6} + } + } + ], + Result + ) + after + file:set_cwd(Cwd) + end, + ok. diff --git a/apps/els_lsp/test/els_fungraph_SUITE.erl b/apps/els_lsp/test/els_fungraph_SUITE.erl index 45e5bca5a..ff0f77ecf 100644 --- a/apps/els_lsp/test/els_fungraph_SUITE.erl +++ b/apps/els_lsp/test/els_fungraph_SUITE.erl @@ -8,24 +8,24 @@ -spec all() -> [atom()]. all() -> - [dense_graph_traversal]. + [dense_graph_traversal]. -spec dense_graph_traversal(_Config) -> ok. dense_graph_traversal(_) -> - MaxID = 40, - % Visited nodes must be unique - ?assertEqual( - lists:reverse(lists:seq(1, MaxID)), - els_fungraph:traverse( - fun(ID, _From, Acc) -> [ID | Acc] end, - [], - 1, - els_fungraph:new( - fun(ID) -> ID end, - fun(ID) -> - % Each node has edges to nodes in range [ID; ID * 2] - [NextID || NextID <- lists:seq(ID, ID * 2), NextID =< MaxID] - end - ) - ) - ). + MaxID = 40, + % Visited nodes must be unique + ?assertEqual( + lists:reverse(lists:seq(1, MaxID)), + els_fungraph:traverse( + fun(ID, _From, Acc) -> [ID | Acc] end, + [], + 1, + els_fungraph:new( + fun(ID) -> ID end, + fun(ID) -> + % Each node has edges to nodes in range [ID; ID * 2] + [NextID || NextID <- lists:seq(ID, ID * 2), NextID =< MaxID] + end + ) + ) + ). diff --git a/apps/els_lsp/test/els_hover_SUITE.erl b/apps/els_lsp/test/els_hover_SUITE.erl index 312c87184..9e06a4f96 100644 --- a/apps/els_lsp/test/els_hover_SUITE.erl +++ b/apps/els_lsp/test/els_hover_SUITE.erl @@ -3,37 +3,45 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ local_call_no_args/1 - , local_call_with_args/1 - , remote_call_multiple_clauses/1 - , local_call_edoc/1 - , remote_call_edoc/1 - , remote_call_otp/1 - , local_fun_expression/1 - , remote_fun_expression/1 - , no_poi/1 - , included_macro/1 - , local_macro/1 - , weird_macro/1 - , macro_with_zero_args/1 - , macro_with_args/1 - , local_record/1 - , included_record/1 - , local_type/1 - , remote_type/1 - , local_opaque/1 - , remote_opaque/1 - , nonexisting_type/1 - ]). +-export([ + local_call_no_args/1, + local_call_with_args/1, + remote_call_multiple_clauses/1, + local_call_edoc/1, + remote_call_edoc/1, + remote_call_otp/1, + local_fun_expression/1, + remote_fun_expression/1, + no_poi/1, + included_macro/1, + edoc_spec/1, + edoc_definition/1, + local_macro/1, + weird_macro/1, + macro_with_zero_args/1, + macro_with_args/1, + local_record/1, + included_record/1, + local_type/1, + local_type_definition/1, + remote_type/1, + local_opaque/1, + local_opaque_definition/1, + remote_opaque/1, + nonexisting_type/1, + nonexisting_module/1, + memoize/1 +]). %%============================================================================== %% Includes @@ -51,328 +59,635 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) when + TestCase == local_call_with_args; + TestCase == local_fun_expression; + TestCase == local_record +-> + case els_utils:is_windows() of + true -> + %% TODO: Testcase fails on windows since OTP 24, fix! + {skip, "Testcase not supported on Windows."}; + false -> + els_test_utils:init_per_testcase(TestCase, Config) + end; init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== local_call_no_args(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 10, 7), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = <<"## local_call/0">>, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + %% this test is for the fallback render when no doc chunks are available + CleanupMock = mock_doc_chunks_unavailable(), + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 10, 7), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = <<"## local_call/0">>, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + CleanupMock(), + ok. local_call_with_args(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 13, 7), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = <<"## local_call/2\n\n" - "---\n\n" - "```erlang\n\n" - " local_call(Arg1, Arg2) \n\n" - "```\n\n" - "```erlang\n" - "-spec local_call(integer(), any()) -> tuple();\n" - " (float(), any()) -> tuple().\n" - "```">>, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + %% this test is for the fallback render when no doc chunks are available + CleanupMock = mock_doc_chunks_unavailable(), + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 13, 7), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = << + "## local_call/2\n\n" + "---\n\n" + "```erlang\n\n" + " local_call(Arg1, Arg2) \n\n" + "```\n\n" + "```erlang\n" + "-spec local_call(integer(), any()) -> tuple();\n" + " (float(), any()) -> tuple().\n" + "```" + >>, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + CleanupMock(), + ok. remote_call_multiple_clauses(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 16, 15), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = <<"## hover_docs:multiple_clauses/1\n\n" - "---\n\n" - "```erlang\n\n" - " multiple_clauses(L) when is_list(L)\n\n" - " multiple_clauses(#{data := Data}) \n\n" - " multiple_clauses(X) \n\n```">>, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + %% this test is for the fallback render when no doc chunks are available + CleanupMock = mock_doc_chunks_unavailable(), + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 16, 15), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = << + "## hover_docs:multiple_clauses/1\n\n" + "---\n\n" + "```erlang\n\n" + " multiple_clauses(L) when is_list(L)\n\n" + " multiple_clauses(#{data := Data}) \n\n" + " multiple_clauses(X) \n\n```" + >>, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + CleanupMock(), + ok. local_call_edoc(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 29, 5), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = case has_eep48_edoc() of - true -> <<"```erlang\nedoc() -> ok.\n```\n\n---\n\nAn edoc hover item\n">>; - false -> <<"## edoc/0\n\n---\n\n```erlang\n-spec edoc() -> ok.\n```\n\n" - "An edoc hover item\n\n">> - end, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 29, 5), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = + case has_eep48_edoc() of + true -> + <<"```erlang\nedoc() -> ok.\n```\n\n---\n\nAn edoc hover item\n">>; + false -> + << + "## edoc/0\n\n---\n\n```erlang\n-spec edoc() -> ok.\n```\n\n" + "An edoc hover item\n\n" + >> + end, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + ok. remote_call_edoc(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 23, 12), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = case has_eep48_edoc() of - true -> <<"```erlang\nedoc() -> ok.\n```\n\n---\n\nAn edoc hover item\n">>; - false -> <<"## hover_docs:edoc/0\n\n---\n\n```erlang\n-spec edoc() -> ok.\n" - "```\n\nAn edoc hover item\n\n">> - end, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 23, 12), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = + case has_eep48_edoc() of + true -> + <<"```erlang\nedoc() -> ok.\n```\n\n---\n\nAn edoc hover item\n">>; + false -> + << + "## hover_docs:edoc/0\n\n---\n\n```erlang\n-spec edoc() -> ok.\n" + "```\n\nAn edoc hover item\n\n" + >> + end, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + ok. remote_call_otp(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 26, 12), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = case has_eep48(file) of - true -> <<"```erlang\nwrite(IoDevice, Bytes) -> ok | {error, Reason}\n" - "when\n IoDevice :: io_device() | atom(),\n Bytes :: iodata()," - "\n Reason :: posix() | badarg | terminated.\n```\n\n---\n\n" - "Writes `Bytes` to the file referenced by `IoDevice`\\. This " - "function is the only way to write to a file opened in `raw` " - "mode \\(although it works for normally opened files too\\)\\. " - "Returns `ok` if successful, and `{error, Reason}` otherwise\\." - "\n\nIf the file is opened with `encoding` set to something else " - "than `latin1`, each byte written can result in many bytes being " - "written to the file, as the byte range 0\\.\\.255 can represent " - "anything between one and four bytes depending on value and UTF " - "encoding type\\.\n\nTypical error reasons:\n\n* **`ebadf`** \n" - " The file is not opened for writing\\.\n\n* **`enospc`** \n" - " No space is left on the device\\.\n">>; - false -> <<"## file:write/2\n\n---\n\n```erlang\n\n write(File, Bytes) " - "when is_pid(File) orelse is_atom(File)\n\n write(#file_" - "descriptor{module = Module} = Handle, Bytes) \n\n " - "write(_, _) \n\n```\n\n```erlang\n-spec write(IoDevice, Bytes)" - " -> ok | {error, Reason} when\n IoDevice :: io_device() |" - " atom(),\n Bytes :: iodata(),\n Reason :: posix() | " - "badarg | terminated.\n```">> - end, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 26, 12), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + OtpRelease = list_to_integer(erlang:system_info(otp_release)), + Value = + case has_eep48(file) of + true when OtpRelease >= 27 -> + << + "```erlang\nwrite(IoDevice, Bytes) -> ok | {error, Reason}" + " when\n IoDevice :: io_device() | io:device(),\n" + " Bytes :: iodata(),\n" + " Reason :: posix() | badarg | terminated.\n" + "```\n\n---\n\n" + "```erlang\nWrites `Bytes` to the file referenced by" + " `IoDevice`. This function is the only\nway to write to a" + " file opened in `raw` mode (although it works for normally\n" + "opened files too). Returns `ok` if successful, and" + " `{error, Reason}` otherwise.\n\nIf the file is opened with" + " `encoding` set to something else than `latin1`, each\nbyte" + " written can result in many bytes being written to the file," + " as the byte\nrange 0..255 can represent anything between" + " one and four bytes depending on\nvalue and UTF encoding" + " type. If you want to write `t:unicode:chardata/0` to the\n" + "`IoDevice` you should use `io:put_chars/2` instead.\n\n" + "Typical error reasons:\n\n" + "- **`ebadf`** - The file is not opened for writing.\n\n" + "- **`enospc`** - No space is left on the device.\n```\n" + >>; + true when OtpRelease == 26 -> + << + "```erlang\nwrite(IoDevice, Bytes) -> ok | {error, " + "Reason}\nwhen\n IoDevice :: io_device() | io:device(),\n" + " Bytes :: iodata()," + "\n Reason :: posix() | badarg | terminated.\n```\n\n" + "---\n\n" + "Writes `Bytes` to the file referenced by `IoDevice`\\." + " This function is the only way to write to a file opened in" + " `raw` mode \\(although it works for normally opened files" + " too\\)\\. Returns `ok` if successful, and" + " `{error, Reason}` otherwise\\.\n\nIf the file is opened" + " with `encoding` set to something else than `latin1`," + " each byte written can result in many bytes being written" + " to the file, as the byte range 0\\.\\.255 can represent" + " anything between one and four bytes depending on value" + " and UTF encoding type\\. If you want to write" + " [`unicode:chardata()`](https://erlang.org/doc/man/unicode" + ".html#type-chardata) to the `IoDevice` you should use" + " [`io:put_chars/2`](https://erlang.org/doc/man/io.html" + "#put_chars-2) instead\\.\n\nTypical error reasons:\n\n" + "* **`ebadf`** \n The file is not opened for writing\\.\n\n" + "* **`enospc`** \n No space is left on the device\\.\n" + >>; + true -> + << + "```erlang\nwrite(IoDevice, Bytes) -> ok | {error, Reason}\n" + "when\n IoDevice :: io_device() | atom(),\n Bytes :: iodata()," + "\n Reason :: posix() | badarg | terminated.\n```\n\n---\n\n" + "Writes `Bytes` to the file referenced by `IoDevice`\\. This " + "function is the only way to write to a file opened in `raw` " + "mode \\(although it works for normally opened files too\\)\\. " + "Returns `ok` if successful, and `{error, Reason}` otherwise\\." + "\n\nIf the file is opened with `encoding` set to something else " + "than `latin1`, each byte written can result in many bytes being " + "written to the file, as the byte range 0\\.\\.255 can represent " + "anything between one and four bytes depending on value and UTF " + "encoding type\\.\n\nTypical error reasons:\n\n* **`ebadf`** \n" + " The file is not opened for writing\\.\n\n* **`enospc`** \n" + " No space is left on the device\\.\n" + >>; + false -> + << + "## file:write/2\n\n---\n\n```erlang\n\n write(File, Bytes) " + "when is_pid(File) orelse is_atom(File)\n\n write(#file_" + "descriptor{module = Module} = Handle, Bytes) \n\n " + "write(_, _) \n\n```\n\n```erlang\n-spec write(IoDevice, Bytes)" + " -> ok | {error, Reason} when\n IoDevice :: io_device() |" + " atom(),\n Bytes :: iodata(),\n Reason :: posix() | " + "badarg | terminated.\n```" + >> + end, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + ok. local_fun_expression(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 19, 5), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = <<"## local_call/2\n\n" - "---\n\n" - "```erlang\n\n" - " local_call(Arg1, Arg2) \n\n" - "```\n\n" - "```erlang\n" - "-spec local_call(integer(), any()) -> tuple();\n" - " (float(), any()) -> tuple().\n" - "```">>, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + %% this test is for the fallback render when no doc chunks are available + CleanupMock = mock_doc_chunks_unavailable(), + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 19, 5), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = << + "## local_call/2\n\n" + "---\n\n" + "```erlang\n\n" + " local_call(Arg1, Arg2) \n\n" + "```\n\n" + "```erlang\n" + "-spec local_call(integer(), any()) -> tuple();\n" + " (float(), any()) -> tuple().\n" + "```" + >>, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + CleanupMock(), + ok. remote_fun_expression(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 20, 10), - ?assert(maps:is_key(contents, Result)), - Contents = maps:get(contents, Result), - Value = <<"## hover_docs:multiple_clauses/1\n\n" - "---\n\n" - "```erlang\n\n" - " multiple_clauses(L) when is_list(L)\n\n" - " multiple_clauses(#{data := Data}) \n\n" - " multiple_clauses(X) \n\n```">>, - Expected = #{ kind => <<"markdown">> - , value => Value - }, - ?assertEqual(Expected, Contents), - ok. + %% this test is for the fallback render when no doc chunks are available + CleanupMock = mock_doc_chunks_unavailable(), + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 20, 10), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = << + "## hover_docs:multiple_clauses/1\n\n" + "---\n\n" + "```erlang\n\n" + " multiple_clauses(L) when is_list(L)\n\n" + " multiple_clauses(#{data := Data}) \n\n" + " multiple_clauses(X) \n\n```" + >>, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + CleanupMock(), + ok. + +edoc_definition(Config) -> + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 41, 3), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = + case has_eep48_edoc() of + true -> + <<"```erlang\nedoc() -> ok.\n```\n\n---\n\nAn edoc hover item\n">>; + false -> + << + "## edoc/0\n\n---\n\n```erlang\n-spec edoc() -> ok.\n```\n\n" + "An edoc hover item\n\n" + >> + end, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + ok. + +edoc_spec(Config) -> + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 40, 10), + ?assert(maps:is_key(contents, Result)), + Contents = maps:get(contents, Result), + Value = + case has_eep48_edoc() of + true -> + <<"```erlang\nedoc() -> ok.\n```\n\n---\n\nAn edoc hover item\n">>; + false -> + << + "## edoc/0\n\n---\n\n```erlang\n-spec edoc() -> ok.\n```\n\n" + "An edoc hover item\n\n" + >> + end, + Expected = #{ + kind => <<"markdown">>, + value => Value + }, + ?assertEqual(Expected, Contents), + ok. local_macro(Config) -> - Uri = ?config(hover_macro_uri, Config), - #{result := Result} = els_client:hover(Uri, 6, 4), - Value = <<"```erlang\n?LOCAL_MACRO = local_macro\n```">>, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_macro_uri, Config), + #{result := Result} = els_client:hover(Uri, 6, 4), + Value = <<"```erlang\n?LOCAL_MACRO = local_macro\n```">>, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. included_macro(Config) -> - Uri = ?config(hover_macro_uri, Config), - #{result := Result} = els_client:hover(Uri, 7, 4), - Value = <<"```erlang\n?INCLUDED_MACRO_A = included_macro_a\n```">>, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_macro_uri, Config), + #{result := Result} = els_client:hover(Uri, 7, 4), + Value = <<"```erlang\n?INCLUDED_MACRO_A = included_macro_a\n```">>, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. weird_macro(Config) -> - Uri = ?config(hover_macro_uri, Config), - #{result := Result} = els_client:hover(Uri, 12, 20), - Value = <<"```erlang\n?WEIRD_MACRO = A when A > 1\n```">>, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_macro_uri, Config), + #{result := Result} = els_client:hover(Uri, 12, 20), + Value = <<"```erlang\n?WEIRD_MACRO = A when A > 1\n```">>, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. macro_with_zero_args(Config) -> - Uri = ?config(hover_macro_uri, Config), - #{result := Result} = els_client:hover(Uri, 18, 10), - Value = <<"```erlang\n?MACRO_WITH_ARGS() = {macro}\n```">>, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_macro_uri, Config), + #{result := Result} = els_client:hover(Uri, 18, 10), + Value = <<"```erlang\n?MACRO_WITH_ARGS() = {macro}\n```">>, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. macro_with_args(Config) -> - Uri = ?config(hover_macro_uri, Config), - #{result := Result} = els_client:hover(Uri, 19, 10), - Value = <<"```erlang\n?MACRO_WITH_ARGS(X, Y) = {macro, X, Y}\n```">>, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_macro_uri, Config), + #{result := Result} = els_client:hover(Uri, 19, 10), + Value = <<"```erlang\n?MACRO_WITH_ARGS(X, Y) = {macro, X, Y}\n```">>, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. no_poi(Config) -> - Uri = ?config(hover_docs_caller_uri, Config), - #{result := Result} = els_client:hover(Uri, 10, 1), - ?assertEqual(null, Result), - ok. + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Result} = els_client:hover(Uri, 10, 1), + ?assertEqual(null, Result), + ok. local_record(Config) -> - Uri = ?config(hover_record_expr_uri, Config), - #{result := Result} = els_client:hover(Uri, 11, 4), - Value = <<"```erlang\n-record(test_record, {\n" - " field1 = 123,\n" - " field2 = xyzzy,\n" - " field3\n" - "}).\n```">>, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_record_expr_uri, Config), + #{result := Result} = els_client:hover(Uri, 11, 4), + Value = << + "```erlang\n-record(test_record, {\n" + " field1 = 123,\n" + " field2 = xyzzy,\n" + " field3\n" + "}).\n```" + >>, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. included_record(Config) -> - Uri = ?config(hover_record_expr_uri, Config), - #{result := Result} = els_client:hover(Uri, 15, 4), - Value = <<"```erlang\n" - "-record(included_record_a, {included_field_a, included_field_b})." - "\n```">>, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_record_expr_uri, Config), + #{result := Result} = els_client:hover(Uri, 15, 4), + Value = << + "```erlang\n" + "-record(included_record_a, {included_field_a, included_field_b})." + "\n```" + >>, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. local_type(Config) -> - Uri = ?config(hover_type_uri, Config), - #{result := Result} = els_client:hover(Uri, 6, 10), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-type type_a() :: any().\n```\n\n---\n\n\n">>; - false -> <<"```erlang\n-type type_a() :: any().\n```">> - end, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_type_uri, Config), + #{result := Result} = els_client:hover(Uri, 6, 10), + Value = + case has_eep48_edoc() of + true -> <<"```erlang\n-type type_a() :: any().\n```\n\n---\n\n\n">>; + false -> <<"```erlang\n-type type_a() :: any().\n```">> + end, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. + +local_type_definition(Config) -> + Uri = ?config(hover_type_uri, Config), + #{result := Result} = els_client:hover(Uri, 3, 8), + Value = + case has_eep48_edoc() of + true -> <<"```erlang\n-type type_a() :: any().\n```\n\n---\n\n\n">>; + false -> <<"```erlang\n-type type_a() :: any().\n```">> + end, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. remote_type(Config) -> - Uri = ?config(hover_type_uri, Config), - #{result := Result} = els_client:hover(Uri, 10, 10), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-type type_a() :: atom().\n```\n\n---\n\n\n">>; - false -> <<"```erlang\n-type type_a() :: atom().\n```">> - end, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_type_uri, Config), + #{result := Result} = els_client:hover(Uri, 10, 10), + Value = + case has_eep48_edoc() of + true -> <<"```erlang\n-type type_a() :: atom().\n```\n\n---\n\n\n">>; + false -> <<"```erlang\n-type type_a() :: atom().\n```">> + end, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. local_opaque(Config) -> - Uri = ?config(hover_type_uri, Config), - #{result := Result} = els_client:hover(Uri, 14, 10), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-opaque opaque_type_a() \n```\n\n---\n\n\n">>; - false -> <<"```erlang\n-opaque opaque_type_a() :: any().\n```">> - end, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_type_uri, Config), + #{result := Result} = els_client:hover(Uri, 14, 10), + Value = + case has_eep48_edoc() of + true -> <<"```erlang\n-opaque opaque_type_a() \n```\n\n---\n\n\n">>; + false -> <<"```erlang\n-opaque opaque_type_a() :: any().\n```">> + end, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. + +local_opaque_definition(Config) -> + Uri = ?config(hover_type_uri, Config), + #{result := Result} = els_client:hover(Uri, 4, 11), + Value = + case has_eep48_edoc() of + true -> <<"```erlang\n-opaque opaque_type_a() \n```\n\n---\n\n\n">>; + false -> <<"```erlang\n-opaque opaque_type_a() :: any().\n```">> + end, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. remote_opaque(Config) -> - Uri = ?config(hover_type_uri, Config), - #{result := Result} = els_client:hover(Uri, 18, 10), - Value = case has_eep48_edoc() of - true -> <<"```erlang\n-opaque opaque_type_a() \n```\n\n---\n\n\n">>; - false -> <<"```erlang\n-opaque opaque_type_a() :: atom().\n```">> - end, - Expected = #{contents => #{ kind => <<"markdown">> - , value => Value - }}, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_type_uri, Config), + #{result := Result} = els_client:hover(Uri, 18, 10), + Value = + case has_eep48_edoc() of + true -> <<"```erlang\n-opaque opaque_type_a() \n```\n\n---\n\n\n">>; + false -> <<"```erlang\n-opaque opaque_type_a() :: atom().\n```">> + end, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. nonexisting_type(Config) -> - Uri = ?config(hover_type_uri, Config), - #{result := Result} = els_client:hover(Uri, 22, 10), - Expected = null, - ?assertEqual(Expected, Result), - ok. + Uri = ?config(hover_type_uri, Config), + #{result := Result} = els_client:hover(Uri, 22, 15), + %% The spec for `j' is shown instead of the type docs. + Value = + case list_to_integer(erlang:system_info(otp_release)) >= 25 of + true -> + << + "```erlang\nj(_ :: doesnt:exist()) -> ok.\n```\n\n" + "---\n\n\n" + >>; + % els_eep48_docs:render returns {error, function_missing} for + % OTP versions under 25 + _ -> + << + "## j/1\n\n---\n\n```erlang\n\n j(_) \n\n```\n\n" + "```erlang\n-spec j(doesnt:exist()) -> ok.\n```" + >> + end, + Expected = #{ + contents => #{ + kind => <<"markdown">>, + value => Value + } + }, + ?assertEqual(Expected, Result), + ok. + +nonexisting_module(Config) -> + Uri = ?config(hover_nonexisting_uri, Config), + #{result := Result} = els_client:hover(Uri, 6, 12), + Expected = #{ + contents => + #{ + kind => <<"markdown">>, + value => <<"## nonexisting:main/0">> + } + }, + ?assertEqual(Expected, Result), + ok. + +memoize(Config) -> + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "docs_memo_true.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + + Uri = ?config(hover_docs_caller_uri, Config), + #{result := Expected} = els_client:hover(Uri, 41, 3), + {ok, [Item]} = els_docs_memo:lookup({hover_docs_caller, edoc, 0, 'local', function}), + #{entries := Entries} = Item, + + %% JSON RPC + Encoded = list_to_binary( + json:encode(#{contents => els_markup_content:new(Entries)}) + ), + Result = els_utils:json_decode_with_atom_keys(Encoded), + + ?assertEqual(Expected, Result), + ok. + +%%============================================================================== +%% Helpers +%%============================================================================== + +%% @doc +%% Returns a function that can be used to unload the mock. +%% @end +mock_doc_chunks_unavailable() -> + meck:expect( + els_docs, + eep48_docs, + fun(_, _, _, _) -> {error, not_available} end + ), + fun() -> + ok = meck:unload(els_docs) + end. has_eep48_edoc() -> - list_to_integer(erlang:system_info(otp_release)) >= 24. + list_to_integer(erlang:system_info(otp_release)) >= 24. + has_eep48(Module) -> - case catch code:get_doc(Module) of - {ok, _} -> true; - _ -> false - end. \ No newline at end of file + case catch code:get_doc(Module) of + {ok, {docs_v1, _, erlang, _, _, _, Docs}} -> + lists:any( + fun + ({_, _, _, Doc, _}) when is_map(Doc) -> true; + ({_, _, _, _, _}) -> false + end, + Docs + ); + _ -> + false + end. diff --git a/apps/els_lsp/test/els_hover_SUITE_data/docs_memo_true.config b/apps/els_lsp/test/els_hover_SUITE_data/docs_memo_true.config new file mode 100644 index 000000000..dfb7b5388 --- /dev/null +++ b/apps/els_lsp/test/els_hover_SUITE_data/docs_memo_true.config @@ -0,0 +1 @@ +docs_memo: true diff --git a/apps/els_lsp/test/els_implementation_SUITE.erl b/apps/els_lsp/test/els_implementation_SUITE.erl index e4fae0782..412379e81 100644 --- a/apps/els_lsp/test/els_implementation_SUITE.erl +++ b/apps/els_lsp/test/els_implementation_SUITE.erl @@ -3,18 +3,21 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ gen_server_call/1 - , callback/1 - ]). +-export([ + gen_server_call/1, + callback/1, + dynamic_call/1 +]). %%============================================================================== %% Includes @@ -32,27 +35,27 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases @@ -60,34 +63,67 @@ end_per_testcase(TestCase, Config) -> -spec gen_server_call(config()) -> ok. gen_server_call(Config) -> - Uri = ?config(my_gen_server_uri, Config), - #{result := Result} = els_client:implementation(Uri, 30, 10), - Expected = [ #{ range => - #{ 'end' => #{character => 4, line => 46} - , start => #{character => 0, line => 46} - } - , uri => Uri - } - ], - ?assertEqual(Expected, Result), - ok. + Uri = ?config(my_gen_server_uri, Config), + #{result := Result} = els_client:implementation(Uri, 30, 10), + Expected = [ + #{ + range => + #{ + 'end' => #{character => 4, line => 46}, + start => #{character => 0, line => 46} + }, + uri => Uri + } + ], + ?assertEqual(Expected, Result), + ok. -spec callback(config()) -> ok. callback(Config) -> - Uri = ?config(implementation_uri, Config), - #{result := Result} = els_client:implementation(Uri, 3, 20), - Expected = [ #{ range => - #{ 'end' => #{character => 17, line => 6} - , start => #{character => 0, line => 6} - } - , uri => ?config(implementation_a_uri, Config) - } - , #{ range => - #{ 'end' => #{character => 17, line => 6} - , start => #{character => 0, line => 6} - } - , uri => ?config(implementation_b_uri, Config) - } - ], - ?assertEqual(Expected, Result), - ok. + Uri = ?config(implementation_uri, Config), + #{result := Result} = els_client:implementation(Uri, 3, 20), + Expected = [ + #{ + range => + #{ + 'end' => #{character => 17, line => 6}, + start => #{character => 0, line => 6} + }, + uri => ?config(implementation_a_uri, Config) + }, + #{ + range => + #{ + 'end' => #{character => 17, line => 6}, + start => #{character => 0, line => 6} + }, + uri => ?config(implementation_b_uri, Config) + } + ], + ?assertEqual(Expected, Result), + ok. + +-spec dynamic_call(config()) -> ok. +dynamic_call(Config) -> + Uri = ?config(implementation_uri, Config), + #{result := Result} = els_client:implementation(Uri, 6, 14), + Expected = [ + #{ + range => + #{ + 'end' => #{character => 17, line => 6}, + start => #{character => 0, line => 6} + }, + uri => ?config(implementation_a_uri, Config) + }, + #{ + range => + #{ + 'end' => #{character => 17, line => 6}, + start => #{character => 0, line => 6} + }, + uri => ?config(implementation_b_uri, Config) + } + ], + ?assertEqual(Expected, Result), + ok. diff --git a/apps/els_lsp/test/els_indexer_SUITE.erl b/apps/els_lsp/test/els_indexer_SUITE.erl index 07d24433e..83a46f2f7 100644 --- a/apps/els_lsp/test/els_indexer_SUITE.erl +++ b/apps/els_lsp/test/els_indexer_SUITE.erl @@ -1,25 +1,31 @@ -module(els_indexer_SUITE). %% CT Callbacks --export([ all/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - ]). +-export([ + all/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2 +]). %% Test cases --export([ index_dir_not_dir/1 - , index_erl_file/1 - , index_hrl_file/1 - , index_unkown_extension/1 - ]). +-export([ + index_dir_not_dir/1, + index_erl_file/1, + index_hrl_file/1, + index_unkown_extension/1, + do_not_skip_generated_file_by_tag_by_default/1, + skip_generated_file_by_tag/1, + skip_generated_file_by_custom_tag/1 +]). %%============================================================================== %% Includes %%============================================================================== -include_lib("common_test/include/ct.hrl"). -include_lib("stdlib/include/assert.hrl"). +-include_lib("els_core/include/els_core.hrl"). %%============================================================================== %% Types @@ -31,60 +37,146 @@ %%============================================================================== -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) when + TestCase =:= skip_generated_file_by_tag +-> + meck:new(els_config_indexing, [passthrough, no_link]), + meck:expect(els_config_indexing, get_skip_generated_files, fun() -> true end), + els_test_utils:init_per_testcase(TestCase, Config); +init_per_testcase(TestCase, Config) when + TestCase =:= skip_generated_file_by_custom_tag +-> + meck:new(els_config_indexing, [passthrough, no_link]), + meck:expect( + els_config_indexing, + get_skip_generated_files, + fun() -> true end + ), + meck:expect( + els_config_indexing, + get_generated_files_tag, + fun() -> "@customgeneratedtag" end + ), + els_test_utils:init_per_testcase(TestCase, Config); init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(TestCase, Config) when + TestCase =:= skip_generated_file_by_tag +-> + meck:unload(els_config_indexing), + els_test_utils:end_per_testcase(TestCase, Config); end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec index_dir_not_dir(config()) -> ok. index_dir_not_dir(Config) -> - DataDir = ?config(data_dir, Config), - NotDirPath = filename:join(DataDir, "not_a_dir"), - file:write_file(NotDirPath, <<>>), - ok = els_utils:fold_files( fun(_, _) -> ok end - , fun(_) -> true end - , NotDirPath - , ok - ), - file:delete(NotDirPath), - ok. + DataDir = ?config(data_dir, Config), + NotDirPath = filename:join(DataDir, "not_a_dir"), + file:write_file(NotDirPath, <<>>), + ok = els_utils:fold_files( + fun(_, _) -> ok end, + fun(_) -> true end, + NotDirPath, + ok + ), + file:delete(NotDirPath), + ok. -spec index_erl_file(config()) -> ok. index_erl_file(Config) -> - DataDir = ?config(data_dir, Config), - Path = filename:join(els_utils:to_binary(DataDir), "test.erl"), - {ok, Uri} = els_indexing:index_file(Path), - {ok, [#{id := test, kind := module}]} = els_dt_document:lookup(Uri), - ok. + DataDir = ?config(data_dir, Config), + Path = filename:join(els_utils:to_binary(DataDir), "test.erl"), + {ok, Uri} = els_indexing:shallow_index(Path, app), + {ok, [#{id := test, kind := module}]} = els_dt_document:lookup(Uri), + ok. -spec index_hrl_file(config()) -> ok. index_hrl_file(Config) -> - DataDir = ?config(data_dir, Config), - Path = filename:join(els_utils:to_binary(DataDir), "test.hrl"), - {ok, Uri} = els_indexing:index_file(Path), - {ok, [#{id := test, kind := header}]} = els_dt_document:lookup(Uri), - ok. + DataDir = ?config(data_dir, Config), + Path = filename:join(els_utils:to_binary(DataDir), "test.hrl"), + {ok, Uri} = els_indexing:shallow_index(Path, app), + {ok, [#{id := test, kind := header}]} = els_dt_document:lookup(Uri), + ok. -spec index_unkown_extension(config()) -> ok. index_unkown_extension(Config) -> - DataDir = ?config(data_dir, Config), - Path = filename:join(els_utils:to_binary(DataDir), "test.foo"), - {ok, Uri} = els_indexing:index_file(Path), - {ok, [#{kind := other}]} = els_dt_document:lookup(Uri), - ok. + DataDir = ?config(data_dir, Config), + Path = filename:join(els_utils:to_binary(DataDir), "test.foo"), + {ok, Uri} = els_indexing:shallow_index(Path, app), + {ok, [#{kind := other}]} = els_dt_document:lookup(Uri), + ok. + +-spec do_not_skip_generated_file_by_tag_by_default(config()) -> ok. +do_not_skip_generated_file_by_tag_by_default(Config) -> + DataDir = data_dir(Config), + GeneratedByTagUri = uri(DataDir, "generated_file_by_tag.erl"), + GeneratedByCustomTagUri = uri(DataDir, "generated_file_by_custom_tag.erl"), + ?assertEqual({4, 0, 0}, els_indexing:index_dir(DataDir, app)), + {ok, [ + #{ + id := generated_file_by_tag, + kind := module + } + ]} = els_dt_document:lookup(GeneratedByTagUri), + {ok, [ + #{ + id := generated_file_by_custom_tag, + kind := module + } + ]} = els_dt_document:lookup(GeneratedByCustomTagUri), + ok. + +-spec skip_generated_file_by_tag(config()) -> ok. +skip_generated_file_by_tag(Config) -> + DataDir = data_dir(Config), + GeneratedByTagUri = uri(DataDir, "generated_file_by_tag.erl"), + GeneratedByCustomTagUri = uri(DataDir, "generated_file_by_custom_tag.erl"), + ?assertEqual({3, 1, 0}, els_indexing:index_dir(DataDir, app)), + {ok, []} = els_dt_document:lookup(GeneratedByTagUri), + {ok, [ + #{ + id := generated_file_by_custom_tag, + kind := module + } + ]} = els_dt_document:lookup(GeneratedByCustomTagUri), + ok. + +-spec skip_generated_file_by_custom_tag(config()) -> ok. +skip_generated_file_by_custom_tag(Config) -> + DataDir = data_dir(Config), + GeneratedByTagUri = uri(DataDir, "generated_file_by_tag.erl"), + GeneratedByCustomTagUri = uri(DataDir, "generated_file_by_custom_tag.erl"), + ?assertEqual({3, 1, 0}, els_indexing:index_dir(DataDir, app)), + {ok, [ + #{ + id := generated_file_by_tag, + kind := module + } + ]} = els_dt_document:lookup(GeneratedByTagUri), + {ok, []} = els_dt_document:lookup(GeneratedByCustomTagUri), + ok. + +-spec data_dir(proplists:proplist()) -> binary(). +data_dir(Config) -> + ?config(data_dir, Config). + +-spec uri(binary(), string()) -> uri(). +uri(DataDir, FileName) -> + Path = els_utils:to_binary(filename:join(DataDir, FileName)), + els_uri:uri(Path). diff --git a/apps/els_lsp/test/els_indexer_SUITE_data/generated_file_by_custom_tag.erl b/apps/els_lsp/test/els_indexer_SUITE_data/generated_file_by_custom_tag.erl new file mode 100644 index 000000000..8f5a6a40f --- /dev/null +++ b/apps/els_lsp/test/els_indexer_SUITE_data/generated_file_by_custom_tag.erl @@ -0,0 +1,7 @@ +%% This file is @customgeneratedtag +-module(generated_file_by_custom_tag). + +-export([main/0]). + +main() -> + ok. diff --git a/apps/els_lsp/test/els_indexer_SUITE_data/generated_file_by_tag.erl b/apps/els_lsp/test/els_indexer_SUITE_data/generated_file_by_tag.erl new file mode 100644 index 000000000..4f47a37b6 --- /dev/null +++ b/apps/els_lsp/test/els_indexer_SUITE_data/generated_file_by_tag.erl @@ -0,0 +1,7 @@ +%% This file is @generated +-module(generated_file_by_tag). + +-export([main/0]). + +main() -> + ok. diff --git a/apps/els_lsp/test/els_indexing_SUITE.erl b/apps/els_lsp/test/els_indexing_SUITE.erl index 212024754..79353edc1 100644 --- a/apps/els_lsp/test/els_indexing_SUITE.erl +++ b/apps/els_lsp/test/els_indexing_SUITE.erl @@ -1,17 +1,19 @@ -module(els_indexing_SUITE). %% CT Callbacks --export([ all/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - ]). +-export([ + all/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2 +]). %% Test cases --export([ index_otp/1 - , reindex_otp/1 - ]). +-export([ + index_otp/1, + reindex_otp/1 +]). %%============================================================================== %% Includes @@ -29,79 +31,80 @@ %%============================================================================== -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(_TestCase, Config) -> - {ok, Started} = application:ensure_all_started(els_lsp), - RootDir = code:root_dir(), - RootUri = els_uri:uri(els_utils:to_binary(RootDir)), - %% Do not index the entire list of OTP apps in the pipelines. - Cfg = #{"otp_apps_exclude" => otp_apps_exclude()}, - els_config:do_initialize(RootUri, [], #{}, {undefined, Cfg}), - [{started, Started}|Config]. + {ok, Started} = application:ensure_all_started(els_lsp), + RootDir = code:root_dir(), + RootUri = els_uri:uri(els_utils:to_binary(RootDir)), + %% Do not index the entire list of OTP apps in the pipelines. + Cfg = #{"otp_apps_exclude" => otp_apps_exclude()}, + els_config:do_initialize(RootUri, [], #{}, {undefined, Cfg}), + [{started, Started} | Config]. -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(_TestCase, Config) -> - [application:stop(App) || App <- ?config(started, Config)], - ok. + [application:stop(App) || App <- ?config(started, Config)], + ok. %%============================================================================== %% Testcases %%============================================================================== -spec index_otp(config()) -> ok. index_otp(_Config) -> - do_index_otp(). + do_index_otp(). -spec reindex_otp(config()) -> ok. reindex_otp(_Config) -> - do_index_otp(), - ok. + do_index_otp(), + ok. -spec do_index_otp() -> ok. do_index_otp() -> - [els_indexing:index_dir(Dir, 'shallow') || Dir <- els_config:get(otp_paths)], - ok. + [els_indexing:index_dir(Dir, otp) || Dir <- els_config:get(otp_paths)], + ok. -spec otp_apps_exclude() -> [string()]. otp_apps_exclude() -> - [ "asn1" - , "common_test" - , "compiler" - , "crypto" - , "debugger" - , "dialyzer" - , "diameter" - , "edoc" - , "eldap" - , "erl_docgen" - , "erl_interface" - , "et" - , "eunit" - , "ftp" - , "inets" - , "jinterface" - , "megaco" - , "mnesia" - , "observer" - , "os_mon" - , "otp_mibs" - , "parsetools" - , "reltool" - , "sasl" - , "snmp" - , "ssh" - , "ssl" - , "syntax_tools" - , "tftp" - , "xmerl" - , "wx" - ]. + [ + "asn1", + "common_test", + "compiler", + "crypto", + "debugger", + "dialyzer", + "diameter", + "edoc", + "eldap", + "erl_docgen", + "erl_interface", + "et", + "eunit", + "ftp", + "inets", + "jinterface", + "megaco", + "mnesia", + "observer", + "os_mon", + "otp_mibs", + "parsetools", + "reltool", + "sasl", + "snmp", + "ssh", + "ssl", + "syntax_tools", + "tftp", + "xmerl", + "wx" + ]. diff --git a/apps/els_lsp/test/els_initialization_SUITE.erl b/apps/els_lsp/test/els_initialization_SUITE.erl index 02ec0ec5f..2b300e690 100644 --- a/apps/els_lsp/test/els_initialization_SUITE.erl +++ b/apps/els_lsp/test/els_initialization_SUITE.erl @@ -3,25 +3,31 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ initialize_default/1 - , initialize_custom_relative/1 - , initialize_custom_absolute/1 - , initialize_diagnostics_default/1 - , initialize_diagnostics_custom/1 - , initialize_diagnostics_invalid/1 - , initialize_lenses_default/1 - , initialize_lenses_custom/1 - , initialize_lenses_invalid/1 - ]). +-export([ + initialize_default/1, + initialize_custom_relative/1, + initialize_custom_absolute/1, + initialize_diagnostics_default/1, + initialize_diagnostics_custom/1, + initialize_diagnostics_invalid/1, + initialize_lenses_default/1, + initialize_lenses_custom/1, + initialize_lenses_invalid/1, + initialize_providers_default/1, + initialize_providers_custom/1, + initialize_providers_invalid/1, + initialize_prepare_rename/1 +]). %%============================================================================== %% Includes @@ -39,30 +45,30 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -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(), - [{started, Started} | Config]. + meck:new(els_distribution_server, [no_link, passthrough]), + meck:expect(els_distribution_server, connect, 0, ok), + Started = els_test_utils:start(), + [{started, Started} | Config]. -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases @@ -70,129 +76,190 @@ end_per_testcase(TestCase, Config) -> -spec initialize_default(config()) -> ok. initialize_default(_Config) -> - RootUri = els_test_utils:root_uri(), - els_client:initialize(RootUri), - Result = els_config:get(macros), - Expected = [#{"name" => "DEFINED_WITHOUT_VALUE"}, - #{"name" => "DEFINED_WITH_VALUE", "value" => 1}], - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + els_client:initialize(RootUri), + Result = els_config:get(macros), + Expected = [ + #{"name" => "DEFINED_WITHOUT_VALUE"}, + #{"name" => "DEFINED_WITH_VALUE", "value" => 1} + ], + ?assertEqual(Expected, Result), + ok. -spec initialize_custom_relative(config()) -> ok. initialize_custom_relative(_Config) -> - RootUri = els_test_utils:root_uri(), - ConfigPath = <<"../rebar3_release/erlang_ls.config">>, - InitOpts = #{ <<"erlang">> - => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Result = els_config:get(macros), - Expected = [#{"name" => "DEFINED_FOR_RELATIVE_TEST"}], - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + ConfigPath = <<"../rebar3_release/erlang_ls.config">>, + InitOpts = #{ + <<"erlang">> => + #{<<"config_path">> => ConfigPath} + }, + els_client:initialize(RootUri, InitOpts), + Result = els_config:get(macros), + Expected = [#{"name" => "DEFINED_FOR_RELATIVE_TEST"}], + ?assertEqual(Expected, Result), + ok. -spec initialize_custom_absolute(config()) -> ok. initialize_custom_absolute(_Config) -> - RootUri = els_test_utils:root_uri(), - ConfigPath = filename:join( els_uri:path(RootUri) - , "../rebar3_release/erlang_ls.config"), - InitOpts = #{ <<"erlang">> - => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Result = els_config:get(macros), - Expected = [#{"name" => "DEFINED_FOR_RELATIVE_TEST"}], - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + ConfigPath = filename:join( + els_uri:path(RootUri), + "../rebar3_release/erlang_ls.config" + ), + InitOpts = #{ + <<"erlang">> => + #{<<"config_path">> => ConfigPath} + }, + els_client:initialize(RootUri, InitOpts), + Result = els_config:get(macros), + Expected = [#{"name" => "DEFINED_FOR_RELATIVE_TEST"}], + ?assertEqual(Expected, Result), + ok. -spec initialize_diagnostics_default(config()) -> ok. initialize_diagnostics_default(Config) -> - RootUri = els_test_utils:root_uri(), - DataDir = ?config(data_dir, Config), - ConfigPath = filename:join(DataDir, "diagnostics_default.config"), - InitOpts = #{ <<"erlang">> => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Expected = els_diagnostics:default_diagnostics(), - Result = els_diagnostics:enabled_diagnostics(), - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "diagnostics_default.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Expected = els_diagnostics:default_diagnostics(), + Result = els_diagnostics:enabled_diagnostics(), + ?assertEqual(Expected, Result), + ok. -spec initialize_diagnostics_custom(config()) -> ok. initialize_diagnostics_custom(Config) -> - RootUri = els_test_utils:root_uri(), - DataDir = ?config(data_dir, Config), - ConfigPath = filename:join(DataDir, "diagnostics_custom.config"), - InitOpts = #{ <<"erlang">> => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Expected = [ <<"bound_var_in_pattern">> - , <<"compiler">> - , <<"crossref">> - , <<"dialyzer">> - , <<"unused_includes">> - , <<"unused_macros">> - , <<"unused_record_fields">> - ], - Result = els_diagnostics:enabled_diagnostics(), - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "diagnostics_custom.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Expected = [ + <<"bound_var_in_pattern">>, + <<"compiler">>, + <<"crossref">>, + <<"dialyzer">>, + <<"unused_includes">>, + <<"unused_macros">>, + <<"unused_record_fields">> + ], + Result = els_diagnostics:enabled_diagnostics(), + ?assertEqual(Expected, Result), + ok. -spec initialize_diagnostics_invalid(config()) -> ok. initialize_diagnostics_invalid(Config) -> - RootUri = els_test_utils:root_uri(), - DataDir = ?config(data_dir, Config), - ConfigPath = filename:join(DataDir, "diagnostics_invalid.config"), - InitOpts = #{ <<"erlang">> => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Result = els_diagnostics:enabled_diagnostics(), - Expected = [ <<"bound_var_in_pattern">> - , <<"compiler">> - , <<"crossref">> - , <<"dialyzer">> - , <<"elvis">> - , <<"unused_includes">> - , <<"unused_macros">> - , <<"unused_record_fields">> - ], - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "diagnostics_invalid.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Result = els_diagnostics:enabled_diagnostics(), + Expected = [ + <<"bound_var_in_pattern">>, + <<"compiler">>, + <<"crossref">>, + <<"dialyzer">>, + <<"elvis">>, + <<"unused_includes">>, + <<"unused_macros">>, + <<"unused_record_fields">> + ], + ?assertEqual(Expected, Result), + ok. -spec initialize_lenses_default(config()) -> ok. initialize_lenses_default(Config) -> - RootUri = els_test_utils:root_uri(), - DataDir = ?config(data_dir, Config), - ConfigPath = filename:join(DataDir, "lenses_default.config"), - InitOpts = #{ <<"erlang">> => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Expected = lists:sort(els_code_lens:default_lenses()), - Result = els_code_lens:enabled_lenses(), - ?assertEqual(Expected, lists:sort(Result)), - ok. + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "lenses_default.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Expected = lists:sort(els_code_lens:default_lenses()), + Result = els_code_lens:enabled_lenses(), + ?assertEqual(Expected, lists:sort(Result)), + ok. -spec initialize_lenses_custom(config()) -> ok. initialize_lenses_custom(Config) -> - RootUri = els_test_utils:root_uri(), - DataDir = ?config(data_dir, Config), - ConfigPath = filename:join(DataDir, "lenses_custom.config"), - InitOpts = #{ <<"erlang">> => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Expected = [ <<"function-references">> - , <<"server-info">> - , <<"suggest-spec">> - ], - Result = els_code_lens:enabled_lenses(), - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "lenses_custom.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Expected = [ + <<"function-references">>, + <<"server-info">> + ], + Result = els_code_lens:enabled_lenses(), + ?assertEqual(Expected, Result), + ok. -spec initialize_lenses_invalid(config()) -> ok. initialize_lenses_invalid(Config) -> - RootUri = els_test_utils:root_uri(), - DataDir = ?config(data_dir, Config), - ConfigPath = filename:join(DataDir, "lenses_invalid.config"), - InitOpts = #{ <<"erlang">> => #{ <<"config_path">> => ConfigPath }}, - els_client:initialize(RootUri, InitOpts), - Result = els_code_lens:enabled_lenses(), - Expected = [ <<"ct-run-test">> - , <<"function-references">> - , <<"show-behaviour-usages">> - , <<"suggest-spec">> - ], - ?assertEqual(Expected, Result), - ok. + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "lenses_invalid.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Result = els_code_lens:enabled_lenses(), + Expected = [ + <<"ct-run-test">>, + <<"function-references">>, + <<"show-behaviour-usages">> + ], + ?assertEqual(Expected, Result), + ok. + +-spec initialize_providers_default(config()) -> ok. +initialize_providers_default(Config) -> + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "providers_default.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Result = els_general_provider:enabled_providers(), + Expected = lists:usort(els_general_provider:default_providers()), + ?assertEqual(Expected, Result), + #{capabilities := Capabilities} = els_general_provider:server_capabilities(), + ?assertEqual(true, maps:is_key(hoverProvider, Capabilities)), + ok. + +-spec initialize_providers_custom(config()) -> ok. +initialize_providers_custom(Config) -> + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "providers_custom.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + EnabledProviders = els_general_provider:enabled_providers(), + ?assertEqual(false, lists:member("hover", EnabledProviders)), + ?assertEqual(true, lists:member("document-on-type-formatting", EnabledProviders)), + #{capabilities := Capabilities} = els_general_provider:server_capabilities(), + ?assertEqual(false, maps:is_key(hoverProvider, Capabilities)), + ok. + +-spec initialize_providers_invalid(config()) -> ok. +initialize_providers_invalid(Config) -> + RootUri = els_test_utils:root_uri(), + DataDir = ?config(data_dir, Config), + ConfigPath = filename:join(DataDir, "providers_invalid.config"), + InitOpts = #{<<"erlang">> => #{<<"config_path">> => ConfigPath}}, + els_client:initialize(RootUri, InitOpts), + Result = els_general_provider:enabled_providers(), + Expected = lists:usort(els_general_provider:default_providers()), + ?assertEqual(Expected, Result), + #{capabilities := Capabilities} = els_general_provider:server_capabilities(), + ?assertEqual(true, maps:is_key(hoverProvider, Capabilities)), + ok. + +-spec initialize_prepare_rename(config()) -> ok. +initialize_prepare_rename(_Config) -> + RootUri = els_test_utils:root_uri(), + els_client:initialize(RootUri), + #{capabilities := #{renameProvider := RenameProvider}} = + els_general_provider:server_capabilities(), + ?assertEqual(#{prepareProvider => true}, RenameProvider), + ok. diff --git a/apps/els_lsp/test/els_initialization_SUITE_data/providers_custom.config b/apps/els_lsp/test/els_initialization_SUITE_data/providers_custom.config new file mode 100644 index 000000000..214b469f3 --- /dev/null +++ b/apps/els_lsp/test/els_initialization_SUITE_data/providers_custom.config @@ -0,0 +1,5 @@ +providers: + enabled: + - document-on-type-formatting + disabled: + - hover diff --git a/apps/els_lsp/test/els_initialization_SUITE_data/providers_default.config b/apps/els_lsp/test/els_initialization_SUITE_data/providers_default.config new file mode 100644 index 000000000..e69de29bb diff --git a/apps/els_lsp/test/els_initialization_SUITE_data/providers_invalid.config b/apps/els_lsp/test/els_initialization_SUITE_data/providers_invalid.config new file mode 100644 index 000000000..21f6a93b7 --- /dev/null +++ b/apps/els_lsp/test/els_initialization_SUITE_data/providers_invalid.config @@ -0,0 +1,5 @@ +providers: + enabled: + - hover + disabled: + - ssignaturee-hhelpp # Typos intentional diff --git a/apps/els_lsp/test/els_inlay_hint_SUITE.erl b/apps/els_lsp/test/els_inlay_hint_SUITE.erl new file mode 100644 index 000000000..a460b0f53 --- /dev/null +++ b/apps/els_lsp/test/els_inlay_hint_SUITE.erl @@ -0,0 +1,185 @@ +-module(els_inlay_hint_SUITE). + +%% CT Callbacks +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). + +%% Test cases +-export([ + basic/1 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). +-include("els_lsp.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 init_per_suite(config()) -> config(). +init_per_suite(Config) -> + els_test_utils:init_per_suite(Config). + +-spec end_per_suite(config()) -> ok. +end_per_suite(Config) -> + els_test_utils:end_per_suite(Config). + +-spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) -> + els_test_utils:init_per_testcase(TestCase, Config). + +-spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(TestCase, Config) -> + els_test_utils:end_per_testcase(TestCase, Config), + ok. + +%%============================================================================== +%% Testcases +%%============================================================================== +-spec basic(config()) -> ok. +basic(Config) -> + Uri = ?config(inlay_hint_uri, Config), + Range = #{ + start => #{line => 0, character => 0}, + 'end' => #{line => 999, character => 0} + }, + #{result := Result} = els_client:inlay_hint(Uri, Range), + assert_result( + [ + #{ + label => <<"List1:">>, + position => #{line => 13, character => 17}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"List2:">>, + position => #{line => 13, character => 21}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"G1:">>, + position => #{line => 12, character => 6}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"G2:">>, + position => #{line => 12, character => 9}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + + #{ + label => <<"G3:">>, + position => #{line => 12, character => 12}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + + #{ + label => <<"F1:">>, + position => #{line => 11, character => 6}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"F2:">>, + position => #{line => 11, character => 9}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"E1:">>, + position => #{line => 10, character => 6}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"E2:">>, + position => #{line => 10, character => 9}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"D1:">>, + position => #{line => 9, character => 6}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"D2:">>, + position => #{line => 9, character => 9}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"Foo:">>, + position => #{line => 8, character => 6}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"B1:">>, + position => #{line => 7, character => 6}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"B2:">>, + position => #{line => 7, character => 9}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"A1:">>, + position => #{line => 6, character => 6}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"A2:">>, + position => #{line => 6, character => 9}, + kind => ?INLAY_HINT_KIND_PARAMETER, + paddingRight => true + }, + #{ + label => <<"exp">>, + position => #{line => 5, character => 0}, + kind => ?INLAY_HINT_KIND_TYPE, + paddingRight => true + } + ], + Result + ), + ok. + +assert_result([], []) -> + ok; +assert_result([Same | ExpectRest], [Same | ResultRest]) -> + assert_result(ExpectRest, ResultRest); +assert_result([Expect | _], [Result | _]) -> + ?assertEqual(Expect, Result). diff --git a/apps/els_lsp/test/els_io_string_SUITE.erl b/apps/els_lsp/test/els_io_string_SUITE.erl index d978b400c..a740b0e84 100644 --- a/apps/els_lsp/test/els_io_string_SUITE.erl +++ b/apps/els_lsp/test/els_io_string_SUITE.erl @@ -1,16 +1,16 @@ -module(els_io_string_SUITE). %% CT Callbacks --export([ init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ scan_forms/1 - ]). +-export([scan_forms/1]). %%============================================================================== %% Includes @@ -31,11 +31,11 @@ all() -> els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(_TestCase, Config) -> Config. @@ -49,19 +49,19 @@ end_per_testcase(_TestCase, _Config) -> ok. -spec scan_forms(config()) -> ok. scan_forms(_Config) -> - Path = path(), - {ok, IoFile} = file:open(Path, [read]), - Expected = scan_all_forms(IoFile, []), - ok = file:close(IoFile), + Path = path(), + {ok, IoFile} = file:open(Path, [read]), + Expected = scan_all_forms(IoFile, []), + ok = file:close(IoFile), - {ok, Text} = file:read_file(Path), - IoString = els_io_string:new(Text), - Result = scan_all_forms(IoString, []), - ok = file:close(IoString), + {ok, Text} = file:read_file(Path), + IoString = els_io_string:new(Text), + Result = scan_all_forms(IoString, []), + ok = file:close(IoString), - ?assertEqual(Expected, Result), + ?assertEqual(Expected, Result), - ok. + ok. %%============================================================================== %% Helper functions @@ -69,14 +69,14 @@ scan_forms(_Config) -> -spec scan_all_forms(file:io_device(), [any()]) -> [any()]. scan_all_forms(IoDevice, Acc) -> - case io:scan_erl_form(IoDevice, "") of - {ok, Tokens, _} -> - scan_all_forms(IoDevice, [Tokens | Acc]); - {eof, _} -> - Acc - end. + case io:scan_erl_form(IoDevice, "") of + {ok, Tokens, _} -> + scan_all_forms(IoDevice, [Tokens | Acc]); + {eof, _} -> + Acc + end. -spec path() -> string(). path() -> - RootPath = els_test_utils:root_path(), - filename:join([RootPath, "src", "code_navigation.erl"]). + RootPath = els_test_utils:root_path(), + filename:join([RootPath, "src", "code_navigation.erl"]). diff --git a/apps/els_lsp/test/els_mock_diagnostics.erl b/apps/els_lsp/test/els_mock_diagnostics.erl index 1e3ebd6a0..2be39d315 100644 --- a/apps/els_lsp/test/els_mock_diagnostics.erl +++ b/apps/els_lsp/test/els_mock_diagnostics.erl @@ -1,43 +1,46 @@ -module(els_mock_diagnostics). --export([ setup/0 - , teardown/0 - , subscribe/0 - , wait_until_complete/0 - ]). +-export([ + setup/0, + teardown/0, + subscribe/0, + wait_until_complete/0 +]). -spec setup() -> ok. setup() -> - meck:new(els_diagnostics_provider, [passthrough, no_link]). + meck:new(els_diagnostics_provider, [passthrough, no_link]). -spec teardown() -> ok. teardown() -> - meck:unload(els_diagnostics_provider). + meck:unload(els_diagnostics_provider). -spec subscribe() -> ok. subscribe() -> - Self = self(), - meck:expect( els_diagnostics_provider - , publish - , fun(Uri, Diagnostics) -> - Self ! {on_complete, Diagnostics}, - meck:passthrough([Uri, Diagnostics]) - end - ), - ok. + Self = self(), + meck:expect( + els_diagnostics_provider, + publish, + fun(Uri, Diagnostics) -> + Self ! {on_complete, Diagnostics}, + meck:passthrough([Uri, Diagnostics]) + end + ), + ok. -spec wait_until_complete() -> [els_diagnostics:diagnostic()]. wait_until_complete() -> - wait_until_complete(els_diagnostics:enabled_diagnostics(), []). + wait_until_complete(els_diagnostics:enabled_diagnostics(), []). --spec wait_until_complete( [els_diagnostics:diagnostic_id()] - , [els_diagnostics:diagnostic()] - ) -> - [els_diagnostics:diagnostic()]. +-spec wait_until_complete( + [els_diagnostics:diagnostic_id()], + [els_diagnostics:diagnostic()] +) -> + [els_diagnostics:diagnostic()]. wait_until_complete([], Diagnostics) -> - Diagnostics; -wait_until_complete([_|Rest], _Diagnostics) -> - receive - {on_complete, Diagnostics} -> - wait_until_complete(Rest, Diagnostics) - end. + Diagnostics; +wait_until_complete([_ | Rest], _Diagnostics) -> + receive + {on_complete, Diagnostics} -> + wait_until_complete(Rest, Diagnostics) + end. diff --git a/apps/els_lsp/test/els_parser_SUITE.erl b/apps/els_lsp/test/els_parser_SUITE.erl index d5318b5a0..7f9ea507b 100644 --- a/apps/els_lsp/test/els_parser_SUITE.erl +++ b/apps/els_lsp/test/els_parser_SUITE.erl @@ -1,29 +1,48 @@ -module(els_parser_SUITE). %% CT Callbacks --export([ all/0 - , init_per_suite/1 - , end_per_suite/1 - ]). +-export([ + all/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2 +]). %% Test cases --export([ specs_location/1 - , parse_invalid_code/1 - , underscore_macro/1 - , specs_with_record/1 - , types_with_record/1 - , types_with_types/1 - , record_def_with_types/1 - , record_def_with_record_type/1 - , callback_recursive/1 - , specs_recursive/1 - , types_recursive/1 - , opaque_recursive/1 - , record_def_recursive/1 - , var_in_application/1 - , unicode_clause_pattern/1 - , latin1_source_code/1 - ]). +-export([ + specs_location/1, + parse_invalid_code/1, + parse_incomplete_function/1, + parse_incomplete_spec/1, + parse_incomplete_type/1, + parse_no_tokens/1, + define/1, + ifdef/1, + ifndef/1, + undef/1, + underscore_macro/1, + specs_with_record/1, + types_with_record/1, + types_with_types/1, + record_def_with_types/1, + record_def_with_record_type/1, + record_index/1, + callback_recursive/1, + specs_recursive/1, + types_recursive/1, + opaque_recursive/1, + record_def_recursive/1, + var_in_application/1, + comprehensions/1, + map_comprehensions/1, + maybe_expr/1, + unicode_clause_pattern/1, + latin1_source_code/1, + record_comment/1, + pragma_noformat/1, + implicit_fun/1, + spec_args/1 +]). %%============================================================================== %% Includes @@ -48,168 +67,551 @@ end_per_suite(_Config) -> ok. -spec all() -> [atom()]. all() -> els_test_utils:all(?MODULE). +init_per_testcase(map_comprehensions, Config) -> + case list_to_integer(erlang:system_info(otp_release)) < 26 of + true -> + {skip, "Map comprehensions are only supported from OTP 26"}; + false -> + Config + end; +init_per_testcase(maybe_expr, Config) -> + case list_to_integer(erlang:system_info(otp_release)) < 25 of + true -> + {skip, "Maybe expressions are only supported from OTP 25"}; + false -> + Config + end; +init_per_testcase(_, Config) -> + Config. + %%============================================================================== %% Testcases %%============================================================================== %% Issue #120 -spec specs_location(config()) -> ok. specs_location(_Config) -> - Text = "-spec foo(integer()) -> any(); (atom()) -> pid().", - ?assertMatch([_], parse_find_pois(Text, spec, {foo, 1})), - ok. + Text = "-spec foo(integer()) -> any(); (atom()) -> pid().", + ?assertMatch([_], parse_find_pois(Text, spec, {foo, 1})), + ok. -%% Issue #170 +%% Issue #170 - scanning error does not crash the parser -spec parse_invalid_code(config()) -> ok. parse_invalid_code(_Config) -> - Text = "foo(X) -> 16#.", - {ok, _POIs} = els_parser:parse(Text), - ok. + Text = "foo(X) -> 16#.", + %% Currently, if scanning fails (eg. invalid integer), no POIs are created + {ok, []} = els_parser:parse(Text), + %% In the future, it would be nice to have at least the POIs before the error + %% ?assertMatch([#{id := {foo, 1}}], parse_find_pois(Text, function)), + %% ?assertMatch([#{id := 'X'}], parse_find_pois(Text, variable)), + + %% Or at least the POIs from the previous forms + Text2 = + "bar() -> ok.\n" + "foo() -> 'ato", + %% (unterminated atom) + {ok, []} = els_parser:parse(Text2), + %% ?assertMatch([#{id := {bar, 0}}], parse_find_pois(Text2, function)), + ok. + +%% Issue #1037 +-spec parse_incomplete_function(config()) -> ok. +parse_incomplete_function(_Config) -> + Text = "f(VarA) -> VarB = g(), case h() of VarC -> Var", + + %% VarA and VarB are found, but VarC is not + ?assertMatch( + [ + #{id := 'VarA'}, + #{id := 'VarB'} + ], + parse_find_pois(Text, variable) + ), + %% g() is found but h() is not + ?assertMatch([#{id := {g, 0}}], parse_find_pois(Text, application)), + + ?assertMatch([#{id := {f, 1}}], parse_find_pois(Text, function)), + ok. + +-spec parse_incomplete_spec(config()) -> ok. +parse_incomplete_spec(_Config) -> + Text = "-spec f() -> aa bb cc\n.", + + %% spec range ends where the original dot ends, including ignored parts + ?assertMatch( + [#{id := {f, 0}, range := #{from := {1, 1}, to := {2, 2}}}], + parse_find_pois(Text, spec) + ), + %% only first atom is found + ?assertMatch([#{id := aa}], parse_find_pois(Text, atom)), + ok. + +-spec parse_incomplete_type(config()) -> ok. +parse_incomplete_type(_Config) -> + Text = "-type t(A) :: {A aa bb cc}\n.", + + %% type range ends where the original dot ends, including ignored parts + ?assertMatch( + [#{id := {t, 1}, range := #{from := {1, 1}, to := {2, 2}}}], + parse_find_pois(Text, type_definition) + ), + %% only first var is found + ?assertMatch([#{id := 'A'}], parse_find_pois(Text, variable)), + + Text2 = "-type t", + ?assertMatch( + {ok, [#{kind := type_definition, id := {t, 0}}]}, + els_parser:parse(Text2) + ), + Text3 = "-type ?t", + ?assertMatch( + {ok, [#{kind := macro, id := t}]}, + els_parser:parse(Text3) + ), + %% this is not incomplete - there is no way this will become valid erlang + %% but erlfmt can parse it + Text4 = "-type T", + ?assertMatch( + {ok, [#{kind := variable, id := 'T'}]}, + els_parser:parse(Text4) + ), + Text5 = "-type [1, 2]", + {ok, []} = els_parser:parse(Text5), + + %% no type args - assume zero args + Text11 = "-type t :: 1.", + ?assertMatch( + {ok, [#{kind := type_definition, id := {t, 0}}]}, + els_parser:parse(Text11) + ), + %% no macro args - this is 100% valid code + Text12 = "-type ?t :: 1.", + ?assertMatch( + {ok, [#{kind := macro, id := t}]}, + els_parser:parse(Text12) + ), + Text13 = "-type T :: 1.", + ?assertMatch( + {ok, [#{kind := variable, id := 'T'}]}, + els_parser:parse(Text13) + ), + + ok. + +%% Issue #1171 +parse_no_tokens(_Config) -> + %% scanning text containing only whitespaces returns an empty list of tokens, + %% which used to crash els_parser + Text1 = " \n ", + {ok, []} = els_parser:parse(Text1), + %% `els_parser:parse' actually catches the exception and only prints a warning + %% log. In order to make sure there is no crash, we need to call an internal + %% debug function that would really crash and make the test case fail + error = els_parser:parse_incomplete_text(Text1, {1, 1}), + + %% same for text only containing comments + Text2 = "%% only a comment", + {ok, []} = els_parser:parse(Text2), + error = els_parser:parse_incomplete_text(Text2, {1, 1}), + + %% trailing comment, also used to crash els_parser + Text3 = + "-module(m).\n" + "%% trailing comment", + {ok, [#{id := m, kind := module}]} = els_parser:parse(Text3). + +-spec define(config()) -> ok. +define(_Config) -> + ?assertMatch( + {ok, [ + #{id := {'MACRO', 2}, kind := define}, + #{id := {'A', 'B', 0}, kind := application}, + #{id := 'A', kind := variable}, + #{id := 'B', kind := variable}, + #{id := 'B', kind := variable}, + #{id := 'A', kind := variable} + ]}, + els_parser:parse("-define(MACRO(A, B), A:B()).") + ). + +-spec ifdef(config()) -> ok. +ifdef(_Config) -> + Text = "-ifdef(FOO).", + ?assertMatch([#{id := 'FOO'}], parse_find_pois(Text, macro)), + Text2 = "-ifdef(foo).", + ?assertMatch([#{id := 'foo'}], parse_find_pois(Text2, macro)). + +-spec ifndef(config()) -> ok. +ifndef(_Config) -> + Text = "-ifndef(FOO).", + ?assertMatch([#{id := 'FOO'}], parse_find_pois(Text, macro)). + +-spec undef(config()) -> ok. +undef(_Config) -> + Text = "-undef(FOO).", + ?assertMatch([#{id := 'FOO'}], parse_find_pois(Text, macro)). -spec underscore_macro(config()) -> ok. underscore_macro(_Config) -> - ?assertMatch({ok, [#{id := {'_', 1}, kind := define} | _]}, - els_parser:parse("-define(_(Text), gettexter:gettext(Text)).")), - ?assertMatch({ok, [#{id := '_', kind := define} | _]}, - els_parser:parse("-define(_, smth).")), - ?assertMatch({ok, [#{id := '_', kind := macro}]}, - els_parser:parse("?_.")), - ?assertMatch({ok, [#{id := {'_', 1}, kind := macro} | _]}, - els_parser:parse("?_(ok).")), - ok. + ?assertMatch( + {ok, [#{id := {'_', 1}, kind := define} | _]}, + els_parser:parse("-define(_(Text), gettexter:gettext(Text)).") + ), + ?assertMatch( + {ok, [#{id := '_', kind := define} | _]}, + els_parser:parse("-define(_, smth).") + ), + ?assertMatch( + {ok, [#{id := '_', kind := macro}]}, + els_parser:parse("?_.") + ), + ?assertMatch( + {ok, [#{id := {'_', 1}, kind := macro} | _]}, + els_parser:parse("?_(ok).") + ), + ok. %% Issue #815 -spec specs_with_record(config()) -> ok. specs_with_record(_Config) -> - Text = "-record(bar, {a, b}). -spec foo(#bar{}) -> any().", - ?assertMatch([_], parse_find_pois(Text, record_expr, bar)), - ok. + Text = "-record(bar, {a, b}). -spec foo(#bar{}) -> any().", + ?assertMatch([_], parse_find_pois(Text, record_expr, bar)), + ok. %% Issue #818 -spec types_with_record(config()) -> ok. types_with_record(_Config) -> - Text1 = "-record(bar, {a, b}). -type foo() :: #bar{}.", - ?assertMatch([_], parse_find_pois(Text1, record_expr, bar)), + Text1 = "-record(bar, {a, b}). -type foo() :: #bar{}.", + ?assertMatch([_], parse_find_pois(Text1, record_expr, bar)), - Text2 = "-record(bar, {a, b}). -type foo() :: #bar{f1 :: t()}.", - ?assertMatch([_], parse_find_pois(Text2, record_expr, bar)), - ?assertMatch([_], parse_find_pois(Text2, record_field, {bar, f1})), - ok. + Text2 = "-record(bar, {a, b}). -type foo() :: #bar{f1 :: t()}.", + ?assertMatch([_], parse_find_pois(Text2, record_expr, bar)), + ?assertMatch([_], parse_find_pois(Text2, record_field, {bar, f1})), + ok. %% Issue #818 -spec types_with_types(config()) -> ok. types_with_types(_Config) -> - Text = "-type bar() :: {a,b}. -type foo() :: bar().", - ?assertMatch([_], parse_find_pois(Text, type_application, {bar, 0})), - ok. + Text = "-type bar() :: {a,b}. -type foo() :: bar().", + ?assertMatch([_], parse_find_pois(Text, type_application, {bar, 0})), + ok. -spec record_def_with_types(config()) -> ok. record_def_with_types(_Config) -> - Text1 = "-record(r1, {f1 :: t1()}).", - ?assertMatch([_], parse_find_pois(Text1, type_application, {t1, 0})), + Text1 = "-record(r1, {f1 :: t1()}).", + ?assertMatch([_], parse_find_pois(Text1, type_application, {t1, 0})), - Text2 = "-record(r1, {f1 = defval :: t2()}).", - ?assertMatch([_], parse_find_pois(Text2, type_application, {t2, 0})), - %% No redundanct atom POIs - ?assertMatch([#{id := defval}], parse_find_pois(Text2, atom)), + Text2 = "-record(r1, {f1 = defval :: t2()}).", + ?assertMatch([_], parse_find_pois(Text2, type_application, {t2, 0})), + %% No redundant atom POIs + ?assertMatch([#{id := defval}], parse_find_pois(Text2, atom)), - Text3 = "-record(r1, {f1 :: t1(integer())}).", - ?assertMatch([_], parse_find_pois(Text3, type_application, {t1, 1})), - %% POI for builtin types like integer() - ?assertMatch([#{id := {t1, 1}}, #{id := {erlang, integer, 0}} ], - parse_find_pois(Text3, type_application)), + Text3 = "-record(r1, {f1 :: t1(integer())}).", + ?assertMatch([_], parse_find_pois(Text3, type_application, {t1, 1})), + %% POI for builtin types like integer() + ?assertMatch( + [#{id := {t1, 1}}, #{id := {erlang, integer, 0}}], + parse_find_pois(Text3, type_application) + ), - Text4 = "-record(r1, {f1 :: m:t1(integer())}).", - ?assertMatch([_], parse_find_pois(Text4, type_application, {m, t1, 1})), - %% No redundanct atom POIs - ?assertMatch([], parse_find_pois(Text4, atom)), + Text4 = "-record(r1, {f1 :: m:t1(integer())}).", + ?assertMatch([_], parse_find_pois(Text4, type_application, {m, t1, 1})), + %% No redundant atom POIs + ?assertMatch([], parse_find_pois(Text4, atom)), - ok. + ok. -spec record_def_with_record_type(config()) -> ok. record_def_with_record_type(_Config) -> - Text1 = "-record(r1, {f1 :: #r2{}}).", - ?assertMatch([_], parse_find_pois(Text1, record_expr, r2)), - %% No redundanct atom POIs - ?assertMatch([], parse_find_pois(Text1, atom)), - - Text2 = "-record(r1, {f1 :: #r2{f2 :: t2()}}).", - ?assertMatch([_], parse_find_pois(Text2, record_expr, r2)), - ?assertMatch([_], parse_find_pois(Text2, record_field, {r2, f2})), - %% No redundanct atom POIs - ?assertMatch([], parse_find_pois(Text2, atom)), - ok. + Text1 = "-record(r1, {f1 :: #r2{}}).", + ?assertMatch([_], parse_find_pois(Text1, record_expr, r2)), + %% No redundant atom POIs + ?assertMatch([], parse_find_pois(Text1, atom)), + + Text2 = "-record(r1, {f1 :: #r2{f2 :: t2()}}).", + ?assertMatch([_], parse_find_pois(Text2, record_expr, r2)), + ?assertMatch([_], parse_find_pois(Text2, record_field, {r2, f2})), + %% No redundant atom POIs + ?assertMatch([], parse_find_pois(Text2, atom)), + ok. + +-spec record_index(config()) -> ok. +record_index(_Config) -> + Text1 = "#r1.f1.", + ?assertMatch([_], parse_find_pois(Text1, record_expr, r1)), + ?assertMatch([_], parse_find_pois(Text1, record_field, {r1, f1})), + %% No redundant atom POIs + ?assertMatch([#{id := '*exprs*'}], parse_find_pois(Text1, atom)). -spec callback_recursive(config()) -> ok. callback_recursive(_Config) -> - Text = "-callback foo(#r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}) -> any().", - assert_recursive_types(Text). + Text = "-callback foo(#r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}) -> any().", + assert_recursive_types(Text). -spec specs_recursive(config()) -> ok. specs_recursive(_Config) -> - Text = "-spec foo(#r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}) -> any().", - assert_recursive_types(Text). + Text = "-spec foo(#r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}) -> any().", + assert_recursive_types(Text). -spec types_recursive(config()) -> ok. types_recursive(_Config) -> - Text = "-type foo() :: #r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}.", - assert_recursive_types(Text). + Text = "-type foo() :: #r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}.", + assert_recursive_types(Text). -spec opaque_recursive(config()) -> ok. opaque_recursive(_Config) -> - Text = "-opaque foo() :: #r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}.", - assert_recursive_types(Text). + Text = "-opaque foo() :: #r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}.", + assert_recursive_types(Text). -spec record_def_recursive(config()) -> ok. record_def_recursive(_Config) -> - Text = "-record(foo, {field :: #r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}}).", - assert_recursive_types(Text). + Text = "-record(foo, {field :: #r1{f1 :: m:t1(#r2{f2 :: t2(t3())})}}).", + assert_recursive_types(Text). assert_recursive_types(Text) -> - ?assertMatch([#{id := r1}, - #{id := r2}], - parse_find_pois(Text, record_expr)), - ?assertMatch([#{id := {r1, f1}}, - #{id := {r2, f2}}], - parse_find_pois(Text, record_field)), - ?assertMatch([#{id := {m, t1, 1}}, - #{id := {t2, 1}}, - #{id := {t3, 0}} | _], - parse_find_pois(Text, type_application)), - ok. + ?assertMatch( + [ + #{id := r1}, + #{id := r2} + ], + parse_find_pois(Text, record_expr) + ), + ?assertMatch( + [ + #{id := {r1, f1}}, + #{id := {r2, f2}} + ], + parse_find_pois(Text, record_field) + ), + ?assertMatch( + [ + #{id := {m, t1, 1}}, + #{id := {t2, 1}}, + #{id := {t3, 0}} + | _ + ], + parse_find_pois(Text, type_application) + ), + ok. var_in_application(_Config) -> - Text1 = "f() -> Mod:f(42).", - ?assertMatch([#{id := 'Mod'}], parse_find_pois(Text1, variable)), - - Text2 = "f() -> mod:Fun(42).", - ?assertMatch([#{id := 'Fun'}], parse_find_pois(Text2, variable)), - ok. + Text1 = "f() -> Mod:f(42).", + ?assertMatch([#{id := 'Mod'}], parse_find_pois(Text1, variable)), + ?assertMatch( + [ + #{ + id := {'Mod', f, 1}, + data := #{ + mod_is_variable := true, + fun_is_variable := false + } + } + ], + parse_find_pois(Text1, application) + ), + + Text2 = "f() -> mod:Fun(42).", + ?assertMatch([#{id := 'Fun'}], parse_find_pois(Text2, variable)), + ?assertMatch( + [ + #{ + id := {mod, 'Fun', 1}, + data := #{ + mod_is_variable := false, + fun_is_variable := true + } + } + ], + parse_find_pois(Text2, application) + ), + + Text3 = "f() -> Mod:Fun(42).", + ?assertMatch([#{id := 'Mod'}, #{id := 'Fun'}], parse_find_pois(Text3, variable)), + ?assertMatch( + [ + #{ + id := {'Mod', 'Fun', 1}, + data := #{ + mod_is_variable := true, + fun_is_variable := true + } + } + ], + parse_find_pois(Text3, application) + ), + Text4 = "f() -> Fun(42).", + ?assertMatch([#{id := 'Fun'}], parse_find_pois(Text4, variable)), + ?assertMatch( + [ + #{ + id := {'Fun', 1}, + data := #{fun_is_variable := true} + } + ], + parse_find_pois(Text4, application) + ), + + Text5 = "f() -> (Mod):(Fun)(42).", + ?assertMatch([#{id := 'Mod'}, #{id := 'Fun'}], parse_find_pois(Text5, variable)), + ?assertMatch( + [ + #{ + id := {'Mod', 'Fun', 1}, + data := #{ + mod_is_variable := true, + fun_is_variable := true + } + } + ], + parse_find_pois(Text5, application) + ), + ok. + +comprehensions(_Config) -> + Text1 = "[X || X <- L]", + ?assertMatch( + [#{id := 'X'}, #{id := 'X'}, #{id := 'L'}], + parse_find_pois(Text1, variable) + ), + + Text2 = "<< <<Y, X>> || <<X, Y>> <= B >>", + ?assertMatch( + [#{id := 'Y'}, #{id := 'X'}, #{id := 'X'}, #{id := 'Y'}, #{id := 'B'}], + parse_find_pois(Text2, variable) + ), + ok. + +map_comprehensions(_Config) -> + Text3 = "#{ Y => X || X := Y <- M }", + ?assertMatch( + [#{id := 'Y'}, #{id := 'X'}, #{id := 'X'}, #{id := 'Y'}, #{id := 'M'}], + parse_find_pois(Text3, variable) + ), + ok. + +maybe_expr(_Config) -> + Text1 = "maybe {ok, X} ?= f(), {ok, Y} ?= g() end", + ?assertMatch( + [#{id := 'X'}, #{id := 'Y'}], + parse_find_pois(Text1, variable) + ), + + Text2 = "maybe {ok, X} ?= f() else {error, Err} -> Err end", + ?assertMatch( + [#{id := 'X'}, #{id := 'Err'}, #{id := 'Err'}], + parse_find_pois(Text2, variable) + ), + ok. -spec unicode_clause_pattern(config()) -> ok. unicode_clause_pattern(_Config) -> - %% From OTP compiler's bs_utf_SUITE.erl - Text = "match_literal(<<\"Мастер и Маргарита\"/utf8>>) -> mm_utf8.", - ?assertMatch([#{data := <<"(<<\"", _/binary>>}], - parse_find_pois(Text, function_clause, {match_literal, 1, 1})), - ok. + %% From OTP compiler's bs_utf_SUITE.erl + Text = "match_literal(<<\"Мастер и Маргарита\"/utf8>>) -> mm_utf8.", + ?assertMatch( + [#{data := <<"(<<\"", _/binary>>}], + parse_find_pois(Text, function_clause, {match_literal, 1, 1}) + ), + ok. %% Issue #306, PR #592 -spec latin1_source_code(config()) -> ok. latin1_source_code(_Config) -> - Text = lists:flatten(["f(\"", 200, "\") -> 200. %% ", 200]), - ?assertMatch([#{data := <<"(\"È\") "/utf8>>}], - parse_find_pois(Text, function_clause, {f, 1, 1})), - ok. + Text = lists:flatten(["f(\"", 200, "\") -> 200. %% ", 200]), + ?assertMatch( + [#{data := <<"(\"È\") "/utf8>>}], + parse_find_pois(Text, function_clause, {f, 1, 1}) + ), + ok. + +%% Issue #1290 Parsing error +-spec record_comment(config()) -> ok. +record_comment(_Config) -> + Text = <<"#my_record{\n%% TODO\n}.">>, + ?assertMatch( + [#{id := my_record}], + parse_find_pois(Text, record_expr) + ), + ok. + +%% Issue #1482 +%% Erlfmt returns {skip, _} if the text contains pragma @noformat. +%% This would cause els_parser to crash. +-spec pragma_noformat(config()) -> ok. +pragma_noformat(_Config) -> + Text = <<"%% @noformat\nfoo">>, + ?assertMatch({ok, _}, els_parser:parse(Text)), + ?assertMatch({ok, _}, els_parser:parse_text(Text)). + +implicit_fun(_Config) -> + ?assertMatch( + [#{id := {foo, 0}}], + parse_find_pois(<<"fun foo/0">>, implicit_fun) + ), + ?assertMatch( + [#{id := {foo, foo, 0}}], + parse_find_pois(<<"fun foo:foo/0">>, implicit_fun) + ), + ?assertMatch( + [#{id := {'Var', foo, 0}, data := #{mod_is_variable := true}}], + parse_find_pois(<<"fun Var:foo/0">>, implicit_fun) + ), + ?assertMatch( + [#{id := {foo, 'Var', 0}, data := #{fun_is_variable := true}}], + parse_find_pois(<<"fun foo:Var/0">>, implicit_fun) + ), + ?assertMatch( + [#{id := {foo, 0}}], + parse_find_pois(<<"fun ?MODULE:foo/0">>, implicit_fun) + ), + ok. + +-spec spec_args(config()) -> ok. +spec_args(_Config) -> + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := "Foo"}]}}], + parse_find_pois("-spec f(Foo :: any()) -> any()).", spec) + ), + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := "Foo"}]}}], + parse_find_pois("-spec f(Foo) -> any() when Foo :: any().", spec) + ), + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := {type, "Any"}}]}}], + parse_find_pois("-spec f(any()) -> any().", spec) + ), + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := {type, "Foo"}}]}}], + parse_find_pois("-spec f(foo()) -> any().", spec) + ), + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := {type, "Bar"}}]}}], + parse_find_pois("-spec f(foo:bar()) -> any().", spec) + ), + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := {type, "Bars"}}]}}], + parse_find_pois("-spec f([foo:bar()]) -> any().", spec) + ), + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := {type, "Foo"}}]}}], + parse_find_pois("-spec f(#foo{}) -> any().", spec) + ), + ?assertMatch( + [#{id := {f, 1}, data := #{args := [#{name := {type, "Foos"}}]}}], + parse_find_pois("-spec f([#foo{}]) -> any().", spec) + ), + ok. %%============================================================================== %% Helper functions %%============================================================================== --spec parse_find_pois(string(), poi_kind()) -> [poi()]. +-spec parse_find_pois(string() | binary(), els_poi:poi_kind()) -> + [els_poi:poi()]. +parse_find_pois(Text, Kind) when is_list(Text) -> + parse_find_pois(unicode:characters_to_binary(Text), Kind); parse_find_pois(Text, Kind) -> - {ok, POIs} = els_parser:parse(Text), - SortedPOIs = els_poi:sort(POIs), - [POI || #{kind := Kind1} = POI <- SortedPOIs, Kind1 =:= Kind]. + {ok, POIs} = els_parser:parse(Text), + SortedPOIs = els_poi:sort(POIs), + [POI || #{kind := Kind1} = POI <- SortedPOIs, Kind1 =:= Kind]. --spec parse_find_pois(string(), poi_kind(), poi_id()) -> [poi()]. +-spec parse_find_pois(string(), els_poi:poi_kind(), els_poi:poi_id()) -> [els_poi:poi()]. parse_find_pois(Text, Kind, Id) -> - [POI || #{id := Id1} = POI <- parse_find_pois(Text, Kind), Id1 =:= Id]. + [POI || #{id := Id1} = POI <- parse_find_pois(Text, Kind), Id1 =:= Id]. diff --git a/apps/els_lsp/test/els_parser_macros_SUITE.erl b/apps/els_lsp/test/els_parser_macros_SUITE.erl index e628031f3..4ded8d361 100644 --- a/apps/els_lsp/test/els_parser_macros_SUITE.erl +++ b/apps/els_lsp/test/els_parser_macros_SUITE.erl @@ -1,24 +1,28 @@ -module(els_parser_macros_SUITE). %% CT Callbacks --export([ all/0 - , init_per_suite/1 - , end_per_suite/1 - ]). +-export([ + all/0, + init_per_suite/1, + end_per_suite/1 +]). %% Test cases --export([ callback_macro/1 - , spec_macro/1 - , type_macro/1 - , opaque_macro/1 - , wild_attrbibute_macro/1 - , type_name_macro/1 - , spec_name_macro/1 - , macro_in_application/1 - , record_def_field_macro/1 - , module_macro_as_record_name/1 - , other_macro_as_record_name/1 - ]). +-export([ + callback_macro/1, + spec_macro/1, + type_macro/1, + opaque_macro/1, + wild_attrbibute_macro/1, + type_name_macro/1, + spec_name_macro/1, + macro_in_application/1, + record_def_field_macro/1, + module_macro_as_record_name/1, + other_macro_as_record_name/1, + macro_guards/1, + macro_as_case_clause/1 +]). %%============================================================================== %% Includes @@ -49,171 +53,217 @@ all() -> els_test_utils:all(?MODULE). %%============================================================================== -spec callback_macro(config()) -> ok. callback_macro(_Config) -> - Text = "-callback foo() -> ?M.", - ?assertMatch([_], parse_find_pois(Text, callback, {foo, 0})), - ?assertMatch([_], parse_find_pois(Text, macro, 'M')), - ok. + Text = "-callback foo() -> ?M.", + ?assertMatch([_], parse_find_pois(Text, callback, {foo, 0})), + ?assertMatch([_], parse_find_pois(Text, macro, 'M')), + ok. -spec spec_macro(config()) -> ok. spec_macro(_Config) -> - Text = "-spec foo() -> ?M().", - ?assertMatch([_], parse_find_pois(Text, spec, {foo, 0})), - ?assertMatch([_], parse_find_pois(Text, macro, {'M', 0})), - ok. + Text = "-spec foo() -> ?M().", + ?assertMatch([_], parse_find_pois(Text, spec, {foo, 0})), + ?assertMatch([_], parse_find_pois(Text, macro, {'M', 0})), + ok. -spec type_macro(config()) -> ok. type_macro(_Config) -> - Text = "-type t() :: ?M(a, b, c).", - ?assertMatch([_], parse_find_pois(Text, type_definition, {t, 0})), - ?assertMatch([_], parse_find_pois(Text, macro, {'M', 3})), - ok. + Text = "-type t() :: ?M(a, b, c).", + ?assertMatch([_], parse_find_pois(Text, type_definition, {t, 0})), + ?assertMatch([_], parse_find_pois(Text, macro, {'M', 3})), + ok. -spec opaque_macro(config()) -> ok. opaque_macro(_Config) -> - Text = "-opaque o() :: ?M(a, b).", - ?assertMatch([_], parse_find_pois(Text, type_definition, {o, 0})), - ?assertMatch([_], parse_find_pois(Text, macro, {'M', 2})), - ok. + Text = "-opaque o() :: ?M(a, b).", + ?assertMatch([_], parse_find_pois(Text, type_definition, {o, 0})), + ?assertMatch([_], parse_find_pois(Text, macro, {'M', 2})), + ok. -spec wild_attrbibute_macro(config()) -> ok. wild_attrbibute_macro(_Config) -> - %% This is parsed as -(?M(foo)), rather than -(?M)(foo) - Text = "-?M(foo).", - ?assertMatch([_], parse_find_pois(Text, macro, {'M', 1})), - ?assertMatch([_], parse_find_pois(Text, atom, foo)), - ok. + %% This is parsed as -(?M(foo)), rather than -(?M)(foo) + Text = "-?M(foo).", + ?assertMatch([_], parse_find_pois(Text, macro, {'M', 1})), + ?assertMatch([_], parse_find_pois(Text, atom, foo)), + ok. type_name_macro(_Config) -> - Text1 = "-type ?M() :: integer() | t().", - ?assertMatch({ok, [#{kind := type_application, id := {t, 0}}, - #{kind := type_application, id := {erlang, integer, 0}}, - #{kind := macro, id := 'M'}]}, - els_parser:parse(Text1)), - - %% The macro is parsed as (?M()), rather than (?M)() - Text2 = "-type t() :: ?M().", - ?assertMatch({ok, [#{kind := type_definition, id := {t, 0}}, - #{kind := macro, id := {'M', 0}}]}, - els_parser:parse(Text2)), - - %% In this case the macro is parsed as expected as (?T)() - Text3 = "-type t() :: ?M:?T().", - ?assertMatch({ok, [#{kind := type_definition, id := {t, 0}}, - #{kind := macro, id := 'M'}, - #{kind := macro, id := 'T'}]}, - els_parser:parse(Text3)), - ok. + Text1 = "-type ?M() :: integer() | t().", + ?assertMatch( + {ok, [ + #{kind := type_application, id := {t, 0}}, + #{kind := type_application, id := {erlang, integer, 0}}, + #{kind := macro, id := {'M', 0}} + ]}, + els_parser:parse(Text1) + ), + + %% The macro is parsed as (?M()), rather than (?M)() + Text2 = "-type t() :: ?M().", + ?assertMatch( + {ok, [ + #{kind := type_definition, id := {t, 0}}, + #{kind := macro, id := {'M', 0}} + ]}, + els_parser:parse(Text2) + ), + + %% In this case the macro is parsed as expected as (?T)() + Text3 = "-type t() :: ?M:?T().", + ?assertMatch( + {ok, [ + #{kind := type_definition, id := {t, 0}}, + #{kind := macro, id := 'M'}, + #{kind := macro, id := 'T'} + ]}, + els_parser:parse(Text3) + ), + ok. spec_name_macro(_Config) -> - %% Verify the parser does not crash on macros in spec function names and it - %% still returns an unnamed spec-context and POIs from the definition body - Text1 = "-spec ?M() -> integer() | t().", - ?assertMatch([#{id := undefined}], parse_find_pois(Text1, spec)), - ?assertMatch([_], parse_find_pois(Text1, type_application, {t, 0})), + %% Verify the parser does not crash on macros in spec function names and it + %% still returns an unnamed spec-context and POIs from the definition body + Text1 = "-spec ?M() -> integer() | t().", + ?assertMatch([#{id := undefined}], parse_find_pois(Text1, spec)), + ?assertMatch([_], parse_find_pois(Text1, type_application, {t, 0})), - Text2 = "-spec ?MODULE:b() -> integer() | t().", - ?assertMatch([#{id := undefined}], parse_find_pois(Text2, spec)), - ?assertMatch([_], parse_find_pois(Text2, type_application, {t, 0})), + Text2 = "-spec ?MODULE:b() -> integer() | t().", + ?assertMatch([#{id := undefined}], parse_find_pois(Text2, spec)), + ?assertMatch([_], parse_find_pois(Text2, type_application, {t, 0})), - Text3 = "-spec mod:?M() -> integer() | t().", - ?assertMatch([#{id := undefined}], parse_find_pois(Text3, spec)), - ?assertMatch([_], parse_find_pois(Text3, type_application, {t, 0})), - ok. + Text3 = "-spec mod:?M() -> integer() | t().", + ?assertMatch([#{id := undefined}], parse_find_pois(Text3, spec)), + ?assertMatch([_], parse_find_pois(Text3, type_application, {t, 0})), + ok. macro_in_application(_Config) -> - Text1 = "f() -> ?M:f(42).", - ?assertMatch([#{id := 'M'}], parse_find_pois(Text1, macro)), + Text1 = "f() -> ?M:f(42).", + ?assertMatch([#{id := 'M'}], parse_find_pois(Text1, macro)), - Text2 = "f() -> ?M(mod):f(42).", - ?assertMatch([#{id := {'M', 1}}], parse_find_pois(Text2, macro)), + Text2 = "f() -> ?M(mod):f(42).", + ?assertMatch([#{id := {'M', 1}}], parse_find_pois(Text2, macro)), - %% This is not an application, only a module qualifier before macro M/1 - Text3 = "f() -> mod:?M(42).", - ?assertMatch([#{id := {'M', 1}}], parse_find_pois(Text3, macro)), + %% This is not an application, only a module qualifier before macro M/1 + Text3 = "f() -> mod:?M(42).", + ?assertMatch([#{id := {'M', 1}}], parse_find_pois(Text3, macro)), - %% Application with macro M/0 as function name - Text4 = "f() -> mod:?M()(42).", - ?assertMatch([#{id := {'M', 0}}], parse_find_pois(Text4, macro)), + %% Application with macro M/0 as function name + Text4 = "f() -> mod:?M()(42).", + ?assertMatch([#{id := {'M', 0}}], parse_find_pois(Text4, macro)), - %% Known limitation of the current implementation, - %% ?MODULE is handled specially, converted to a local call - Text5 = "f() -> ?MODULE:foo().", - ?assertMatch([], parse_find_pois(Text5, macro)), - ?assertMatch([#{id := {foo, 0}}], parse_find_pois(Text5, application)), + %% Known limitation of the current implementation, + %% ?MODULE is handled specially, converted to a local call + Text5 = "f() -> ?MODULE:foo().", + ?assertMatch([], parse_find_pois(Text5, macro)), + ?assertMatch([#{id := {foo, 0}}], parse_find_pois(Text5, application)), - ok. + ok. record_def_field_macro(_Config) -> - Text1 = "-record(rec, {?M = 1}).", - ?assertMatch({ok, [#{kind := record, id := rec}, - #{kind := macro, id := 'M'}]}, - els_parser:parse(Text1)), - - %% typed record field - Text2 = "-record(rec, {?M :: integer()}).", - ?assertMatch({ok, [#{kind := record, id := rec}, - #{kind := type_application, id := {erlang, integer, 0}}, - #{kind := macro, id := 'M'}]}, - els_parser:parse(Text2)), - ok. + Text1 = "-record(rec, {?M = 1}).", + ?assertMatch( + {ok, [ + #{kind := record, id := rec}, + #{kind := macro, id := 'M'} + ]}, + els_parser:parse(Text1) + ), + + %% typed record field + Text2 = "-record(rec, {?M :: integer()}).", + ?assertMatch( + {ok, [ + #{kind := record, id := rec}, + #{kind := type_application, id := {erlang, integer, 0}}, + #{kind := macro, id := 'M'} + ]}, + els_parser:parse(Text2) + ), + ok. %% Verify that record-related POIs are created with '?MODULE' name %% also verify that no macro POI is created in these cases module_macro_as_record_name(_Config) -> - Text1 = "-record(?MODULE, {f = 1}).", - ?assertMatch([#{data := #{field_list := [f]}}], - parse_find_pois(Text1, record, '?MODULE')), - ?assertMatch([_], parse_find_pois(Text1, record_def_field, {'?MODULE', f})), - ?assertMatch([], parse_find_pois(Text1, macro)), - - Text2 = "-type t() :: #?MODULE{f :: integer()}.", - ?assertMatch([_], parse_find_pois(Text2, record_expr, '?MODULE')), - ?assertMatch([_], parse_find_pois(Text2, record_field, {'?MODULE', f})), - ?assertMatch([], parse_find_pois(Text2, macro)), - - Text3 = "f(M) -> M#?MODULE.f.", - ?assertMatch([_], parse_find_pois(Text3, record_expr, '?MODULE')), - ?assertMatch([_], parse_find_pois(Text3, record_field, {'?MODULE', f})), - ?assertMatch([], parse_find_pois(Text3, macro)), - - Text4 = "f(M) -> M#?MODULE{f = 1}.", - ?assertMatch([_], parse_find_pois(Text4, record_expr, '?MODULE')), - ?assertMatch([_], parse_find_pois(Text4, record_field, {'?MODULE', f})), - ?assertMatch([], parse_find_pois(Text4, macro)), - ok. + Text1 = "-record(?MODULE, {f = 1}).", + ?assertMatch( + [#{data := #{field_list := [f]}}], + parse_find_pois(Text1, record, '?MODULE') + ), + ?assertMatch([_], parse_find_pois(Text1, record_def_field, {'?MODULE', f})), + ?assertMatch([], parse_find_pois(Text1, macro)), + + Text2 = "-type t() :: #?MODULE{f :: integer()}.", + ?assertMatch([_], parse_find_pois(Text2, record_expr, '?MODULE')), + ?assertMatch([_], parse_find_pois(Text2, record_field, {'?MODULE', f})), + ?assertMatch([], parse_find_pois(Text2, macro)), + + Text3 = "f(M) -> M#?MODULE.f.", + ?assertMatch([_], parse_find_pois(Text3, record_expr, '?MODULE')), + ?assertMatch([_], parse_find_pois(Text3, record_field, {'?MODULE', f})), + ?assertMatch([], parse_find_pois(Text3, macro)), + + Text4 = "f(M) -> M#?MODULE{f = 1}.", + ?assertMatch([_], parse_find_pois(Text4, record_expr, '?MODULE')), + ?assertMatch([_], parse_find_pois(Text4, record_field, {'?MODULE', f})), + ?assertMatch([], parse_find_pois(Text4, macro)), + ok. %% Verify macro POIs are created in record name positions other_macro_as_record_name(_Config) -> - Text1 = "-record(?M, {f = 1}).", - ?assertMatch([], parse_find_pois(Text1, record)), - ?assertMatch([], parse_find_pois(Text1, record_def_field)), - ?assertMatch([_], parse_find_pois(Text1, macro)), - - Text2 = "-type t() :: #?M{f :: integer()}.", - ?assertMatch([], parse_find_pois(Text2, record_expr)), - ?assertMatch([], parse_find_pois(Text2, record_field)), - ?assertMatch([_], parse_find_pois(Text2, macro, 'M')), - - Text3 = "f(M) -> M#?M.f.", - ?assertMatch([], parse_find_pois(Text3, record_expr)), - ?assertMatch([], parse_find_pois(Text3, record_field)), - ?assertMatch([_], parse_find_pois(Text3, macro, 'M')), - - Text4 = "f(M) -> M#?M{f = 1}.", - ?assertMatch([], parse_find_pois(Text4, record_expr)), - ?assertMatch([], parse_find_pois(Text4, record_field)), - ?assertMatch([_], parse_find_pois(Text4, macro, 'M')), - ok. + Text1 = "-record(?M, {f = 1}).", + ?assertMatch([], parse_find_pois(Text1, record)), + ?assertMatch([], parse_find_pois(Text1, record_def_field)), + ?assertMatch([_], parse_find_pois(Text1, macro)), + + Text2 = "-type t() :: #?M{f :: integer()}.", + ?assertMatch([], parse_find_pois(Text2, record_expr)), + ?assertMatch([], parse_find_pois(Text2, record_field)), + ?assertMatch([_], parse_find_pois(Text2, macro, 'M')), + + Text3 = "f(M) -> M#?M.f.", + ?assertMatch([], parse_find_pois(Text3, record_expr)), + ?assertMatch([], parse_find_pois(Text3, record_field)), + ?assertMatch([_], parse_find_pois(Text3, macro, 'M')), + + Text4 = "f(M) -> M#?M{f = 1}.", + ?assertMatch([], parse_find_pois(Text4, record_expr)), + ?assertMatch([], parse_find_pois(Text4, record_field)), + ?assertMatch([_], parse_find_pois(Text4, macro, 'M')), + ok. + +macro_guards(_Config) -> + Text1 = "?foo(Expr when Guard1)", + ?assertMatch([#{id := 'Expr'}, #{id := 'Guard1'}], parse_find_pois(Text1, variable)), + + Text2 = "?foo(Expr when Guard1; Guard2)", + ?assertMatch( + [#{id := 'Expr'}, #{id := 'Guard1'}, #{id := 'Guard2'}], + parse_find_pois(Text2, variable) + ), + ok. + +%% Supperted by erlfmt since erlfmt#350 +macro_as_case_clause(_Config) -> + Text1 = "case X of ?M1(Y); ?M2(2) end", + ?assertMatch( + [#{id := {'M1', 1}}, #{id := {'M2', 1}}], + parse_find_pois(Text1, macro) + ), + ?assertMatch( + [#{id := 'X'}, #{id := 'Y'}], + parse_find_pois(Text1, variable) + ), + ok. %%============================================================================== %% Helper functions %%============================================================================== --spec parse_find_pois(string(), poi_kind()) -> [poi()]. +-spec parse_find_pois(string(), els_poi:poi_kind()) -> [els_poi:poi()]. parse_find_pois(Text, Kind) -> - {ok, POIs} = els_parser:parse(Text), - SortedPOIs = els_poi:sort(POIs), - [POI || #{kind := Kind1} = POI <- SortedPOIs, Kind1 =:= Kind]. + {ok, POIs} = els_parser:parse(Text), + SortedPOIs = els_poi:sort(POIs), + [POI || #{kind := Kind1} = POI <- SortedPOIs, Kind1 =:= Kind]. --spec parse_find_pois(string(), poi_kind(), poi_id()) -> [poi()]. +-spec parse_find_pois(string(), els_poi:poi_kind(), els_poi:poi_id()) -> [els_poi:poi()]. parse_find_pois(Text, Kind, Id) -> - [POI || #{id := Id1} = POI <- parse_find_pois(Text, Kind), Id1 =:= Id]. + [POI || #{id := Id1} = POI <- parse_find_pois(Text, Kind), Id1 =:= Id]. diff --git a/apps/els_lsp/test/els_progress_SUITE.erl b/apps/els_lsp/test/els_progress_SUITE.erl index 7bf68cd07..79ea69c95 100644 --- a/apps/els_lsp/test/els_progress_SUITE.erl +++ b/apps/els_lsp/test/els_progress_SUITE.erl @@ -6,20 +6,23 @@ %%============================================================================== %% Common Test Callbacks %%============================================================================== --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %%============================================================================== %% Testcases %%============================================================================== --export([ sample_job/1 - , failing_job/1 - ]). +-export([ + sample_job/1, + failing_job/1, + stop_job/1 +]). %%============================================================================== %% Includes @@ -39,34 +42,44 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(sample_job = TestCase, Config) -> - Task = fun(_, _) -> ok end, - setup_mocks(Task), - [{task, Task} | els_test_utils:init_per_testcase(TestCase, Config)]; + Task = fun(_, _) -> ok end, + setup_mocks(Task), + [{task, Task} | els_test_utils:init_per_testcase(TestCase, Config)]; init_per_testcase(failing_job = TestCase, Config) -> - Task = fun(_, _) -> exit(fail) end, - setup_mocks(Task), - [{task, Task} | els_test_utils:init_per_testcase(TestCase, Config)]. + Task = fun(_, _) -> exit(fail) end, + setup_mocks(Task), + [{task, Task} | els_test_utils:init_per_testcase(TestCase, Config)]; +init_per_testcase(stop_job = TestCase, Config) -> + Task = fun(_, _) -> + sample_job:task_called(), + timer:sleep(timer:seconds(100)) + end, + setup_mocks(Task), + %% Bit of a hack, because meck only count history after mocked function returns it seems, + %% and the above function will not return. + meck:expect(sample_job, task_called, fun() -> ok end), + [{task, Task} | els_test_utils:init_per_testcase(TestCase, Config)]. -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config), - teardown_mocks(). + els_test_utils:end_per_testcase(TestCase, Config), + teardown_mocks(). %%============================================================================== %% Testcases @@ -74,58 +87,70 @@ end_per_testcase(TestCase, Config) -> -spec sample_job(config()) -> ok. sample_job(_Config) -> - {ok, Pid} = new_background_job(), - wait_for_completion(Pid), - ?assertEqual(length(entries()), meck:num_calls(sample_job, task, '_')), - ?assertEqual(1, meck:num_calls(sample_job, on_complete, '_')), - ?assertEqual(0, meck:num_calls(sample_job, on_error, '_')), - ok. + {ok, Pid} = new_background_job(), + wait_for_completion(Pid), + ?assertEqual(length(entries()), meck:num_calls(sample_job, task, '_')), + ?assertEqual(1, meck:num_calls(sample_job, on_complete, '_')), + ?assertEqual(0, meck:num_calls(sample_job, on_error, '_')), + ok. -spec failing_job(config()) -> ok. failing_job(_Config) -> - {ok, Pid} = new_background_job(), - wait_for_completion(Pid), - ?assertEqual(1, meck:num_calls(sample_job, task, '_')), - ?assertEqual(0, meck:num_calls(sample_job, on_complete, '_')), - ?assertEqual(1, meck:num_calls(sample_job, on_error, '_')), - ok. + {ok, Pid} = new_background_job(), + wait_for_completion(Pid), + ?assertEqual(1, meck:num_calls(sample_job, task, '_')), + ?assertEqual(0, meck:num_calls(sample_job, on_complete, '_')), + ?assertEqual(1, meck:num_calls(sample_job, on_error, '_')), + ok. + +-spec stop_job(config()) -> ok. +stop_job(_Config) -> + {ok, Pid} = new_background_job(), + meck:wait(sample_job, task_called, '_', 5000), + els_background_job:stop(Pid), + wait_for_completion(Pid), + ?assertEqual(1, meck:num_calls(sample_job, task_called, '_')), + ?assertEqual(0, meck:num_calls(sample_job, on_complete, '_')), + ?assertEqual(1, meck:num_calls(sample_job, on_error, '_')), + ok. %%============================================================================== %% Internal Functions %%============================================================================== -spec wait_for_completion(pid()) -> ok. wait_for_completion(Pid) -> - case is_process_alive(Pid) of - false -> - ok; - true -> - timer:sleep(10), - wait_for_completion(Pid) - end. + case is_process_alive(Pid) of + false -> + ok; + true -> + timer:sleep(10), + wait_for_completion(Pid) + end. -spec setup_mocks(fun((_, _) -> ok)) -> ok. setup_mocks(Task) -> - meck:new(sample_job, [non_strict, no_link]), - meck:expect(sample_job, task, Task), - meck:expect(sample_job, on_complete, fun(_) -> ok end), - meck:expect(sample_job, on_error, fun(_) -> ok end), - ok. + meck:new(sample_job, [non_strict, no_link]), + meck:expect(sample_job, task, Task), + meck:expect(sample_job, on_complete, fun(_) -> ok end), + meck:expect(sample_job, on_error, fun(_) -> ok end), + ok. -spec teardown_mocks() -> ok. teardown_mocks() -> - meck:unload(sample_job), - ok. + meck:unload(sample_job), + ok. -spec new_background_job() -> {ok, pid()}. new_background_job() -> - Config = #{ task => fun sample_job:task/2 - , entries => entries() - , on_complete => fun sample_job:on_complete/1 - , on_error => fun sample_job:on_error/1 - , title => <<"Sample job">> - }, - els_background_job:new(Config). + Config = #{ + task => fun sample_job:task/2, + entries => entries(), + on_complete => fun sample_job:on_complete/1, + on_error => fun sample_job:on_error/1, + title => <<"Sample job">> + }, + els_background_job:new(Config). -spec entries() -> [any()]. entries() -> - lists:seq(1, 27). + lists:seq(1, 27). diff --git a/apps/els_lsp/test/els_proper_gen.erl b/apps/els_lsp/test/els_proper_gen.erl index b312fb556..ccf3071fe 100644 --- a/apps/els_lsp/test/els_proper_gen.erl +++ b/apps/els_lsp/test/els_proper_gen.erl @@ -17,28 +17,36 @@ %% Generators %%============================================================================== uri() -> - ?LET( B - , document() - , <<"file:///tmp/", B/binary, ".erl">> - ). + ?LET( + B, + document(), + els_uri:uri(filename:join([system_tmp_dir(), B ++ ".erl"])) + ). root_uri() -> - <<"file:///tmp">>. + els_uri:uri(system_tmp_dir()). init_options() -> - #{<<"indexingEnabled">> => false}. + #{<<"indexingEnabled">> => false}. document() -> - elements([<<"a">>, <<"b">>, <<"c">>]). + elements(["a", "b", "c"]). tokens() -> - ?LET( Tokens - , vector(10, token()) - , begin - Concatenated = [string:join(Tokens, ","), "."], - els_utils:to_binary(Concatenated) + ?LET( + Tokens, + vector(10, token()), + begin + Concatenated = [string:join(Tokens, ","), "."], + els_utils:to_binary(Concatenated) end - ). + ). token() -> - elements(["foo", "Bar", "\"baz\""]). + elements(["foo", "Bar", "\"baz\""]). + +%%============================================================================== +%% Internal Functions +%%============================================================================== +system_tmp_dir() -> + els_utils:to_binary(els_utils:system_tmp_dir()). diff --git a/apps/els_lsp/test/els_rebar3_release_SUITE.erl b/apps/els_lsp/test/els_rebar3_release_SUITE.erl index fce638d8d..01ae7a399 100644 --- a/apps/els_lsp/test/els_rebar3_release_SUITE.erl +++ b/apps/els_lsp/test/els_rebar3_release_SUITE.erl @@ -4,16 +4,17 @@ -module(els_rebar3_release_SUITE). %% CT Callbacks --export([ all/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , suite/0 - ]). +-export([ + all/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + suite/0 +]). %% Test cases --export([ code_navigation/1 ]). +-export([code_navigation/1]). %%============================================================================== %% Includes @@ -37,58 +38,61 @@ %%============================================================================== -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - RootPath = root_path(), - AppPath = src_path(RootPath, "rebar3_release_app.erl"), - SupPath = src_path(RootPath, "rebar3_release_sup.erl"), - application:load(els_lsp), - [ {root_uri, els_uri:uri(RootPath)} - , {app_uri, els_uri:uri(AppPath)} - , {sup_uri, els_uri:uri(SupPath)} - | Config - ]. + RootPath = root_path(), + AppPath = src_path(RootPath, "rebar3_release_app.erl"), + SupPath = src_path(RootPath, "rebar3_release_sup.erl"), + application:load(els_lsp), + [ + {root_uri, els_uri:uri(RootPath)}, + {app_uri, els_uri:uri(AppPath)}, + {sup_uri, els_uri:uri(SupPath)} + | Config + ]. -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -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), - AppUri = ?config(app_uri, Config), - els_client:initialize(RootUri), - {ok, AppText} = file:read_file(els_uri:path(AppUri)), - els_client:did_open(AppUri, erlang, 1, AppText), - els_indexing:find_and_index_file("rebar3_release_app.erl"), - els_indexing:find_and_index_file("rebar3_release_sup.erl"), - [{started, Started}|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), + AppUri = ?config(app_uri, Config), + els_client:initialize(RootUri), + {ok, AppText} = file:read_file(els_uri:path(AppUri)), + els_client:did_open(AppUri, erlang, 1, AppText), + els_indexing:find_and_deeply_index_file("rebar3_release_app.erl"), + els_indexing:find_and_deeply_index_file("rebar3_release_sup.erl"), + [{started, Started} | Config]. -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. %%============================================================================== %% Testcases %%============================================================================== -spec code_navigation(config()) -> ok. code_navigation(Config) -> - AppUri = ?config(app_uri, Config), - SupUri = ?config(sup_uri, Config), - #{result := Result} = els_client:definition(AppUri, 13, 12), - #{range := DefRange, uri := SupUri} = Result, - ?assertEqual( els_protocol:range(#{from => {16, 1}, to => {16, 11}}) - , DefRange), - ok. + AppUri = ?config(app_uri, Config), + SupUri = ?config(sup_uri, Config), + #{result := Result} = els_client:definition(AppUri, 13, 12), + [#{range := DefRange, uri := SupUri}] = Result, + ?assertEqual( + els_protocol:range(#{from => {16, 1}, to => {16, 11}}), + DefRange + ), + ok. %%============================================================================== %% Internal Functions @@ -96,9 +100,9 @@ code_navigation(Config) -> -spec root_path() -> binary(). root_path() -> - RootPath = filename:join([code:priv_dir(els_lsp), ?TEST_APP]), - els_utils:to_binary(RootPath). + RootPath = filename:join([code:priv_dir(els_lsp), ?TEST_APP]), + els_utils:to_binary(RootPath). -spec src_path(binary(), [any()]) -> binary(). src_path(RootPath, FileName) -> - filename:join([RootPath, apps, ?TEST_APP, src, FileName]). + filename:join([RootPath, apps, ?TEST_APP, src, FileName]). diff --git a/apps/els_lsp/test/els_references_SUITE.erl b/apps/els_lsp/test/els_references_SUITE.erl index 508d7030f..eaa3fc692 100644 --- a/apps/els_lsp/test/els_references_SUITE.erl +++ b/apps/els_lsp/test/els_references_SUITE.erl @@ -1,43 +1,50 @@ -module(els_references_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ application_local/1 - , application_remote/1 - , function_definition/1 - , function_multiple_clauses/1 - , fun_local/1 - , fun_remote/1 - , export_entry/1 - , macro/1 - , included_macro/1 - , undefined_macro/1 - , module/1 - , record/1 - , record_field/1 - , included_record/1 - , included_record_field/1 - , undefined_record/1 - , undefined_record_field/1 - , purge_references/1 - , type_local/1 - , type_remote/1 - , type_included/1 - ]). +-export([ + application_local/1, + application_remote/1, + function_definition/1, + function_multiple_clauses/1, + fun_local/1, + fun_remote/1, + export_entry/1, + macro/1, + included_macro/1, + undefined_macro/1, + module/1, + record/1, + record_field/1, + included_record/1, + included_record_field/1, + undefined_record/1, + undefined_record_field/1, + type_local/1, + type_remote/1, + type_included/1, + atom/1, + refresh_after_watched_file_deleted/1, + refresh_after_watched_file_changed/1, + refresh_after_watched_file_added/1, + ignore_open_watched_file_added/1 +]). %%============================================================================== %% Includes %%============================================================================== -include_lib("common_test/include/ct.hrl"). -include_lib("stdlib/include/assert.hrl"). +-include_lib("els_core/include/els_core.hrl"). %%============================================================================== %% Types @@ -49,443 +56,675 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config0) when + TestCase =:= refresh_after_watched_file_changed; + TestCase =:= refresh_after_watched_file_deleted +-> + Config = els_test_utils:init_per_testcase(TestCase, Config0), + PathB = ?config(watched_file_b_path, Config), + {ok, OldContent} = file:read_file(PathB), + [{old_content, OldContent} | Config]; init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(TestCase, Config) when + TestCase =:= refresh_after_watched_file_changed; + TestCase =:= refresh_after_watched_file_deleted +-> + PathB = ?config(watched_file_b_path, Config), + ok = file:write_file(PathB, ?config(old_content, Config)), + els_test_utils:end_per_testcase(TestCase, Config); +end_per_testcase(TestCase, Config) when + TestCase =:= refresh_after_watched_file_added; + TestCase =:= ignore_open_watched_file_added +-> + PathB = ?config(watched_file_b_path, Config), + ok = file:delete( + filename:join( + filename:dirname(PathB), + "watched_file_c.erl" + ) + ), + els_test_utils:end_per_testcase(TestCase, Config); end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec application_local(config()) -> ok. application_local(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:references(Uri, 22, 5), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {22, 3}, to => {22, 13}} - } - , #{ uri => Uri - , range => #{from => {51, 7}, to => {51, 23}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:references(Uri, 22, 5), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {22, 3}, to => {22, 13}} + }, + #{ + uri => Uri, + range => #{from => {51, 7}, to => {51, 23}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec application_remote(config()) -> ok. application_remote(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:references(Uri, 32, 13), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {10, 34}, to => {10, 38}} - } - , #{ uri => Uri - , range => #{from => {32, 3}, to => {32, 27}} - } - , #{ uri => Uri - , range => #{from => {52, 8}, to => {52, 38}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:references(Uri, 32, 13), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {10, 34}, to => {10, 38}} + }, + #{ + uri => Uri, + range => #{from => {32, 3}, to => {32, 27}} + }, + #{ + uri => Uri, + range => #{from => {52, 8}, to => {52, 38}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec function_definition(config()) -> ok. function_definition(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:references(Uri, 25, 1), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {22, 3}, to => {22, 13}} - } - , #{ uri => Uri - , range => #{from => {51, 7}, to => {51, 23}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:references(Uri, 25, 1), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {22, 3}, to => {22, 13}} + }, + #{ + uri => Uri, + range => #{from => {51, 7}, to => {51, 23}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec function_multiple_clauses(config()) -> ok. function_multiple_clauses(Config) -> - Uri = ?config(hover_docs_uri, Config), - UriCaller = ?config(hover_docs_caller_uri, Config), - #{result := Locations} = els_client:references(Uri, 7, 1), - ExpectedLocations = [ #{ uri => UriCaller - , range => #{from => {16, 3}, to => {16, 30}} - } - , #{ uri => UriCaller - , range => #{from => {20, 4}, to => {20, 37}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(hover_docs_uri, Config), + UriCaller = ?config(hover_docs_caller_uri, Config), + #{result := Locations} = els_client:references(Uri, 7, 1), + ExpectedLocations = [ + #{ + uri => UriCaller, + range => #{from => {16, 3}, to => {16, 30}} + }, + #{ + uri => UriCaller, + range => #{from => {20, 4}, to => {20, 37}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec fun_local(config()) -> ok. fun_local(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:references(Uri, 51, 16), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {22, 3}, to => {22, 13}} - } - , #{ uri => Uri - , range => #{from => {51, 7}, to => {51, 23}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:references(Uri, 51, 16), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {22, 3}, to => {22, 13}} + }, + #{ + uri => Uri, + range => #{from => {51, 7}, to => {51, 23}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec fun_remote(config()) -> ok. fun_remote(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:references(Uri, 52, 14), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {10, 34}, to => {10, 38}} - } - , #{ uri => Uri - , range => #{from => {32, 3}, to => {32, 27}} - } - , #{ uri => Uri - , range => #{from => {52, 8}, to => {52, 38}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:references(Uri, 52, 14), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {10, 34}, to => {10, 38}} + }, + #{ + uri => Uri, + range => #{from => {32, 3}, to => {32, 27}} + }, + #{ + uri => Uri, + range => #{from => {52, 8}, to => {52, 38}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec export_entry(config()) -> ok. export_entry(Config) -> - Uri = ?config(code_navigation_uri, Config), - #{result := Locations} = els_client:references(Uri, 5, 25), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {22, 3}, to => {22, 13}} - } - , #{ uri => Uri - , range => #{from => {51, 7}, to => {51, 23}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(code_navigation_uri, Config), + #{result := Locations} = els_client:references(Uri, 5, 25), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {22, 3}, to => {22, 13}} + }, + #{ + uri => Uri, + range => #{from => {51, 7}, to => {51, 23}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec macro(config()) -> ok. macro(Config) -> - Uri = ?config(code_navigation_uri, Config), - - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {26, 3}, to => {26, 11}} - } - , #{ uri => Uri - , range => #{from => {75, 23}, to => {75, 31}} - } - ], + Uri = ?config(code_navigation_uri, Config), + + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {26, 3}, to => {26, 11}} + }, + #{ + uri => Uri, + range => #{from => {75, 23}, to => {75, 31}} + } + ], - ct:comment("References for MACRO_A from usage"), - #{result := Locations1} = els_client:references(Uri, 26, 6), - assert_locations(Locations1, ExpectedLocations), + ct:comment("References for MACRO_A from usage"), + #{result := Locations1} = els_client:references(Uri, 26, 6), + assert_locations(Locations1, ExpectedLocations), - ct:comment("References for MACRO_A from define"), - #{result := Locations2} = els_client:references(Uri, 18, 12), - assert_locations(Locations2, ExpectedLocations), + ct:comment("References for MACRO_A from define"), + #{result := Locations2} = els_client:references(Uri, 18, 12), + assert_locations(Locations2, ExpectedLocations), - ok. + ok. -spec included_macro(config()) -> ok. included_macro(Config) -> - Uri = ?config(diagnostics_unused_includes_uri, Config), - HeaderUri = ?config(definition_h_uri, Config), - - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {14, 23}, to => {14, 54}} - } - ], + Uri = ?config(diagnostics_unused_includes_uri, Config), + HeaderUri = ?config(definition_h_uri, Config), + + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {14, 23}, to => {14, 54}} + } + ], - ct:comment("References for MACRO_FOR_TRANSITIVE_INCLUSION from usage"), - #{result := Locations} = els_client:references(Uri, 14, 30), - ct:comment("References for MACRO_FOR_TRANSITIVE_INCLUSION from define"), - #{result := Locations} = els_client:references(HeaderUri, 1, 20), - assert_locations(Locations, ExpectedLocations), + ct:comment("References for MACRO_FOR_TRANSITIVE_INCLUSION from usage"), + #{result := Locations} = els_client:references(Uri, 14, 30), + ct:comment("References for MACRO_FOR_TRANSITIVE_INCLUSION from define"), + #{result := Locations} = els_client:references(HeaderUri, 1, 20), + assert_locations(Locations, ExpectedLocations), - ok. + ok. -spec undefined_macro(config()) -> ok. undefined_macro(Config) -> - Uri = ?config(code_navigation_undefined_uri, Config), - - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {6, 28}, to => {6, 40}} - } - , #{ uri => Uri - , range => #{from => {8, 29}, to => {8, 41}} - } - ], + Uri = ?config(code_navigation_undefined_uri, Config), + + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {6, 28}, to => {6, 40}} + }, + #{ + uri => Uri, + range => #{from => {8, 29}, to => {8, 41}} + } + ], - ct:comment("References for UNDEF_MACRO from usage"), - #{result := Locations1} = els_client:references(Uri, 6, 30), - assert_locations(Locations1, ExpectedLocations), - ok. + ct:comment("References for UNDEF_MACRO from usage"), + #{result := Locations1} = els_client:references(Uri, 6, 30), + assert_locations(Locations1, ExpectedLocations), + ok. -spec module(config()) -> ok. module(Config) -> - Uri = ?config(code_navigation_extra_uri, Config), - #{result := Locations} = els_client:references(Uri, 1, 12), - LocUri = ?config(code_navigation_uri, Config), - ExpectedLocations = [ #{ uri => LocUri - , range => #{from => {10, 34}, to => {10, 38}} - } - , #{ uri => LocUri - , range => #{from => {32, 3}, to => {32, 27}} - } - , #{ uri => LocUri - , range => #{from => {52, 8}, to => {52, 38}} - } - ], - assert_locations(Locations, ExpectedLocations), - ok. + Uri = ?config(code_navigation_extra_uri, Config), + #{result := Locations} = els_client:references(Uri, 1, 12), + LocUri = ?config(code_navigation_uri, Config), + ExpectedLocations = [ + #{ + uri => LocUri, + range => #{from => {10, 34}, to => {10, 38}} + }, + #{ + uri => LocUri, + range => #{from => {32, 3}, to => {32, 27}} + }, + #{ + uri => LocUri, + range => #{from => {52, 8}, to => {52, 38}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. -spec record(config()) -> ok. record(Config) -> - Uri = ?config(code_navigation_uri, Config), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {23, 3}, to => {23, 12}} - } - , #{ uri => Uri - , range => #{from => {33, 7}, to => {33, 16}} - } - , #{ uri => Uri - , range => #{from => {34, 9}, to => {34, 18}} - } - , #{ uri => Uri - , range => #{from => {34, 34}, to => {34, 43}} - } - , #{ uri => Uri - , range => #{from => {99, 8}, to => {99, 17}} - } - ], - - ct:comment("Find references record_a from a usage"), - #{result := Locations} = els_client:references(Uri, 23, 3), - ct:comment("Find references record_a from an access"), - #{result := Locations} = els_client:references(Uri, 34, 15), - ct:comment("Find references record_a from beginning of definition"), - #{result := Locations} = els_client:references(Uri, 16, 9), - ct:comment("Find references record_a from end of definition"), - #{result := Locations} = els_client:references(Uri, 16, 17), - - assert_locations(Locations, ExpectedLocations), - - ct:comment("Check limits of record_a"), - #{result := null} = els_client:references(Uri, 16, 8), - #{result := null} = els_client:references(Uri, 16, 18), - - ok. + Uri = ?config(code_navigation_uri, Config), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {23, 3}, to => {23, 12}} + }, + #{ + uri => Uri, + range => #{from => {33, 7}, to => {33, 16}} + }, + #{ + uri => Uri, + range => #{from => {34, 9}, to => {34, 18}} + }, + #{ + uri => Uri, + range => #{from => {34, 34}, to => {34, 43}} + }, + #{ + uri => Uri, + range => #{from => {99, 8}, to => {99, 17}} + } + ], + + ct:comment("Find references record_a from a usage"), + #{result := Locations} = els_client:references(Uri, 23, 3), + ct:comment("Find references record_a from an access"), + #{result := Locations} = els_client:references(Uri, 34, 15), + ct:comment("Find references record_a from beginning of definition"), + #{result := Locations} = els_client:references(Uri, 16, 9), + ct:comment("Find references record_a from end of definition"), + #{result := Locations} = els_client:references(Uri, 16, 17), + + assert_locations(Locations, ExpectedLocations), + + ct:comment("Check limits of record_a"), + #{result := null} = els_client:references(Uri, 16, 8), + #{result := null} = els_client:references(Uri, 16, 18), + + ok. -spec record_field(config()) -> ok. record_field(Config) -> - Uri = ?config(code_navigation_uri, Config), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {33, 18}, to => {33, 25}} - } - , #{ uri => Uri - , range => #{from => {34, 19}, to => {34, 26}} - } - , #{ uri => Uri - , range => #{from => {34, 44}, to => {34, 51}} - } - ], - - ct:comment("Find references field_a from a usage"), - #{result := Locations} = els_client:references(Uri, 33, 18), - ct:comment("Find references field_a from an access"), - #{result := Locations} = els_client:references(Uri, 34, 19), - ct:comment("Find references field_a from beginning of definition"), - #{result := Locations} = els_client:references(Uri, 16, 20), - ct:comment("Find references field_a from end of definition"), - #{result := Locations} = els_client:references(Uri, 16, 27), - - assert_locations(Locations, ExpectedLocations), - - ct:comment("Check limits of field_a"), - #{result := null} = els_client:references(Uri, 16, 19), - #{result := null} = els_client:references(Uri, 16, 28), - - ok. + Uri = ?config(code_navigation_uri, Config), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {33, 18}, to => {33, 25}} + }, + #{ + uri => Uri, + range => #{from => {34, 19}, to => {34, 26}} + }, + #{ + uri => Uri, + range => #{from => {34, 44}, to => {34, 51}} + } + ], + + ct:comment("Find references field_a from a usage"), + #{result := Locations} = els_client:references(Uri, 33, 18), + ct:comment("Find references field_a from an access"), + #{result := Locations} = els_client:references(Uri, 34, 19), + ct:comment("Find references field_a from beginning of definition"), + #{result := Locations} = els_client:references(Uri, 16, 20), + ct:comment("Find references field_a from end of definition"), + #{result := Locations} = els_client:references(Uri, 16, 27), + + assert_locations(Locations, ExpectedLocations), + + ct:comment("Check limits of field_a"), + #{result := null} = els_client:references(Uri, 16, 19), + #{result := null} = els_client:references(Uri, 16, 28), + + ok. -spec included_record(config()) -> ok. included_record(Config) -> - Uri = ?config(code_navigation_uri, Config), - HeaderUri = ?config(code_navigation_h_uri, Config), - - ExpectedRecordLocations = - [ #{ uri => Uri - , range => #{from => {52, 41}, to => {52, 59}} - } - , #{ uri => Uri - , range => #{from => {53, 23}, to => {53, 41}} - } - , #{ uri => Uri - , range => #{from => {75, 4}, to => {75, 22}} - } - ], - ct:comment("Find references of included_record_a from a usage"), - #{result := RecordLocations} = els_client:references(Uri, 53, 30), - ct:comment("Find references of included_record_a from definition"), - #{result := RecordLocations} = els_client:references(HeaderUri, 1, 10), - assert_locations(RecordLocations, ExpectedRecordLocations), - - ok. + Uri = ?config(code_navigation_uri, Config), + HeaderUri = ?config(code_navigation_h_uri, Config), + + ExpectedRecordLocations = + [ + #{ + uri => Uri, + range => #{from => {52, 41}, to => {52, 59}} + }, + #{ + uri => Uri, + range => #{from => {53, 23}, to => {53, 41}} + }, + #{ + uri => Uri, + range => #{from => {75, 4}, to => {75, 22}} + } + ], + ct:comment("Find references of included_record_a from a usage"), + #{result := RecordLocations} = els_client:references(Uri, 53, 30), + ct:comment("Find references of included_record_a from definition"), + #{result := RecordLocations} = els_client:references(HeaderUri, 1, 10), + assert_locations(RecordLocations, ExpectedRecordLocations), + + ok. -spec included_record_field(config()) -> ok. included_record_field(Config) -> - Uri = ?config(code_navigation_uri, Config), - HeaderUri = ?config(code_navigation_h_uri, Config), - - ExpectedFieldLocations = - [ #{ uri => Uri - , range => #{from => {53, 42}, to => {53, 58}} - } - ], - ct:comment("Find references of included_field_a from a usage"), - #{result := FieldLocations} = els_client:references(Uri, 53, 45), - ct:comment("Find references of included_field_a from definition"), - #{result := FieldLocations} = els_client:references(HeaderUri, 1, 30), - assert_locations(FieldLocations, ExpectedFieldLocations), - - ok. + Uri = ?config(code_navigation_uri, Config), + HeaderUri = ?config(code_navigation_h_uri, Config), + + ExpectedFieldLocations = + [ + #{ + uri => Uri, + range => #{from => {53, 42}, to => {53, 58}} + } + ], + ct:comment("Find references of included_field_a from a usage"), + #{result := FieldLocations} = els_client:references(Uri, 53, 45), + ct:comment("Find references of included_field_a from definition"), + #{result := FieldLocations} = els_client:references(HeaderUri, 1, 30), + assert_locations(FieldLocations, ExpectedFieldLocations), + + ok. -spec undefined_record(config()) -> ok. undefined_record(Config) -> - Uri = ?config(code_navigation_undefined_uri, Config), - - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {6, 3}, to => {6, 13}} - } - , #{ uri => Uri - , range => #{from => {8, 4}, to => {8, 14}} - } - ], + Uri = ?config(code_navigation_undefined_uri, Config), + + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {6, 3}, to => {6, 13}} + }, + #{ + uri => Uri, + range => #{from => {8, 4}, to => {8, 14}} + } + ], - ct:comment("References for undef_rec from usage"), - #{result := Locations1} = els_client:references(Uri, 6, 10), - assert_locations(Locations1, ExpectedLocations), - ok. + ct:comment("References for undef_rec from usage"), + #{result := Locations1} = els_client:references(Uri, 6, 10), + assert_locations(Locations1, ExpectedLocations), + ok. -spec undefined_record_field(config()) -> ok. undefined_record_field(Config) -> - Uri = ?config(code_navigation_undefined_uri, Config), - - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {6, 14}, to => {6, 25}} - } - , #{ uri => Uri - , range => #{from => {8, 15}, to => {8, 26}} - } - ], - - ct:comment("References for undef_field from usage"), - #{result := Locations1} = els_client:references(Uri, 6, 20), - assert_locations(Locations1, ExpectedLocations), - ok. - -%% Issue #245 --spec purge_references(config()) -> ok. -purge_references(_Config) -> - els_db:clear_tables(), - Uri = <<"file:///tmp/foo.erl">>, - Text0 = <<"-spec foo() -> ok.\nfoo(_X) -> ok.\nbar() -> foo().">>, - Text1 = <<"\n-spec foo() -> ok.\nfoo(_X)-> ok.\nbar() -> foo().">>, - Doc0 = els_dt_document:new(Uri, Text0), - Doc1 = els_dt_document:new(Uri, Text1), - - ok = els_indexing:index(Uri, Text0, 'deep'), - ?assertEqual({ok, [Doc0]}, els_dt_document:lookup(Uri)), - ?assertEqual({ok, [#{ id => {foo, foo, 0} - , range => #{from => {3, 10}, to => {3, 13}} - , uri => <<"file:///tmp/foo.erl">> - }]} - , els_dt_references:find_all() - ), - - ok = els_indexing:index(Uri, Text1, 'deep'), - ?assertEqual({ok, [Doc1]}, els_dt_document:lookup(Uri)), - ?assertEqual({ok, [#{ id => {foo, foo, 0} - , range => #{from => {4, 10}, to => {4, 13}} - , uri => <<"file:///tmp/foo.erl">> - }]} - , els_dt_references:find_all() - ), - ok. + Uri = ?config(code_navigation_undefined_uri, Config), + + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {6, 14}, to => {6, 25}} + }, + #{ + uri => Uri, + range => #{from => {8, 15}, to => {8, 26}} + } + ], + + ct:comment("References for undef_field from usage"), + #{result := Locations1} = els_client:references(Uri, 6, 20), + assert_locations(Locations1, ExpectedLocations), + ok. -spec type_local(config()) -> ok. type_local(Config) -> - Uri = ?config(code_navigation_uri, Config), - ExpectedLocations = [ #{ uri => Uri - , range => #{from => {55, 23}, to => {55, 29}} - } - ], + Uri = ?config(code_navigation_uri, Config), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {55, 23}, to => {55, 29}} + } + ], - ct:comment("Find references type_a from its definition"), - #{result := Locations} = els_client:references(Uri, 37, 9), - ct:comment("Find references type_a from a usage"), - #{result := Locations} = els_client:references(Uri, 55, 23), - ct:comment("Find references type_a from beginning of definition"), - #{result := Locations} = els_client:references(Uri, 37, 7), - ct:comment("Find references type_a from end of definition"), - #{result := Locations} = els_client:references(Uri, 37, 12), + ct:comment("Find references type_a from its definition"), + #{result := Locations} = els_client:references(Uri, 37, 9), + ct:comment("Find references type_a from a usage"), + #{result := Locations} = els_client:references(Uri, 55, 23), + ct:comment("Find references type_a from beginning of definition"), + #{result := Locations} = els_client:references(Uri, 37, 7), + ct:comment("Find references type_a from end of definition"), + #{result := Locations} = els_client:references(Uri, 37, 12), - assert_locations(Locations, ExpectedLocations), + assert_locations(Locations, ExpectedLocations), - ok. + ok. -spec type_remote(config()) -> ok. type_remote(Config) -> - UriTypes = ?config(code_navigation_types_uri, Config), - Uri = ?config(code_navigation_extra_uri, Config), - ExpectedLocations = [ %% local reference from another type definition - #{ uri => UriTypes - , range => #{from => {11, 24}, to => {11, 30}} - } - %% remote reference from a spec - , #{ uri => Uri - , range => #{from => {11, 38}, to => {11, 66}} - } - ], - - ct:comment("Find references for type_a from a remote usage"), - #{result := Locations} = els_client:references(Uri, 11, 45), - ct:comment("Find references type_a from definition"), - #{result := Locations} = els_client:references(UriTypes, 3, 8), - - assert_locations(Locations, ExpectedLocations), - - ok. + UriTypes = ?config(code_navigation_types_uri, Config), + Uri = ?config(code_navigation_extra_uri, Config), + %% local reference from another type definition + ExpectedLocations = [ + #{ + uri => UriTypes, + range => #{from => {11, 24}, to => {11, 30}} + }, + %% remote reference from a spec + #{ + uri => Uri, + range => #{from => {11, 38}, to => {11, 66}} + } + ], + + ct:comment("Find references for type_a from a remote usage"), + #{result := Locations} = els_client:references(Uri, 11, 45), + ct:comment("Find references type_a from definition"), + #{result := Locations} = els_client:references(UriTypes, 3, 8), + + assert_locations(Locations, ExpectedLocations), + + ok. -spec type_included(config()) -> ok. type_included(Config) -> - UriTypes = ?config(code_navigation_types_uri, Config), - UriHeader = ?config(definition_h_uri, Config), - - ExpectedLocations = [ #{ uri => UriTypes - , range => #{from => {15, 24}, to => {15, 30}} - } - ], - ct:comment("Find references for type_b from a remote usage"), - #{result := Locations} = els_client:references(UriTypes, 15, 25), - ct:comment("Find references for type_b from definition"), - #{result := Locations} = els_client:references(UriHeader, 2, 7), - assert_locations(Locations, ExpectedLocations), - ok. + UriTypes = ?config(code_navigation_types_uri, Config), + UriHeader = ?config(definition_h_uri, Config), + + ExpectedLocations = [ + #{ + uri => UriTypes, + range => #{from => {15, 24}, to => {15, 30}} + } + ], + ct:comment("Find references for type_b from a remote usage"), + #{result := Locations} = els_client:references(UriTypes, 15, 25), + ct:comment("Find references for type_b from definition"), + #{result := Locations} = els_client:references(UriHeader, 2, 7), + assert_locations(Locations, ExpectedLocations), + ok. + +-spec atom(config()) -> ok. +atom(Config) -> + Uri = ?config(code_navigation_uri, Config), + %% References for the `code_navigation_extra' atom + #{result := Locations} = els_client:references(Uri, 85, 5), + ExpectedLocations = [ + #{ + uri => Uri, + range => #{from => {85, 3}, to => {85, 24}} + }, + #{ + uri => Uri, + range => #{from => {86, 14}, to => {86, 35}} + }, + #{ + uri => Uri, + range => #{from => {132, 36}, to => {132, 57}} + }, + #{ + uri => Uri, + range => #{from => {134, 35}, to => {134, 56}} + } + ], + assert_locations(Locations, ExpectedLocations), + ok. + +-spec refresh_after_watched_file_deleted(config()) -> ok. +refresh_after_watched_file_deleted(Config) -> + %% Before + UriA = ?config(watched_file_a_uri, Config), + UriB = ?config(watched_file_b_uri, Config), + PathB = ?config(watched_file_b_path, Config), + ExpectedLocationsBefore = [ + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + } + ], + #{result := LocationsBefore} = els_client:references(UriA, 5, 2), + assert_locations(LocationsBefore, ExpectedLocationsBefore), + %% Delete (Simulate a checkout, rebase or similar) + ok = file:delete(PathB), + els_client:did_change_watched_files([{UriB, ?FILE_CHANGE_TYPE_DELETED}]), + %% After + #{result := null} = els_client:references(UriA, 5, 2), + ok. + +-spec refresh_after_watched_file_changed(config()) -> ok. +refresh_after_watched_file_changed(Config) -> + %% Before + UriA = ?config(watched_file_a_uri, Config), + UriB = ?config(watched_file_b_uri, Config), + PathB = ?config(watched_file_b_path, Config), + ExpectedLocationsBefore = [ + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + } + ], + #{result := LocationsBefore} = els_client:references(UriA, 5, 2), + assert_locations(LocationsBefore, ExpectedLocationsBefore), + %% Edit (Simulate a checkout, rebase or similar) + NewContent = re:replace( + ?config(old_content, Config), + "watched_file_a:main()", + "watched_file_a:main(), watched_file_a:main()" + ), + ok = file:write_file(PathB, NewContent), + els_client:did_change_watched_files([{UriB, ?FILE_CHANGE_TYPE_CHANGED}]), + %% After + ExpectedLocationsAfter = [ + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + }, + #{ + uri => UriB, + range => #{from => {6, 26}, to => {6, 45}} + } + ], + #{result := LocationsAfter} = els_client:references(UriA, 5, 2), + assert_locations(LocationsAfter, ExpectedLocationsAfter), + ok. + +-spec refresh_after_watched_file_added(config()) -> ok. +refresh_after_watched_file_added(Config) -> + %% Before + UriA = ?config(watched_file_a_uri, Config), + UriB = ?config(watched_file_b_uri, Config), + PathB = ?config(watched_file_b_path, Config), + ExpectedLocationsBefore = [ + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + } + ], + #{result := LocationsBefore} = els_client:references(UriA, 5, 2), + assert_locations(LocationsBefore, ExpectedLocationsBefore), + %% Add (Simulate a checkout, rebase or similar) + DataDir = ?config(data_dir, Config), + PathC = filename:join([DataDir, "watched_file_c.erl"]), + NewPathC = filename:join(filename:dirname(PathB), "watched_file_c.erl"), + NewUriC = els_uri:uri(NewPathC), + {ok, _} = file:copy(PathC, NewPathC), + els_client:did_change_watched_files([{NewUriC, ?FILE_CHANGE_TYPE_CREATED}]), + %% After + ExpectedLocationsAfter = [ + #{ + uri => NewUriC, + range => #{from => {6, 3}, to => {6, 22}} + }, + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + } + ], + #{result := LocationsAfter} = els_client:references(UriA, 5, 2), + assert_locations(LocationsAfter, ExpectedLocationsAfter), + ok. + +-spec ignore_open_watched_file_added(config()) -> ok. +ignore_open_watched_file_added(Config) -> + %% Before + UriA = ?config(watched_file_a_uri, Config), + UriB = ?config(watched_file_b_uri, Config), + PathB = ?config(watched_file_b_path, Config), + ExpectedLocationsBefore = [ + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + } + ], + #{result := LocationsBefore} = els_client:references(UriA, 5, 2), + assert_locations(LocationsBefore, ExpectedLocationsBefore), + %% Add (Simulate a checkout, rebase or similar) + DataDir = ?config(data_dir, Config), + PathC = filename:join([DataDir, "watched_file_c.erl"]), + NewPathC = filename:join(filename:dirname(PathB), "watched_file_c.erl"), + NewUriC = els_uri:uri(NewPathC), + {ok, _} = file:copy(PathC, NewPathC), + %% Open file, did_change_watched_files requests should be ignored + els_client:did_open(NewUriC, erlang, 1, <<"dummy">>), + els_client:did_change_watched_files([{NewUriC, ?FILE_CHANGE_TYPE_CREATED}]), + %% After + ExpectedLocationsOpen = [ + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + } + ], + #{result := LocationsOpen} = els_client:references(UriA, 5, 2), + assert_locations(LocationsOpen, ExpectedLocationsOpen), + %% Close file, did_change_watched_files requests should be resumed + els_client:did_close(NewUriC), + els_client:did_change_watched_files([{NewUriC, ?FILE_CHANGE_TYPE_CREATED}]), + %% After + ExpectedLocationsClose = [ + #{ + uri => NewUriC, + range => #{from => {6, 3}, to => {6, 22}} + }, + #{ + uri => UriB, + range => #{from => {6, 3}, to => {6, 22}} + } + ], + #{result := LocationsClose} = els_client:references(UriA, 5, 2), + assert_locations(LocationsClose, ExpectedLocationsClose), + ok. %%============================================================================== %% Internal functions @@ -493,24 +732,30 @@ type_included(Config) -> -spec assert_locations([map()], [map()]) -> ok. assert_locations(Locations, ExpectedLocations) -> - ?assertEqual(length(ExpectedLocations), length(Locations)), - Pairs = lists:zip(sort_locations(Locations), ExpectedLocations), - [ begin - #{range := Range} = Location, - #{uri := ExpectedUri, range := ExpectedRange} = Expected, - ?assertMatch(#{uri := ExpectedUri}, Location), - ?assertEqual( els_protocol:range(ExpectedRange) - , Range - ) - end - || {Location, Expected} <- Pairs - ], - ok. + ?assertEqual( + length(ExpectedLocations), + length(Locations), + {{expected, ExpectedLocations}, {actual, Locations}} + ), + Pairs = lists:zip(sort_locations(Locations), ExpectedLocations), + [ + begin + #{range := Range} = Location, + #{uri := ExpectedUri, range := ExpectedRange} = Expected, + ?assertMatch(#{uri := ExpectedUri}, Location), + ?assertEqual( + els_protocol:range(ExpectedRange), + Range + ) + end + || {Location, Expected} <- Pairs + ], + ok. sort_locations(Locations) -> - lists:sort(fun compare_locations/2, Locations). + lists:sort(fun compare_locations/2, Locations). compare_locations(#{range := R1}, #{range := R2}) -> - #{start := #{line := L1, character := C1}} = R1, - #{start := #{line := L2, character := C2}} = R2, - {L1, C1} < {L2, C2}. + #{start := #{line := L1, character := C1}} = R1, + #{start := #{line := L2, character := C2}} = R2, + {L1, C1} < {L2, C2}. diff --git a/apps/els_lsp/test/els_references_SUITE_data/watched_file_c.erl b/apps/els_lsp/test/els_references_SUITE_data/watched_file_c.erl new file mode 100644 index 000000000..c49876fff --- /dev/null +++ b/apps/els_lsp/test/els_references_SUITE_data/watched_file_c.erl @@ -0,0 +1,6 @@ +-module(watched_file_c). + +-export([ main/0 ]). + +main() -> + watched_file_a:main(). diff --git a/apps/els_lsp/test/els_rename_SUITE.erl b/apps/els_lsp/test/els_rename_SUITE.erl index de530aaba..549b0156e 100644 --- a/apps/els_lsp/test/els_rename_SUITE.erl +++ b/apps/els_lsp/test/els_rename_SUITE.erl @@ -3,27 +3,32 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ rename_behaviour_callback/1 - , rename_macro/1 - , rename_variable/1 - , rename_function/1 - , rename_function_quoted_atom/1 - , rename_type/1 - , rename_opaque/1 - , rename_parametrized_macro/1 - , rename_macro_from_usage/1 - , rename_record/1 - , rename_record_field/1 - ]). +-export([ + rename_behaviour_callback/1, + rename_macro/1, + rename_module/1, + rename_variable/1, + rename_variable_list_comp/1, + rename_function/1, + rename_function_quoted_atom/1, + rename_type/1, + rename_opaque/1, + rename_parametrized_macro/1, + rename_macro_from_usage/1, + rename_record/1, + rename_record_field/1, + prepare_rename/1 +]). %%============================================================================== %% Includes @@ -41,404 +46,839 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec rename_behaviour_callback(config()) -> ok. rename_behaviour_callback(Config) -> - Uri = ?config(rename_uri, Config), - Line = 2, - Char = 9, - NewName = <<"new_awesome_name">>, - #{result := Result} = els_client:document_rename(Uri, Line, Char, NewName), - Expected = #{changes => - #{ binary_to_atom(Uri, utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 19, line => 2} - , start => #{character => 10, line => 2}}} - ] - , binary_to_atom(?config(rename_usage1_uri, Config), utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 18, line => 6} - , start => #{character => 9, line => 6}}} - , #{ newText => NewName - , range => - #{ 'end' => #{character => 9, line => 9} - , start => #{character => 0, line => 9}}} - , #{ newText => NewName - , range => - #{ 'end' => #{character => 9, line => 11} - , start => #{character => 0, line => 11}}} - , #{ newText => NewName - , range => - #{ 'end' => #{character => 15, line => 8} - , start => #{character => 6, line => 8}}} - ] - , binary_to_atom(?config(rename_usage2_uri, Config), utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 18, line => 6} - , start => #{character => 9, line => 6}}} - , #{ newText => NewName - , range => - #{ 'end' => #{character => 9, line => 8} - , start => #{character => 0, line => 8}}} - ] - } - }, - assert_changes(Expected, Result). + Uri = ?config(rename_uri, Config), + Line = 2, + Char = 9, + NewName = <<"new_awesome_name">>, + Result = rename(Uri, Line, Char, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 19, line => 2}, + start => #{character => 10, line => 2} + } + } + ], + binary_to_atom(?config(rename_usage1_uri, Config), utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 18, line => 6}, + start => #{character => 9, line => 6} + } + }, + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 9, line => 9}, + start => #{character => 0, line => 9} + } + }, + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 9, line => 11}, + start => #{character => 0, line => 11} + } + }, + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 15, line => 8}, + start => #{character => 6, line => 8} + } + } + ], + binary_to_atom(?config(rename_usage2_uri, Config), utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 18, line => 6}, + start => #{character => 9, line => 6} + } + }, + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 9, line => 8}, + start => #{character => 0, line => 8} + } + } + ] + } + }, + assert_changes(Expected, Result). -spec rename_variable(config()) -> ok. rename_variable(Config) -> - Uri = ?config(rename_variable_uri, Config), - UriAtom = binary_to_atom(Uri, utf8), - NewName = <<"NewAwesomeName">>, - #{result := Result1} = els_client:document_rename(Uri, 3, 3, NewName), - Expected1 = #{changes => #{UriAtom => [ change(NewName, {3, 2}, {3, 5}) - , change(NewName, {2, 4}, {2, 7}) - ]}}, - #{result := Result2} = els_client:document_rename(Uri, 2, 5, NewName), - Expected2 = #{changes => #{UriAtom => [ change(NewName, {3, 2}, {3, 5}) - , change(NewName, {2, 4}, {2, 7}) - ]}}, - #{result := Result3} = els_client:document_rename(Uri, 6, 3, NewName), - Expected3 = #{changes => #{UriAtom => [ change(NewName, {6, 18}, {6, 21}) - , change(NewName, {6, 2}, {6, 5}) - , change(NewName, {5, 9}, {5, 12}) - , change(NewName, {4, 4}, {4, 7}) - ]}}, - #{result := Result4} = els_client:document_rename(Uri, 11, 3, NewName), - Expected4 = #{changes => #{UriAtom => [ change(NewName, {11, 2}, {11, 5}) - , change(NewName, {10, 4}, {10, 7}) - ]}}, - assert_changes(Expected1, Result1), - assert_changes(Expected2, Result2), - assert_changes(Expected3, Result3), - assert_changes(Expected4, Result4). + Uri = ?config(rename_variable_uri, Config), + UriAtom = binary_to_atom(Uri, utf8), + NewName = <<"NewAwesomeName">>, + %% + Result1 = rename(Uri, 3, 3, NewName), + Expected1 = #{ + changes => #{ + UriAtom => [ + change(NewName, {3, 2}, {3, 5}), + change(NewName, {2, 4}, {2, 7}) + ] + } + }, + Result2 = rename(Uri, 2, 5, NewName), + Expected2 = #{ + changes => #{ + UriAtom => [ + change(NewName, {3, 2}, {3, 5}), + change(NewName, {2, 4}, {2, 7}) + ] + } + }, + Result3 = rename(Uri, 6, 3, NewName), + Expected3 = #{ + changes => #{ + UriAtom => [ + change(NewName, {6, 18}, {6, 21}), + change(NewName, {6, 2}, {6, 5}), + change(NewName, {5, 9}, {5, 12}), + change(NewName, {4, 4}, {4, 7}) + ] + } + }, + Result4 = rename(Uri, 11, 3, NewName), + Expected4 = #{ + changes => #{ + UriAtom => [ + change(NewName, {11, 2}, {11, 5}), + change(NewName, {10, 4}, {10, 7}) + ] + } + }, + %% Spec + Result5 = rename(Uri, 13, 10, NewName), + Expected5 = #{ + changes => #{ + UriAtom => [ + change(NewName, {14, 15}, {14, 18}), + change(NewName, {13, 18}, {13, 21}), + change(NewName, {13, 10}, {13, 13}) + ] + } + }, + %% Record + Result6 = rename(Uri, 18, 19, NewName), + Expected6 = #{ + changes => #{ + UriAtom => [ + change(NewName, {19, 20}, {19, 23}), + change(NewName, {18, 19}, {18, 22}) + ] + } + }, + %% Macro + Result7 = rename(Uri, 21, 20, NewName), + Expected7 = #{ + changes => #{ + UriAtom => [ + change(NewName, {21, 26}, {21, 29}), + change(NewName, {21, 20}, {21, 23}), + change(NewName, {21, 14}, {21, 17}) + ] + } + }, + %% Type + Result8 = rename(Uri, 23, 11, NewName), + Expected8 = #{ + changes => #{ + UriAtom => [ + change(NewName, {23, 11}, {23, 14}), + change(NewName, {23, 19}, {23, 22}) + ] + } + }, + %% Opaque + Result9 = rename(Uri, 24, 15, NewName), + Expected9 = #{ + changes => #{ + UriAtom => [ + change(NewName, {24, 15}, {24, 18}), + change(NewName, {24, 23}, {24, 26}) + ] + } + }, + %% Callback + Result10 = rename(Uri, 1, 15, NewName), + Expected10 = #{ + changes => #{ + UriAtom => [ + change(NewName, {1, 23}, {1, 26}), + change(NewName, {1, 15}, {1, 18}) + ] + } + }, + %% If + Result11 = rename(Uri, 29, 4, NewName), + Expected11 = #{ + changes => #{ + UriAtom => [ + change(NewName, {29, 11}, {29, 14}), + change(NewName, {29, 4}, {29, 7}) + ] + } + }, + assert_changes(Expected1, Result1), + assert_changes(Expected2, Result2), + assert_changes(Expected3, Result3), + assert_changes(Expected4, Result4), + assert_changes(Expected5, Result5), + assert_changes(Expected6, Result6), + assert_changes(Expected7, Result7), + assert_changes(Expected8, Result8), + assert_changes(Expected9, Result9), + assert_changes(Expected10, Result10), + assert_changes(Expected11, Result11). + +-spec rename_variable_list_comp(config()) -> ok. +rename_variable_list_comp(Config) -> + Uri = ?config(variable_list_comp_uri, Config), + UriAtom = binary_to_atom(Uri, utf8), + NewName = <<"NewAwesomeName">>, + %% one: Skip LC + Result1 = rename(Uri, 3, 4, NewName), + Result1 = rename(Uri, 5, 4, NewName), + Expected1 = #{ + changes => #{ + UriAtom => [ + change(NewName, {3, 4}, {3, 7}), + change(NewName, {5, 4}, {5, 7}) + ] + } + }, + %% one: Rename in LC only + Result2 = rename(Uri, 4, 6, NewName), + Result2 = rename(Uri, 4, 13, NewName), + Expected2 = #{ + changes => #{ + UriAtom => [ + change(NewName, {4, 6}, {4, 9}), + change(NewName, {4, 13}, {4, 16}) + ] + } + }, + %% two: Rename in first LC only + Result3 = rename(Uri, 8, 6, NewName), + Result3 = rename(Uri, 8, 13, NewName), + Expected3 = #{ + changes => #{ + UriAtom => [ + change(NewName, {8, 6}, {8, 9}), + change(NewName, {8, 13}, {8, 16}) + ] + } + }, + %% two: Rename in second LC only + Result4 = rename(Uri, 9, 6, NewName), + Result4 = rename(Uri, 9, 13, NewName), + Expected4 = #{ + changes => #{ + UriAtom => [ + change(NewName, {9, 6}, {9, 9}), + change(NewName, {9, 13}, {9, 16}) + ] + } + }, + %% three: Rename all Var (no Var in LC pattern) + Result5 = rename(Uri, 12, 4, NewName), + Result5 = rename(Uri, 13, 6, NewName), + Result5 = rename(Uri, 14, 4, NewName), + Expected5 = #{ + changes => #{ + UriAtom => [ + change(NewName, {12, 4}, {12, 7}), + change(NewName, {13, 6}, {13, 9}), + change(NewName, {14, 4}, {14, 7}) + ] + } + }, + %% four: Rename Var2, second LC generator pattern + Result6 = rename(Uri, 17, 12, NewName), + Result6 = rename(Uri, 18, 21, NewName), + Expected6 = #{ + changes => #{ + UriAtom => [ + change(NewName, {17, 12}, {17, 16}), + change(NewName, {18, 21}, {18, 25}) + ] + } + }, + %% four: FIXME: Bug, shouldn't rename Var inside second LC + %% Result7 = rename(Uri, 17, 7, NewName), + %% Result7 = rename(Uri, 17, 21, NewName), + %% Expected7 = #{ + %% changes => #{ + %% UriAtom => [ + %% change(NewName, {17, 7}, {17, 10}), + %% change(NewName, {17, 21}, {17, 24}) + %% ] + %% } + %% }, + assert_changes(Expected1, Result1), + assert_changes(Expected2, Result2), + assert_changes(Expected3, Result3), + assert_changes(Expected4, Result4), + assert_changes(Expected5, Result5), + assert_changes(Expected6, Result6), + %% assert_changes(Expected7, Result7), + ok. -spec rename_macro(config()) -> ok. rename_macro(Config) -> - Uri = ?config(rename_h_uri, Config), - Line = 0, - Char = 13, - NewName = <<"NEW_AWESOME_NAME">>, - NewNameUsage = <<"?NEW_AWESOME_NAME">>, - #{result := Result} = els_client:document_rename(Uri, Line, Char, NewName), - Expected = #{changes => - #{ binary_to_atom(Uri, utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 17, line => 0} - , start => #{character => 8, line => 0}}} - ] - , binary_to_atom(?config(rename_usage1_uri, Config), utf8) => - [ #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 13, line => 15} - , start => #{character => 3, line => 15}}} - , #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 25, line => 15} - , start => #{character => 15, line => 15}}} - ] - , binary_to_atom(?config(rename_usage2_uri, Config), utf8) => - [ #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 26, line => 11} - , start => #{character => 16, line => 11}}} - ] - } - }, - assert_changes(Expected, Result). + Uri = ?config(rename_h_uri, Config), + Line = 0, + Char = 13, + NewName = <<"NEW_AWESOME_NAME">>, + NewNameUsage = <<"?NEW_AWESOME_NAME">>, + Result = rename(Uri, Line, Char, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 17, line => 0}, + start => #{character => 8, line => 0} + } + } + ], + binary_to_atom(?config(rename_usage1_uri, Config), utf8) => + [ + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 13, line => 15}, + start => #{character => 3, line => 15} + } + }, + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 25, line => 15}, + start => #{character => 15, line => 15} + } + } + ], + binary_to_atom(?config(rename_usage2_uri, Config), utf8) => + [ + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 26, line => 11}, + start => #{character => 16, line => 11} + } + } + ] + } + }, + assert_changes(Expected, Result). + +-spec rename_module(config()) -> ok. +rename_module(Config) -> + UriA = ?config(rename_module_a_uri, Config), + UriB = ?config(rename_module_b_uri, Config), + NewName = <<"new_module">>, + Path = filename:dirname(els_uri:path(UriA)), + NewUri = els_uri:uri(filename:join(Path, <<NewName/binary, ".erl">>)), + #{documentChanges := Result} = rename(UriA, 0, 14, NewName), + Expected = [ + %% Module attribute + #{ + edits => [change(NewName, {0, 8}, {0, 23})], + textDocument => #{uri => UriA, version => null} + }, + %% Rename file + #{ + kind => <<"rename">>, + newUri => NewUri, + oldUri => UriA + }, + %% Implicit function + #{ + edits => [change(NewName, {12, 10}, {12, 25})], + textDocument => #{uri => UriB, version => null} + }, + %% Function application + #{ + edits => [change(NewName, {11, 2}, {11, 17})], + textDocument => #{uri => UriB, version => null} + }, + %% Import + #{ + edits => [change(NewName, {3, 8}, {3, 23})], + textDocument => #{uri => UriB, version => null} + }, + %% Type application + #{ + edits => [change(NewName, {7, 18}, {7, 33})], + textDocument => #{uri => UriB, version => null} + }, + %% Behaviour + #{ + edits => [change(NewName, {2, 11}, {2, 26})], + textDocument => #{uri => UriB, version => null} + } + ], + ?assertEqual([], Result -- Expected), + ?assertEqual([], Expected -- Result), + ?assertEqual(lists:sort(Expected), lists:sort(Result)). -spec rename_function(config()) -> ok. rename_function(Config) -> - Uri = ?config(rename_function_uri, Config), - ImportUri = ?config(rename_function_import_uri, Config), - NewName = <<"new_function">>, - %% Function - #{result := Result} = els_client:document_rename(Uri, 4, 2, NewName), - %% Function clause - #{result := Result} = els_client:document_rename(Uri, 6, 2, NewName), - %% Application - #{result := Result} = els_client:document_rename(ImportUri, 7, 18, NewName), - %% Implicit fun - #{result := Result} = els_client:document_rename(Uri, 13, 10, NewName), - %% Export entry - #{result := Result} = els_client:document_rename(Uri, 1, 9, NewName), - %% Import entry - #{result := Result} = els_client:document_rename(ImportUri, 2, 26, NewName), - Expected = #{changes => - #{binary_to_atom(Uri, utf8) => - [ change(NewName, {12, 23}, {12, 26}) - , change(NewName, {13, 10}, {13, 13}) - , change(NewName, {15, 27}, {15, 30}) - , change(NewName, {17, 11}, {17, 14}) - , change(NewName, {18, 2}, {18, 5}) - , change(NewName, {1, 9}, {1, 12}) - , change(NewName, {3, 6}, {3, 9}) - , change(NewName, {4, 0}, {4, 3}) - , change(NewName, {6, 0}, {6, 3}) - , change(NewName, {8, 0}, {8, 3}) - ], - binary_to_atom(ImportUri, utf8) => - [ change(NewName, {7, 18}, {7, 21}) - , change(NewName, {2, 26}, {2, 29}) - , change(NewName, {6, 2}, {6, 5}) - ]}}, - assert_changes(Expected, Result). + Uri = ?config(rename_function_uri, Config), + ImportUri = ?config(rename_function_import_uri, Config), + NewName = <<"new_function">>, + %% Function + Result = rename(Uri, 4, 2, NewName), + %% Function clause + Result = rename(Uri, 6, 2, NewName), + %% Application + Result = rename(ImportUri, 7, 18, NewName), + %% Implicit fun + Result = rename(Uri, 13, 10, NewName), + %% Export entry + Result = rename(Uri, 1, 9, NewName), + %% Import entry + Result = rename(ImportUri, 2, 26, NewName), + %% Spec + Result = rename(Uri, 3, 2, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + change(NewName, {12, 23}, {12, 26}), + change(NewName, {13, 10}, {13, 13}), + change(NewName, {15, 27}, {15, 30}), + change(NewName, {17, 11}, {17, 14}), + change(NewName, {18, 2}, {18, 5}), + change(NewName, {1, 9}, {1, 12}), + change(NewName, {3, 6}, {3, 9}), + change(NewName, {4, 0}, {4, 3}), + change(NewName, {6, 0}, {6, 3}), + change(NewName, {8, 0}, {8, 3}) + ], + binary_to_atom(ImportUri, utf8) => + [ + change(NewName, {7, 18}, {7, 21}), + change(NewName, {2, 26}, {2, 29}), + change(NewName, {6, 2}, {6, 5}) + ] + } + }, + assert_changes(Expected, Result). -spec rename_function_quoted_atom(config()) -> ok. rename_function_quoted_atom(Config) -> - Uri = ?config(rename_function_uri, Config), - Line = 21, - Char = 2, - NewName = <<"new_function">>, - #{result := Result} = els_client:document_rename(Uri, Line, Char, NewName), - Expected = #{changes => - #{binary_to_atom(Uri, utf8) => - [ change(NewName, {29, 23}, {29, 36}) - , change(NewName, {30, 10}, {30, 23}) - , change(NewName, {32, 27}, {32, 40}) - , change(NewName, {34, 2}, {34, 15}) - , change(NewName, {1, 16}, {1, 29}) - , change(NewName, {20, 6}, {20, 19}) - , change(NewName, {21, 0}, {21, 13}) - , change(NewName, {23, 0}, {23, 13}) - , change(NewName, {25, 0}, {25, 13}) - ]}}, - assert_changes(Expected, Result). + Uri = ?config(rename_function_uri, Config), + Line = 21, + Char = 2, + NewName = <<"new_function">>, + Result = rename(Uri, Line, Char, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + change(NewName, {29, 23}, {29, 36}), + change(NewName, {30, 10}, {30, 23}), + change(NewName, {32, 27}, {32, 40}), + change(NewName, {34, 2}, {34, 15}), + change(NewName, {1, 16}, {1, 29}), + change(NewName, {20, 6}, {20, 19}), + change(NewName, {21, 0}, {21, 13}), + change(NewName, {23, 0}, {23, 13}), + change(NewName, {25, 0}, {25, 13}) + ] + } + }, + assert_changes(Expected, Result). -spec rename_type(config()) -> ok. rename_type(Config) -> - Uri = ?config(rename_type_uri, Config), - NewName = <<"new_type">>, - %% Definition - #{result := Result} = els_client:document_rename(Uri, 3, 7, NewName), - %% Application - #{result := Result} = els_client:document_rename(Uri, 5, 18, NewName), - %% Fully qualified application - #{result := Result} = els_client:document_rename(Uri, 4, 30, NewName), - %% Export - #{result := Result} = els_client:document_rename(Uri, 1, 14, NewName), - Expected = #{changes => - #{binary_to_atom(Uri, utf8) => - [ change(NewName, {5, 18}, {5, 21}) - , change(NewName, {4, 30}, {4, 33}) - , change(NewName, {1, 14}, {1, 17}) - , change(NewName, {3, 6}, {3, 9}) - ]}}, - assert_changes(Expected, Result). + Uri = ?config(rename_type_uri, Config), + NewName = <<"new_type">>, + %% Definition + Result = rename(Uri, 3, 7, NewName), + %% Application + Result = rename(Uri, 5, 18, NewName), + %% Fully qualified application + Result = rename(Uri, 4, 30, NewName), + %% Export + Result = rename(Uri, 1, 14, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + change(NewName, {5, 18}, {5, 21}), + change(NewName, {4, 30}, {4, 33}), + change(NewName, {1, 14}, {1, 17}), + change(NewName, {3, 6}, {3, 9}) + ] + } + }, + assert_changes(Expected, Result). -spec rename_opaque(config()) -> ok. rename_opaque(Config) -> - Uri = ?config(rename_type_uri, Config), - NewName = <<"new_opaque">>, - %% Definition - #{result := Result} = els_client:document_rename(Uri, 4, 10, NewName), - %% Application - #{result := Result} = els_client:document_rename(Uri, 5, 29, NewName), - %% Export - #{result := Result} = els_client:document_rename(Uri, 1, 24, NewName), - Expected = #{changes => - #{binary_to_atom(Uri, utf8) => - [ change(NewName, {5, 26}, {5, 29}) - , change(NewName, {1, 21}, {1, 24}) - , change(NewName, {4, 8}, {4, 11}) - ]}}, - assert_changes(Expected, Result). + Uri = ?config(rename_type_uri, Config), + NewName = <<"new_opaque">>, + %% Definition + Result = rename(Uri, 4, 10, NewName), + %% Application + Result = rename(Uri, 5, 29, NewName), + %% Export + Result = rename(Uri, 1, 24, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + change(NewName, {5, 26}, {5, 29}), + change(NewName, {1, 21}, {1, 24}), + change(NewName, {4, 8}, {4, 11}) + ] + } + }, + assert_changes(Expected, Result). -spec rename_parametrized_macro(config()) -> ok. rename_parametrized_macro(Config) -> - Uri = ?config(rename_h_uri, Config), - Line = 2, - Char = 16, - NewName = <<"NEW_AWESOME_NAME">>, - NewNameUsage = <<"?NEW_AWESOME_NAME">>, - #{result := Result} = els_client:document_rename(Uri, Line, Char, NewName), - Expected = #{changes => - #{ binary_to_atom(Uri, utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 30, line => 2} - , start => #{character => 8, line => 2}}} - ] - , binary_to_atom(?config(rename_usage1_uri, Config), utf8) => - [ #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 26, line => 18} - , start => #{character => 3, line => 18}}} - , #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 54, line => 18} - , start => #{character => 31, line => 18}}} - ] - , binary_to_atom( - ?config(rename_usage2_uri, Config), utf8) => - [ #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 52, line => 14} - , start => #{character => 29, line => 14}}} - ] - } - }, - assert_changes(Expected, Result). + Uri = ?config(rename_h_uri, Config), + Line = 2, + Char = 16, + NewName = <<"NEW_AWESOME_NAME">>, + NewNameUsage = <<"?NEW_AWESOME_NAME">>, + Result = rename(Uri, Line, Char, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 30, line => 2}, + start => #{character => 8, line => 2} + } + } + ], + binary_to_atom(?config(rename_usage1_uri, Config), utf8) => + [ + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 26, line => 18}, + start => #{character => 3, line => 18} + } + }, + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 54, line => 18}, + start => #{character => 31, line => 18} + } + } + ], + binary_to_atom( + ?config(rename_usage2_uri, Config), utf8 + ) => + [ + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 52, line => 14}, + start => #{character => 29, line => 14} + } + } + ] + } + }, + assert_changes(Expected, Result). -spec rename_macro_from_usage(config()) -> ok. rename_macro_from_usage(Config) -> - Uri = ?config(rename_usage1_uri, Config), - Line = 15, - Char = 7, - NewName = <<"NEW_AWESOME_NAME">>, - NewNameUsage = <<"?NEW_AWESOME_NAME">>, - #{result := Result} = els_client:document_rename(Uri, Line, Char, NewName), - Expected = #{changes => - #{ binary_to_atom(?config(rename_h_uri, Config), utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 17, line => 0} - , start => #{character => 8, line => 0}}} - ] - , binary_to_atom(?config(rename_usage1_uri, Config), utf8) => - [ #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 13, line => 15} - , start => #{character => 3, line => 15}}} - , #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 25, line => 15} - , start => #{character => 15, line => 15}}} - ] - , binary_to_atom(?config(rename_usage2_uri, Config), utf8) => - [ #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 26, line => 11} - , start => #{character => 16, line => 11}}} - ] - } - }, - assert_changes(Expected, Result). + Uri = ?config(rename_usage1_uri, Config), + Line = 15, + Char = 7, + NewName = <<"NEW_AWESOME_NAME">>, + NewNameUsage = <<"?NEW_AWESOME_NAME">>, + Result = rename(Uri, Line, Char, NewName), + Expected = #{ + changes => + #{ + binary_to_atom(?config(rename_h_uri, Config), utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 17, line => 0}, + start => #{character => 8, line => 0} + } + } + ], + binary_to_atom(?config(rename_usage1_uri, Config), utf8) => + [ + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 13, line => 15}, + start => #{character => 3, line => 15} + } + }, + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 25, line => 15}, + start => #{character => 15, line => 15} + } + } + ], + binary_to_atom(?config(rename_usage2_uri, Config), utf8) => + [ + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 26, line => 11}, + start => #{character => 16, line => 11} + } + } + ] + } + }, + assert_changes(Expected, Result). -spec rename_record(config()) -> ok. rename_record(Config) -> - HdrUri = ?config(rename_h_uri, Config), - UsageUri = ?config(rename_usage1_uri, Config), - NewName = <<"new_record_name">>, - NewNameUsage = <<"#new_record_name">>, - - Expected = #{changes => - #{ binary_to_atom(HdrUri, utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 18, line => 4} - , start => #{character => 8, line => 4}}} - ] - , binary_to_atom(UsageUri, utf8) => - [ #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 29, line => 20} - , start => #{character => 18, line => 20}}} - , #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 18, line => 22} - , start => #{character => 7, line => 22}}} - , #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 18, line => 24} - , start => #{character => 7, line => 24}}} - , #{ newText => NewNameUsage - , range => - #{ 'end' => #{character => 15, line => 26} - , start => #{character => 4, line => 26}}} - ] - } - }, - - %% definition - #{result := Result} = els_client:document_rename(HdrUri, 4, 10, NewName), - assert_changes(Expected, Result), - %% usage - #{result := Result2} = els_client:document_rename(UsageUri, 22, 10, NewName), - assert_changes(Expected, Result2). + HdrUri = ?config(rename_h_uri, Config), + UsageUri = ?config(rename_usage1_uri, Config), + NewName = <<"new_record_name">>, + NewNameUsage = <<"#new_record_name">>, + + Expected = #{ + changes => + #{ + binary_to_atom(HdrUri, utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 18, line => 4}, + start => #{character => 8, line => 4} + } + } + ], + binary_to_atom(UsageUri, utf8) => + [ + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 29, line => 20}, + start => #{character => 18, line => 20} + } + }, + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 18, line => 22}, + start => #{character => 7, line => 22} + } + }, + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 18, line => 24}, + start => #{character => 7, line => 24} + } + }, + #{ + newText => NewNameUsage, + range => + #{ + 'end' => #{character => 15, line => 26}, + start => #{character => 4, line => 26} + } + } + ] + } + }, + + %% definition + assert_changes(Expected, rename(HdrUri, 4, 10, NewName)), + assert_changes(Expected, rename(UsageUri, 22, 10, NewName)). -spec rename_record_field(config()) -> ok. rename_record_field(Config) -> - HdrUri = ?config(rename_h_uri, Config), - UsageUri = ?config(rename_usage1_uri, Config), - NewName = <<"new_record_field_name">>, - - Expected = #{changes => - #{ binary_to_atom(HdrUri, utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 33, line => 4} - , start => #{character => 21, line => 4}}} - ] - , binary_to_atom(UsageUri, utf8) => - [ #{ newText => NewName - , range => - #{ 'end' => #{character => 42, line => 20} - , start => #{character => 30, line => 20}}} - , #{ newText => NewName - , range => - #{ 'end' => #{character => 31, line => 22} - , start => #{character => 19, line => 22}}} - , #{ newText => NewName - , range => - #{ 'end' => #{character => 31, line => 24} - , start => #{character => 19, line => 24}}} - , #{ newText => NewName - , range => - #{ 'end' => #{character => 28, line => 26} - , start => #{character => 16, line => 26}}} - ] - } - }, - - %% definition - #{result := Result} = els_client:document_rename(HdrUri, 4, 25, NewName), - assert_changes(Expected, Result), - %% usage - #{result := Result2} = els_client:document_rename(UsageUri, 22, 25, NewName), - assert_changes(Expected, Result2). - -assert_changes(#{ changes := ExpectedChanges }, #{ changes := Changes }) -> - ?assertEqual(maps:keys(ExpectedChanges), maps:keys(Changes)), - Pairs = lists:zip(lists:sort(maps:to_list(Changes)), - lists:sort(maps:to_list(ExpectedChanges))), - [ begin - ?assertEqual(ExpectedKey, Key), - ?assertEqual(Expected, Change) - end - || {{Key, Change}, {ExpectedKey, Expected}} <- Pairs - ], - ok. + HdrUri = ?config(rename_h_uri, Config), + UsageUri = ?config(rename_usage1_uri, Config), + NewName = <<"new_record_field_name">>, + + Expected = #{ + changes => + #{ + binary_to_atom(HdrUri, utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 33, line => 4}, + start => #{character => 21, line => 4} + } + } + ], + binary_to_atom(UsageUri, utf8) => + [ + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 42, line => 20}, + start => #{character => 30, line => 20} + } + }, + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 31, line => 22}, + start => #{character => 19, line => 22} + } + }, + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 31, line => 24}, + start => #{character => 19, line => 24} + } + }, + #{ + newText => NewName, + range => + #{ + 'end' => #{character => 28, line => 26}, + start => #{character => 16, line => 26} + } + } + ] + } + }, + + %% definition + Result = rename(HdrUri, 4, 25, NewName), + assert_changes(Expected, Result), + %% usage + assert_changes(Expected, rename(UsageUri, 22, 25, NewName)). + +-spec prepare_rename(config()) -> ok. +prepare_rename(Config) -> + Uri = ?config(rename_uri, Config), + %% Pointing to something that isn't a renameable POI should + %% cause prepareRename to fail. + ?assertEqual(prepare_rename_failed, rename(Uri, 1, 1, <<"NewName">>)), + ?assertEqual(prepare_rename_failed, rename(Uri, 99, 99, <<"NewName">>)). + +assert_changes(#{changes := ExpectedChanges}, #{changes := Changes}) -> + ?assertEqual(maps:keys(ExpectedChanges), maps:keys(Changes)), + Pairs = lists:zip( + lists:sort(maps:to_list(Changes)), + lists:sort(maps:to_list(ExpectedChanges)) + ), + [ + begin + ?assertEqual(ExpectedKey, Key), + ?assertEqual(lists:sort(Expected), lists:sort(Change)) + end + || {{Key, Change}, {ExpectedKey, Expected}} <- Pairs + ], + ok. change(NewName, {FromL, FromC}, {ToL, ToC}) -> - #{ newText => NewName - , range => #{ start => #{character => FromC, line => FromL} - , 'end' => #{character => ToC, line => ToL}}}. + #{ + newText => NewName, + range => #{ + start => #{character => FromC, line => FromL}, + 'end' => #{character => ToC, line => ToL} + } + }. + +rename(Uri, Line, Character, NewName) -> + case els_client:prepare_rename(Uri, Line, Character) of + #{result := null} -> + prepare_rename_failed; + #{result := _PreResult} -> + case els_client:rename(Uri, Line, Character, NewName) of + #{result := null} -> + rename_failed; + #{result := Result} -> + Result + end + end. diff --git a/apps/els_lsp/test/els_server_SUITE.erl b/apps/els_lsp/test/els_server_SUITE.erl index 50ab37e8c..b873f7ee2 100644 --- a/apps/els_lsp/test/els_server_SUITE.erl +++ b/apps/els_lsp/test/els_server_SUITE.erl @@ -1,17 +1,17 @@ -module(els_server_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ cancel_request/1 - ]). +-export([cancel_request/1]). %%============================================================================== %% Includes @@ -29,81 +29,85 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(cancel_request = TestCase, Config0) -> - Config = els_test_utils:init_per_testcase(TestCase, Config0), - %% Ensure the background job triggered by the code lens will never return - meck:new(els_background_job, [no_link, passthrough]), - meck:expect(els_background_job, new, - fun(C0) -> - C = C0#{ task => fun(_, _) -> - timer:sleep(infinity) - end}, - meck:passthrough([C]) - end), - [ {mocks, [els_background_job]} | Config]. + Config = els_test_utils:init_per_testcase(TestCase, Config0), + %% Ensure the background job triggered by the code lens will never return + meck:new(els_background_job, [no_link, passthrough]), + meck:expect( + els_background_job, + new, + fun(C0) -> + C = C0#{ + task => fun(_, _) -> + timer:sleep(infinity) + end + }, + meck:passthrough([C]) + end + ), + [{mocks, [els_background_job]} | Config]. -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - Mocks = ?config(mocks, Config), - [meck:unload(Mock) || Mock <- Mocks], - els_test_utils:end_per_testcase(TestCase, Config). + Mocks = ?config(mocks, Config), + [meck:unload(Mock) || Mock <- Mocks], + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec cancel_request(config()) -> ok. cancel_request(Config) -> - %% Trigger a document/CodeLens request in the background - Uri = ?config(code_navigation_uri, Config), - spawn(fun() -> els_client:document_codelens(Uri) end), - %% Extract the current background job (from the lens provider) - Job = wait_until_one_lens_job(), - %% Verify the background job is triggered - ?assert(lists:member(Job, els_background_job:list())), - %% Cancel the original request - Result = els_client:'$_cancelrequest'(), - %% Ensure the previous request is canceled - wait_until_no_lens_jobs(), - ?assertEqual(ok, Result). + %% Trigger a document/CodeLens request in the background + Uri = ?config(code_navigation_uri, Config), + spawn(fun() -> els_client:document_codelens(Uri) end), + %% Extract the current background job (from the lens provider) + Job = wait_until_one_lens_job(), + %% Verify the background job is triggered + ?assert(lists:member(Job, els_background_job:list())), + %% Cancel the original request + Result = els_client:'$_cancelrequest'(), + %% Ensure the previous request is canceled + wait_until_no_lens_jobs(), + ?assertEqual(ok, Result). wait_until_one_lens_job() -> - Jobs = get_current_lens_jobs(), - case Jobs of - [Job] -> - Job; - [] -> - timer:sleep(100), - wait_until_one_lens_job() - end. + Jobs = get_current_lens_jobs(), + case Jobs of + [Job] -> + Job; + [] -> + timer:sleep(100), + wait_until_one_lens_job() + end. -spec wait_until_no_lens_jobs() -> ok. wait_until_no_lens_jobs() -> - case get_current_lens_jobs() of - [] -> - ok; - _ -> - timer:sleep(100), - wait_until_no_lens_jobs() - end. + case get_current_lens_jobs() of + [] -> + ok; + _ -> + timer:sleep(100), + wait_until_no_lens_jobs() + end. -spec get_current_lens_jobs() -> [pid()]. get_current_lens_jobs() -> - #{internal_state := InternalState} = - sys:get_state(els_code_lens_provider, 30 * 1000), - #{in_progress := InProgress} = InternalState, - [Job || {_Uri, Job} <- InProgress]. + State = sys:get_state(els_server, 30 * 1000), + #{in_progress := InProgress} = State, + [Job || {_Uri, Job} <- InProgress]. diff --git a/apps/els_lsp/test/els_signature_help_SUITE.erl b/apps/els_lsp/test/els_signature_help_SUITE.erl new file mode 100644 index 000000000..5cdf0fe6c --- /dev/null +++ b/apps/els_lsp/test/els_signature_help_SUITE.erl @@ -0,0 +1,231 @@ +-module(els_signature_help_SUITE). +%% CT Callbacks +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). + +%% Test cases +-export([ + remote_call/1, + switch_signature_between_arities/1, + non_trigger_character_request/1, + argument_expressions_may_contain_commas/1, + multiline_call/1 +]). + +%%============================================================================== +%% Includes +%%============================================================================== +%% -include_lib("els_core/include/els_core.hrl"). +-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 init_per_suite(config()) -> config(). +init_per_suite(Config) -> + els_test_utils:init_per_suite(Config). + +-spec end_per_suite(config()) -> ok. +end_per_suite(Config) -> + els_test_utils:end_per_suite(Config). + +-spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) -> + els_test_utils:init_per_testcase(TestCase, Config). + +-spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(TestCase, Config) -> + els_test_utils:end_per_testcase(TestCase, Config). + +%%============================================================================== +%% Testcases +%%============================================================================== +-spec remote_call(config()) -> ok. +remote_call(Config) -> + %% Line 6 of this document: " erlang:min(A, B)." + Uri = ?config(signature_help_uri, Config), + %% On the "(" of "erlang:min(" + #{result := Result1} = els_client:signature_help(Uri, 6, 16), + ?assertMatch( + #{ + activeSignature := 0, + activeParameter := 0, + signatures := [ + #{ + label := <<"min(A, B)">>, + parameters := [#{label := <<"A">>}, #{label := <<"B">>}], + documentation := #{ + kind := <<"markdown">> + } + } + ] + }, + Result1 + ), + %% On the "," of "erlang:min(A," + #{result := Result2} = els_client:signature_help(Uri, 6, 18), + ?assertMatch( + #{ + activeSignature := 0, + activeParameter := 1, + signatures := [#{label := <<"min(A, B)">>}] + }, + Result2 + ), + %% On the ")" of "erlang:min(A, B)" + #{result := Result3} = els_client:signature_help(Uri, 6, 21), + ?assertEqual(null, Result3), + ok. + +-spec switch_signature_between_arities(config()) -> ok. +switch_signature_between_arities(Config) -> + %% Line 9 of this document: " maps:get(key, #{}, false)." + Uri = ?config(signature_help_uri, Config), + %% On the "(" of "maps:get(" + #{result := Result1} = els_client:signature_help(Uri, 9, 14), + ?assertMatch( + #{ + activeSignature := 0, + activeParameter := 0, + signatures := [ + #{ + label := <<"get(Arg1, Arg2)">>, + parameters := [#{label := <<"Arg1">>}, #{label := <<"Arg2">>}], + documentation := #{ + kind := <<"markdown">> + } + }, + #{ + label := <<"get(Key, Map, Default)">>, + parameters := [ + #{label := <<"Key">>}, #{label := <<"Map">>}, #{label := <<"Default">>} + ], + documentation := #{ + kind := <<"markdown">> + } + } + ] + }, + Result1 + ), + %% On the "," of "maps:get(key," + #{result := Result2} = els_client:signature_help(Uri, 9, 18), + ?assertMatch(#{activeSignature := 0, activeParameter := 1}, Result2), + %% On the second "," of "maps:get(key, #{},", we switch to the higher + %% arity `maps:get/3' signature + #{result := Result3} = els_client:signature_help(Uri, 9, 23), + ?assertMatch(#{activeSignature := 1, activeParameter := 2}, Result3), + %% On the ")" of "maps:get(key, #{}, false)" + #{result := Result4} = els_client:signature_help(Uri, 9, 30), + ?assertEqual(null, Result4), + ok. + +-spec non_trigger_character_request(config()) -> ok. +non_trigger_character_request(Config) -> + %% Line 9 of this document: " maps:get(key, #{}, false)." + Uri = ?config(signature_help_uri, Config), + %% On the "e" of "maps:get(key" + #{result := Result} = els_client:signature_help(Uri, 9, 16), + ?assertMatch( + #{ + activeSignature := 0, + activeParameter := 0, + signatures := [ + #{ + label := <<"get(Arg1, Arg2)">>, + parameters := [#{label := <<"Arg1">>}, #{label := <<"Arg2">>}], + documentation := #{ + kind := <<"markdown">> + } + } + | _ + ] + }, + Result + ), + ok. + +-spec argument_expressions_may_contain_commas(config()) -> ok. +argument_expressions_may_contain_commas(Config) -> + %% Line 12 of this document: " erlang:max({a, b, c}, {d, e, f})." + Uri = ?config(signature_help_uri, Config), + %% On the "," of "erlang:max({a," + %% The comma belongs to the argument expression instead of separating + %% arguments. + #{result := Result1} = els_client:signature_help(Uri, 12, 19), + ?assertMatch( + #{ + activeSignature := 0, + activeParameter := 0, + signatures := [ + #{ + label := <<"max(A, B)">>, + parameters := [#{label := <<"A">>}, #{label := <<"B">>}], + documentation := #{ + kind := <<"markdown">> + } + } + ] + }, + Result1 + ), + %% On the last "," of "erlang:max({a, b, c}, {d," + #{result := Result2} = els_client:signature_help(Uri, 12, 30), + ?assertMatch( + #{ + activeSignature := 0, + activeParameter := 1, + signatures := [#{label := <<"max(A, B)">>}] + }, + Result2 + ), + ok. + +-spec multiline_call(config()) -> ok. +multiline_call(Config) -> + %% The block being tested here is lines 15-18: + %% + %% erlang:min( + %% 1, + %% 2 + %% ). + Uri = ?config(signature_help_uri, Config), + %% On the "," on line 16 + #{result := Result} = els_client:signature_help(Uri, 16, 9), + ?assertMatch( + #{ + activeSignature := 0, + activeParameter := 1, + signatures := [ + #{ + label := <<"min(A, B)">>, + parameters := [#{label := <<"A">>}, #{label := <<"B">>}], + documentation := #{ + kind := <<"markdown">> + } + } + ] + }, + Result + ), + ok. diff --git a/apps/els_lsp/test/els_test.erl b/apps/els_lsp/test/els_test.erl index 1eb703bea..1879c4656 100644 --- a/apps/els_lsp/test/els_test.erl +++ b/apps/els_lsp/test/els_test.erl @@ -6,124 +6,151 @@ -opaque session() :: #{uri := uri()}. -type source() :: binary(). -type code() :: binary(). --type simplified_diagnostic() :: #{ code => code() - , range => {pos(), pos()} - }. --export_type([ session/0 ]). - --export([ run_diagnostics_test/5 - , start_session/1 - , wait_for_diagnostics/2 - , assert_errors/3 - , assert_warnings/3 - , assert_hints/3 - , assert_contains/2 - , compiler_returns_column_numbers/0 - ]). - --spec run_diagnostics_test(string(), source(), - [els_diagnostics:diagnostic()], - [els_diagnostics:diagnostic()], - [els_diagnostics:diagnostic()]) -> ok. +-type simplified_diagnostic() :: #{ + code => code(), + range => {pos(), pos()} +}. +-export_type([session/0]). + +-export([ + run_diagnostics_test/5, + start_session/1, + wait_for_diagnostics/2, + assert_errors/3, + assert_warnings/3, + assert_hints/3, + assert_contains/2, + compiler_returns_column_numbers/0 +]). + +-spec run_diagnostics_test( + string(), + source(), + [els_diagnostics:diagnostic()], + [els_diagnostics:diagnostic()], + [els_diagnostics:diagnostic()] +) -> ok. run_diagnostics_test(Path, Source, Errors, Warnings, Hints) -> - {ok, Session} = start_session(Path), - Diagnostics = wait_for_diagnostics(Session, Source), - assert_errors(Source, Errors, Diagnostics), - assert_warnings(Source, Warnings, Diagnostics), - assert_hints(Source, Hints, Diagnostics). + {ok, Session} = start_session(Path), + Diagnostics = wait_for_diagnostics(Session, Source), + assert_errors(Source, Errors, Diagnostics), + assert_warnings(Source, Warnings, Diagnostics), + assert_hints(Source, Hints, Diagnostics). -spec start_session(string()) -> {ok, session()}. start_session(Path0) -> - PrivDir = code:priv_dir(els_lsp), - Path = filename:join([els_utils:to_binary(PrivDir), Path0]), - Uri = els_uri:uri(Path), - {ok, #{uri => Uri}}. + PrivDir = code:priv_dir(els_lsp), + Path = filename:join([els_utils:to_binary(PrivDir), Path0]), + Uri = els_uri:uri(Path), + {ok, #{uri => Uri}}. -spec wait_for_diagnostics(session(), source()) -> - [els_diagnostics:diagnostic()]. + [els_diagnostics:diagnostic()]. wait_for_diagnostics(#{uri := Uri}, Source) -> - els_mock_diagnostics:subscribe(), - ok = els_client:did_save(Uri), - Diagnostics = els_mock_diagnostics:wait_until_complete(), - [D || #{source := S} = D <- Diagnostics, S =:= Source]. - --spec assert_errors(source(), - [els_diagnostics:diagnostic()], - [els_diagnostics:diagnostic()]) -> ok. + els_mock_diagnostics:subscribe(), + ok = els_client:did_save(Uri), + Diagnostics = els_mock_diagnostics:wait_until_complete(), + [D || #{source := S} = D <- Diagnostics, S =:= Source]. + +-spec assert_errors( + source(), + [els_diagnostics:diagnostic()], + [els_diagnostics:diagnostic()] +) -> ok. assert_errors(Source, Expected, Diagnostics) -> - assert_diagnostics(Source, Expected, Diagnostics, ?DIAGNOSTIC_ERROR). + assert_diagnostics(Source, Expected, Diagnostics, ?DIAGNOSTIC_ERROR). --spec assert_warnings(source(), - [els_diagnostics:diagnostic()], - [els_diagnostics:diagnostic()]) -> ok. +-spec assert_warnings( + source(), + [els_diagnostics:diagnostic()], + [els_diagnostics:diagnostic()] +) -> ok. assert_warnings(Source, Expected, Diagnostics) -> - assert_diagnostics(Source, Expected, Diagnostics, ?DIAGNOSTIC_WARNING). + assert_diagnostics(Source, Expected, Diagnostics, ?DIAGNOSTIC_WARNING). --spec assert_hints(source(), - [els_diagnostics:diagnostic()], - [els_diagnostics:diagnostic()]) -> ok. +-spec assert_hints( + source(), + [els_diagnostics:diagnostic()], + [els_diagnostics:diagnostic()] +) -> ok. assert_hints(Source, Expected, Diagnostics) -> - assert_diagnostics(Source, Expected, Diagnostics, ?DIAGNOSTIC_HINT). + assert_diagnostics(Source, Expected, Diagnostics, ?DIAGNOSTIC_HINT). --spec assert_contains(els_diagnostics:diagnostic(), - [els_diagnostics:diagnostic()]) -> ok. +-spec assert_contains( + els_diagnostics:diagnostic(), + [els_diagnostics:diagnostic()] +) -> ok. assert_contains(Diagnostic, Diagnostics) -> - Simplified = [simplify_diagnostic(D) || D <- Diagnostics], - ?assert(lists:member(Diagnostic, Simplified)). - --spec assert_diagnostics(source(), - [els_diagnostics:diagnostic()], - [els_diagnostics:diagnostic()], - els_diagnostics:severity()) -> ok. + Simplified = [simplify_diagnostic(D) || D <- Diagnostics], + ?assert(lists:member(Diagnostic, Simplified)). + +-spec assert_diagnostics( + source(), + [els_diagnostics:diagnostic()], + [els_diagnostics:diagnostic()], + els_diagnostics:severity() +) -> ok. assert_diagnostics(Source, Expected, Diagnostics, Severity) -> - Filtered = [D || #{severity := S} = D <- Diagnostics, S =:= Severity], - Simplified = [simplify_diagnostic(D) || D <- Filtered], - FixedExpected = [maybe_fix_range(Source, D) || D <- Expected], - ?assertEqual(FixedExpected, Simplified, Filtered). + Filtered = [D || #{severity := S} = D <- Diagnostics, S =:= Severity], + Simplified = [simplify_diagnostic(D) || D <- Filtered], + FixedExpected = [maybe_fix_range(Source, D) || D <- Expected], + ?assertEqual( + lists:sort(FixedExpected), + lists:sort(Simplified), + Filtered + ). -spec simplify_diagnostic(els_diagnostics:diagnostic()) -> - simplified_diagnostic(). + simplified_diagnostic(). simplify_diagnostic(Diagnostic) -> - Range = simplify_range(maps:get(range, Diagnostic)), - maps:put(range, Range, - maps:remove(severity, - maps:remove(source, Diagnostic))). + Range = simplify_range(maps:get(range, Diagnostic)), + maps:put( + range, + Range, + maps:remove( + severity, + maps:remove(source, Diagnostic) + ) + ). -spec simplify_range(range()) -> {pos(), pos()}. simplify_range(Range) -> - #{ start := #{ character := CharacterStart - , line := LineStart - } - , 'end' := #{ character := CharacterEnd - , line := LineEnd - } - } = Range, - {{LineStart, CharacterStart}, {LineEnd, CharacterEnd}}. + #{ + start := #{ + character := CharacterStart, + line := LineStart + }, + 'end' := #{ + character := CharacterEnd, + line := LineEnd + } + } = Range, + {{LineStart, CharacterStart}, {LineEnd, CharacterEnd}}. maybe_fix_range(<<"Compiler">>, Diagnostic) -> - case compiler_returns_column_numbers() of - false -> - fix_range(Diagnostic); - true -> - Diagnostic - end; + case compiler_returns_column_numbers() of + false -> + fix_range(Diagnostic); + true -> + Diagnostic + end; maybe_fix_range(_Source, Diagnostic) -> - Diagnostic. + Diagnostic. fix_range(#{code := <<"L0000">>} = Diagnostic) -> - Diagnostic; + Diagnostic; fix_range(Diagnostic) -> - #{ range := Range } = Diagnostic, - {{StartLine, StartCol}, {EndLine, EndCol}} = Range, - case StartCol =/= 0 orelse EndCol =/= 0 of - true -> - Diagnostic#{range => {{StartLine, 0}, {EndLine + 1, 0}}}; - false -> - Diagnostic - end. + #{range := Range} = Diagnostic, + {{StartLine, StartCol}, {EndLine, EndCol}} = Range, + case StartCol =/= 0 orelse EndCol =/= 0 of + true -> + Diagnostic#{range => {{StartLine, 0}, {EndLine + 1, 0}}}; + false -> + Diagnostic + end. compiler_returns_column_numbers() -> - %% If epp:open/5 is exported we know that columns are not - %% returned by the compiler warnings and errors. - %% Should find a better heuristic for this. - not erlang:function_exported(epp, open, 5). + %% If epp:open/5 is exported we know that columns are not + %% returned by the compiler warnings and errors. + %% Should find a better heuristic for this. + not erlang:function_exported(epp, open, 5). diff --git a/apps/els_lsp/test/els_test_utils.erl b/apps/els_lsp/test/els_test_utils.erl index 2d96aa3a1..0f5b4ab33 100644 --- a/apps/els_lsp/test/els_test_utils.erl +++ b/apps/els_lsp/test/els_test_utils.erl @@ -1,18 +1,19 @@ -module(els_test_utils). --export([ all/1 - , all/2 - , end_per_suite/1 - , end_per_testcase/2 - , init_per_suite/1 - , init_per_testcase/2 - , start/0 - , wait_for/2 - , wait_for_fun/3 - , wait_until_mock_called/2 - , root_path/0 - , root_uri/0 - ]). +-export([ + all/1, + all/2, + end_per_suite/1, + end_per_testcase/2, + init_per_suite/1, + init_per_testcase/2, + start/0, + wait_for/2, + wait_for_fun/3, + wait_until_mock_called/2, + root_path/0, + root_uri/0 +]). -include_lib("common_test/include/ct.hrl"). @@ -35,93 +36,97 @@ 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)]. + 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) -> - application:load(els_core), - Config. + application:load(els_core), + Config. -spec end_per_suite(config()) -> ok. end_per_suite(_Config) -> - ok. + 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 = start(), - els_client:initialize(root_uri(), #{indexingEnabled => false}), - els_client:initialized(), - SrcConfig = lists:flatten([index_file(S) || S <- sources()]), - TestConfig = lists:flatten([index_file(S) || S <- tests()]), - EscriptConfig = lists:flatten([index_file(S) || S <- escripts()]), - IncludeConfig = lists:flatten([index_file(S) || S <- includes()]), - lists:append( [ SrcConfig - , TestConfig - , EscriptConfig - , IncludeConfig - , [ {started, Started} - | Config] - ]). + meck:new(els_distribution_server, [no_link, passthrough]), + meck:expect(els_distribution_server, connect, 0, ok), + Started = start(), + els_client:initialize(root_uri(), #{indexingEnabled => false}), + els_client:initialized(), + SrcConfig = lists:flatten([index_file(S) || S <- sources()]), + TestConfig = lists:flatten([index_file(S) || S <- tests()]), + EscriptConfig = lists:flatten([index_file(S) || S <- escripts()]), + IncludeConfig = lists:flatten([index_file(S) || S <- includes()]), + lists:append([ + SrcConfig, + TestConfig, + EscriptConfig, + IncludeConfig, + [ + {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. + meck:unload(els_distribution_server), + [application:stop(App) || App <- ?config(started, Config)], + ok. -spec start() -> [atom()]. start() -> - ClientIo = els_fake_stdio:start(), - ServerIo = els_fake_stdio:start(), - els_fake_stdio:connect(ClientIo, ServerIo), - els_fake_stdio:connect(ServerIo, ClientIo), - ok = application:set_env(els_core, io_device, ServerIo), - {ok, Started} = application:ensure_all_started(els_lsp), - els_client:start_link(#{io_device => ClientIo}), - Started. + ClientIo = els_fake_stdio:start(), + ServerIo = els_fake_stdio:start(), + els_fake_stdio:connect(ClientIo, ServerIo), + els_fake_stdio:connect(ServerIo, ClientIo), + ok = application:set_env(els_core, io_device, ServerIo), + {ok, Started} = application:ensure_all_started(els_lsp), + els_client:start_link(#{io_device => ClientIo}), + Started. -spec wait_for(any(), non_neg_integer()) -> ok. wait_for(_Message, Timeout) when Timeout =< 0 -> - timeout; + timeout; wait_for(Message, Timeout) -> - receive Message -> ok - after 10 -> wait_for(Message, Timeout - 10) - end. + receive + Message -> ok + after 10 -> wait_for(Message, Timeout - 10) + end. --spec wait_for_fun(fun(), non_neg_integer(), non_neg_integer()) - -> {ok, any()} | timeout. +-spec wait_for_fun(fun(), non_neg_integer(), non_neg_integer()) -> + {ok, any()} | timeout. wait_for_fun(_CheckFun, _WaitTime, 0) -> - timeout; + 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. + case CheckFun() of + true -> + ok; + {true, Value} -> + {ok, Value}; + false -> + timer:sleep(WaitTime), + wait_for_fun(CheckFun, WaitTime, Retries - 1) + end. -spec sources() -> [binary()]. sources() -> - wildcard("*.erl", "src"). + wildcard("*.erl", "src"). -spec tests() -> [binary()]. tests() -> - wildcard("*.erl", "test"). + wildcard("*.erl", "test"). -spec escripts() -> [binary()]. escripts() -> - wildcard("*.escript", "src"). + wildcard("*.escript", "src"). -spec includes() -> [binary()]. includes() -> - wildcard("*.hrl", "include"). + wildcard("*.hrl", "include"). %% @doc Index a file and produce the respective config entries %% @@ -130,13 +135,15 @@ includes() -> %% accessing this information from test cases. -spec index_file(binary()) -> [{atom(), any()}]. index_file(Path) -> - {ok, Uri} = els_indexing:index_file(Path), - {ok, Text} = file:read_file(Path), - ConfigId = config_id(Path), - [ {atoms_append(ConfigId, '_path'), Path} - , {atoms_append(ConfigId, '_uri'), Uri} - , {atoms_append(ConfigId, '_text'), Text} - ]. + Uri = els_uri:uri(Path), + els_indexing:ensure_deeply_indexed(Uri), + {ok, Text} = file:read_file(Path), + ConfigId = config_id(Path), + [ + {atoms_append(ConfigId, '_path'), Path}, + {atoms_append(ConfigId, '_uri'), Uri}, + {atoms_append(ConfigId, '_text'), Text} + ]. -spec suffix(binary()) -> binary(). suffix(<<".erl">>) -> <<"">>; @@ -145,38 +152,40 @@ suffix(<<".escript">>) -> <<"_escript">>. -spec config_id(string()) -> atom(). config_id(Path) -> - Extension = filename:extension(Path), - BaseName = filename:basename(Path, Extension), - Suffix = suffix(Extension), - binary_to_atom(<<BaseName/binary, Suffix/binary>>, utf8). + Extension = filename:extension(Path), + BaseName = filename:basename(Path, Extension), + Suffix = suffix(Extension), + binary_to_atom(<<BaseName/binary, Suffix/binary>>, utf8). -spec atoms_append(atom(), atom()) -> atom(). atoms_append(Atom1, Atom2) -> - Bin1 = atom_to_binary(Atom1, utf8), - Bin2 = atom_to_binary(Atom2, utf8), - binary_to_atom(<<Bin1/binary, Bin2/binary>>, utf8). + Bin1 = atom_to_binary(Atom1, utf8), + Bin2 = atom_to_binary(Atom2, utf8), + binary_to_atom(<<Bin1/binary, Bin2/binary>>, utf8). -spec wait_until_mock_called(atom(), atom()) -> ok. wait_until_mock_called(M, F) -> - case meck:num_calls(M, F, '_') of - 0 -> - timer:sleep(100), - wait_until_mock_called(M, F); - _ -> - ok - end. + case meck:num_calls(M, F, '_') of + 0 -> + timer:sleep(100), + wait_until_mock_called(M, F); + _ -> + ok + end. -spec root_path() -> binary(). root_path() -> - PrivDir = code:priv_dir(els_lsp), - els_utils:to_binary(filename:join([PrivDir, ?TEST_APP])). + PrivDir = code:priv_dir(els_lsp), + els_utils:to_binary(filename:join([PrivDir, ?TEST_APP])). -spec root_uri() -> els_uri:uri(). root_uri() -> - els_uri:uri(root_path()). + els_uri:uri(root_path()). -spec wildcard(string(), string()) -> [binary()]. wildcard(Extension, Dir) -> - RootDir = els_utils:to_list(root_path()), - [els_utils:to_binary(filename:join([RootDir, Dir, Path])) || - Path <- filelib:wildcard(Extension, filename:join([RootDir, Dir]))]. + RootDir = els_utils:to_list(root_path()), + [ + els_utils:to_binary(filename:join([RootDir, Dir, Path])) + || Path <- filelib:wildcard(Extension, filename:join([RootDir, Dir])) + ]. diff --git a/apps/els_lsp/test/els_text_SUITE.erl b/apps/els_lsp/test/els_text_SUITE.erl index aa1f9c7f8..ebb2df412 100644 --- a/apps/els_lsp/test/els_text_SUITE.erl +++ b/apps/els_lsp/test/els_text_SUITE.erl @@ -1,19 +1,21 @@ -module(els_text_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ apply_edits_single_line/1 - , apply_edits_multi_line/1 - , apply_edits_unicode/1 - ]). +-export([ + apply_edits_single_line/1, + apply_edits_multi_line/1, + apply_edits_unicode/1 +]). %%============================================================================== %% Includes @@ -30,136 +32,156 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(_TestCase, Config) -> - Config. + Config. -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(_TestCase, Config) -> - Config. + Config. %%============================================================================== %% Testcases %%============================================================================== apply_edits_single_line(_Config) -> - In = <<"a b c">>, - C = fun(FromC, ToC, Str) -> {#{from => {0, FromC}, to => {0, ToC}}, Str} end, - F = fun els_text:apply_edits/2, - ?assertEqual(<<"a b c">>, F(In, [])), - ?assertEqual(<<"a b c">>, F(In, [C(0, 0, "")])), - ?assertEqual(<<"_a b c">>, F(In, [C(0, 0, "_")])), - ?assertEqual(<<"_ b c">>, F(In, [C(0, 1, "_")])), - ?assertEqual(<<"__ b c">>, F(In, [C(0, 1, "__")])), - ?assertEqual(<<"__b c">>, F(In, [C(0, 2, "__")])), - ?assertEqual(<<"a _ c">>, F(In, [C(2, 3, "_")])), - ?assertEqual(<<"c">>, F(In, [C(0, 4, "")])), - ?assertEqual(<<"a">>, F(In, [C(1, 5, "")])), - ?assertEqual(<<"">>, F(In, [C(0, 5, "")])), - ?assertEqual(<<"a b c!">>, F(In, [C(5, 5, "!")])), - ?assertEqual(<<>>, F(<<>>, [ C(0, 0, "") - , C(0, 0, "") - ])), - ?assertEqual(<<"cba">>, F(<<>>, [ C(0, 0, "a") - , C(0, 0, "b") - , C(0, 0, "c") - ])), - ?assertEqual(<<"abc">>, F(<<>>, [ C(0, 0, "c") - , C(0, 0, "b") - , C(0, 0, "a") - ])), - ?assertEqual(<<"ab">>, F(In, [ C(0, 0, "a") - , C(1, 6, "") - , C(1, 1, "b") - ])), - ?assertEqual(<<"aba b c">>, F(In, [ C(0, 0, "a") - , C(1, 1, "b") - ])), - ?assertEqual(<<"_ c!">>, F(In, [ C(0, 3, "") - , C(0, 0, "_") - , C(3, 3, "!") - ])), - ok. + In = <<"a b c">>, + C = fun(FromC, ToC, Str) -> {#{from => {0, FromC}, to => {0, ToC}}, Str} end, + F = fun els_text:apply_edits/2, + ?assertEqual(<<"a b c">>, F(In, [])), + ?assertEqual(<<"a b c">>, F(In, [C(0, 0, "")])), + ?assertEqual(<<"_a b c">>, F(In, [C(0, 0, "_")])), + ?assertEqual(<<"_ b c">>, F(In, [C(0, 1, "_")])), + ?assertEqual(<<"__ b c">>, F(In, [C(0, 1, "__")])), + ?assertEqual(<<"__b c">>, F(In, [C(0, 2, "__")])), + ?assertEqual(<<"a _ c">>, F(In, [C(2, 3, "_")])), + ?assertEqual(<<"c">>, F(In, [C(0, 4, "")])), + ?assertEqual(<<"a">>, F(In, [C(1, 5, "")])), + ?assertEqual(<<"">>, F(In, [C(0, 5, "")])), + ?assertEqual(<<"a b c!">>, F(In, [C(5, 5, "!")])), + ?assertEqual( + <<>>, + F(<<>>, [ + C(0, 0, ""), + C(0, 0, "") + ]) + ), + ?assertEqual( + <<"cba">>, + F(<<>>, [ + C(0, 0, "a"), + C(0, 0, "b"), + C(0, 0, "c") + ]) + ), + ?assertEqual( + <<"abc">>, + F(<<>>, [ + C(0, 0, "c"), + C(0, 0, "b"), + C(0, 0, "a") + ]) + ), + ?assertEqual( + <<"ab">>, + F(In, [ + C(0, 0, "a"), + C(1, 6, ""), + C(1, 1, "b") + ]) + ), + ?assertEqual( + <<"aba b c">>, + F(In, [ + C(0, 0, "a"), + C(1, 1, "b") + ]) + ), + ?assertEqual( + <<"_ c!">>, + F(In, [ + C(0, 3, ""), + C(0, 0, "_"), + C(3, 3, "!") + ]) + ), + ok. apply_edits_multi_line(_Config) -> - In = <<"a b c\n" - "d e f\n" - "g h i\n">>, - C = fun(FromL, FromC, ToL, ToC, Str) -> - {#{from => {FromL, FromC}, to => {ToL, ToC}}, Str} - end, - F = fun els_text:apply_edits/2, - ?assertEqual(In, F(In, [])), - ?assertEqual(In, F(In, [C(0, 0, 0, 0, "")])), - ?assertEqual(<<"_ b c\n", - "d e f\n", - "g h i\n">>, F(In, [C(0, 0, 0, 1, "_")])), - ?assertEqual(<<"_a b c\n", - "d e f\n", - "g h i\n">>, F(In, [C(0, 0, 0, 0, "_")])), - ?assertEqual(<<"a b c\n", - "_d e f\n", - "g h i\n">>, F(In, [C(1, 0, 1, 0, "_")])), - ?assertEqual(<<"a b c\n", - "d e f\n", - "_g h i\n">>, F(In, [C(2, 0, 2, 0, "_")])), - ?assertEqual(<<"a b c\n", - "d e f\n", - "g h i_\n">>, F(In, [C(2, 5, 2, 5, "_")])), - ?assertEqual(<<"a b c\n", - "d e f\n", - "g h _\n">>, F(In, [C(2, 4, 2, 5, "_")])), - ?assertEqual(<<"a b c\n", - "d e f\n", - "g _ i\n">>, F(In, [C(2, 2, 2, 3, "_")])), - ?assertEqual(<<"a b c\n", - "d e f\n", - "g h i\n", - "_">>, F(In, [C(3, 0, 3, 0, "_")])), - ?assertEqual(<<"_">>, F(In, [C(0, 0, 3, 0, "_")])), - ?assertEqual(<<"a b _\n", - "d e _\n", - "g h _\n">>, F(In, [ C(0, 4, 0, 5, "_") - , C(1, 4, 1, 5, "_") - , C(2, 4, 2, 5, "_") - ])), - ?assertEqual(<<"a b c\n", - "g h i\n">>, F(In, [C(1, 0, 2, 0, "")])), - ?assertEqual(<<"a b h i\n">>, F(In, [C(0, 3, 2, 1, "")])), - ?assertEqual(<<"a b _ i\n">>, F(In, [ C(0, 3, 2, 1, "") - , C(0, 4, 0, 5, "_") - ])), - ?assertEqual(<<"a b h _\n">>, F(In, [ C(0, 3, 2, 1, "") - , C(0, 6, 0, 7, "_") - ])), - ?assertEqual(<<"a ba\n", - "_\nc\n", - " h i\n">>, F(In, [ C(0, 3, 2, 1, "a\nb\nc\n") - , C(1, 0, 1, 1, "_") - ])), - ok. + In = << + "a b c\n" + "d e f\n" + "g h i\n" + >>, + C = fun(FromL, FromC, ToL, ToC, Str) -> + {#{from => {FromL, FromC}, to => {ToL, ToC}}, Str} + end, + F = fun els_text:apply_edits/2, + ?assertEqual(In, F(In, [])), + ?assertEqual(In, F(In, [C(0, 0, 0, 0, "")])), + ?assertEqual(<<"_ b c\n", "d e f\n", "g h i\n">>, F(In, [C(0, 0, 0, 1, "_")])), + ?assertEqual(<<"_a b c\n", "d e f\n", "g h i\n">>, F(In, [C(0, 0, 0, 0, "_")])), + ?assertEqual(<<"a b c\n", "_d e f\n", "g h i\n">>, F(In, [C(1, 0, 1, 0, "_")])), + ?assertEqual(<<"a b c\n", "d e f\n", "_g h i\n">>, F(In, [C(2, 0, 2, 0, "_")])), + ?assertEqual(<<"a b c\n", "d e f\n", "g h i_\n">>, F(In, [C(2, 5, 2, 5, "_")])), + ?assertEqual(<<"a b c\n", "d e f\n", "g h _\n">>, F(In, [C(2, 4, 2, 5, "_")])), + ?assertEqual(<<"a b c\n", "d e f\n", "g _ i\n">>, F(In, [C(2, 2, 2, 3, "_")])), + ?assertEqual(<<"a b c\n", "d e f\n", "g h i\n", "_">>, F(In, [C(3, 0, 3, 0, "_")])), + ?assertEqual(<<"_">>, F(In, [C(0, 0, 3, 0, "_")])), + ?assertEqual( + <<"a b _\n", "d e _\n", "g h _\n">>, + F(In, [ + C(0, 4, 0, 5, "_"), + C(1, 4, 1, 5, "_"), + C(2, 4, 2, 5, "_") + ]) + ), + ?assertEqual(<<"a b c\n", "g h i\n">>, F(In, [C(1, 0, 2, 0, "")])), + ?assertEqual(<<"a b h i\n">>, F(In, [C(0, 3, 2, 1, "")])), + ?assertEqual( + <<"a b _ i\n">>, + F(In, [ + C(0, 3, 2, 1, ""), + C(0, 4, 0, 5, "_") + ]) + ), + ?assertEqual( + <<"a b h _\n">>, + F(In, [ + C(0, 3, 2, 1, ""), + C(0, 6, 0, 7, "_") + ]) + ), + ?assertEqual( + <<"a ba\n", "_\nc\n", " h i\n">>, + F(In, [ + C(0, 3, 2, 1, "a\nb\nc\n"), + C(1, 0, 1, 1, "_") + ]) + ), + ok. apply_edits_unicode(_Config) -> - In = <<"二郎"/utf8>>, - C = fun(FromC, ToC, Str) -> {#{from => {0, FromC}, to => {0, ToC}}, Str} end, - F = fun els_text:apply_edits/2, - ?assertEqual(<<"㇐郎"/utf8>>, F(In, [C(0, 1, "㇐")])), - ?assertEqual(<<"二㇐郎"/utf8>>, F(In, [C(1, 1, "㇐")])), - ?assertEqual(<<"二㇐"/utf8>>, F(In, [C(1, 2, "㇐")])), - ?assertEqual(<<"二郎㇐"/utf8>>, F(In, [C(2, 2, "㇐")])), - ?assertEqual(<<"二郎㇐"/utf8>>, F(In, [C(2, 2, "㇐")])), - ?assertEqual(<<"㇐二郎"/utf8>>, F(In, [C(0, 0, "㇐")])), - ok. + In = <<"二郎"/utf8>>, + C = fun(FromC, ToC, Str) -> {#{from => {0, FromC}, to => {0, ToC}}, Str} end, + F = fun els_text:apply_edits/2, + ?assertEqual(<<"㇐郎"/utf8>>, F(In, [C(0, 1, "㇐")])), + ?assertEqual(<<"二㇐郎"/utf8>>, F(In, [C(1, 1, "㇐")])), + ?assertEqual(<<"二㇐"/utf8>>, F(In, [C(1, 2, "㇐")])), + ?assertEqual(<<"二郎㇐"/utf8>>, F(In, [C(2, 2, "㇐")])), + ?assertEqual(<<"二郎㇐"/utf8>>, F(In, [C(2, 2, "㇐")])), + ?assertEqual(<<"㇐二郎"/utf8>>, F(In, [C(0, 0, "㇐")])), + ok. diff --git a/apps/els_lsp/test/els_text_edit_SUITE.erl b/apps/els_lsp/test/els_text_edit_SUITE.erl index 94dc5eec9..b64bf2c64 100644 --- a/apps/els_lsp/test/els_text_edit_SUITE.erl +++ b/apps/els_lsp/test/els_text_edit_SUITE.erl @@ -3,17 +3,17 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ text_edit_diff/1 - ]). +-export([text_edit_diff/1]). %%============================================================================== %% Includes @@ -31,47 +31,69 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(TestCase, Config) when + TestCase == text_edit_diff +-> + case els_utils:is_windows() of + true -> + %% TODO: Testcase fails on windows since OTP 24, fix! + {skip, "Testcase not supported on Windows."}; + false -> + els_test_utils:init_per_testcase(TestCase, Config) + end; init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec text_edit_diff(config()) -> ok. text_edit_diff(Config) -> - DiagnosticsUri = ?config(diagnostics_uri, Config), - DiagnosticsPath = els_uri:path(DiagnosticsUri), - DiagnosticsDiffPath = ?config('diagnostics.new_path', Config), - Result = els_text_edit:diff_files(DiagnosticsPath, DiagnosticsDiffPath), - [Edit1, Edit2] = Result, - ?assertEqual( #{newText => - <<"%% Changed diagnostics.erl, to test diff generation\n">>, - range => - #{'end' => #{character => 0, line => 1}, - start => #{character => 0, line => 1}}} - , Edit1), - ?assertEqual( #{newText => <<"main(X) -> X + 1.\n">>, - range => - #{'end' => #{character => 0, line => 8}, - start => #{character => 0, line => 6}}} - , Edit2), - ok. + DiagnosticsUri = ?config(diagnostics_uri, Config), + DiagnosticsPath = els_uri:path(DiagnosticsUri), + DiagnosticsDiffPath = ?config('diagnostics.new_path', Config), + Result = els_text_edit:diff_files(DiagnosticsPath, DiagnosticsDiffPath), + [Edit1, Edit2] = Result, + ?assertEqual( + #{ + newText => + <<"%% Changed diagnostics.erl, to test diff generation\n">>, + range => + #{ + 'end' => #{character => 0, line => 1}, + start => #{character => 0, line => 1} + } + }, + Edit1 + ), + ?assertEqual( + #{ + newText => <<"main(X) -> X + 1.\n">>, + range => + #{ + 'end' => #{character => 0, line => 8}, + start => #{character => 0, line => 6} + } + }, + Edit2 + ), + ok. diff --git a/apps/els_lsp/test/els_workspace_symbol_SUITE.erl b/apps/els_lsp/test/els_workspace_symbol_SUITE.erl index 95dc60103..2d4aae6fb 100644 --- a/apps/els_lsp/test/els_workspace_symbol_SUITE.erl +++ b/apps/els_lsp/test/els_workspace_symbol_SUITE.erl @@ -1,20 +1,21 @@ -module(els_workspace_symbol_SUITE). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ query_multiple/1 - , query_single/1 - , query_none/1 - ]). - +-export([ + query_multiple/1, + query_single/1, + query_none/1 +]). -include("els_lsp.hrl"). @@ -34,104 +35,135 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> - els_test_utils:init_per_suite(Config). + els_test_utils:init_per_suite(Config). -spec end_per_suite(config()) -> ok. end_per_suite(Config) -> - els_test_utils:end_per_suite(Config). + els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). init_per_testcase(TestCase, Config) -> - els_test_utils:init_per_testcase(TestCase, Config). + els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. end_per_testcase(TestCase, Config) -> - els_test_utils:end_per_testcase(TestCase, Config). + els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== -spec query_multiple(config()) -> ok. query_multiple(Config) -> - Query = <<"code_navigation">>, - Uri = ?config(code_navigation_uri, Config), - ExtraUri = ?config(code_navigation_extra_uri, Config), - TypesUri = ?config(code_navigation_types_uri, Config), - UndefUri = ?config(code_navigation_undefined_uri, Config), - #{result := Result} = els_client:workspace_symbol(Query), - Expected = [ #{ kind => ?SYMBOLKIND_MODULE - , location => - #{ range => - #{ 'end' => #{character => 0, line => 0} - , start => #{character => 0, line => 0} - } - , uri => Uri - } - , name => <<"code_navigation">> - } - , #{ kind => ?SYMBOLKIND_MODULE - , location => - #{ range => - #{ 'end' => #{character => 0, line => 0} - , start => #{character => 0, line => 0} - } - , uri => ExtraUri - } - , name => <<"code_navigation_extra">> - } - , #{ kind => ?SYMBOLKIND_MODULE - , location => - #{ range => - #{ 'end' => #{character => 0, line => 0} - , start => #{character => 0, line => 0} - } - , uri => TypesUri - } - , name => <<"code_navigation_types">> - } - , #{ kind => ?SYMBOLKIND_MODULE - , location => - #{ range => - #{ 'end' => #{character => 0, line => 0} - , start => #{character => 0, line => 0} - } - , uri => UndefUri - } - , name => <<"code_navigation_undefined">> - } - ], - ?assertEqual(lists:sort(Expected), lists:sort(Result)), - ok. + Query = <<"code_navigation">>, + Uri = ?config(code_navigation_uri, Config), + ExtraUri = ?config(code_navigation_extra_uri, Config), + TypesUri = ?config(code_navigation_types_uri, Config), + UndefUri = ?config(code_navigation_undefined_uri, Config), + BrokenUri = ?config(code_navigation_broken_uri, Config), + #{result := Result} = els_client:workspace_symbol(Query), + Expected = [ + #{ + kind => ?SYMBOLKIND_MODULE, + location => + #{ + range => + #{ + 'end' => #{character => 0, line => 0}, + start => #{character => 0, line => 0} + }, + uri => Uri + }, + name => <<"code_navigation">> + }, + #{ + kind => ?SYMBOLKIND_MODULE, + location => + #{ + range => + #{ + 'end' => #{character => 0, line => 0}, + start => #{character => 0, line => 0} + }, + uri => ExtraUri + }, + name => <<"code_navigation_extra">> + }, + #{ + kind => ?SYMBOLKIND_MODULE, + location => + #{ + range => + #{ + 'end' => #{character => 0, line => 0}, + start => #{character => 0, line => 0} + }, + uri => TypesUri + }, + name => <<"code_navigation_types">> + }, + #{ + kind => ?SYMBOLKIND_MODULE, + location => + #{ + range => + #{ + 'end' => #{character => 0, line => 0}, + start => #{character => 0, line => 0} + }, + uri => UndefUri + }, + name => <<"code_navigation_undefined">> + }, + #{ + kind => ?SYMBOLKIND_MODULE, + location => + #{ + range => + #{ + 'end' => #{character => 0, line => 0}, + start => #{character => 0, line => 0} + }, + uri => BrokenUri + }, + name => <<"code_navigation_broken">> + } + ], + ?assertEqual(lists:sort(Expected), lists:sort(Result)), + ok. -spec query_single(config()) -> ok. query_single(Config) -> - Query = <<"extra">>, - ExtraUri = ?config(code_navigation_extra_uri, Config), - #{result := Result} = els_client:workspace_symbol(Query), - Expected = [ #{ kind => ?SYMBOLKIND_MODULE - , location => - #{ range => - #{ 'end' => #{character => 0, line => 0} - , start => #{character => 0, line => 0} - } - , uri => ExtraUri - } - , name => <<"code_navigation_extra">> - } - ], - ?assertEqual(lists:sort(Expected), lists:sort(Result)), - ok. + Query = <<"_extra">>, + ExtraUri = ?config(code_navigation_extra_uri, Config), + #{result := Result} = els_client:workspace_symbol(Query), + Expected = [ + #{ + kind => ?SYMBOLKIND_MODULE, + location => + #{ + range => + #{ + 'end' => #{character => 0, line => 0}, + start => #{character => 0, line => 0} + }, + uri => ExtraUri + }, + name => <<"code_navigation_extra">> + } + ], + ?assertEqual(lists:sort(Expected), lists:sort(Result)), + ok. -spec query_none(config()) -> ok. query_none(_Config) -> - #{result := Result} = els_client:workspace_symbol(<<"invalid_query">>), - ?assertEqual([], Result), - ok. + #{result := Result} = els_client:workspace_symbol(<<"invalid_query">>), + ?assertEqual([], Result), + ok. diff --git a/apps/els_lsp/test/erlang_ls_SUITE.erl b/apps/els_lsp/test/erlang_ls_SUITE.erl index c5ffdfbe0..ca5d027e8 100644 --- a/apps/els_lsp/test/erlang_ls_SUITE.erl +++ b/apps/els_lsp/test/erlang_ls_SUITE.erl @@ -3,18 +3,20 @@ -include("els_lsp.hrl"). %% CT Callbacks --export([ suite/0 - , init_per_suite/1 - , end_per_suite/1 - , init_per_testcase/2 - , end_per_testcase/2 - , all/0 - ]). +-export([ + suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0 +]). %% Test cases --export([ parse_args/1 - , log_root/1 - ]). +-export([ + parse_args/1, + log_root/1 +]). %%============================================================================== %% Includes @@ -32,64 +34,67 @@ %%============================================================================== -spec suite() -> [tuple()]. suite() -> - [{timetrap, {seconds, 30}}]. + [{timetrap, {seconds, 30}}]. -spec all() -> [atom()]. all() -> - els_test_utils:all(?MODULE). + els_test_utils:all(?MODULE). -spec init_per_suite(config()) -> config(). init_per_suite(_Config) -> - []. + []. -spec end_per_suite(config()) -> ok. end_per_suite(_Config) -> - ok. + 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. + 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). + 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, []) -> + ok; unset_env(Application, [{Par, _Val} | Rest]) -> - application:unset_env(Application, Par), - unset_env(Application, 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" - ], - erlang_ls:parse_args(Args), - ?assertEqual('error', application:get_env(els_core, log_level, undefined)), - ok. + Args = [ + "--log-dir", + "/test", + "--log-level", + "error" + ], + erlang_ls: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_lsp"} end), + meck:new(file, [unstick]), + meck:expect(file, get_cwd, fun() -> {ok, "/root/els_lsp"} end), - Args = [ "--log-dir", "/somewhere_else/logs" - ], - erlang_ls:parse_args(Args), - ?assertEqual("/somewhere_else/logs/els_lsp", erlang_ls:log_root()), + Args = ["--log-dir", "/somewhere_else/logs"], + erlang_ls:parse_args(Args), + ?assertEqual("/somewhere_else/logs/els_lsp", erlang_ls:log_root()), - meck:unload(file), - ok. + meck:unload(file), + ok. diff --git a/apps/els_lsp/test/prop_statem.erl b/apps/els_lsp/test/prop_statem.erl index d1f05d909..ad4a73352 100644 --- a/apps/els_lsp/test/prop_statem.erl +++ b/apps/els_lsp/test/prop_statem.erl @@ -26,22 +26,23 @@ %% Initial State %%============================================================================== initial_state() -> - #{ connected => false - , initialized => false - , initialized_sent => false - , shutdown => false - , documents => [] - }. + #{ + connected => false, + initialized => false, + initialized_sent => false, + shutdown => false, + documents => [] + }. %%============================================================================== %% Weights %%============================================================================== -weight(_S, '$_cancelrequest') -> 1; +weight(_S, '$_cancelrequest') -> 1; weight(_S, '$_settracenotification') -> 1; -weight(_S, '$_unexpectedrequest') -> 1; -weight(_S, shutdown) -> 1; -weight(_S, exit) -> 1; -weight(_S, _Cmd) -> 5. +weight(_S, '$_unexpectedrequest') -> 1; +weight(_S, shutdown) -> 1; +weight(_S, exit) -> 1; +weight(_S, _Cmd) -> 5. %%============================================================================== %% Commands @@ -51,347 +52,377 @@ weight(_S, _Cmd) -> 5. %% Connect %%------------------------------------------------------------------------------ connect() -> - ClientIo = els_fake_stdio:start(), - {ok, ServerIo} = application:get_env(els_core, io_device), - els_fake_stdio:connect(ClientIo, ServerIo), - els_fake_stdio:connect(ServerIo, ClientIo), - els_client:start_link(#{io_device => ClientIo}). + ClientIo = els_fake_stdio:start(), + {ok, ServerIo} = application:get_env(els_core, io_device), + els_fake_stdio:connect(ClientIo, ServerIo), + els_fake_stdio:connect(ServerIo, ClientIo), + els_client:start_link(#{io_device => ClientIo}). connect_args(_S) -> - []. + []. connect_pre(#{connected := Connected} = _S) -> - not Connected. + not Connected. connect_next(S, _R, _Args) -> - S#{connected => true}. + S#{connected => true}. connect_post(_S, _Args, Res) -> - ?assertMatch({ok, _}, Res), - true. + ?assertMatch({ok, _}, Res), + true. %%------------------------------------------------------------------------------ %% Initialize %%------------------------------------------------------------------------------ initialize(RootUri, InitOptions) -> - els_client:initialize(RootUri, InitOptions). + els_client:initialize(RootUri, InitOptions). initialize_args(_S) -> - [ els_proper_gen:root_uri() - , els_proper_gen:init_options() - ]. + [ + els_proper_gen:root_uri(), + els_proper_gen:init_options() + ]. initialize_pre(#{connected := Connected} = _S) -> - Connected. + Connected. initialize_next(#{shutdown := true} = S, _R, _Args) -> - S; + S; initialize_next(S, _R, _Args) -> - S#{initialized => true}. + S#{initialized => true}. initialize_post(#{shutdown := true}, _Args, Res) -> - assert_invalid_request(Res), - true; + assert_invalid_request(Res), + true; initialize_post(_S, _Args, Res) -> - Expected = els_general_provider:server_capabilities(), - ?assertEqual(Expected, maps:get(result, Res)), - true. + Expected = els_general_provider:server_capabilities(), + ?assertEqual(Expected, maps:get(result, Res)), + true. %%------------------------------------------------------------------------------ %% Initialized %%------------------------------------------------------------------------------ initialized() -> - els_client:initialized(). + els_client:initialized(). initialized_args(_S) -> - []. + []. -initialized_pre(#{ connected := Connected - , initialized := Initialized - , initialized_sent := InitializedSent - } = _S) -> - Connected andalso Initialized andalso not InitializedSent. +initialized_pre( + #{ + connected := Connected, + initialized := Initialized, + initialized_sent := InitializedSent + } = _S +) -> + Connected andalso Initialized andalso not InitializedSent. initialized_next(#{shutdown := true} = S, _R, _Args) -> - S; + S; initialized_next(S, _R, _Args) -> - S#{initialized_sent => true}. + S#{initialized_sent => true}. initialized_post(_S, _Args, Res) -> - ?assertEqual(ok, Res), - true. + ?assertEqual(ok, Res), + true. %%------------------------------------------------------------------------------ %% $/cancelRequest %%------------------------------------------------------------------------------ '$_cancelrequest'(RequestId) -> - els_client:'$_cancelrequest'(RequestId). + els_client:'$_cancelrequest'(RequestId). '$_cancelrequest_args'(_S) -> - [pos_integer()]. + [pos_integer()]. '$_cancelrequest_pre'(#{connected := Connected} = _S) -> - Connected. + Connected. '$_cancelrequest_pre'(_S, [_Id]) -> - true. + true. '$_cancelrequest_next'(S, _R, [_Id]) -> - S. + S. '$_cancelrequest_post'(_S, _Args, Res) -> - ?assertEqual(ok, Res), - true. + ?assertEqual(ok, Res), + true. %%------------------------------------------------------------------------------ %% $/setTraceNotification %%------------------------------------------------------------------------------ '$_settracenotification'() -> - els_client:'$_settracenotification'(). + els_client:'$_settracenotification'(). '$_settracenotification_args'(_S) -> - []. + []. '$_settracenotification_pre'(#{connected := Connected} = _S) -> - Connected. + Connected. '$_settracenotification_pre'(_S, []) -> - true. + true. '$_settracenotification_next'(S, _R, []) -> - S. + S. '$_settracenotification_post'(_S, _Args, Res) -> - ?assertEqual(ok, Res), - true. + ?assertEqual(ok, Res), + true. %%------------------------------------------------------------------------------ %% $/unexpectedRequest %%------------------------------------------------------------------------------ '$_unexpectedrequest'() -> - els_client:'$_unexpectedrequest'(). + els_client:'$_unexpectedrequest'(). '$_unexpectedrequest_args'(_S) -> - []. + []. '$_unexpectedrequest_pre'(#{connected := Connected} = _S) -> - Connected. + Connected. '$_unexpectedrequest_pre'(_S, []) -> - true. + true. '$_unexpectedrequest_next'(S, _R, []) -> - S. + S. '$_unexpectedrequest_post'(_S, _Args, Res) -> - assert_method_not_found(Res), - true. + assert_method_not_found(Res), + true. %%------------------------------------------------------------------------------ %% textDocument/didOpen %%------------------------------------------------------------------------------ did_open(Uri, LanguageId, Version, Text) -> - els_client:did_open(Uri, LanguageId, Version, Text). + els_client:did_open(Uri, LanguageId, Version, Text). did_open_args(_S) -> - [els_proper_gen:uri(), <<"erlang">>, 0, els_proper_gen:tokens()]. + [els_proper_gen:uri(), <<"erlang">>, 0, els_proper_gen:tokens()]. -did_open_pre(#{ connected := Connected - , initialized_sent := InitializedSent - } = _S) -> - Connected andalso InitializedSent. +did_open_pre( + #{ + connected := Connected, + initialized_sent := InitializedSent + } = _S +) -> + Connected andalso InitializedSent. did_open_next(#{documents := Documents0} = S, _R, [Uri, _, _, _]) -> - S#{ documents => Documents0 ++ [Uri]}. + file:write_file(els_uri:path(Uri), <<"dummy">>), + S#{documents => Documents0 ++ [Uri]}. did_open_post(_S, _Args, Res) -> - ?assertEqual(ok, Res), - true. + ?assertEqual(ok, Res), + true. %%------------------------------------------------------------------------------ %% textDocument/didSave %%------------------------------------------------------------------------------ did_save(Uri) -> - els_client:did_save(Uri). + els_client:did_save(Uri). did_save_args(_S) -> - [els_proper_gen:uri()]. + [els_proper_gen:uri()]. -did_save_pre(#{ connected := Connected - , initialized_sent := InitializedSent - } = _S) -> - Connected andalso InitializedSent. +did_save_pre( + #{ + connected := Connected, + initialized_sent := InitializedSent + } = _S +) -> + Connected andalso InitializedSent. -did_save_next(S, _R, _Args) -> - S. +did_save_next(S, _R, [Uri]) -> + file:write_file(els_uri:path(Uri), <<"dummy">>), + S. did_save_post(_S, _Args, Res) -> - ?assertEqual(ok, Res), - true. + ?assertEqual(ok, Res), + true. %%------------------------------------------------------------------------------ %% textDocument/didClose %%------------------------------------------------------------------------------ did_close(Uri) -> - els_client:did_close(Uri). + els_client:did_close(Uri). did_close_args(_S) -> - [els_proper_gen:uri()]. + [els_proper_gen:uri()]. -did_close_pre(#{ connected := Connected - , initialized_sent := InitializedSent - } = _S) -> - Connected andalso InitializedSent. +did_close_pre( + #{ + connected := Connected, + initialized_sent := InitializedSent + } = _S +) -> + Connected andalso InitializedSent. did_close_next(S, _R, _Args) -> - S. + S. did_close_post(_S, _Args, Res) -> - ?assertEqual(ok, Res), - true. + ?assertEqual(ok, Res), + true. %%------------------------------------------------------------------------------ %% Shutdown %%------------------------------------------------------------------------------ shutdown() -> - els_client:shutdown(). + els_client:shutdown(). shutdown_args(_S) -> - []. + []. shutdown_pre(#{connected := Connected} = _S) -> - Connected. + Connected. shutdown_next(#{initialized := false} = S, _R, _Args) -> - S; + S; shutdown_next(S, _R, _Args) -> - S#{shutdown => true}. + S#{shutdown => true}. shutdown_post(#{shutdown := true}, _Args, Res) -> - assert_invalid_request(Res), - true; + assert_invalid_request(Res), + true; shutdown_post(#{initialized := false}, _Args, Res) -> - assert_server_not_initialized(Res), - true; + assert_server_not_initialized(Res), + true; shutdown_post(_S, _Args, Res) -> - ?assertMatch(#{result := null}, Res), - true. + ?assertMatch(#{result := null}, Res), + true. %%------------------------------------------------------------------------------ %% Shutdown %%------------------------------------------------------------------------------ exit() -> - els_client:exit(). + els_client:exit(). exit_args(_S) -> - []. + []. exit_pre(#{connected := Connected} = _S) -> - Connected. + Connected. exit_next(S, _R, _Args) -> - %% We disconnect to simulate the server goes down - catch disconnect(), - S#{shutdown => false, connected => false, initialized => false}. + %% We disconnect to simulate the server goes down + catch disconnect(), + S#{shutdown => false, connected => false, initialized => false}. exit_post(S, _Args, Res) -> - ExpectedExitCode = case maps:get(shutdown, S, false) of - true -> 0; - false -> 1 - end, - els_test_utils:wait_for(halt_called, 1000), - ?assert(meck:called(els_utils, halt, [ExpectedExitCode])), - ?assertMatch(ok, Res), - true. + ExpectedExitCode = + case maps:get(shutdown, S, false) of + true -> 0; + false -> 1 + end, + els_test_utils:wait_for(halt_called, 6400), + ?assert(meck:called(els_utils, halt, [ExpectedExitCode])), + ?assertMatch(ok, Res), + true. %%------------------------------------------------------------------------------ %% Disconnect %%------------------------------------------------------------------------------ disconnect() -> - els_client:stop(). + els_client:stop(). disconnect_args(_S) -> - []. + []. disconnect_pre(#{connected := Connected} = _S) -> - Connected. + Connected. disconnect_next(S, _R, _Args) -> - S#{connected => false}. + S#{connected => false}. disconnect_post(_S, _Args, Res) -> - ?assertEqual(ok, Res), - true. + ?assertEqual(ok, Res), + true. %%============================================================================== %% The statem's property %%============================================================================== prop_main() -> - Config = #{ setup_fun => fun setup/0 - , teardown_fun => fun teardown/1 - , cleanup_fun => fun cleanup/0 - }, - proper_contrib_statem:run(?MODULE, Config). + Config = #{ + setup_fun => fun setup/0, + teardown_fun => fun teardown/1, + cleanup_fun => fun cleanup/0 + }, + proper_contrib_statem:run(?MODULE, Config). %%============================================================================== %% Setup %%============================================================================== setup() -> - meck:new(els_distribution_server, [no_link, passthrough]), - meck:new(els_compiler_diagnostics, [no_link, passthrough]), - meck:new(els_dialyzer_diagnostics, [no_link, passthrough]), - meck:new(els_elvis_diagnostics, [no_link, passthrough]), - meck:new(els_utils, [no_link, passthrough]), - meck:expect(els_distribution_server, start_distribution, 1, ok), - meck:expect(els_distribution_server, connect, 0, ok), - meck:expect(els_compiler_diagnostics, run, 1, []), - meck:expect(els_dialyzer_diagnostics, run, 1, []), - meck:expect(els_elvis_diagnostics, run, 1, []), - Self = erlang:self(), - HaltFun = fun(_X) -> Self ! halt_called, ok end, - meck:expect(els_utils, halt, HaltFun), - ServerIo = els_fake_stdio:start(), - ok = application:set_env(els_core, io_device, ServerIo), - application:ensure_all_started(els_lsp), - file:write_file("/tmp/erlang_ls.config", <<"">>), - ok. + meck:new(els_distribution_server, [no_link, passthrough]), + meck:new(els_compiler_diagnostics, [no_link, passthrough]), + meck:new(els_dialyzer_diagnostics, [no_link, passthrough]), + meck:new(els_elvis_diagnostics, [no_link, passthrough]), + meck:new(els_utils, [no_link, passthrough]), + meck:expect(els_distribution_server, start_distribution, 1, ok), + meck:expect(els_distribution_server, connect, 0, ok), + meck:expect(els_compiler_diagnostics, run, 1, []), + meck:expect(els_dialyzer_diagnostics, run, 1, []), + meck:expect(els_elvis_diagnostics, run, 1, []), + Self = erlang:self(), + HaltFun = fun(_X) -> + Self ! halt_called, + ok + end, + meck:expect(els_utils, halt, HaltFun), + ServerIo = els_fake_stdio:start(), + ok = application:set_env(els_core, io_device, ServerIo), + application:ensure_all_started(els_lsp), + ConfigFile = filename:join([els_utils:system_tmp_dir(), "erlang_ls.config"]), + file:write_file(ConfigFile, <<"">>), + %% An empty config file makes the config loader log warnings, + %% we don't need to see them when running the tests. + logger:set_module_level(els_config, error), + ok. %%============================================================================== %% Teardown %%============================================================================== teardown(_) -> - meck:unload(els_distribution_server), - meck:unload(els_compiler_diagnostics), - meck:unload(els_dialyzer_diagnostics), - meck:unload(els_elvis_diagnostics), - meck:unload(els_utils), - ok. + meck:unload(els_distribution_server), + meck:unload(els_compiler_diagnostics), + meck:unload(els_dialyzer_diagnostics), + meck:unload(els_elvis_diagnostics), + meck:unload(els_utils), + ok. %%============================================================================== %% Cleanup %%============================================================================== cleanup() -> - catch disconnect(), - %% Restart the server, since though the client disconnects the - %% server keeps its state. - els_server:reset_internal_state(), - ok. + catch disconnect(), + %% Restart the server, since though the client disconnects the + %% server keeps its state. + els_server:reset_state(), + ok. %%============================================================================== %% Helper functions %%============================================================================== assert_invalid_request(Res) -> - ?assertMatch( #{error := #{code := ?ERR_INVALID_REQUEST, message := _}} - , Res). + ?assertMatch( + #{error := #{code := ?ERR_INVALID_REQUEST, message := _}}, + Res + ). assert_server_not_initialized(Res) -> - ?assertMatch( #{error := #{code := ?ERR_SERVER_NOT_INITIALIZED, message := _}} - , Res). + ?assertMatch( + #{error := #{code := ?ERR_SERVER_NOT_INITIALIZED, message := _}}, + Res + ). assert_method_not_found(Res) -> - ?assertMatch( #{error := #{code := ?ERR_METHOD_NOT_FOUND, message := _}} - , Res). + ?assertMatch( + #{error := #{code := ?ERR_METHOD_NOT_FOUND, message := _}}, + Res + ). meck_matcher_integer(N) -> - meck_matcher:new(fun(X) -> X =:= N end). + meck_matcher:new(fun(X) -> X =:= N end). diff --git a/elvis.config b/elvis.config index b7b1f3ffc..02fc18c6c 100644 --- a/elvis.config +++ b/elvis.config @@ -1,89 +1,115 @@ -[{ elvis - , [ - { config, - [ #{ dirs => [ "apps/els_core/src" - , "apps/els_core/test" - , "apps/els_dap/src" - , "apps/els_dap/test" - , "apps/els_lsp/src" - , "apps/els_lsp/test" - ] - , filter => "*.erl" - , ruleset => erl_files - , rules => [ {elvis_style, god_modules, #{ignore => [ els_client - , els_completion_SUITE - , els_dap_general_provider_SUITE - , els_definition_SUITE - , els_diagnostics_SUITE - , els_document_highlight_SUITE - , els_hover_SUITE - , els_parser_SUITE - , els_references_SUITE - , els_methods - ]}} - , {elvis_style, dont_repeat_yourself, #{ ignore => [ els_diagnostics_SUITE - , els_references_SUITE - , els_dap_general_provider_SUITE - ] - , min_complexity => 20}} - , {elvis_style, invalid_dynamic_call, #{ignore => [ els_compiler_diagnostics - , els_server - , els_dap_server - , els_app - , els_stdio - , els_tcp - , els_dap_test_utils - , els_test_utils - ]}} - , {elvis_text_style, line_length, #{limit => 80, skip_comments => false}} - , {elvis_style, operator_spaces, #{ rules => [ {right, ","} - , {left , "-"} - , {right, "+"} - , {left , "+"} - , {right, "*"} - , {left , "*"} - , {right, "--"} - , {left , "--"} - , {right, "++"} - , {left , "++"} - , {right, "->"} - , {left , "->"} - , {right, "=>"} - , {left , "=>"} - , {right, "<-"} - , {left , "<-"} - , {right, "<="} - , {left , "<="} - , {right, "||"} - , {left , "||"} - , {right, "!"} - , {left , "!"} - , {right, "=:="} - , {left , "=:="} - , {right, "=/="} - , {left , "=/="} - ]}} - , {elvis_style, function_naming_convention, #{ignore => [ els_client - , prop_statem - ]}} - , {elvis_style, no_debug_call, #{ignore => [erlang_ls, els_dap]}} - , {elvis_style, atom_naming_convention, disable} - , {elvis_style, state_record_and_type, disable} - ] - , ignore => [els_dodger, els_typer, els_erlfmt_ast, els_eep48_docs] - } - , #{ dirs => ["."] - , filter => "Makefile" - , ruleset => makefiles - } - , #{ dirs => ["."] - , filter => "rebar.config" - , ruleset => rebar_config - } - , #{ dirs => ["."] - , filter => "elvis.config" - , ruleset => elvis_config - } - ]} - ]} +[ + {elvis, [ + {config, [ + #{ + dirs => [ + "apps/els_core/src", + "apps/els_core/test", + "apps/els_lsp/src", + "apps/els_lsp/test" + ], + filter => "*.erl", + ruleset => erl_files, + rules => [ + {elvis_style, god_modules, #{ + ignore => [ + els_client, + els_code_action_SUITE, + els_completion_SUITE, + els_definition_SUITE, + els_diagnostics_SUITE, + els_document_highlight_SUITE, + els_hover_SUITE, + els_parser_SUITE, + els_references_SUITE, + els_methods, + %% TODO: We should probably split this up + els_utils + ] + }}, + {elvis_style, dont_repeat_yourself, #{ + ignore => [ + els_diagnostics_SUITE, + els_references_SUITE + ], + min_complexity => 20 + }}, + {elvis_style, invalid_dynamic_call, #{ + ignore => [ + els_compiler_diagnostics, + els_server, + els_app, + els_stdio, + els_tcp, + els_test_utils, + edoc_report + ] + }}, + {elvis_text_style, line_length, #{limit => 100, skip_comments => false}}, + {elvis_style, operator_spaces, #{ + rules => [ + {right, ","}, + {left, "-"}, + {right, "+"}, + {left, "+"}, + {right, "*"}, + {left, "*"}, + {right, "--"}, + {left, "--"}, + {right, "++"}, + {left, "++"}, + {right, "->"}, + {left, "->"}, + {right, "=>"}, + {left, "=>"}, + {right, "<-"}, + {left, "<-"}, + {right, "<="}, + {left, "<="}, + {right, "||"}, + {left, "||"}, + {right, "!"}, + {left, "!"}, + {right, "=:="}, + {left, "=:="}, + {right, "=/="}, + {left, "=/="} + ] + }}, + {elvis_style, function_naming_convention, #{ + ignore => [ + els_client, + prop_statem + ] + }}, + {elvis_style, no_debug_call, #{ignore => [erlang_ls]}}, + {elvis_style, atom_naming_convention, disable}, + {elvis_style, state_record_and_type, disable}, + {elvis_style, export_used_types, disable}, + {elvis_style, no_throw, disable}, + {elvis_style, no_catch_expressions, disable}, + {elvis_style, no_block_expressions, disable}, + {elvis_style, param_pattern_matching, disable} + ], + ignore => [els_dodger, els_typer, els_erlfmt_ast, els_eep48_docs] + }, + #{ + dirs => ["."], + filter => "Makefile", + ruleset => makefiles + }, + %% Commented out due to: + %% Error: 'function_clause' while applying rule 'protocol_for_deps_rebar'. + %% #{ + %% dirs => ["."], + %% filter => "rebar.config", + %% ruleset => rebar_config + %% }, + #{ + dirs => ["."], + filter => "elvis.config", + ruleset => elvis_config + } + ]} + ]} ]. diff --git a/erlang_ls.config.sample b/erlang_ls.config.sample index 45d1ea58b..0f6e27701 100644 --- a/erlang_ls.config.sample +++ b/erlang_ls.config.sample @@ -3,6 +3,12 @@ apps_dirs: deps_dirs: - "_build/default/lib/*" - "_build/test/lib/*" + +%% If your code exists in directories other than "src", add them through this configuration +erls_dirs: + - "common" + - "game" + include_dirs: - "apps" - "apps/*/include" diff --git a/misc/dotemacs b/misc/dotemacs index fbeec4db0..e20021b99 100644 --- a/misc/dotemacs +++ b/misc/dotemacs @@ -23,12 +23,13 @@ ;; Install the official Erlang mode (package-require 'erlang) -;; Include the Language Server Protocol Clients -(package-require 'lsp-mode) - ;; Customize prefix for key-bindings +;; This has to be done before lsp-mode itself is loaded (setq lsp-keymap-prefix "C-l") +;; Include the Language Server Protocol Clients +(package-require 'lsp-mode) + ;; Enable LSP for Erlang files (add-hook 'erlang-mode-hook #'lsp) @@ -40,7 +41,7 @@ (setq lsp-log-io t) ;; Show line and column numbers -(add-hook 'erlang-mode-hook 'linum-mode) +(add-hook 'erlang-mode-hook #'display-line-numbers-mode) (add-hook 'erlang-mode-hook 'column-number-mode) ;; Enable and configure the LSP UI Package diff --git a/rebar.config b/rebar.config index aa7377986..099d23b32 100644 --- a/rebar.config +++ b/rebar.config @@ -1,88 +1,113 @@ -{erl_opts, [ debug_info - , warnings_as_errors - , warn_export_vars - , warn_unused_import - , warn_missing_spec_all - ] -}. +{erl_opts, [ + debug_info, + warnings_as_errors, + warn_export_vars, + warn_unused_import, + warn_missing_spec_all +]}. -{deps, [ {jsx, "3.0.0"} - , {redbug, "2.0.6"} - , {yamerl, "0.8.1"} - , {docsh, "0.7.2"} - , {elvis_core, "1.1.1"} - , {rebar3_format, "0.8.2"} - , {erlfmt, "1.0.0"} - , {ephemeral, "2.0.4"} - , {tdiff, "0.1.2"} - , {uuid, "2.0.1", {pkg, uuid_erl}} - , {gradualizer, {git, "https://github.com/josefs/Gradualizer.git", {ref, "e93db1c"}}} - ] -}. +{deps, [ + {json_polyfill, "0.1.4"}, + {redbug, "2.0.6"}, + {yamerl, + {git, "https://github.com/erlang-ls/yamerl.git", + {ref, "9a9f7a2e84554992f2e8e08a8060bfe97776a5b7"}}}, + {docsh, "0.7.2"}, + {elvis_core, "~> 3.2.2"}, + {rebar3_format, "0.8.2"}, + {erlfmt, "1.5.0"}, + {ephemeral, "2.0.4"}, + {tdiff, "0.1.2"}, + {uuid, "2.0.1", {pkg, uuid_erl}}, + {gradualizer, {git, "https://github.com/josefs/Gradualizer.git", {tag, "0.3.0"}}} +]}. -{shell, [ {apps, [els_lsp]} ]}. +{shell, [{apps, [els_lsp]}]}. -{plugins, [ rebar3_proper - , coveralls - , rebar3_lint - , {rebar3_bsp, {git, "https://github.com/erlang-ls/rebar3_bsp.git", {ref, "master"}}} - ] -}. +{plugins, [ + rebar3_proper, + coveralls, + {rebar3_lint, "3.2.3"}, + {rebar3_bsp, {git, "https://github.com/erlang-ls/rebar3_bsp.git", {ref, "master"}}} +]}. -{minimum_otp_vsn, "21.0"}. +{project_plugins, [ + erlfmt +]}. -{escript_emu_args, "%%! -connect_all false\n" }. +{minimum_otp_vsn, "23"}. + +{escript_emu_args, "%%! -connect_all false -hidden\n"}. {escript_incl_extra, [{"els_lsp/priv/snippets/*", "_build/default/lib/"}]}. {escript_main_app, els_lsp}. {escript_name, erlang_ls}. %% Keeping the debug profile as an alias, since many clients were %% relying on it when starting the server. -{profiles, [ { debug, [] } - , { dap, [ {escript_name, els_dap} - , {escript_main_app, els_dap} - ] - } - , { test - , [ { erl_opts, [ nowarn_export_all - , nowarn_missing_spec_all - ] - } - , { deps - , [ {meck, "0.9.0"} - , {proper, "1.3.0"} - , {proper_contrib, "0.2.0"} - , {coveralls, "2.2.0"} - ] - } - ] - } - ] -}. +{profiles, [ + {debug, []}, + {test, [ + {erl_opts, [ + nowarn_export_all, + nowarn_missing_spec_all + ]}, + {deps, [ + {meck, "0.9.0"}, + {proper, "1.3.0"}, + {proper_contrib, "0.2.0"}, + {coveralls, "2.2.0"} + ]} + ]} +]}. {cover_enabled, true}. {cover_export_enabled, true}. {coveralls_coverdata, ["_build/test/cover/ct.coverdata", "_build/test/cover/proper.coverdata"]}. {coveralls_service_name, "github"}. -{dialyzer, [ {warnings, [unknown]} - , {plt_apps, all_deps} - %% Depending on the OTP version, erl_types (used by - %% els_typer), is either part of hipe or dialyzer. - , {plt_extra_apps, [dialyzer, hipe, mnesia, common_test, debugger]} - ]}. +{dialyzer, [ + {warnings, [unknown]}, + {plt_apps, all_deps}, + %% Depending on the OTP version, erl_types (used by + %% els_typer), is either part of hipe or dialyzer. + {plt_extra_apps, [ + common_test, + debugger, + dialyzer, + eunit, + hipe, + mnesia + ]} +]}. {edoc_opts, [{preprocess, true}]}. -{xref_checks, [ undefined_function_calls - , undefined_functions - , locals_not_used - , deprecated_function_calls - , deprecated_functions - ]}. -%% Set xref ignores for functions introduced in OTP 23 -{xref_ignores, [{code, get_doc, 1}, {shell_docs, render, 4}]}. +{xref_checks, [ + undefined_function_calls, + undefined_functions, + locals_not_used, + deprecated_function_calls, + deprecated_functions +]}. +%% Set xref ignores for functions introduced in OTP 23 & function of wrangler +{xref_ignores, [ + {code, get_doc, 1}, + {http_uri, decode, 1}, + {shell_docs, render, 4}, + {uri_string, percent_decode, 1}, + wrangler_handler, + api_wrangler, + wls_code_lens, + wls_highlight, + wls_semantic_tokens, + wls_code_actions, + wls_execute_command_provider +]}. %% Disable warning_as_errors for redbug to avoid deprecation warnings. -{overrides, [ {del, redbug, [{erl_opts, [warnings_as_errors]}]} - ]}. +{overrides, [{del, redbug, [{erl_opts, [warnings_as_errors]}]}]}. + +{erlfmt, [ + write, + {files, ["apps/*/{src,include,test}/*.{erl,hrl,app.src}", "rebar.config", "elvis.config"]} +]}. diff --git a/rebar.lock b/rebar.lock index 36a73a456..a0c24d88b 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,57 +1,58 @@ {"1.2.0", [{<<"bucs">>,{pkg,<<"bucs">>,<<"1.0.16">>},1}, {<<"docsh">>,{pkg,<<"docsh">>,<<"0.7.2">>},0}, - {<<"elvis_core">>,{pkg,<<"elvis_core">>,<<"1.1.1">>},0}, + {<<"elvis_core">>,{pkg,<<"elvis_core">>,<<"3.2.5">>},0}, {<<"ephemeral">>,{pkg,<<"ephemeral">>,<<"2.0.4">>},0}, - {<<"erlfmt">>,{pkg,<<"erlfmt">>,<<"1.0.0">>},0}, + {<<"erlfmt">>,{pkg,<<"erlfmt">>,<<"1.5.0">>},0}, {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},2}, {<<"gradualizer">>, {git,"https://github.com/josefs/Gradualizer.git", - {ref,"e93db1c6725760def005c69d72f53b1a889b4c2f"}}, + {ref,"3021d29d82741399d131e3be38d2a8db79d146d4"}}, 0}, - {<<"jsx">>,{pkg,<<"jsx">>,<<"3.0.0">>},0}, - {<<"katana_code">>,{pkg,<<"katana_code">>,<<"0.2.1">>},1}, + {<<"json_polyfill">>,{pkg,<<"json_polyfill">>,<<"0.1.4">>},0}, + {<<"katana_code">>,{pkg,<<"katana_code">>,<<"2.1.1">>},1}, {<<"providers">>,{pkg,<<"providers">>,<<"1.8.1">>},1}, - {<<"quickrand">>,{pkg,<<"quickrand">>,<<"2.0.1">>},1}, + {<<"quickrand">>,{pkg,<<"quickrand">>,<<"2.0.7">>},1}, {<<"rebar3_format">>,{pkg,<<"rebar3_format">>,<<"0.8.2">>},0}, {<<"redbug">>,{pkg,<<"redbug">>,<<"2.0.6">>},0}, {<<"tdiff">>,{pkg,<<"tdiff">>,<<"0.1.2">>},0}, {<<"uuid">>,{pkg,<<"uuid_erl">>,<<"2.0.1">>},0}, - {<<"yamerl">>,{pkg,<<"yamerl">>,<<"0.8.1">>},0}, + {<<"yamerl">>, + {git,"https://github.com/erlang-ls/yamerl.git", + {ref,"9a9f7a2e84554992f2e8e08a8060bfe97776a5b7"}}, + 0}, {<<"zipper">>,{pkg,<<"zipper">>,<<"1.0.1">>},1}]}. [ {pkg_hash,[ {<<"bucs">>, <<"D69A4CD6D1238CD1ADC5C95673DBDE0F8459A5DBB7D746516434D8C6D935E96F">>}, {<<"docsh">>, <<"F893D5317A0E14269DD7FE79CF95FB6B9BA23513DA0480EC6E77C73221CAE4F2">>}, - {<<"elvis_core">>, <<"EB7864CE4BC87D13FBF1C222A82230A4C3D327C21080B73E97FC6343C3A5264D">>}, + {<<"elvis_core">>, <<"7845047A1CABD0F575EE8A95D2223F2F2040FBDA78C81EEE933090B857611BA0">>}, {<<"ephemeral">>, <<"B3E57886ADD5D90C82FE3880F5954978222A122CB8BAA123667401BBAAEC51D6">>}, - {<<"erlfmt">>, <<"4F3853A48F791DBB9EA5B578BAE4DB24FF7ED47BBA063698F6AF2168F55E7C6B">>}, + {<<"erlfmt">>, <<"5DDECA120A6E8E0A0FAB7D0BB9C2339D841B1C9E51DD135EE583256DEF20DE25">>}, {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}, - {<<"jsx">>, <<"20A170ABD4335FC6DB24D5FAD1E5D677C55DADF83D1B20A8A33B5FE159892A39">>}, - {<<"katana_code">>, <<"B2195859DF57D8BEBF619A9FD3327CD7D01563A98417156D0F4C5FAB435F2630">>}, + {<<"json_polyfill">>, <<"ED9AD7A8CBDB8D1F1E59E22CDAE23A153A5BB93B5529AEBAEB189CA4B4C536C8">>}, + {<<"katana_code">>, <<"9AC515E6B5AE4903CD7B6C9161ABFBA49B610B6F3E19E8F0542802A4316C2405">>}, {<<"providers">>, <<"70B4197869514344A8A60E2B2A4EF41CA03DEF43CFB1712ECF076A0F3C62F083">>}, - {<<"quickrand">>, <<"6D861FA11E6EB51BB2343A2616EFF704C2681A9997F41ABC78E58FA76DA33981">>}, + {<<"quickrand">>, <<"D2BD76676A446E6A058D678444B7FDA1387B813710D1AF6D6E29BB92186C8820">>}, {<<"rebar3_format">>, <<"2D64DA61E0B87FCA6C4512ADA6D9CBC2B27ADC9AE6844178561147E7121761BD">>}, {<<"redbug">>, <<"A764690B012B67C404562F9C6E1BA47A73892EE17DF5C15F670B1A5BF9D2F25A">>}, {<<"tdiff">>, <<"4E1B30321F1B3D600DF65CD60858EDE1235FE4E5EE042110AB5AD90CD6464AC5">>}, {<<"uuid">>, <<"1FD9079C544D521063897887A1C5B3302DCA98F9BB06AADCDC6FB0663F256797">>}, - {<<"yamerl">>, <<"07DA13FFA1D8E13948943789665C62CCD679DFA7B324A4A2ED3149DF17F453A4">>}, {<<"zipper">>, <<"3CCB4F14B97C06B2749B93D8B6C204A1ECB6FAFC6050CACC3B93B9870C05952A">>}]}, {pkg_hash_ext,[ {<<"bucs">>, <<"FF6A5C72A500AD7AEC1EE3BA164AE3C450EADEE898B0D151E1FACA18AC8D0D62">>}, {<<"docsh">>, <<"4E7DB461BB07540D2BC3D366B8513F0197712D0495BB85744F367D3815076134">>}, - {<<"elvis_core">>, <<"391C95BAA49F2718D7FB498BCF08046DDFC202CF0AAB63B2E439271485C9DC42">>}, + {<<"elvis_core">>, <<"34D9218F0B8072511903BF6CCBF59EB1765DECFC73FCC6833BA5C8959DB7F383">>}, {<<"ephemeral">>, <<"4B293D80F75F9C4575FF4B9C8E889A56802F40B018BF57E74F19644EFEE6C850">>}, - {<<"erlfmt">>, <<"44BE0BE03CE69902DC6DCD8F65E7B3ADED6AAF5D0BE70964188CABD4AD24F04E">>}, + {<<"erlfmt">>, <<"3933A40CFBE790AD94E5B650B36881DE70456319263C1479B556E9AFDBD80C75">>}, {<<"getopt">>, <<"53E1AB83B9CEB65C9672D3E7A35B8092E9BDC9B3EE80721471A161C10C59959C">>}, - {<<"jsx">>, <<"37BECA0435F5CA8A2F45F76A46211E76418FBEF80C36F0361C249FC75059DC6D">>}, - {<<"katana_code">>, <<"8448AD3F56D9814F98A28BE650F7191BDD506575E345CC16D586660B10F6E992">>}, + {<<"json_polyfill">>, <<"48C397EE2547FA459EDE01A30EC0E85717ABED3010867A63EEAAC5F203274303">>}, + {<<"katana_code">>, <<"0680F33525B9A882E6F4D3022518B15C46F648BD7B0DBE86900980FE1C291404">>}, {<<"providers">>, <<"E45745ADE9C476A9A469EA0840E418AB19360DC44F01A233304E118A44486BA0">>}, - {<<"quickrand">>, <<"14DB67D4AEF6B8815810EC9F3CCEF5E324B73B56CAE3687F99D752B85BDD4C96">>}, + {<<"quickrand">>, <<"B8ACBF89A224BC217C3070CA8BEBC6EB236DBE7F9767993B274084EA044D35F0">>}, {<<"rebar3_format">>, <<"CA8FF27638C2169593D1449DACBE8895634193ED3334E906B54FC97F081F5213">>}, {<<"redbug">>, <<"AAD9498671F4AB91EACA5099FE85A61618158A636E6286892C4F7CF4AF171D04">>}, {<<"tdiff">>, <<"E0C2E168F99252A5889768D5C8F1E6510A184592D4CFA06B22778A18D33D7875">>}, {<<"uuid">>, <<"AB57CACCD51F170011E5F444CE865F84B41605E483A9EFCC468C1AFAEC87553B">>}, - {<<"yamerl">>, <<"96CB30F9D64344FED0EF8A92E9F16F207DE6C04DFFF4F366752CA79F5BCEB23F">>}, {<<"zipper">>, <<"6A1FD3E1F0CC1D1DF5642C9A0CE2178036411B0A5C9642851D1DA276BD737C2D">>}]} ]. diff --git a/specs/runtime_node.md b/specs/runtime_node.md index aee0b78e1..73b1dddf4 100644 --- a/specs/runtime_node.md +++ b/specs/runtime_node.md @@ -46,7 +46,7 @@ Initially we envisage ones for - "plain" project, i.e. just some random .erl files on the filesystem somewhere -Becase the protocol between the `Server` and the `Runtime Node` will be +Because the protocol between the `Server` and the `Runtime Node` will be standardised, and we will have out of the box implementations for popular project build systems, it becomes easy for particular Erlang production sites to adapt these nodes for use in their particular environments. This includes the @@ -63,7 +63,7 @@ way to either launch or attach to the node. That config file also enumerates the capabilities supported in the `Runtime Node`, similar to the way code lens and diagnostic sources are currently -configured in the `erlang_ls.comfig` file. The initial assumption is that there +configured in the `erlang_ls.config` file. The initial assumption is that there is a well-known set of supported ones. At a later stage we may include some sort of capability query process on starting the `Runtime Node`. @@ -77,7 +77,7 @@ The `Runtime Node` should be able to respond to the following requests. - provide xref information for a file - run dialyzer -### Current Unkowns / Questions +### Current Unknowns / Questions - Should the `Server` and `Runtime Node` share a file system? i.e. should a URI be usable directly in any context for the current state of a file. diff --git a/src/erlang_ls.app.src b/src/erlang_ls.app.src new file mode 100644 index 000000000..92856e198 --- /dev/null +++ b/src/erlang_ls.app.src @@ -0,0 +1,11 @@ +{application, erlang_ls, [ + {description, "Erlang LS"}, + {vsn, git}, + {registered, []}, + {applications, [kernel, stdlib, els_core, els_lsp]}, + {env, []}, + {modules, []}, + + {licenses, ["Apache 2.0"]}, + {links, []} +]}.