diff --git a/.credo.exs b/.credo.exs
index c189252a..98886e41 100644
--- a/.credo.exs
+++ b/.credo.exs
@@ -23,6 +23,7 @@
#
included: [
"lib/",
+ "priv/monkey/",
"src/",
"test/",
"web/",
@@ -94,47 +95,47 @@
#
## Readability Checks
#
- #{Credo.Check.Readability.AliasOrder, []},
- #{Credo.Check.Readability.FunctionNames, []},
- #{Credo.Check.Readability.LargeNumbers, []},
- #{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
- #{Credo.Check.Readability.ModuleAttributeNames, []},
- #{Credo.Check.Readability.ModuleDoc, []},
- #{Credo.Check.Readability.ModuleNames, []},
- #{Credo.Check.Readability.ParenthesesInCondition, []},
- #{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
- #{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
- #{Credo.Check.Readability.PredicateFunctionNames, []},
- #{Credo.Check.Readability.PreferImplicitTry, []},
- #{Credo.Check.Readability.RedundantBlankLines, []},
- #{Credo.Check.Readability.Semicolons, []},
- #{Credo.Check.Readability.SpaceAfterCommas, []},
- #{Credo.Check.Readability.StringSigils, []},
- #{Credo.Check.Readability.TrailingBlankLine, []},
- #{Credo.Check.Readability.TrailingWhiteSpace, []},
- #{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
- #{Credo.Check.Readability.VariableNames, []},
- #{Credo.Check.Readability.WithSingleClause, []},
+ # {Credo.Check.Readability.AliasOrder, []},
+ # {Credo.Check.Readability.FunctionNames, []},
+ # {Credo.Check.Readability.LargeNumbers, []},
+ # {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
+ # {Credo.Check.Readability.ModuleAttributeNames, []},
+ # {Credo.Check.Readability.ModuleDoc, []},
+ # {Credo.Check.Readability.ModuleNames, []},
+ # {Credo.Check.Readability.ParenthesesInCondition, []},
+ # {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
+ # {Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
+ # {Credo.Check.Readability.PredicateFunctionNames, []},
+ # {Credo.Check.Readability.PreferImplicitTry, []},
+ # {Credo.Check.Readability.RedundantBlankLines, []},
+ # {Credo.Check.Readability.Semicolons, []},
+ # {Credo.Check.Readability.SpaceAfterCommas, []},
+ # {Credo.Check.Readability.StringSigils, []},
+ # {Credo.Check.Readability.TrailingBlankLine, []},
+ # {Credo.Check.Readability.TrailingWhiteSpace, []},
+ # {Credo.Check.Readability.UnnecessaryAliasExpansion, []},
+ # {Credo.Check.Readability.VariableNames, []},
+ # {Credo.Check.Readability.WithSingleClause, []},
##
### Refactoring Opportunities
##
- #{Credo.Check.Refactor.Apply, []},
- #{Credo.Check.Refactor.CondStatements, []},
- #{Credo.Check.Refactor.CyclomaticComplexity, []},
- #{Credo.Check.Refactor.FilterCount, []},
- #{Credo.Check.Refactor.FilterFilter, []},
- #{Credo.Check.Refactor.FunctionArity, []},
- #{Credo.Check.Refactor.LongQuoteBlocks, []},
- #{Credo.Check.Refactor.MapJoin, []},
- #{Credo.Check.Refactor.MatchInCondition, []},
- #{Credo.Check.Refactor.NegatedConditionsInUnless, []},
- #{Credo.Check.Refactor.NegatedConditionsWithElse, []},
- #{Credo.Check.Refactor.Nesting, []},
- #{Credo.Check.Refactor.RedundantWithClauseResult, []},
- #{Credo.Check.Refactor.RejectReject, []},
- #{Credo.Check.Refactor.UnlessWithElse, []},
- #{Credo.Check.Refactor.WithClauses, []},
+ # {Credo.Check.Refactor.Apply, []},
+ # {Credo.Check.Refactor.CondStatements, []},
+ # {Credo.Check.Refactor.CyclomaticComplexity, []},
+ # {Credo.Check.Refactor.FilterCount, []},
+ # {Credo.Check.Refactor.FilterFilter, []},
+ # {Credo.Check.Refactor.FunctionArity, []},
+ # {Credo.Check.Refactor.LongQuoteBlocks, []},
+ # {Credo.Check.Refactor.MapJoin, []},
+ # {Credo.Check.Refactor.MatchInCondition, []},
+ # {Credo.Check.Refactor.NegatedConditionsInUnless, []},
+ # {Credo.Check.Refactor.NegatedConditionsWithElse, []},
+ # {Credo.Check.Refactor.Nesting, []},
+ # {Credo.Check.Refactor.RedundantWithClauseResult, []},
+ # {Credo.Check.Refactor.RejectReject, []},
+ # {Credo.Check.Refactor.UnlessWithElse, []},
+ # {Credo.Check.Refactor.WithClauses, []},
#
## Warnings
@@ -144,7 +145,7 @@
{Credo.Check.Warning.Dbg, []},
# {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
- {Credo.Check.Warning.IoInspect, []},
+ {Credo.Check.Warning.IoInspect, []}
# {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
# {Credo.Check.Warning.OperationOnSameValues, []},
# {Credo.Check.Warning.OperationWithConstantResult, []},
diff --git a/.formatter.exs b/.formatter.exs
index ada3d7cd..19ef713c 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -5,7 +5,8 @@
assert_result: 3,
assert_notification: 3,
notify: 2,
- request: 2
+ request: 2,
+ assert_match: 1
],
line_length: 120,
import_deps: [:gen_lsp],
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index b64883ac..49102ae6 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -107,13 +107,14 @@ jobs:
- run: gh release edit ${{ needs.release.outputs.tag_name }} --draft=false --repo='elixir-tools/next-ls'
homebrew:
- needs: [publish]
+ needs: [release, publish]
runs-on: ubuntu-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
- token: ${{secrets.GH_API_TOKEN}}
+ token: ${{secrets.GH_API_KEY}}
no_fork: true
tap: elixir-tools/tap
formula: next-ls
+ tag: ${{ needs.release.outputs.tag_name }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4841adde..9a654725 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,83 @@
# Changelog
+## [0.22.1](https://github.com/elixir-tools/next-ls/compare/v0.22.0...v0.22.1) (2024-05-13)
+
+
+### Bug Fixes
+
+* compiler warning in compiler ([9360059](https://github.com/elixir-tools/next-ls/commit/9360059c98cda923fc95ea0082b1abd97be25f81))
+* remove unnecessary logs ([e59901b](https://github.com/elixir-tools/next-ls/commit/e59901b3f3d654b47ff4bbd33fc2b414dc76d782))
+
+## [0.22.0](https://github.com/elixir-tools/next-ls/compare/v0.21.4...v0.22.0) (2024-05-13)
+
+
+### Features
+
+* include `do` as a completions item/snippet ([#472](https://github.com/elixir-tools/next-ls/issues/472)) ([13a344b](https://github.com/elixir-tools/next-ls/commit/13a344b9ca96b60f5064d1267ea6cb569e4f2de6))
+
+
+### Bug Fixes
+
+* respect client capabilities ([#469](https://github.com/elixir-tools/next-ls/issues/469)) ([535d0ee](https://github.com/elixir-tools/next-ls/commit/535d0eec963dad27ffb4c609322ced782ab3cd9b))
+* use unified logger in more places ([535d0ee](https://github.com/elixir-tools/next-ls/commit/535d0eec963dad27ffb4c609322ced782ab3cd9b))
+
+## [0.21.4](https://github.com/elixir-tools/next-ls/compare/v0.21.3...v0.21.4) (2024-05-09)
+
+
+### Bug Fixes
+
+* correctly set MIX_HOME when using bundled Elixir ([#461](https://github.com/elixir-tools/next-ls/issues/461)) ([1625877](https://github.com/elixir-tools/next-ls/commit/16258776e32d4f8d7839d84f5d20de58214d1b25)), closes [#460](https://github.com/elixir-tools/next-ls/issues/460)
+
+## [0.21.3](https://github.com/elixir-tools/next-ls/compare/v0.21.2...v0.21.3) (2024-05-09)
+
+
+### Bug Fixes
+
+* **completions:** dont leak <- matches from for/with ([#454](https://github.com/elixir-tools/next-ls/issues/454)) ([3cecf51](https://github.com/elixir-tools/next-ls/commit/3cecf51c4ac0119e2fa68680d807d263bb10e9ca)), closes [#447](https://github.com/elixir-tools/next-ls/issues/447)
+
+## [0.21.2](https://github.com/elixir-tools/next-ls/compare/v0.21.1...v0.21.2) (2024-05-09)
+
+
+### Bug Fixes
+
+* **runtime:** correctly set MIX_HOME in runtime ([#452](https://github.com/elixir-tools/next-ls/issues/452)) ([03db965](https://github.com/elixir-tools/next-ls/commit/03db965289c0e7127b92b5136f71dbd9492533cf)), closes [#451](https://github.com/elixir-tools/next-ls/issues/451)
+
+## [0.21.1](https://github.com/elixir-tools/next-ls/compare/v0.21.0...v0.21.1) (2024-05-08)
+
+
+### Bug Fixes
+
+* **runtime:** remove unused variable warnings ([904a3d1](https://github.com/elixir-tools/next-ls/commit/904a3d10072263d3145ee4e71c6d9e1f06d4b933))
+* **runtime:** use correct path for bundled elixir ([#448](https://github.com/elixir-tools/next-ls/issues/448)) ([904a3d1](https://github.com/elixir-tools/next-ls/commit/904a3d10072263d3145ee4e71c6d9e1f06d4b933))
+
+## [0.21.0](https://github.com/elixir-tools/next-ls/compare/v0.20.2...v0.21.0) (2024-05-08)
+
+
+### Features
+
+* add remove debugger code action ([#426](https://github.com/elixir-tools/next-ls/issues/426)) ([7f2f4f4](https://github.com/elixir-tools/next-ls/commit/7f2f4f413348dc33d55ea17c2473007518627320))
+* alias-refactor workspace command ([#386](https://github.com/elixir-tools/next-ls/issues/386)) ([e14a611](https://github.com/elixir-tools/next-ls/commit/e14a611e157c0c4f6b54db5fce4719a51c4b7fc6))
+* **completions:** imports, aliases, module attributes ([#410](https://github.com/elixir-tools/next-ls/issues/410)) ([306f512](https://github.com/elixir-tools/next-ls/commit/306f512db9872746f6c71939114788325a520513)), closes [#45](https://github.com/elixir-tools/next-ls/issues/45) [#360](https://github.com/elixir-tools/next-ls/issues/360) [#334](https://github.com/elixir-tools/next-ls/issues/334)
+* **snippets:** more of them ([#414](https://github.com/elixir-tools/next-ls/issues/414)) ([2d4fddb](https://github.com/elixir-tools/next-ls/commit/2d4fddbf7c7e36925aa7761f060a2930a3732b96))
+* undefined function code action ([#441](https://github.com/elixir-tools/next-ls/issues/441)) ([d03c1ad](https://github.com/elixir-tools/next-ls/commit/d03c1adc16dfed96e8ddaeab2d33dd6da86f386a))
+
+
+### Bug Fixes
+
+* accuracy of get_surrounding_module ([#440](https://github.com/elixir-tools/next-ls/issues/440)) ([9c2ff68](https://github.com/elixir-tools/next-ls/commit/9c2ff68a7a0ead32bb1c356742b992903b41c440))
+* bump spitfire ([#429](https://github.com/elixir-tools/next-ls/issues/429)) ([23f7a6d](https://github.com/elixir-tools/next-ls/commit/23f7a6d13d0db43f9aa9718abc3003c28bf153c1))
+* bump spitfire to handle code that runs out of fuel ([#418](https://github.com/elixir-tools/next-ls/issues/418)) ([1bb590e](https://github.com/elixir-tools/next-ls/commit/1bb590ebedbe1b9efc7e480f56abe0a8c0743a5e))
+* **completions:** completions inside alias/import/require special forms ([#422](https://github.com/elixir-tools/next-ls/issues/422)) ([d62809e](https://github.com/elixir-tools/next-ls/commit/d62809ec470855703311d3b8cd72f7d6cb9eabec)), closes [#421](https://github.com/elixir-tools/next-ls/issues/421)
+* **completions:** correctly accumulate variables in `<-` expressions ([#424](https://github.com/elixir-tools/next-ls/issues/424)) ([b3bf75b](https://github.com/elixir-tools/next-ls/commit/b3bf75b8e70cc8e21f7efbbd9f3bbe5ae07951f9))
+* **completions:** imports inside blocks that generate functions ([#423](https://github.com/elixir-tools/next-ls/issues/423)) ([04d3010](https://github.com/elixir-tools/next-ls/commit/04d3010b4c004022782b70af02dcab263b2039f3)), closes [#420](https://github.com/elixir-tools/next-ls/issues/420)
+* **completions:** log source code when env fails to build ([#404](https://github.com/elixir-tools/next-ls/issues/404)) ([9c7ff4d](https://github.com/elixir-tools/next-ls/commit/9c7ff4df880582eb20f22226bb5c442c0274143c)), closes [#403](https://github.com/elixir-tools/next-ls/issues/403)
+* **credo:** calculate accurate span from trigger ([#427](https://github.com/elixir-tools/next-ls/issues/427)) ([90cd35a](https://github.com/elixir-tools/next-ls/commit/90cd35a750f724a323232023fffe70df7aeff1be))
+* precompile Elixir with OTP25 ([b9b67bd](https://github.com/elixir-tools/next-ls/commit/b9b67bd3663a6841e67a31e6a2f3c7a4862d8f1c))
+* **references,definition:** better references of symbols ([#430](https://github.com/elixir-tools/next-ls/issues/430)) ([4bfeb2b](https://github.com/elixir-tools/next-ls/commit/4bfeb2bc3203775732aab504936bcc5f812dafb8)), closes [#342](https://github.com/elixir-tools/next-ls/issues/342) [#184](https://github.com/elixir-tools/next-ls/issues/184) [#304](https://github.com/elixir-tools/next-ls/issues/304)
+* request utf8 encoding ([#419](https://github.com/elixir-tools/next-ls/issues/419)) ([edd5a2a](https://github.com/elixir-tools/next-ls/commit/edd5a2a070671ca7cd3f6419ec520afdcbc96d91))
+* revert "fix: request utf8 encoding ([#419](https://github.com/elixir-tools/next-ls/issues/419))" ([c21cda6](https://github.com/elixir-tools/next-ls/commit/c21cda68702ead4585de1a3f962cc85e10c43f75))
+* update burrito ([ed1bc3c](https://github.com/elixir-tools/next-ls/commit/ed1bc3cb347a43448de6d97d29a0bd8d90a7330c))
+
## [0.20.2](https://github.com/elixir-tools/next-ls/compare/v0.20.1...v0.20.2) (2024-03-27)
diff --git a/README.md b/README.md
index 06ec947f..b5b5d4d8 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ Please see the [docs](https://www.elixir-tools.dev/docs/next-ls/quickstart) to g
- [The elixir-tools Update Vol. 3](https://www.elixir-tools.dev/news/the-elixir-tools-update-vol-3/)
- [The elixir-tools Update Vol. 4](https://www.elixir-tools.dev/news/the-elixir-tools-update-vol-4/)
- [The 2023 elixir-tools Update (Vol. 5) ](https://www.elixir-tools.dev/news/the-2023-elixir-tools-update-vol-5/)
+- [The elixir-tools Update Vol. 6](https://www.elixir-tools.dev/news/the-elixir-tools-update-vol-6/)
## Sponsors
@@ -32,7 +33,7 @@ https://github.com/sponsors/mhanberg
### Remaining tiers
-
+
## Development
diff --git a/flake.nix b/flake.nix
index b526cd2e..10830b1d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -14,7 +14,7 @@
}: let
inherit (nixpkgs) lib;
- version = "0.20.2"; # x-release-please-version
+ version = "0.22.1"; # x-release-please-version
# Helper to provide system-specific attributes
forAllSystems = f:
@@ -115,7 +115,7 @@
src = self.outPath;
inherit version elixir;
pname = "next-ls-deps";
- hash = "sha256-XAcErVbGOz0m832H+qistfJwTTdgz3mfdHK2JvI3voc=";
+ hash = "sha256-sIV8/KS5hcIiqk5sSwcIEnPFZNvefzvNyt6af829ri8=";
mixEnv = "prod";
};
diff --git a/lib/next_ls.ex b/lib/next_ls.ex
index 2d9a5247..c0d86a44 100644
--- a/lib/next_ls.ex
+++ b/lib/next_ls.ex
@@ -83,7 +83,7 @@ defmodule NextLS do
runtime_task_supervisor = Keyword.fetch!(args, :runtime_task_supervisor)
dynamic_supervisor = Keyword.fetch!(args, :dynamic_supervisor)
bundle_base = Keyword.get(args, :bundle_base, Path.expand("~/.cache/elixir-tools/nextls"))
- mixhome = Keyword.get(args, :mix_home, Path.expand("~/.mix"))
+ mix_home = Keyword.get(args, :mix_home)
registry = Keyword.fetch!(args, :registry)
@@ -93,11 +93,11 @@ defmodule NextLS do
cache = Keyword.fetch!(args, :cache)
{:ok, logger} = DynamicSupervisor.start_child(dynamic_supervisor, {NextLS.Logger, lsp: lsp})
- NextLS.Runtime.BundledElixir.install(bundle_base, logger, mix_home: mixhome)
-
{:ok,
assign(lsp,
auto_update: Keyword.get(args, :auto_update, false),
+ bundle_base: bundle_base,
+ mix_home: mix_home,
exit_code: 1,
documents: %{},
refresh_refs: %{},
@@ -134,6 +134,13 @@ defmodule NextLS do
{:ok, init_opts} = __MODULE__.InitOpts.validate(init_opts)
+ mix_home =
+ if init_opts.experimental.completions.enable do
+ NextLS.Runtime.BundledElixir.mix_home(lsp.assigns.bundle_base)
+ else
+ nil
+ end
+
{:reply,
%InitializeResult{
capabilities: %ServerCapabilities{
@@ -148,7 +155,8 @@ defmodule NextLS do
completion_provider:
if init_opts.experimental.completions.enable do
%GenLSP.Structures.CompletionOptions{
- trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"]
+ trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"],
+ resolve_provider: true
}
else
nil
@@ -179,6 +187,7 @@ defmodule NextLS do
server_info: %{name: "Next LS"}
},
assign(lsp,
+ mix_home: mix_home,
root_uri: root_uri,
workspace_folders: workspace_folders,
client_capabilities: caps,
@@ -408,32 +417,16 @@ defmodule NextLS do
end)
value =
- with {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mod_doc}, _, fdocs}} <- result do
+ with {:ok, result} <- result,
+ %NextLS.Docs{} = doc <- NextLS.Docs.new(result, mod) do
case reference.type do
"alias" ->
- """
- ## #{reference.module}
-
- #{NextLS.DocsHelpers.to_markdown(content_type, mod_doc)}
- """
+ NextLS.Docs.module(doc)
"function" ->
- doc =
- Enum.find(fdocs, fn {{type, name, _a}, _, _, _doc, _} ->
- type in [:function, :macro] and to_string(name) == reference.identifier
- end)
-
- case doc do
- {_, _, _, %{"en" => fdoc}, _} ->
- """
- ## #{Macro.to_string(mod)}.#{reference.identifier}/#{reference.arity}
-
- #{NextLS.DocsHelpers.to_markdown(content_type, fdoc)}
- """
-
- _ ->
- nil
- end
+ NextLS.Docs.function(doc, fn name, a, documentation, _other ->
+ to_string(name) == reference.identifier and documentation != :hidden and a >= reference.arity
+ end)
_ ->
nil
@@ -572,16 +565,16 @@ defmodule NextLS do
{:reply, nil, lsp}
_ ->
- GenLSP.warning(lsp, "[Next LS] Failed to format the file: #{uri}")
+ NextLS.Logger.warning(lsp.assigns.logger, "Failed to format the file: #{uri}")
{:reply, nil, lsp}
end
end
end)
else
- GenLSP.warning(
- lsp,
- "[Next LS] The file #{uri} was not found in the server's process state. Something must have gone wrong when opening, changing, or saving the file."
+ NextLS.Logger.warning(
+ lsp.assigns.logger,
+ "The file #{uri} was not found in the server's process state. Something must have gone wrong when opening, changing, or saving the file."
)
[{:reply, nil, lsp}]
@@ -590,47 +583,49 @@ defmodule NextLS do
resp
end
- def handle_request(%TextDocumentCompletion{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
- document = lsp.assigns.documents[uri]
+ def handle_request(%GenLSP.Requests.CompletionItemResolve{params: completion_item}, lsp) do
+ completion_item =
+ with nil <- completion_item.data do
+ completion_item
+ else
+ %{"uri" => uri, "data" => data} ->
+ data = data |> Base.decode64!() |> :erlang.binary_to_term()
- spliced =
- document
- |> List.update_at(position.line, fn row ->
- {front, back} = String.split_at(row, position.character)
- # all we need to do is insert the cursor so we can find the spot to then
- # calculate the environment, it doens't really matter if its valid code,
- # it probably isn't already
- front <> "\n__cursor__()\n" <> back
- end)
- |> Enum.join("\n")
+ module =
+ case data do
+ {mod, _function, _arity} -> mod
+ mod -> mod
+ end
- ast =
- spliced
- |> Spitfire.parse(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
- |> then(fn
- {:ok, ast} -> ast
- {:error, ast, _} -> ast
- {:error, :no_fuel_remaining} -> nil
- end)
+ result =
+ dispatch_to_workspace(lsp.assigns.registry, uri, fn runtime, _wuri ->
+ Runtime.call(runtime, {Code, :fetch_docs, [module]})
+ end)
- env =
- ast
- |> NextLS.ASTHelpers.find_cursor()
- |> then(fn
- {:ok, cursor} ->
- cursor
+ docs =
+ with {:ok, doc} <- result,
+ %NextLS.Docs{} = doc <- NextLS.Docs.new(doc, module) do
+ case data do
+ {_mod, function, arity} ->
+ NextLS.Docs.function(doc, fn name, a, documentation, _other ->
+ to_string(name) == function and documentation != :hidden and a >= arity
+ end)
- {:error, :not_found} ->
- NextLS.Logger.warning(lsp.assigns.logger, "Could not locate cursor when building environment")
+ mod when is_atom(mod) ->
+ NextLS.Docs.module(doc)
+ end
+ else
+ _ -> nil
+ end
- NextLS.Logger.warning(
- lsp.assigns.logger,
- "Source code that produced the above warning: #{spliced}"
- )
+ %{completion_item | documentation: docs}
+ end
- nil
- end)
- |> NextLS.ASTHelpers.Env.build()
+ {:reply, completion_item, lsp}
+ end
+
+ def handle_request(%TextDocumentCompletion{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
+ document = lsp.assigns.documents[uri]
document_slice =
document
@@ -643,34 +638,23 @@ defmodule NextLS do
|> Enum.reverse()
|> Enum.join("\n")
+ with_cursor =
+ case Spitfire.container_cursor_to_quoted(document_slice) do
+ {:ok, with_cursor} -> with_cursor
+ {:error, with_cursor, _} -> with_cursor
+ end
+
{root_path, entries} =
- dispatch(lsp.assigns.registry, :runtimes, fn entries ->
- [{wuri, result}] =
- for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
- ast =
- spliced
- |> Spitfire.parse()
- |> then(fn
- {:ok, ast} -> ast
- {:error, ast, _} -> ast
- {:error, :no_fuel_remaining} -> nil
- end)
-
- {:ok, {_, _, _, macro_env}} = Runtime.expand(runtime, ast, Path.basename(uri))
-
- env =
- env
- |> Map.put(:functions, macro_env.functions)
- |> Map.put(:macros, macro_env.macros)
- |> Map.put(:aliases, macro_env.aliases)
- |> Map.put(:attrs, macro_env.attrs)
-
- {wuri,
- document_slice
- |> String.to_charlist()
- |> Enum.reverse()
- |> NextLS.Autocomplete.expand(runtime, env)}
- end
+ dispatch_to_workspace(lsp.assigns.registry, uri, fn runtime, wuri ->
+ {:ok, {_, _, _, macro_env}} =
+ Runtime.expand(runtime, with_cursor, Path.basename(uri))
+
+ doc =
+ document_slice
+ |> String.to_charlist()
+ |> Enum.reverse()
+
+ result = NextLS.Autocomplete.expand(doc, runtime, macro_env)
case result do
{:yes, entries} -> {wuri, entries}
@@ -687,13 +671,13 @@ defmodule NextLS do
{name, GenLSP.Enumerations.CompletionItemKind.struct(), ""}
:function ->
- {"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs}
+ {"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol[:docs] || ""}
:module ->
- {name, GenLSP.Enumerations.CompletionItemKind.module(), symbol.docs}
+ {name, GenLSP.Enumerations.CompletionItemKind.module(), symbol[:docs] || ""}
:variable ->
- {name, GenLSP.Enumerations.CompletionItemKind.variable(), ""}
+ {to_string(name), GenLSP.Enumerations.CompletionItemKind.variable(), ""}
:dir ->
{name, GenLSP.Enumerations.CompletionItemKind.folder(), ""}
@@ -701,6 +685,9 @@ defmodule NextLS do
:file ->
{name, GenLSP.Enumerations.CompletionItemKind.file(), ""}
+ :reserved ->
+ {name, GenLSP.Enumerations.CompletionItemKind.keyword(), ""}
+
:keyword ->
{name, GenLSP.Enumerations.CompletionItemKind.field(), ""}
@@ -718,8 +705,14 @@ defmodule NextLS do
%GenLSP.Structures.CompletionItem{
label: label,
kind: kind,
- insert_text: name,
- documentation: docs
+ insert_text: to_string(name),
+ documentation: docs,
+ data:
+ if symbol[:data] do
+ %{uri: uri, data: symbol[:data] |> :erlang.term_to_binary() |> Base.encode64()}
+ else
+ nil
+ end
}
root_path = root_path |> URI.parse() |> Map.get(:path)
@@ -734,9 +727,9 @@ defmodule NextLS do
{:reply, results, lsp}
rescue
e ->
- GenLSP.warning(
- lsp,
- "[Next LS] Failed to run completion request: #{Exception.format(:error, e, __STACKTRACE__)}"
+ NextLS.Logger.warning(
+ lsp.assigns.logger,
+ "Failed to run completion request: #{Exception.format(:error, e, __STACKTRACE__)}"
)
{:reply, [], lsp}
@@ -859,7 +852,7 @@ defmodule NextLS do
end
def handle_request(%{method: method}, lsp) do
- GenLSP.warning(lsp, "[Next LS] Method Not Found: #{method}")
+ NextLS.Logger.warning(lsp.assigns.logger, "Method Not Found: #{method}")
{:reply,
%ErrorResponse{
@@ -870,7 +863,8 @@ defmodule NextLS do
@impl true
def handle_notification(%Initialized{}, lsp) do
- GenLSP.log(lsp, "[Next LS] NextLS v#{version()} has initialized!")
+ NextLS.Logger.log(lsp.assigns.logger, "NextLS v#{version()} has initialized!")
+ NextLS.Logger.log(lsp.assigns.logger, "Log file located at #{Path.join(File.cwd!(), ".elixir-tools/next-ls.log")}")
with opts when is_list(opts) <- lsp.assigns.auto_update do
{:ok, _} =
@@ -921,7 +915,8 @@ defmodule NextLS do
})
end
- GenLSP.log(lsp, "[Next LS] Booting runtimes...")
+ NextLS.Runtime.BundledElixir.install(lsp.assigns.bundle_base, lsp.assigns.logger)
+ NextLS.Logger.log(lsp.assigns.logger, "Booting runtimes...")
parent = self()
@@ -931,7 +926,7 @@ defmodule NextLS do
lsp.assigns.init_opts.elixir_bin_path
lsp.assigns.init_opts.experimental.completions.enable ->
- NextLS.Runtime.BundledElixir.binpath()
+ NextLS.Runtime.BundledElixir.binpath(lsp.assigns.bundle_base)
true ->
"elixir" |> System.find_executable() |> Path.dirname()
@@ -958,11 +953,12 @@ defmodule NextLS do
uri: uri,
mix_env: lsp.assigns.init_opts.mix_env,
mix_target: lsp.assigns.init_opts.mix_target,
+ mix_home: lsp.assigns.mix_home,
elixir_bin_path: elixir_bin_path,
on_initialized: fn status ->
if status == :ready do
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
- GenLSP.log(lsp, "[Next LS] Runtime for folder #{name} is ready...")
+ NextLS.Logger.log(lsp.assigns.logger, "Runtime for folder #{name} is ready...")
msg = {:runtime_ready, name, self()}
@@ -976,7 +972,7 @@ defmodule NextLS do
send(parent, {:runtime_failed, name, status})
- GenLSP.error(lsp, "[Next LS] Runtime for folder #{name} failed to initialize")
+ NextLS.Logger.error(lsp.assigns.logger, "Runtime for folder #{name} failed to initialize")
end
end,
logger: lsp.assigns.logger
@@ -1060,7 +1056,7 @@ defmodule NextLS do
names = Enum.map(entries, fn {_, %{name: name}} -> name end)
for %{name: name, uri: uri} <- added, name not in names do
- GenLSP.log(lsp, "[Next LS] Adding workspace folder #{name}")
+ NextLS.Logger.log(lsp.assigns.logger, "Adding workspace folder #{name}")
token = Progress.token()
Progress.start(lsp, token, "Initializing NextLS runtime for folder #{name}...")
parent = self()
@@ -1080,10 +1076,11 @@ defmodule NextLS do
uri: uri,
mix_env: lsp.assigns.init_opts.mix_env,
mix_target: lsp.assigns.init_opts.mix_target,
+ mix_home: lsp.assigns.mix_home,
on_initialized: fn status ->
if status == :ready do
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
- GenLSP.log(lsp, "[Next LS] Runtime for folder #{name} is ready...")
+ NextLS.Logger.log(lsp.assigns.logger, "Runtime for folder #{name} is ready...")
msg = {:runtime_ready, name, self()}
@@ -1097,7 +1094,7 @@ defmodule NextLS do
send(parent, {:runtime_failed, name, status})
- GenLSP.error(lsp, "[Next LS] Runtime for folder #{name} failed to initialize")
+ NextLS.Logger.error(lsp.assigns.logger, "Runtime for folder #{name} failed to initialize")
end
end,
logger: lsp.assigns.logger
@@ -1108,7 +1105,7 @@ defmodule NextLS do
names = Enum.map(removed, & &1.name)
for {pid, %{name: name}} <- entries, name in names do
- GenLSP.log(lsp, "[Next LS] Removing workspace folder #{name}")
+ NextLS.Logger.log(lsp.assigns.logger, "Removing workspace folder #{name}")
NextLS.Runtime.stop(lsp.assigns.dynamic_supervisor, pid)
end
end)
@@ -1268,7 +1265,7 @@ defmodule NextLS do
:ok = DynamicSupervisor.terminate_child(lsp.assigns.dynamic_supervisor, pid)
- if status == {:error, :deps} do
+ if status == {:error, :deps} && lsp.assigns.client_capabilities.window.show_message do
resp =
GenLSP.request(
lsp,
@@ -1325,6 +1322,10 @@ defmodule NextLS do
_ ->
NextLS.Logger.info(lsp.assigns.logger, "Not running `mix deps.get`")
end
+ else
+ unless lsp.assigns.client_capabilities.window.show_message do
+ NextLS.Logger.info(lsp.assigns.logger, "Client does not support window/showMessageRequest")
+ end
end
{:noreply, lsp}
@@ -1342,8 +1343,8 @@ defmodule NextLS do
end
def handle_info(message, lsp) do
- GenLSP.log(lsp, "[Next LS] Unhandled message: #{inspect(message)}")
- GenLSP.log(lsp, "[Next LS] process assigns=#{inspect(lsp.assigns)}")
+ NextLS.Logger.log(lsp.assigns.logger, "Unhandled message: #{inspect(message)}")
+ NextLS.Logger.log(lsp.assigns.logger, "process assigns=#{inspect(lsp.assigns)}")
{:noreply, lsp}
end
@@ -1382,6 +1383,27 @@ defmodule NextLS do
end
end
+ defp dispatch_to_workspace(registry, uri, callback) do
+ ref = make_ref()
+ me = self()
+
+ Registry.dispatch(registry, :runtimes, fn entries ->
+ [result] =
+ for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
+ callback.(runtime, wuri)
+ end
+
+ send(me, {ref, result})
+ end)
+
+ receive do
+ {^ref, result} -> result
+ after
+ 1000 ->
+ :timeout
+ end
+ end
+
defp symbol_info(file, line, col, database) do
definition_query = ~Q"""
SELECT module, type, name
diff --git a/lib/next_ls/autocomplete.ex b/lib/next_ls/autocomplete.ex
index 35a58245..14166268 100644
--- a/lib/next_ls/autocomplete.ex
+++ b/lib/next_ls/autocomplete.ex
@@ -2,6 +2,7 @@ defmodule NextLS.Autocomplete do
# Based on `IEx.Autocomplete` from github.com/elixir-lang/elixir from 10/17/2023
@moduledoc false
+ require Logger
require NextLS.Runtime
@bitstring_modifiers [
@@ -13,8 +14,8 @@ defmodule NextLS.Autocomplete do
%{kind: :variable, name: "little"},
%{kind: :variable, name: "native"},
%{kind: :variable, name: "signed"},
- %{kind: :function, name: "size", arity: 1, docs: nil},
- %{kind: :function, name: "unit", arity: 1, docs: nil},
+ %{kind: :function, name: "size", arity: 1},
+ %{kind: :function, name: "unit", arity: 1},
%{kind: :variable, name: "unsigned"},
%{kind: :variable, name: "utf8"},
%{kind: :variable, name: "utf16"},
@@ -33,7 +34,6 @@ defmodule NextLS.Autocomplete do
defp expand_code(code, runtime, env) do
code = Enum.reverse(code)
- # helper = get_helper(code)
case Code.Fragment.cursor_context(code) do
{:alias, alias} ->
@@ -65,7 +65,8 @@ defmodule NextLS.Autocomplete do
expand_dot_call(path, List.to_atom(hint), runtime, env)
:expr ->
- expand_container_context(code, :expr, "", runtime, env) || expand_local_or_var(code, "", runtime, env)
+ expand_container_context(code, :expr, "", runtime, env) ||
+ expand_local_or_var(code, "", runtime, env)
{:local_or_var, local_or_var} ->
hint = List.to_string(local_or_var)
@@ -93,7 +94,8 @@ defmodule NextLS.Autocomplete do
expand_local(List.to_string(operator), true, runtime, env)
{:operator_call, operator} when operator in ~w(|)c ->
- expand_container_context(code, :expr, "", runtime, env) || expand_local_or_var("", "", runtime, env)
+ expand_container_context(code, :expr, "", runtime, env) ||
+ expand_local_or_var("", "", runtime, env)
{:operator_call, _operator} ->
expand_local_or_var("", "", runtime, env)
@@ -164,37 +166,20 @@ defmodule NextLS.Autocomplete do
## Expand call
- defp expand_local_call(fun, runtime, env) do
+ defp expand_local_call(fun, _runtime, env) do
env
|> imports_from_env()
|> Enum.filter(fn {_, funs} -> List.keymember?(funs, fun, 0) end)
- |> Enum.flat_map(fn {module, _} -> get_signatures(fun, module) end)
- |> expand_signatures(runtime, env)
+ |> format_expansion()
end
- defp expand_dot_call(path, fun, runtime, env) do
+ defp expand_dot_call(path, fun, _runtime, env) do
case expand_dot_path(path, env) do
- {:ok, mod} when is_atom(mod) -> fun |> get_signatures(mod) |> expand_signatures(runtime)
+ {:ok, mod} when is_atom(mod) -> format_expansion(fun)
_ -> no()
end
end
- defp get_signatures(name, module) when is_atom(module) do
- with docs when is_list(docs) <- get_docs(module, [:function, :macro], name) do
- Enum.map(docs, fn {_, _, signatures, _, _} -> Enum.join(signatures, " ") end)
- else
- _ -> []
- end
- end
-
- defp expand_signatures([_ | _] = signatures, _runtime) do
- [head | _tail] = Enum.sort(signatures, &(String.length(&1) <= String.length(&2)))
- # if tail != [], do: IO.write("\n" <> (tail |> Enum.reverse() |> Enum.join("\n")))
- yes([head])
- end
-
- defp expand_signatures([], runtime, env), do: expand_local_or_var("", "", runtime, env)
-
## Expand dot
defp expand_dot(path, hint, exact?, runtime, env) do
@@ -261,7 +246,15 @@ defmodule NextLS.Autocomplete do
## Expand local or var
defp expand_local_or_var(code, hint, runtime, env) do
- format_expansion(match_var(code, hint, runtime, env) ++ match_local(hint, false, runtime, env))
+ format_expansion(
+ match_keywords(hint) ++ match_var(code, hint, runtime, env) ++ match_local(hint, false, runtime, env)
+ )
+ end
+
+ defp match_keywords(hint) do
+ for %{name: name} = kw <- [%{kind: :reserved, name: "do"}], String.starts_with?(name, hint) do
+ kw
+ end
end
defp expand_local(hint, exact?, runtime, env) do
@@ -291,7 +284,7 @@ defmodule NextLS.Autocomplete do
defp match_var(code, hint, _runtime, env) do
code
|> variables_from_binding(env)
- |> Enum.filter(&String.starts_with?(&1, hint))
+ |> Enum.filter(&String.starts_with?(to_string(&1), hint))
|> Enum.sort()
|> Enum.map(&%{kind: :variable, name: &1})
end
@@ -304,23 +297,10 @@ defmodule NextLS.Autocomplete do
defp match_erlang_modules(hint, runtime) do
for mod <- match_modules(hint, false, runtime), usable_as_unquoted_module?(mod) do
- {content_type, mdoc} =
- case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do
- {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
- {content_type, mdoc}
-
- _ ->
- {"text/markdown", nil}
- end
-
%{
kind: :module,
name: mod,
- docs: """
- ## #{Macro.to_string(mod)}
-
- #{NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
- """
+ data: String.to_atom(mod)
}
end
end
@@ -449,7 +429,9 @@ defmodule NextLS.Autocomplete do
alias = value_from_alias(aliases, env),
true <-
Keyword.keyword?(pairs) and ensure_loaded?(alias, runtime) and
- NextLS.Runtime.execute!(runtime, do: Kernel.function_exported?(alias, :__struct__, 1)) do
+ NextLS.Runtime.execute!(runtime,
+ do: Kernel.function_exported?(alias, :__struct__, 1)
+ ) do
{:struct, alias, pairs}
else
_ -> nil
@@ -512,28 +494,15 @@ defmodule NextLS.Autocomplete do
end
end
- defp match_aliases(hint, runtime, env) do
+ defp match_aliases(hint, _runtime, env) do
for {alias, module} <- aliases_from_env(env),
[name] = Module.split(alias),
String.starts_with?(name, hint) do
- {content_type, mdoc} =
- case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(module)) do
- {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
- {content_type, mdoc}
-
- _ ->
- {"text/markdown", nil}
- end
-
%{
kind: :module,
name: name,
- module: module,
- docs: """
- ## #{Macro.to_string(module)}
-
- #{NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
- """
+ data: module,
+ module: module
}
end
end
@@ -551,23 +520,10 @@ defmodule NextLS.Autocomplete do
valid_alias_piece?("." <> name) do
alias = Module.concat([mod])
- {content_type, mdoc} =
- case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(alias)) do
- {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
- {content_type, mdoc}
-
- _ ->
- {"text/markdown", nil}
- end
-
%{
kind: :module,
- name: name,
- docs: """
- ## #{Macro.to_string(alias)}
-
- #{NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
- """
+ data: alias,
+ name: name
}
end
@@ -637,10 +593,7 @@ defmodule NextLS.Autocomplete do
end
defp get_modules(false, runtime) do
- {:ok, mods} =
- NextLS.Runtime.execute runtime do
- :code.all_loaded()
- end
+ mods = NextLS.Runtime.execute!(runtime, do: :code.all_loaded())
modules =
Enum.map(mods, &Atom.to_string(elem(&1, 0)))
@@ -678,43 +631,34 @@ defmodule NextLS.Autocomplete do
apps
end
- defp match_module_funs(runtime, mod, funs, hint, exact?) do
- {content_type, fdocs} =
- case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do
- {:ok, {:docs_v1, _, _lang, content_type, _, _, fdocs}} ->
- {content_type, fdocs}
-
- _ ->
- {"text/markdown", []}
- end
-
+ defp match_module_funs(_runtime, mod, funs, hint, exact?) do
functions =
for {fun, arity} <- funs,
name = Atom.to_string(fun),
if(exact?, do: name == hint, else: String.starts_with?(name, hint)) do
- doc =
- Enum.find(fdocs, fn {{type, fname, _a}, _, _, _doc, _} ->
- type in [:function, :macro] and to_string(fname) == name
- end)
+ # doc =
+ # Enum.find(fdocs, fn {{type, fname, _a}, _, _, _doc, _} ->
+ # type in [:function, :macro] and to_string(fname) == name
+ # end)
- doc =
- case doc do
- {_, _, _, %{"en" => fdoc}, _} ->
- """
- ## #{Macro.to_string(mod)}.#{name}/#{arity}
+ # doc =
+ # case doc do
+ # {_, _, _, %{"en" => fdoc}, _} ->
+ # """
+ # ## #{Macro.to_string(mod)}.#{name}/#{arity}
- #{NextLS.DocsHelpers.to_markdown(content_type, fdoc)}
- """
+ # #{NextLS.DocsHelpers.to_markdown(content_type, fdoc)}
+ # """
- _ ->
- nil
- end
+ # _ ->
+ # nil
+ # end
%{
kind: :function,
+ data: {mod, name, arity},
name: name,
- arity: arity,
- docs: doc
+ arity: arity
}
end
@@ -737,12 +681,6 @@ defmodule NextLS.Autocomplete do
not ensure_loaded?(mod, runtime) ->
[]
- docs = get_docs(mod, [:function, :macro]) ->
- mod
- |> exports(runtime)
- |> Kernel.--(default_arg_functions_with_doc_false(docs))
- |> Enum.reject(&hidden_fun?(&1, docs))
-
true ->
exports(mod, runtime)
end
@@ -783,34 +721,6 @@ defmodule NextLS.Autocomplete do
# end
# end
- defp get_docs(mod, kinds, fun \\ nil) do
- case Code.fetch_docs(mod) do
- {:docs_v1, _, _, _, _, _, docs} ->
- if is_nil(fun) do
- for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc
- else
- for {{kind, ^fun, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc
- end
-
- {:error, _} ->
- nil
- end
- end
-
- defp default_arg_functions_with_doc_false(docs) do
- for {{_, fun_name, arity}, _, _, :hidden, %{defaults: count}} <- docs,
- new_arity <- (arity - count)..arity,
- do: {fun_name, new_arity}
- end
-
- defp hidden_fun?({name, arity}, docs) do
- case Enum.find(docs, &match?({{_, ^name, ^arity}, _, _, _, _}, &1)) do
- nil -> hd(Atom.to_charlist(name)) == ?_
- {_, _, _, :hidden, _} -> true
- {_, _, _, _, _} -> false
- end
- end
-
defp ensure_loaded?(Elixir, _runtime), do: false
defp ensure_loaded?(mod, runtime) do
@@ -833,11 +743,6 @@ defmodule NextLS.Autocomplete do
end
defp value_from_binding([_var | _path], _runtime) do
- # with {evaluator, server} <- IEx.Broker.evaluator(runtime) do
- # IEx.Evaluator.value_from_binding(evaluator, server, var, path)
- # else
- # _ -> :error
- # end
[]
end
diff --git a/lib/next_ls/docs.ex b/lib/next_ls/docs.ex
new file mode 100644
index 00000000..8359d840
--- /dev/null
+++ b/lib/next_ls/docs.ex
@@ -0,0 +1,154 @@
+defmodule NextLS.Docs do
+ @moduledoc false
+
+ defstruct module: nil, mdoc: nil, functions: [], content_type: nil
+
+ def new({:docs_v1, _, _lang, content_type, mdoc, _, fdocs}, module) do
+ mdoc =
+ case mdoc do
+ %{"en" => mdoc} -> mdoc
+ _ -> nil
+ end
+
+ %__MODULE__{
+ content_type: content_type,
+ module: module,
+ mdoc: mdoc,
+ functions: fdocs
+ }
+ end
+
+ def new(_, _) do
+ nil
+ end
+
+ def module(%__MODULE__{} = doc) do
+ """
+ ## #{Macro.to_string(doc.module)}
+
+ #{to_markdown(doc.content_type, doc.mdoc)}
+ """
+ end
+
+ def function(%__MODULE__{} = doc, callback) do
+ result =
+ Enum.find(doc.functions, fn {{type, name, arity}, _some_number, _signature, doc, other} ->
+ type in [:function, :macro] and callback.(name, arity, doc, other)
+ end)
+
+ case result do
+ {{_, name, arity}, _some_number, signature, %{"en" => fdoc}, _other} ->
+ """
+ ## #{Macro.to_string(doc.module)}.#{name}/#{arity}
+
+ `#{signature}`
+
+ #{to_markdown(doc.content_type, fdoc)}
+ """
+
+ _ ->
+ nil
+ end
+ end
+
+ @spec to_markdown(String.t(), String.t() | list()) :: String.t()
+ def to_markdown(type, docs)
+ def to_markdown("text/markdown", docs), do: docs
+
+ def to_markdown("application/erlang+html" = type, [{:p, _, children} | rest]) do
+ String.trim(to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest))
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:div, attrs, children} | rest]) do
+ prefix =
+ if attrs[:class] in ~w do
+ "> "
+ else
+ ""
+ end
+
+ prefix <> to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:a, attrs, children} | rest]) do
+ space = if List.last(children) == " ", do: " ", else: ""
+
+ "[#{String.trim(to_markdown(type, children))}](#{attrs[:href]})" <> space <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [doc | rest]) when is_binary(doc) do
+ doc <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:h1, _, children} | rest]) do
+ "# #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:h2, _, children} | rest]) do
+ "## #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:h3, _, children} | rest]) do
+ "### #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:h4, _, children} | rest]) do
+ "#### #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:h5, _, children} | rest]) do
+ "##### #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:pre, _, [{:code, _, children}]} | rest]) do
+ "```erlang\n#{to_markdown(type, children)}\n```\n\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:ul, [class: "types"], lis} | rest]) do
+ "### Types\n\n#{to_markdown(type, lis)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:ul, _, lis} | rest]) do
+ "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:li, [name: text], _} | rest]) do
+ "* #{text}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:li, _, children} | rest]) do
+ "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:code, _, bins} | rest]) do
+ "`#{IO.iodata_to_binary(bins)}`" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:em, _, bins} | rest]) do
+ "_#{IO.iodata_to_binary(bins)}_" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:dl, _, lis} | rest]) do
+ "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:dt, _, children} | rest]) do
+ "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:dd, _, children} | rest]) do
+ "#{to_markdown(type, children)}\n" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html" = type, [{:i, _, children} | rest]) do
+ "_#{IO.iodata_to_binary(children)}_" <> to_markdown(type, rest)
+ end
+
+ def to_markdown("application/erlang+html", []) do
+ ""
+ end
+
+ def to_markdown("application/erlang+html", nil) do
+ ""
+ end
+end
diff --git a/lib/next_ls/extensions/credo_extension/code_action.ex b/lib/next_ls/extensions/credo_extension/code_action.ex
index 0a7e6303..3edf79c1 100644
--- a/lib/next_ls/extensions/credo_extension/code_action.ex
+++ b/lib/next_ls/extensions/credo_extension/code_action.ex
@@ -4,7 +4,23 @@ defmodule NextLS.CredoExtension.CodeAction do
@behaviour NextLS.CodeActionable
alias NextLS.CodeActionable.Data
+ alias NextLS.CredoExtension.CodeAction.RemoveDebugger
+ @debug_checks ~w(
+ Elixir.Credo.Check.Warning.Dbg
+ Elixir.Credo.Check.Warning.IExPry
+ Elixir.Credo.Check.Warning.IoInspect
+ Elixir.Credo.Check.Warning.IoPuts
+ Elixir.Credo.Check.Warning.MixEnv
+ )
@impl true
- def from(%Data{} = _data), do: []
+ def from(%Data{} = data) do
+ case data.diagnostic.data do
+ %{"check" => check} when check in @debug_checks ->
+ RemoveDebugger.new(data.diagnostic, data.document, data.uri)
+
+ _ ->
+ []
+ end
+ end
end
diff --git a/lib/next_ls/extensions/credo_extension/code_action/remove_debugger.ex b/lib/next_ls/extensions/credo_extension/code_action/remove_debugger.ex
new file mode 100644
index 00000000..19ce29fc
--- /dev/null
+++ b/lib/next_ls/extensions/credo_extension/code_action/remove_debugger.ex
@@ -0,0 +1,149 @@
+defmodule NextLS.CredoExtension.CodeAction.RemoveDebugger do
+ @moduledoc false
+
+ alias GenLSP.Structures.CodeAction
+ alias GenLSP.Structures.Diagnostic
+ alias GenLSP.Structures.Position
+ alias GenLSP.Structures.Range
+ alias GenLSP.Structures.TextEdit
+ alias GenLSP.Structures.WorkspaceEdit
+ alias NextLS.EditHelpers
+ alias Sourceror.Zipper, as: Z
+
+ @line_length 121
+
+ def new(%Diagnostic{} = diagnostic, text, uri) do
+ range = diagnostic.range
+
+ with {:ok, ast, comments} <- parse(text),
+ {:ok, debugger_node} <- find_debugger(ast, range) do
+ indent = EditHelpers.get_indent(text, range.start.line)
+ ast_without_debugger = remove_debugger(debugger_node)
+ range = make_range(debugger_node)
+
+ comments =
+ Enum.filter(comments, fn comment ->
+ comment.line > range.start.line && comment.line <= range.end.line
+ end)
+
+ to_algebra_opts = [comments: comments]
+ doc = Code.quoted_to_algebra(ast_without_debugger, to_algebra_opts)
+ formatted = doc |> Inspect.Algebra.format(@line_length) |> IO.iodata_to_binary()
+
+ [
+ %CodeAction{
+ title: make_title(debugger_node),
+ diagnostics: [diagnostic],
+ edit: %WorkspaceEdit{
+ changes: %{
+ uri => [
+ %TextEdit{
+ new_text: EditHelpers.add_indent_to_edit(formatted, indent),
+ range: range
+ }
+ ]
+ }
+ }
+ }
+ ]
+ else
+ _ ->
+ []
+ end
+ end
+
+ defp find_debugger(ast, range) do
+ pos = %{
+ start: [line: range.start.line + 1, column: range.start.character + 1],
+ end: [line: range.end.line + 1, column: range.end.character + 1]
+ }
+
+ {_, results} =
+ ast
+ |> Z.zip()
+ |> Z.traverse([], fn tree, acc ->
+ node = Z.node(tree)
+ range = Sourceror.get_range(node)
+
+ # range.start <= diagnostic_pos.start <= diagnostic_pos.end <= range.end
+ if (matches_debug?(node) || matches_pipe?(node)) && range &&
+ Sourceror.compare_positions(range.start, pos.start) in [:lt, :eq] &&
+ Sourceror.compare_positions(range.end, pos.end) in [:gt, :eq] do
+ {tree, [node | acc]}
+ else
+ {tree, acc}
+ end
+ end)
+
+ result =
+ Enum.min_by(
+ results,
+ fn node ->
+ range = Sourceror.get_range(node)
+
+ pos.start[:column] - range.start[:column] + range.end[:column] - pos.end[:column]
+ end,
+ fn -> nil end
+ )
+
+ result =
+ Enum.find(results, result, fn
+ {:|>, _, [_first, ^result]} -> true
+ _ -> false
+ end)
+
+ case result do
+ nil -> {:error, "could find a debugger to remove"}
+ node -> {:ok, node}
+ end
+ end
+
+ defp parse(lines) do
+ lines
+ |> Enum.join("\n")
+ |> Spitfire.parse_with_comments(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
+ |> case do
+ {:error, ast, comments, _errors} ->
+ {:ok, ast, comments}
+
+ other ->
+ other
+ end
+ end
+
+ defp make_range(original_ast) do
+ range = Sourceror.get_range(original_ast)
+
+ %Range{
+ start: %Position{line: range.start[:line] - 1, character: range.start[:column] - 1},
+ end: %Position{line: range.end[:line] - 1, character: range.end[:column] - 1}
+ }
+ end
+
+ defp matches_pipe?({:|>, _, [_, arg]}), do: matches_debug?(arg)
+ defp matches_pipe?(_), do: false
+
+ defp matches_debug?({:dbg, _, _}), do: true
+
+ defp matches_debug?({{:., _, [{:__aliases__, _, [:IO]}, f]}, _, _}) when f in [:puts, :inspect], do: true
+
+ defp matches_debug?({{:., _, [{:__aliases__, _, [:IEx]}, :pry]}, _, _}), do: true
+ defp matches_debug?({{:., _, [{:__aliases__, _, [:Mix]}, :env]}, _, _}), do: true
+ defp matches_debug?({{:., _, [{:__aliases__, _, [:Kernel]}, :dbg]}, _, _}), do: true
+ defp matches_debug?(_), do: false
+
+ defp remove_debugger({:|>, _, [arg, _function]}), do: arg
+ defp remove_debugger({{:., _, [{:__aliases__, _, [:IO]}, :inspect]}, _, [arg | _]}), do: arg
+ defp remove_debugger({{:., _, [{:__aliases__, _, [:Kernel]}, :dbg]}, _, [arg | _]}), do: arg
+ defp remove_debugger({:dbg, _, [arg | _]}), do: arg
+ defp remove_debugger(_node), do: {:__block__, [], []}
+
+ defp make_title({_, ctx, _} = node), do: "Remove `#{format_node(node)}` #{ctx[:line]}:#{ctx[:column]}"
+ defp format_node({:|>, _, [_arg, function]}), do: format_node(function)
+
+ defp format_node({{:., _, [{:__aliases__, _, [module]}, function]}, _, args}),
+ do: "{module}.#{function}/#{Enum.count(args)}"
+
+ defp format_node({:dbg, _, args}), do: "&dbg/#{Enum.count(args)}"
+ defp format_node(node), do: Macro.to_string(node)
+end
diff --git a/lib/next_ls/extensions/elixir_extension.ex b/lib/next_ls/extensions/elixir_extension.ex
index a6c7166a..0d161e3e 100644
--- a/lib/next_ls/extensions/elixir_extension.ex
+++ b/lib/next_ls/extensions/elixir_extension.ex
@@ -32,14 +32,6 @@ defmodule NextLS.ElixirExtension do
DiagnosticCache.clear(state.cache, :elixir)
for d <- diagnostics do
- # TODO: some compiler diagnostics only have the line number
- # but we want to only highlight the source code, so we
- # need to read the text of the file (either from the lsp cache
- # if the source code is "open", or read from disk) and calculate the
- # column of the first non-whitespace character.
- #
- # it is not clear to me whether the LSP process or the extension should
- # be responsible for this. The open documents live in the LSP process
DiagnosticCache.put(state.cache, :elixir, d.file, %GenLSP.Structures.Diagnostic{
severity: severity(d.severity),
message: IO.iodata_to_binary(d.message),
@@ -115,6 +107,7 @@ defmodule NextLS.ElixirExtension do
@unused_variable ~r/variable\s\"[^\"]+\"\sis\sunused/
@require_module ~r/you\smust\srequire/
+ @undefined_local_function ~r/undefined function (?.*)\/(?\d) \(expected (?.*) to define such a function or for it to be imported, but none are available\)/
defp metadata(diagnostic) do
base = %{"namespace" => "elixir"}
@@ -125,6 +118,10 @@ defmodule NextLS.ElixirExtension do
is_binary(diagnostic.message) and diagnostic.message =~ @require_module ->
Map.put(base, "type", "require")
+ is_binary(diagnostic.message) and diagnostic.message =~ @undefined_local_function ->
+ info = Regex.named_captures(@undefined_local_function, diagnostic.message)
+ base |> Map.put("type", "undefined-function") |> Map.put("info", info)
+
true ->
base
end
diff --git a/lib/next_ls/extensions/elixir_extension/code_action.ex b/lib/next_ls/extensions/elixir_extension/code_action.ex
index ea1310b7..d7b92a67 100644
--- a/lib/next_ls/extensions/elixir_extension/code_action.ex
+++ b/lib/next_ls/extensions/elixir_extension/code_action.ex
@@ -5,6 +5,7 @@ defmodule NextLS.ElixirExtension.CodeAction do
alias NextLS.CodeActionable.Data
alias NextLS.ElixirExtension.CodeAction.Require
+ alias NextLS.ElixirExtension.CodeAction.UndefinedFunction
alias NextLS.ElixirExtension.CodeAction.UnusedVariable
@impl true
@@ -16,6 +17,9 @@ defmodule NextLS.ElixirExtension.CodeAction do
%{"type" => "require"} ->
Require.new(data.diagnostic, data.document, data.uri)
+ %{"type" => "undefined-function"} ->
+ UndefinedFunction.new(data.diagnostic, data.document, data.uri)
+
_ ->
[]
end
diff --git a/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex b/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex
new file mode 100644
index 00000000..738386ba
--- /dev/null
+++ b/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex
@@ -0,0 +1,78 @@
+defmodule NextLS.ElixirExtension.CodeAction.UndefinedFunction do
+ @moduledoc false
+
+ alias GenLSP.Structures.CodeAction
+ alias GenLSP.Structures.Diagnostic
+ alias GenLSP.Structures.Range
+ alias GenLSP.Structures.TextEdit
+ alias GenLSP.Structures.WorkspaceEdit
+ alias NextLS.ASTHelpers
+
+ def new(diagnostic, text, uri) do
+ %Diagnostic{range: range, data: %{"info" => %{"name" => name, "arity" => arity}}} = diagnostic
+
+ with {:ok, ast} <-
+ text
+ |> Enum.join("\n")
+ |> Spitfire.parse(literal_encoder: &{:ok, {:__block__, &2, [&1]}}),
+ {:ok, {:defmodule, meta, _} = defm} <- ASTHelpers.get_surrounding_module(ast, range.start) do
+ indentation = get_indent(text, defm)
+
+ position = %GenLSP.Structures.Position{
+ line: meta[:end][:line] - 1,
+ character: 0
+ }
+
+ params = if arity == "0", do: "", else: Enum.map_join(1..String.to_integer(arity), ", ", fn i -> "param#{i}" end)
+
+ action = fn title, new_text ->
+ %CodeAction{
+ title: title,
+ diagnostics: [diagnostic],
+ edit: %WorkspaceEdit{
+ changes: %{
+ uri => [
+ %TextEdit{
+ new_text: new_text,
+ range: %Range{
+ start: position,
+ end: position
+ }
+ }
+ ]
+ }
+ }
+ }
+ end
+
+ [
+ action.("Create public function #{name}/#{arity}", """
+
+ #{indentation}def #{name}(#{params}) do
+
+ #{indentation}end
+ """),
+ action.("Create private function #{name}/#{arity}", """
+
+ #{indentation}defp #{name}(#{params}) do
+
+ #{indentation}end
+ """)
+ ]
+ end
+ end
+
+ @one_indentation_level " "
+ @indent ~r/^(\s*).*/
+ defp get_indent(text, {_, defm_context, _}) do
+ line = defm_context[:line] - 1
+
+ indent =
+ text
+ |> Enum.at(line)
+ |> then(&Regex.run(@indent, &1))
+ |> List.last()
+
+ indent <> @one_indentation_level
+ end
+end
diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex
index 11fb5ad3..73325593 100644
--- a/lib/next_ls/helpers/ast_helpers.ex
+++ b/lib/next_ls/helpers/ast_helpers.ex
@@ -155,45 +155,68 @@ defmodule NextLS.ASTHelpers do
end)
end
+ defp sourceror_inside?(range, position) do
+ Sourceror.compare_positions(range.start, position) in [:lt, :eq] &&
+ Sourceror.compare_positions(range.end, position) in [:gt, :eq]
+ end
+
@spec get_surrounding_module(ast :: Macro.t(), position :: Position.t()) :: {:ok, Macro.t()} | {:error, String.t()}
def get_surrounding_module(ast, position) do
- defm =
+ # TODO: this should take elixir positions and not LSP positions
+ position = [line: position.line + 1, column: position.character + 1]
+
+ {_zipper, acc} =
ast
- |> Macro.prewalker()
- |> Enum.filter(fn node -> match?({:defmodule, _, _}, node) end)
- |> Enum.filter(fn {_, ctx, _} ->
- position.line + 1 - ctx[:line] >= 0
+ |> Zipper.zip()
+ |> Zipper.traverse_while(nil, fn tree, acc ->
+ node = Zipper.node(tree)
+ node_range = Sourceror.Range.get_range(node)
+
+ is_inside =
+ with nil <- node_range do
+ false
+ else
+ _ -> sourceror_inside?(node_range, position)
+ end
+
+ acc =
+ with true <- is_inside,
+ {:defmodule, _, _} <- node do
+ node
+ else
+ _ -> acc
+ end
+
+ cond do
+ is_inside and match?({_, _, [_ | _]}, node) ->
+ {:cont, tree, acc}
+
+ is_inside and match?({_, _, []}, node) ->
+ {:halt, tree, acc}
+
+ true ->
+ {:cont, tree, acc}
+ end
end)
- |> Enum.min_by(
- fn {_, ctx, _} ->
- abs(ctx[:line] - 1 - position.line)
- end,
- fn -> nil end
- )
-
- if defm do
- {:ok, defm}
- else
- {:error, "no defmodule definition"}
- end
- end
- def find_cursor(ast) do
- with nil <-
- ast
- |> Zipper.zip()
- |> Zipper.find(fn
- {:@, _, [{:__cursor__, _, []}]} -> true
- {:__cursor__, _, _} -> true
- {{:., _, [_, :__cursor__]}, _, _} -> true
- _ -> false
- end) do
+ with {:ok, nil} <- {:ok, acc} do
{:error, :not_found}
- else
- zipper -> {:ok, zipper}
end
end
+ def top(nil, acc, _callback), do: acc
+
+ def top(%Zipper{path: nil} = zipper, acc, callback), do: callback.(Zipper.node(zipper), zipper, acc)
+
+ def top(zipper, acc, callback) do
+ node = Zipper.node(zipper)
+ acc = callback.(node, zipper, acc)
+
+ zipper = Zipper.up(zipper)
+
+ top(zipper, acc, callback)
+ end
+
defmodule Function do
@moduledoc false
diff --git a/lib/next_ls/helpers/ast_helpers/env.ex b/lib/next_ls/helpers/ast_helpers/env.ex
deleted file mode 100644
index d1cc376b..00000000
--- a/lib/next_ls/helpers/ast_helpers/env.ex
+++ /dev/null
@@ -1,141 +0,0 @@
-defmodule NextLS.ASTHelpers.Env do
- @moduledoc false
- alias Sourceror.Zipper
-
- defp inside?(range, position) do
- Sourceror.compare_positions(range.start, position) in [:lt, :eq] &&
- Sourceror.compare_positions(range.end, position) in [:gt, :eq]
- end
-
- def build(nil) do
- %{variables: []}
- end
-
- def build(cursor) do
- position = cursor |> Zipper.node() |> Sourceror.get_range() |> Map.get(:start)
- zipper = Zipper.prev(cursor)
-
- env =
- ascend(zipper, %{variables: [], attrs: []}, fn node, zipper, acc ->
- is_inside =
- with {_, _, _} <- node,
- range when not is_nil(range) <- Sourceror.get_range(node) do
- inside?(range, position)
- else
- _ ->
- false
- end
-
- case node do
- {match_op, _, [pm | _]} when match_op in [:=] and not is_inside ->
- {_, vars} =
- Macro.prewalk(pm, [], fn node, acc ->
- case node do
- {name, _, nil} ->
- {node, [to_string(name) | acc]}
-
- _ ->
- {node, acc}
- end
- end)
-
- Map.update!(acc, :variables, &(vars ++ &1))
-
- {match_op, _, [pm | rhs]} when match_op in [:<-] ->
- up_node = zipper |> Zipper.up() |> Zipper.node()
-
- # in_match operator comes with for and with normally, so we need to
- # check if we are inside the parent node, which is the for/with
- is_inside_p =
- with {_, _, _} <- up_node, range when not is_nil(range) <- Sourceror.get_range(up_node) do
- inside?(range, position)
- else
- _ ->
- false
- end
-
- is_inside_rhs =
- with range when not is_nil(range) <- Sourceror.get_range(rhs) do
- inside?(range, position)
- else
- _ ->
- false
- end
-
- if is_inside_p and not is_inside_rhs do
- {_, vars} =
- Macro.prewalk(pm, [], fn node, acc ->
- case node do
- {name, _, nil} ->
- {node, [to_string(name) | acc]}
-
- _ ->
- {node, acc}
- end
- end)
-
- Map.update!(acc, :variables, &(vars ++ &1))
- else
- acc
- end
-
- {def, _, [{_, _, args} | _]}
- when def in [:def, :defp, :defmacro, :defmacrop] and args != [] and is_inside ->
- {_, vars} =
- Macro.prewalk(args, [], fn node, acc ->
- case node do
- {name, _, nil} ->
- {node, [to_string(name) | acc]}
-
- _ ->
- {node, acc}
- end
- end)
-
- Map.update!(acc, :variables, &(vars ++ &1))
-
- {:->, _, [args | _]} when args != [] ->
- {_, vars} =
- Macro.prewalk(args, [], fn node, acc ->
- case node do
- {name, _, nil} ->
- {node, [to_string(name) | acc]}
-
- _ ->
- {node, acc}
- end
- end)
-
- Map.update!(acc, :variables, &(vars ++ &1))
-
- _ ->
- acc
- end
- end)
-
- %{
- variables: Enum.uniq(env.variables)
- }
- end
-
- def ascend(nil, acc, _callback), do: acc
-
- def ascend(%Zipper{path: nil} = zipper, acc, callback), do: callback.(Zipper.node(zipper), zipper, acc)
-
- def ascend(zipper, acc, callback) do
- node = Zipper.node(zipper)
- acc = callback.(node, zipper, acc)
-
- zipper =
- cond do
- match?({:->, _, _}, node) ->
- Zipper.up(zipper)
-
- true ->
- left = Zipper.left(zipper)
- if left, do: left, else: Zipper.up(zipper)
- end
-
- ascend(zipper, acc, callback)
- end
-end
diff --git a/lib/next_ls/helpers/docs_helpers.ex b/lib/next_ls/helpers/docs_helpers.ex
deleted file mode 100644
index f7637a01..00000000
--- a/lib/next_ls/helpers/docs_helpers.ex
+++ /dev/null
@@ -1,68 +0,0 @@
-defmodule NextLS.DocsHelpers do
- @moduledoc false
-
- @spec to_markdown(String.t(), String.t() | list()) :: String.t()
- def to_markdown(type, docs)
- def to_markdown("text/markdown", docs), do: docs
-
- def to_markdown("application/erlang+html" = type, [{:p, _, children} | rest]) do
- String.trim(to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest))
- end
-
- def to_markdown("application/erlang+html" = type, [{:div, attrs, children} | rest]) do
- prefix =
- if attrs[:class] in ~w do
- "> "
- else
- ""
- end
-
- prefix <> to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:a, attrs, children} | rest]) do
- space = if List.last(children) == " ", do: " ", else: ""
-
- "[#{String.trim(to_markdown(type, children))}](#{attrs[:href]})" <> space <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [doc | rest]) when is_binary(doc) do
- doc <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:pre, _, [{:code, _, children}]} | rest]) do
- "```erlang\n#{to_markdown(type, children)}\n```\n\n" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:ul, _, lis} | rest]) do
- "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:li, _, children} | rest]) do
- "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:code, _, bins} | rest]) do
- "`#{IO.iodata_to_binary(bins)}`" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:em, _, bins} | rest]) do
- "_#{IO.iodata_to_binary(bins)}_" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:dl, _, lis} | rest]) do
- "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:dt, _, children} | rest]) do
- "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html" = type, [{:dd, _, children} | rest]) do
- "#{to_markdown(type, children)}\n" <> to_markdown(type, rest)
- end
-
- def to_markdown("application/erlang+html", []) do
- ""
- end
-end
diff --git a/lib/next_ls/progress.ex b/lib/next_ls/progress.ex
index a7485635..45709f6b 100644
--- a/lib/next_ls/progress.ex
+++ b/lib/next_ls/progress.ex
@@ -1,11 +1,9 @@
defmodule NextLS.Progress do
@moduledoc false
- @env Mix.env()
+
def start(lsp, token, msg) do
Task.start(fn ->
- # FIXME: gen_lsp should allow stubbing requests so we don't have to
- # set this in every test. For now, don't send it in the test env
- if @env != :test do
+ if lsp.assigns.client_capabilities.window.work_done_progress do
GenLSP.request(lsp, %GenLSP.Requests.WindowWorkDoneProgressCreate{
id: System.unique_integer([:positive]),
params: %GenLSP.Structures.WorkDoneProgressCreateParams{
diff --git a/lib/next_ls/runtime.ex b/lib/next_ls/runtime.ex
index b969106d..4ca68420 100644
--- a/lib/next_ls/runtime.ex
+++ b/lib/next_ls/runtime.ex
@@ -111,6 +111,7 @@ defmodule NextLS.Runtime do
mix_env = Keyword.fetch!(opts, :mix_env)
mix_target = Keyword.fetch!(opts, :mix_target)
elixir_bin_path = Keyword.get(opts, :elixir_bin_path)
+ mix_home = Keyword.get(opts, :mix_home)
elixir_exe = Path.join(elixir_bin_path, "elixir")
@@ -130,7 +131,9 @@ defmodule NextLS.Runtime do
bindir = System.get_env("BINDIR")
path = System.get_env("PATH")
- new_path = String.replace(path, bindir <> ":", "")
+ path_minus_bindir = String.replace(path, bindir <> ":", "")
+ path_minus_bindir2 = path_minus_bindir |> String.split(":") |> List.delete(bindir) |> Enum.join(":")
+ new_path = elixir_bin_path <> ":" <> path_minus_bindir2
with dir when is_list(dir) <- :code.priv_dir(:next_ls) do
exe =
@@ -138,18 +141,24 @@ defmodule NextLS.Runtime do
|> Path.join("cmd")
|> Path.absname()
- env = [
- {~c"LSP", ~c"nextls"},
- {~c"NEXTLS_PARENT_PID", parent},
- {~c"MIX_ENV", ~c"#{mix_env}"},
- {~c"MIX_TARGET", ~c"#{mix_target}"},
- {~c"MIX_BUILD_ROOT", ~c".elixir-tools/_build"},
- {~c"ROOTDIR", false},
- {~c"BINDIR", false},
- {~c"RELEASE_ROOT", false},
- {~c"RELEASE_SYS_CONFIG", false},
- {~c"PATH", String.to_charlist(new_path)}
- ]
+ env =
+ [
+ {~c"LSP", ~c"nextls"},
+ {~c"NEXTLS_PARENT_PID", parent},
+ {~c"MIX_ENV", ~c"#{mix_env}"},
+ {~c"MIX_TARGET", ~c"#{mix_target}"},
+ {~c"MIX_BUILD_ROOT", ~c".elixir-tools/_build"},
+ {~c"ROOTDIR", false},
+ {~c"BINDIR", false},
+ {~c"RELEASE_ROOT", false},
+ {~c"RELEASE_SYS_CONFIG", false},
+ {~c"PATH", String.to_charlist(new_path)}
+ ] ++
+ if mix_home do
+ [{~c"MIX_HOME", ~c"#{mix_home}"}]
+ else
+ []
+ end
args =
[elixir_exe] ++
diff --git a/lib/next_ls/runtime/bundled_elixir.ex b/lib/next_ls/runtime/bundled_elixir.ex
index 6e864bf9..cc24e0be 100644
--- a/lib/next_ls/runtime/bundled_elixir.ex
+++ b/lib/next_ls/runtime/bundled_elixir.ex
@@ -20,8 +20,13 @@ defmodule NextLS.Runtime.BundledElixir do
Path.join([base, @dir])
end
- def install(base, logger, opts \\ []) do
- mixhome = Keyword.get(opts, :mix_home, Path.expand("~/.mix"))
+ def mix_home(base) do
+ Path.join(path(base), ".mix")
+ end
+
+ def install(base, logger) do
+ mixhome = mix_home(base)
+ File.mkdir_p!(mixhome)
binpath = binpath(base)
unless File.exists?(binpath) do
@@ -36,16 +41,16 @@ defmodule NextLS.Runtime.BundledElixir do
for bin <- Path.wildcard(Path.join(binpath, "*")) do
File.chmod(bin, 0o755)
end
+ end
- new_path = "#{binpath}:#{System.get_env("PATH")}"
- mixbin = mixpath(base)
+ new_path = "#{binpath}:#{System.get_env("PATH")}"
+ mixbin = mixpath(base)
- {_, 0} =
- System.cmd(mixbin, ["local.rebar", "--force"], env: [{"PATH", new_path}, {"MIX_HOME", mixhome}])
+ {_, 0} =
+ System.cmd(mixbin, ["local.rebar", "--force"], env: [{"PATH", new_path}, {"MIX_HOME", mixhome}])
- {_, 0} =
- System.cmd(mixbin, ["local.hex", "--force"], env: [{"PATH", new_path}, {"MIX_HOME", mixhome}])
- end
+ {_, 0} =
+ System.cmd(mixbin, ["local.hex", "--force"], env: [{"PATH", new_path}, {"MIX_HOME", mixhome}])
:ok
rescue
diff --git a/lib/next_ls/snippet.ex b/lib/next_ls/snippet.ex
index 9e590614..2dc6db89 100644
--- a/lib/next_ls/snippet.ex
+++ b/lib/next_ls/snippet.ex
@@ -3,6 +3,18 @@ defmodule NextLS.Snippet do
def get(label, trigger_character, opts \\ [])
+ def get("do", nil, _opts) do
+ %{
+ kind: GenLSP.Enumerations.CompletionItemKind.snippet(),
+ insert_text_format: GenLSP.Enumerations.InsertTextFormat.snippet(),
+ insert_text: """
+ do
+ $0
+ end
+ """
+ }
+ end
+
def get("defmodule/2", nil, opts) do
path = Keyword.get(opts, :uri)
diff --git a/mix.exs b/mix.exs
index 44d844eb..73f7a0ae 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,7 +1,7 @@
defmodule NextLS.MixProject do
use Mix.Project
- @version "0.20.2" # x-release-please-version
+ @version "0.22.1" # x-release-please-version
def project do
[
@@ -76,7 +76,7 @@ defmodule NextLS.MixProject do
{:burrito, "~> 1.0", only: [:dev, :prod]},
{:bypass, "~> 2.1", only: :test},
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false},
- {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
+ {:credo, "~> 1.7.6", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev},
{:styler, "~> 0.8", only: :dev}
]
diff --git a/mix.lock b/mix.lock
index b4c863cd..9720278c 100644
--- a/mix.lock
+++ b/mix.lock
@@ -9,7 +9,7 @@
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
- "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
+ "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"},
"ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"},
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
@@ -46,8 +46,8 @@
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"req": {:hex, :req, "0.4.0", "1c759054dd64ef1b1a0e475c2d2543250d18f08395d3174c371b7746984579ce", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "f53eadc32ebefd3e5d50390356ec3a59ed2b8513f7da8c6c3f2e14040e9fe989"},
"schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"},
- "sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"},
- "spitfire": {:git, "https://github.com/elixir-tools/spitfire.git", "f913c6025875c9d69b4d35f94cae3e70c7f6320e", []},
+ "sourceror": {:hex, :sourceror, "1.0.3", "111711c147f4f1414c07a67b45ad0064a7a41569037355407eda635649507f1d", [:mix], [], "hexpm", "56c21ef146c00b51bc3bb78d1f047cb732d193256a7c4ba91eaf828d3ae826af"},
+ "spitfire": {:git, "https://github.com/elixir-tools/spitfire.git", "e47385f64db19f65b8efdd57d003272376446a4e", []},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"styler": {:hex, :styler, "0.8.1", "f3c0f65023e4bfbf7e7aa752d128b8475fdabfd30f96ee7314b84480cc56e788", [:mix], [], "hexpm", "1aa48d3aa689a639289af3d8254d40e068e98c083d6e5e3d1a695e71a147b344"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex
index da8dafb1..1ef1b1dc 100644
--- a/priv/monkey/_next_ls_private_compiler.ex
+++ b/priv/monkey/_next_ls_private_compiler.ex
@@ -1116,7 +1116,8 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
cursor_env.functions,
macros:
Enum.filter(Map.get(state, :macros, []), fn {m, _} -> m == cursor_env.module end) ++ cursor_env.macros,
- attrs: Map.get(cursor_state, :attrs, [])
+ attrs: Enum.uniq(Map.get(cursor_state, :attrs, [])),
+ variables: for({name, nil} <- cursor_env.versioned_vars, do: name)
}
)
@@ -1128,11 +1129,6 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
{node, state, env}
end
- defp expand({{:., _, [_, :__cursor__]}, _, _} = node, state, env) do
- Process.put(:cursor_env, {state, env})
- {node, state, env}
- end
-
defp expand({:@, _, [{:__cursor__, _, _}]} = node, state, env) do
Process.put(:cursor_env, {state, env})
{node, state, env}
@@ -1266,10 +1262,10 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
# you would collect this information in expand_pattern/3 and
# invoke it from all relevant places (such as case, cond, try, etc).
- defp expand({:=, meta, [left, right]}, state, env) do
+ defp expand({match, meta, [left, right]}, state, env) when match in [:=, :<-] do
{left, state, env} = expand_pattern(left, state, env)
{right, state, env} = expand(right, state, env)
- {{:=, meta, [left, right]}, state, env}
+ {{match, meta, [left, right]}, state, env}
end
## quote/1, quote/2
@@ -1297,6 +1293,17 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
{{:^, meta, [arg]}, state, %{env | context: context}}
end
+ defp expand({:->, _, [params, block]}, state, env) do
+ {_, state, penv} =
+ for p <- params, reduce: {nil, state, env} do
+ {_, state, penv} ->
+ expand_pattern(p, state, penv)
+ end
+
+ {res, state, _env} = expand(block, state, penv)
+ {res, state, env}
+ end
+
## Remote call
defp expand({{:., dot_meta, [module, fun]}, meta, args}, state, env) when is_atom(fun) and is_list(args) do
@@ -1319,6 +1326,18 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
end
end
+ # self calling anonymous function
+
+ defp expand({{:., _dmeta, [func]}, _callmeta, args}, state, env) when is_list(args) do
+ {res, state, _env} = expand(func, state, env)
+ {res, state, env}
+ end
+
+ defp expand({:in, meta, [left, right]}, state, %{context: :match} = env) do
+ {left, state, env} = expand_pattern(left, state, env)
+ {{:in, meta, [left, right]}, state, env}
+ end
+
## Imported or local call
defp expand({fun, meta, args}, state, env) when is_atom(fun) and is_list(args) do
@@ -1365,11 +1384,12 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
# For the language server, we only want to capture definitions,
# we don't care when they are used.
- # defp expand({var, meta, ctx} = ast, state, %{context: :match} = env) when is_atom(var) and is_atom(ctx) do
- # ctx = Keyword.get(meta, :context, ctx)
- # # state = update_in(state.vars, &[{var, ctx} | &1])
- # {ast, state, env}
- # end
+ defp expand({var, meta, ctx} = ast, state, %{context: :match} = env) when is_atom(var) and is_atom(ctx) do
+ ctx = Keyword.get(meta, :context, ctx)
+ vv = Map.update(env.versioned_vars, var, ctx, fn _ -> ctx end)
+
+ {ast, state, %{env | versioned_vars: vv}}
+ end
## Fallback
@@ -1384,7 +1404,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
# definition, fully replacing the actual implementation. You could also
# use this to capture module attributes (optionally delegating to the actual
# implementation), function expansion, and more.
- defp expand_macro(_meta, Kernel, :defmodule, [alias, [do: block]], _callback, state, env) do
+ defp expand_macro(_meta, Kernel, :defmodule, [alias, [{_, block}]], _callback, state, env) do
{expanded, state, env} = expand(alias, state, env)
if is_atom(expanded) do
@@ -1405,9 +1425,15 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
end
end
- defp expand_macro(_meta, Kernel, type, [{name, _, params}, block], _callback, state, env)
+ defp expand_macro(_meta, Kernel, type, [{name, _, params}, [{_, block}]], _callback, state, env)
when type in [:def, :defp] and is_tuple(block) and is_atom(name) and is_list(params) do
- {res, state, _env} = expand(block, state, env)
+ {_, state, penv} =
+ for p <- params, reduce: {nil, state, env} do
+ {_, state, penv} ->
+ expand_pattern(p, state, penv)
+ end
+
+ {res, state, _env} = expand(block, state, penv)
arity = length(List.wrap(params))
functions = Map.update(state.functions, env.module, [{name, arity}], &Keyword.put_new(&1, name, arity))
@@ -1416,7 +1442,8 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
defp expand_macro(_meta, Kernel, type, [{name, _, params}, block], _callback, state, env)
when type in [:defmacro, :defmacrop] do
- {res, state, _env} = expand(block, state, env)
+ {_res, state, penv} = expand(params, state, env)
+ {res, state, _env} = expand(block, state, penv)
arity = length(List.wrap(params))
macros = Map.update(state.macros, env.module, [{name, arity}], &Keyword.put_new(&1, name, arity))
@@ -1425,10 +1452,16 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
defp expand_macro(_meta, Kernel, type, [{name, _, params}, blocks], _callback, state, env)
when type in [:def, :defp] and is_atom(name) and is_list(params) and is_list(blocks) do
+ {_, state, penv} =
+ for p <- params, reduce: {nil, state, env} do
+ {_, state, penv} ->
+ expand_pattern(p, state, penv)
+ end
+
{blocks, state} =
for {type, block} <- blocks, reduce: {[], state} do
{acc, state} ->
- {res, state, _env} = expand(block, state, env)
+ {res, state, _env} = expand(block, state, penv)
{[{type, res} | acc], state}
end
@@ -1440,15 +1473,19 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
defp expand_macro(_meta, Kernel, type, [{_name, _, params}, blocks], _callback, state, env)
when type in [:def, :defp] and is_list(params) and is_list(blocks) do
+ {_, state, penv} =
+ for p <- params, reduce: {nil, state, env} do
+ {_, state, penv} ->
+ expand_pattern(p, state, penv)
+ end
+
{blocks, state} =
for {type, block} <- blocks, reduce: {[], state} do
{acc, state} ->
- {res, state, _env} = expand(block, state, env)
+ {res, state, _env} = expand(block, state, penv)
{[{type, res} | acc], state}
end
- arity = length(List.wrap(params))
-
{Enum.reverse(blocks), state, env}
end
@@ -1508,6 +1545,29 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
{{{:., meta, [module, fun]}, meta, args}, state, env}
end
+ defp expand_local(_meta, fun, args, state, env) when fun in [:for, :with] do
+ {params, blocks} =
+ Enum.split_while(args, fn
+ {:<-, _, _} -> true
+ _ -> false
+ end)
+
+ {_, state, penv} =
+ for p <- params, reduce: {nil, state, env} do
+ {_, state, penv} ->
+ expand_pattern(p, state, penv)
+ end
+
+ {blocks, state} =
+ for {type, block} <- blocks, reduce: {[], state} do
+ {acc, state} ->
+ {res, state, _env} = expand(block, state, penv)
+ {[{type, res} | acc], state}
+ end
+
+ {blocks, state, env}
+ end
+
defp expand_local(meta, fun, args, state, env) do
# A compiler may want to emit a :local_function trace in here.
{args, state, env} = expand_list(args, state, env)
@@ -1535,7 +1595,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
{Enum.reverse(acc), state, env}
end
- defp expand_list([h | t] = list, state, env, acc) do
+ defp expand_list([h | t], state, env, acc) do
{h, state, env} = expand(h, state, env)
expand_list(t, state, env, [h | acc])
end
diff --git a/test/next_ls/autocomplete_test.exs b/test/next_ls/autocomplete_test.exs
index 048775f7..290cc9cc 100644
--- a/test/next_ls/autocomplete_test.exs
+++ b/test/next_ls/autocomplete_test.exs
@@ -73,6 +73,7 @@ defmodule NextLS.AutocompleteTest do
db: :some_db,
mix_env: "dev",
mix_target: "host",
+ mix_home: Path.join(cwd, ".mix"),
registry: __MODULE__.Registry}
)
@@ -95,7 +96,7 @@ defmodule NextLS.AutocompleteTest do
end
test "Erlang module completion", %{runtime: runtime} do
- assert expand(runtime, ~c":zl") == {:yes, [%{name: "zlib", kind: :module, docs: "## \"zlib\"\n\n\n"}]}
+ assert expand(runtime, ~c":zl") == {:yes, [%{name: "zlib", data: :zlib, kind: :module}]}
end
test "Erlang module no completion", %{runtime: runtime} do
@@ -123,17 +124,17 @@ defmodule NextLS.AutocompleteTest do
test "Elixir proxy", %{runtime: runtime} do
{:yes, [elixir_entry | _list]} = expand(runtime, ~c"E")
- assert %{name: "Elixir", kind: :module, docs: "## Elixir" <> _} = elixir_entry
+ assert %{name: "Elixir", kind: :module} = elixir_entry
end
test "Elixir completion", %{runtime: runtime} do
assert {:yes,
[
- %{name: "Enum", kind: :module, docs: "## Enum" <> _},
- %{name: "Enumerable", kind: :module, docs: "## Enumerable" <> _}
+ %{name: "Enum", kind: :module},
+ %{name: "Enumerable", kind: :module}
]} = expand(runtime, ~c"En")
- assert {:yes, [%{name: "Enumerable", kind: :module, docs: "## Enumerable" <> _}]} = expand(runtime, ~c"Enumera")
+ assert {:yes, [%{name: "Enumerable", kind: :module}]} = expand(runtime, ~c"Enumera")
end
# test "Elixir type completion", %{runtime: runtime} do
@@ -272,7 +273,7 @@ defmodule NextLS.AutocompleteTest do
end
test "Elixir root submodule completion", %{runtime: runtime} do
- {:yes, [%{name: "Access", kind: :module, docs: "## Access" <> _}]} = assert expand(runtime, ~c"Elixir.Acce")
+ {:yes, [%{name: "Access", kind: :module}]} = assert expand(runtime, ~c"Elixir.Acce")
end
test "Elixir submodule completion", %{runtime: runtime} do
@@ -284,56 +285,56 @@ defmodule NextLS.AutocompleteTest do
end
test "function completion", %{runtime: runtime} do
- assert {:yes, [%{arity: 0, name: "version", docs: _, kind: :function}]} = expand(runtime, ~c"System.ve")
+ assert {:yes, [%{arity: 0, name: "version", kind: :function}]} = expand(runtime, ~c"System.ve")
- assert {:yes, [%{arity: 1, name: "fun2ms", docs: _, kind: :function}]} = expand(runtime, ~c":ets.fun2")
+ assert {:yes, [%{arity: 1, name: "fun2ms", kind: :function}]} = expand(runtime, ~c":ets.fun2")
end
test "function completion with arity", %{runtime: runtime} do
assert {:yes,
[
- %{arity: 1, name: "printable?", docs: _, kind: :function},
- %{arity: 2, name: "printable?", docs: _, kind: :function}
+ %{arity: 1, name: "printable?", kind: :function},
+ %{arity: 2, name: "printable?", kind: :function}
]} = expand(runtime, ~c"String.printable?")
assert {:yes,
[
- %{arity: 1, name: "printable?", docs: _, kind: :function},
- %{arity: 2, name: "printable?", docs: _, kind: :function}
+ %{arity: 1, name: "printable?", kind: :function},
+ %{arity: 2, name: "printable?", kind: :function}
]} = expand(runtime, ~c"String.printable?/")
assert {:yes,
[
- %{arity: 1, name: "count", docs: _, kind: :function},
- %{arity: 2, name: "count", docs: _, kind: :function},
- %{arity: 2, name: "count_until", docs: _, kind: :function},
- %{arity: 3, name: "count_until", docs: _, kind: :function}
+ %{arity: 1, name: "count", kind: :function},
+ %{arity: 2, name: "count", kind: :function},
+ %{arity: 2, name: "count_until", kind: :function},
+ %{arity: 3, name: "count_until", kind: :function}
]} = expand(runtime, ~c"Enum.count")
assert {:yes,
[
- %{arity: 1, name: "count", docs: _, kind: :function},
- %{arity: 2, name: "count", docs: _, kind: :function}
+ %{arity: 1, name: "count", kind: :function},
+ %{arity: 2, name: "count", kind: :function}
]} = expand(runtime, ~c"Enum.count/")
end
test "operator completion", %{runtime: runtime} do
assert {:yes,
[
- %{arity: 1, name: "+", docs: _, kind: :function},
- %{arity: 2, name: "+", docs: _, kind: :function},
- %{arity: 2, name: "++", docs: _, kind: :function}
+ %{arity: 1, name: "+", kind: :function},
+ %{arity: 2, name: "+", kind: :function},
+ %{arity: 2, name: "++", kind: :function}
]} = expand(runtime, ~c"+")
assert {:yes,
[
- %{arity: 1, name: "+", docs: _, kind: :function},
- %{arity: 2, name: "+", docs: _, kind: :function}
+ %{arity: 1, name: "+", kind: :function},
+ %{arity: 2, name: "+", kind: :function}
]} = expand(runtime, ~c"+/")
assert {:yes,
[
- %{arity: 2, name: "++", docs: _, kind: :function}
+ %{arity: 2, name: "++", kind: :function}
]} = expand(runtime, ~c"++/")
end
@@ -423,22 +424,22 @@ defmodule NextLS.AutocompleteTest do
assert is_list(list)
Enum.any?(list, fn i ->
- match?(%{name: "unquote", arity: 1, kind: :function, docs: _}, i)
+ match?(%{name: "unquote", arity: 1, kind: :function}, i)
end)
Enum.any?(list, fn i ->
- match?(%{name: "try", arity: 1, kind: :function, docs: _}, i)
+ match?(%{name: "try", arity: 1, kind: :function}, i)
end)
end
# test "kernel import completion", %{runtime: runtime} do
- # assert {:yes, [%{name: "defstruct", kind: :function, docs: _, arity: 1}]} = expand(runtime, ~c"defstru")
+ # assert {:yes, [%{name: "defstruct", kind: :function, arity: 1}]} = expand(runtime, ~c"defstru")
# assert {:yes,
# [
- # %{arity: 3, name: "put_elem", docs: _, kind: :function},
- # %{arity: 2, name: "put_in", docs: _, kind: :function},
- # %{arity: 3, name: "put_in", docs: _, kind: :function}
+ # %{arity: 3, name: "put_elem", kind: :function},
+ # %{arity: 2, name: "put_in", kind: :function},
+ # %{arity: 3, name: "put_in", kind: :function}
# ]} = expand(runtime, ~c"put_")
# end
@@ -469,27 +470,9 @@ defmodule NextLS.AutocompleteTest do
:yes,
[
%{name: "nothing", kind: :variable},
- %{
- arity: 0,
- name: "node",
- docs:
- "## Kernel.node/0\n\nReturns an atom representing the name of the local node.\nIf the node is not alive, `:nonode@nohost` is returned instead.\n\nAllowed in guard tests. Inlined by the compiler.\n\n",
- kind: :function
- },
- %{
- arity: 1,
- name: "node",
- docs:
- "## Kernel.node/1\n\nReturns an atom representing the name of the local node.\nIf the node is not alive, `:nonode@nohost` is returned instead.\n\nAllowed in guard tests. Inlined by the compiler.\n\n",
- kind: :function
- },
- %{
- arity: 1,
- name: "not",
- docs:
- "## Kernel.not/1\n\nStrictly boolean \"not\" operator.\n\n`value` must be a boolean; if it's not, an `ArgumentError` exception is raised.\n\nAllowed in guard tests. Inlined by the compiler.\n\n## Examples\n\n iex> not false\n true\n\n\n",
- kind: :function
- }
+ %{arity: 0, kind: :function, name: "node", data: {Kernel, "node", 0}},
+ %{arity: 1, kind: :function, name: "node", data: {Kernel, "node", 1}},
+ %{arity: 1, kind: :function, name: "not", data: {Kernel, "not", 1}}
]
}
end
@@ -529,7 +512,7 @@ defmodule NextLS.AutocompleteTest do
# end
test "kernel special form completion", %{runtime: runtime} do
- assert {:yes, [%{arity: 1, name: "unquote_splicing", docs: _, kind: :function}]} = expand(runtime, ~c"unquote_spl")
+ assert {:yes, [%{arity: 1, name: "unquote_splicing", kind: :function}]} = expand(runtime, ~c"unquote_spl")
end
test "completion inside expression", %{runtime: runtime} do
@@ -544,7 +527,7 @@ defmodule NextLS.AutocompleteTest do
test "Elixir completion sublevel", %{runtime: runtime} do
assert expand(runtime, ~c"SublevelTest.") ==
- {:yes, [%{name: "LevelA", kind: :module, docs: "## SublevelTest.LevelA.LevelB\n\n\n"}]}
+ {:yes, [%{name: "LevelA", data: SublevelTest.LevelA.LevelB, kind: :module}]}
end
# TODO: aliases
@@ -665,25 +648,24 @@ defmodule NextLS.AutocompleteTest do
test "completion for bitstring modifiers", %{runtime: runtime} do
assert {:yes, entries} = expand(runtime, ~c"< %{"completions" => %{"enable" => true}}}
+
defmacrop assert_match({:in, _, [left, right]}) do
quote do
assert Enum.any?(unquote(right), fn x ->
@@ -306,29 +307,22 @@ defmodule NextLS.CompletionsTest do
end
""")
- {results, log} =
- with_log(fn ->
- request client, %{
- method: "textDocument/completion",
- id: 2,
- jsonrpc: "2.0",
- params: %{
- textDocument: %{
- uri: uri
- },
- position: %{
- line: 2,
- character: 11
- }
- }
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{
+ uri: uri
+ },
+ position: %{
+ line: 2,
+ character: 11
}
+ }
+ }
- assert_result 2, [_, _, _] = results
- results
- end)
-
- assert log =~ "Could not locate cursor"
- assert log =~ "Source code that produced the above warning:"
+ assert_result 2, [_, _, _] = results
assert %{
"data" => nil,
@@ -355,6 +349,44 @@ defmodule NextLS.CompletionsTest do
} in results
end
+ test "inside interpolation in strings", %{client: client, foo: foo} do
+ uri = uri(foo)
+
+ did_open(client, foo, ~S"""
+ defmodule Foo do
+ def run(thing) do
+ "./lib/#{t}"
+ :ok
+ end
+ end
+ """)
+
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{
+ uri: uri
+ },
+ position: %{
+ line: 2,
+ character: 13
+ }
+ }
+ }
+
+ assert_result 2, results
+
+ assert %{
+ "data" => nil,
+ "documentation" => "",
+ "insertText" => "thing",
+ "kind" => 6,
+ "label" => "thing"
+ } in results
+ end
+
test "defmodule infer name", %{client: client, foo: foo} do
uri = uri(foo)
@@ -377,16 +409,14 @@ defmodule NextLS.CompletionsTest do
}
}
- assert_result 2, [
- %{
- "data" => nil,
- "documentation" => _,
- "insertText" => "defmodule ${1:Foo} do\n $0\nend\n",
- "kind" => 15,
- "label" => "defmodule/2",
- "insertTextFormat" => 2
- }
- ]
+ assert_result 2, results
+
+ assert_match %{
+ "insertText" => "defmodule ${1:Foo} do\n $0\nend\n",
+ "kind" => 15,
+ "label" => "defmodule/2",
+ "insertTextFormat" => 2
+ } in results
end
test "aliases in document", %{client: client, foo: foo} do
@@ -419,9 +449,7 @@ defmodule NextLS.CompletionsTest do
assert_result 2, results
- assert_match(
- %{"data" => _, "documentation" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"} in results
- )
+ assert_match %{"data" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"} in results
end
test "inside alias special form", %{client: client, foo: foo} do
@@ -529,21 +557,21 @@ defmodule NextLS.CompletionsTest do
assert_result 2, [
%{
- "data" => nil,
+ "data" => _,
"documentation" => "",
"insertText" => "var",
"kind" => 6,
"label" => "var"
},
%{
- "data" => nil,
+ "data" => _,
"documentation" => _,
"insertText" => "var!",
"kind" => 3,
"label" => "var!/1"
},
%{
- "data" => nil,
+ "data" => _,
"documentation" => _,
"insertText" => "var!",
"kind" => 3,
@@ -551,4 +579,234 @@ defmodule NextLS.CompletionsTest do
}
]
end
+
+ test "variable and param completions", %{client: client, foo: foo} do
+ uri = uri(foo)
+
+ did_open(client, foo, """
+ defmodule Foo do
+ def run(%Bar{one: %{foo: %{bar: villain}}, two: vim}, vroom) do
+ document = vroom.assigns.documents[vim]
+ v
+ rescue
+ _ ->
+ :error
+ end
+ end
+ """)
+
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{
+ uri: uri
+ },
+ position: %{
+ line: 3,
+ character: 5
+ }
+ }
+ }
+
+ assert_result 2, results
+
+ # assert_match %{"kind" => 6, "label" => "vampire"} in results
+ assert_match %{"kind" => 6, "label" => "villain"} in results
+ assert_match %{"kind" => 6, "label" => "vim"} in results
+ # assert_match %{"kind" => 6, "label" => "vrest"} in results
+ assert_match %{"kind" => 6, "label" => "vroom"} in results
+ # assert_match %{"kind" => 6, "label" => "var"} in results
+ end
+
+ test "variable and param completions in other block identifiers", %{client: client, foo: foo} do
+ uri = uri(foo)
+
+ did_open(client, foo, """
+ defmodule Foo do
+ def run(%Bar{one: %{foo: %{bar: villain}}, two: vim}, vroom) do
+ var1 = vroom.assigns.documents[vim]
+ v
+ rescue
+ verror ->
+ var2 = "hi"
+
+ v
+ end
+ end
+ """)
+
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{
+ uri: uri
+ },
+ position: %{
+ line: 8,
+ character: 7
+ }
+ }
+ }
+
+ assert_result 2, results
+
+ assert_match %{"kind" => 6, "label" => "villain"} in results
+ assert_match %{"kind" => 6, "label" => "vim"} in results
+ assert_match %{"kind" => 6, "label" => "vroom"} in results
+ assert_match %{"kind" => 6, "label" => "verror"} in results
+ assert_match %{"kind" => 6, "label" => "var2"} in results
+
+ assert_match %{"kind" => 6, "label" => "var1"} not in results
+ end
+
+ test "param completions in multi arrow situations", %{client: client, foo: foo} do
+ uri = uri(foo)
+
+ did_open(client, foo, """
+ defmodule Foo do
+ def run(alice) do
+ alice
+ |> then(fn
+ {:ok, ast1} -> ast1
+ {:error, ast2, _} -> a
+ {:error, :no_fuel_remaining} -> nil
+ end)
+ end
+ end
+ """)
+
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{uri: uri},
+ position: %{
+ line: 5,
+ character: 28
+ }
+ }
+ }
+
+ assert_result 2, results
+
+ assert_match %{"kind" => 6, "label" => "alice"} in results
+ # TODO: requires changes to spitfire
+ # assert_match %{"kind" => 6, "label" => "ast2"} in results
+
+ assert_match %{"kind" => 6, "label" => "ast1"} not in results
+ end
+
+ test "variables show up in test blocks", %{client: client, foo: foo} do
+ uri = uri(foo)
+
+ did_open(client, foo, """
+ defmodule Foo do
+ use ExUnit.Case
+ test "something", %{vim: vim} do
+ var = "hi"
+
+ v
+ end
+ end
+ """)
+
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{
+ uri: uri
+ },
+ position: %{
+ line: 5,
+ character: 5
+ }
+ }
+ }
+
+ assert_result 2, results
+
+ assert_match %{"kind" => 6, "label" => "var"} in results
+ assert_match %{"kind" => 6, "label" => "vim"} in results
+ end
+
+ test "<- matches dont leak from for", %{client: client, foo: foo} do
+ uri = uri(foo)
+
+ did_open(client, foo, """
+ defmodule Foo do
+ def run(items) do
+ names =
+ for item <- items do
+ item.name
+ end
+
+ i
+ end
+ end
+ """)
+
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{
+ uri: uri
+ },
+ position: %{
+ line: 7,
+ character: 5
+ }
+ }
+ }
+
+ assert_result 2, results
+
+ assert_match %{"kind" => 6, "label" => "items"} in results
+ assert_match %{"kind" => 6, "label" => "item"} not in results
+ end
+
+ test "<- matches dont leak from with", %{client: client, foo: foo} do
+ uri = uri(foo)
+
+ did_open(client, foo, """
+ defmodule Foo do
+ def run(items) do
+ names =
+ with item <- items do
+ item.name
+ end
+
+ i
+ end
+ end
+ """)
+
+ request client, %{
+ method: "textDocument/completion",
+ id: 2,
+ jsonrpc: "2.0",
+ params: %{
+ textDocument: %{
+ uri: uri
+ },
+ position: %{
+ line: 7,
+ character: 5
+ }
+ }
+ }
+
+ assert_result 2, results
+
+ assert_match %{"kind" => 6, "label" => "items"} in results
+ assert_match %{"kind" => 6, "label" => "item"} not in results
+ end
end
diff --git a/test/next_ls/extensions/credo_extension/remove_debugger_test.exs b/test/next_ls/extensions/credo_extension/remove_debugger_test.exs
new file mode 100644
index 00000000..64987012
--- /dev/null
+++ b/test/next_ls/extensions/credo_extension/remove_debugger_test.exs
@@ -0,0 +1,346 @@
+defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
+ use ExUnit.Case, async: true
+
+ import NextLS.Support.Utils, only: [assert_is_text_edit: 3]
+
+ alias GenLSP.Structures.CodeAction
+ alias GenLSP.Structures.Position
+ alias GenLSP.Structures.Range
+ alias GenLSP.Structures.TextEdit
+ alias GenLSP.Structures.WorkspaceEdit
+ alias NextLS.CredoExtension.CodeAction.RemoveDebugger
+
+ @uri "file:///home/owner/my_project/hello.ex"
+
+ test "removes debugger checks" do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ def hello(arg) do
+ IO.inspect(arg)
+ foo(arg)
+ end
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg
+ foo(arg)
+ end
+ end
+ """)
+
+ start = %Position{character: 4, line: 2}
+ diagnostic = get_diagnostic(start)
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+ assert is_struct(code_action, CodeAction)
+ assert [diagnostic] == code_action.diagnostics
+ assert code_action.title == "Remove `&IO.inspect/1` 3:8"
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+
+ test "works for all credo checks" do
+ checks = [
+ {"Elixir.Credo.Check.Warning.Dbg", "dbg()", "dbg/0", ""},
+ {"Elixir.Credo.Check.Warning.Dbg", "dbg(arg)", "dbg/1", "arg"},
+ {"Elixir.Credo.Check.Warning.Dbg", "Kernel.dbg()", "Kernel.dbg/0", ""},
+ {"Elixir.Credo.Check.Warning.IExPry", "IEx.pry()", "IEx.pry/0", ""},
+ {"Elixir.Credo.Check.Warning.IoInspect", "IO.inspect(foo, label: ~s/bar/)", "IO.inspect/2", "foo"},
+ {"Elixir.Credo.Check.Warning.IoPuts", "IO.puts(arg)", "IO.puts/1", ""},
+ {"Elixir.Credo.Check.Warning.MixEnv", "Mix.env()", "Mix.env/0", ""}
+ ]
+
+ for {check, code, title, edit} <- checks do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ def hello(arg) do
+ #{code}
+ arg
+ end
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ def hello(arg) do
+ #{edit}
+ arg
+ end
+ end
+ """)
+
+ start = %Position{character: 4, line: 2}
+ diagnostic = get_diagnostic(start, check: check, code: code)
+ title = "Remove `{title}` #{start.line + 1}"
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+ assert code_action.title =~ title
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [%TextEdit{} = edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+ end
+
+ test "works on multiple expressions on one line" do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ def hello(arg) do
+ IO.inspect(arg, label: "Debugging"); world(arg)
+ end
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg; world(arg)
+ end
+ end
+ """)
+
+ start = %Position{character: 4, line: 2}
+ diagnostic = get_diagnostic(start)
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+
+ test "handles pipe calls in the middle" do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg
+ |> Enum.map(&(&1 * &1))
+ |> IO.inspect(label: "FOO")
+ |> Enum.sum()
+ end
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg
+ |> Enum.map(&(&1 * &1))
+ |> Enum.sum()
+ end
+ end
+ """)
+
+ start = %Position{character: 10, line: 4}
+ diagnostic = get_diagnostic(start)
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+
+ test "handles pipe calls at the end" do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg
+ |> Enum.map(&(&1 * &1))
+ |> Enum.sum()
+ |> IO.inspect()
+ end
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg
+ |> Enum.map(&(&1 * &1))
+ |> Enum.sum()
+ end
+ end
+ """)
+
+ start = %Position{character: 10, line: 5}
+ diagnostic = get_diagnostic(start)
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+
+ test "handles pipe calls after an expr" do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg |> IO.inspect()
+ end
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg
+ end
+ end
+ """)
+
+ start = %Position{character: 10, line: 2}
+ diagnostic = get_diagnostic(start)
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+
+ test "handles functions with only one expression" do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ def hello(arg) do
+ IO.inspect(arg, label: "DEBUG")
+ end
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ def hello(arg) do
+ arg
+ end
+ end
+ """)
+
+ start = %Position{character: 4, line: 2}
+ diagnostic = get_diagnostic(start)
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+
+ test "handles inspects in module bodies" do
+ text =
+ String.split(
+ """
+ defmodule Test.Debug do
+ @attr "foo"
+ IO.inspect(@attr)
+ end
+ """,
+ "\n"
+ )
+
+ expected =
+ String.trim("""
+ defmodule Test.Debug do
+ @attr "foo"
+ @attr
+ end
+ """)
+
+ start = %Position{character: 4, line: 2}
+ diagnostic = get_diagnostic(start)
+
+ assert [code_action] = RemoveDebugger.new(diagnostic, text, @uri)
+
+ assert %WorkspaceEdit{
+ changes: %{
+ @uri => [edit]
+ }
+ } = code_action.edit
+
+ assert_is_text_edit(text, edit, expected)
+ end
+
+ defp get_diagnostic(start, opts \\ []) do
+ check = Keyword.get(opts, :check, "Elixir.Credo.Check.Warning.IoInspect")
+ code = Keyword.get(opts, :code, "IO.inspect/2")
+
+ %GenLSP.Structures.Diagnostic{
+ data: %{"namespace" => "credo", "check" => check},
+ message: "There should be no calls to `#{code}`",
+ source: "Elixir",
+ range: %Range{
+ start: start,
+ end: start
+ }
+ }
+ end
+end
diff --git a/test/next_ls/extensions/elixir_extension/code_action/require_test.exs b/test/next_ls/extensions/elixir_extension/code_action/require_test.exs
index 39e5942b..e960119c 100644
--- a/test/next_ls/extensions/elixir_extension/code_action/require_test.exs
+++ b/test/next_ls/extensions/elixir_extension/code_action/require_test.exs
@@ -21,7 +21,7 @@ defmodule NextLS.ElixirExtension.RequireTest do
"\n"
)
- start = %Position{character: 0, line: 1}
+ start = %Position{character: 11, line: 2}
diagnostic = %GenLSP.Structures.Diagnostic{
data: %{"namespace" => "elixir", "type" => "require"},
@@ -40,12 +40,14 @@ defmodule NextLS.ElixirExtension.RequireTest do
assert [diagnostic] == code_action.diagnostics
assert code_action.title == "Add missing require for Logger"
+ edit_position = %GenLSP.Structures.Position{line: 1, character: 0}
+
assert %WorkspaceEdit{
changes: %{
^uri => [
%TextEdit{
new_text: " require Logger\n",
- range: %Range{start: ^start, end: ^start}
+ range: %Range{start: ^edit_position, end: ^edit_position}
}
]
}
diff --git a/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs
new file mode 100644
index 00000000..4dc3180e
--- /dev/null
+++ b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs
@@ -0,0 +1,170 @@
+defmodule NextLS.ElixirExtension.UndefinedFunctionTest do
+ use ExUnit.Case, async: true
+
+ alias GenLSP.Structures.CodeAction
+ alias GenLSP.Structures.Position
+ alias GenLSP.Structures.Range
+ alias GenLSP.Structures.TextEdit
+ alias GenLSP.Structures.WorkspaceEdit
+ alias NextLS.ElixirExtension.CodeAction.UndefinedFunction
+
+ test "in outer module creates new private function inside current module" do
+ text =
+ String.split(
+ """
+ defmodule Test.Foo do
+ defmodule Bar do
+ def run() do
+ :ok
+ end
+ end
+
+ def hello() do
+ bar(1, 2)
+ end
+
+ defmodule Baz do
+ def run() do
+ :error
+ end
+ end
+ end
+ """,
+ "\n"
+ )
+
+ start = %Position{character: 4, line: 8}
+
+ diagnostic = %GenLSP.Structures.Diagnostic{
+ data: %{
+ "namespace" => "elixir",
+ "type" => "undefined-function",
+ "info" => %{
+ "name" => "bar",
+ "arity" => "2",
+ "module" => "Elixir.Test.Foo"
+ }
+ },
+ message:
+ "undefined function bar/2 (expected Test.Foo to define such a function or for it to be imported, but none are available)",
+ source: "Elixir",
+ range: %GenLSP.Structures.Range{
+ start: start,
+ end: %{start | character: 6}
+ }
+ }
+
+ uri = "file:///home/owner/my_project/hello.ex"
+
+ assert [public, private] = UndefinedFunction.new(diagnostic, text, uri)
+ assert [diagnostic] == public.diagnostics
+ assert public.title == "Create public function bar/2"
+
+ edit_position = %Position{line: 16, character: 0}
+
+ assert %WorkspaceEdit{
+ changes: %{
+ ^uri => [
+ %TextEdit{
+ new_text: """
+
+ def bar(param1, param2) do
+
+ end
+ """,
+ range: %Range{start: ^edit_position, end: ^edit_position}
+ }
+ ]
+ }
+ } = public.edit
+
+ assert [diagnostic] == private.diagnostics
+ assert private.title == "Create private function bar/2"
+
+ edit_position = %Position{line: 16, character: 0}
+
+ assert %WorkspaceEdit{
+ changes: %{
+ ^uri => [
+ %TextEdit{
+ new_text: """
+
+ defp bar(param1, param2) do
+
+ end
+ """,
+ range: %Range{start: ^edit_position, end: ^edit_position}
+ }
+ ]
+ }
+ } = private.edit
+ end
+
+ test "in inner module creates new private function inside current module" do
+ text =
+ String.split(
+ """
+ defmodule Test.Foo do
+ defmodule Bar do
+ def run() do
+ bar(1, 2)
+ end
+ end
+
+ defmodule Baz do
+ def run() do
+ :error
+ end
+ end
+ end
+ """,
+ "\n"
+ )
+
+ start = %Position{character: 6, line: 3}
+
+ diagnostic = %GenLSP.Structures.Diagnostic{
+ data: %{
+ "namespace" => "elixir",
+ "type" => "undefined-function",
+ "info" => %{
+ "name" => "bar",
+ "arity" => "2",
+ "module" => "Elixir.Test.Foo.Bar"
+ }
+ },
+ message:
+ "undefined function bar/2 (expected Test.Foo to define such a function or for it to be imported, but none are available)",
+ source: "Elixir",
+ range: %GenLSP.Structures.Range{
+ start: start,
+ end: %{start | character: 9}
+ }
+ }
+
+ uri = "file:///home/owner/my_project/hello.ex"
+
+ assert [_, code_action] = UndefinedFunction.new(diagnostic, text, uri)
+ assert %CodeAction{} = code_action
+ assert [diagnostic] == code_action.diagnostics
+ assert code_action.title == "Create private function bar/2"
+
+ edit_position = %Position{line: 5, character: 0}
+
+ assert %WorkspaceEdit{
+ changes: %{
+ ^uri => [
+ %TextEdit{
+ new_text: """
+
+ defp bar(param1, param2) do
+
+ end
+ """,
+ range: %Range{start: ^edit_position, end: ^edit_position}
+ }
+ ]
+ }
+ } = code_action.edit
+ end
+end
diff --git a/test/next_ls/extensions/elixir_extension/code_action_test.exs b/test/next_ls/extensions/elixir_extension/code_action_test.exs
index 09fd4846..58e86d9b 100644
--- a/test/next_ls/extensions/elixir_extension/code_action_test.exs
+++ b/test/next_ls/extensions/elixir_extension/code_action_test.exs
@@ -14,6 +14,7 @@ defmodule NextLS.Extensions.ElixirExtension.CodeActionTest do
cwd = Path.join(tmp_dir, "my_proj")
foo_path = Path.join(cwd, "lib/foo.ex")
+ bar_path = Path.join(cwd, "lib/bar.ex")
foo = """
defmodule MyProj.Foo do
@@ -28,9 +29,20 @@ defmodule NextLS.Extensions.ElixirExtension.CodeActionTest do
end
"""
+ bar = """
+ defmodule MyProj.Bar do
+ def foo() do
+ a = :bar
+ foo(dbg(a), IO.inspect(a))
+ a
+ end
+ end
+ """
+
File.write!(foo_path, foo)
+ File.write!(bar_path, bar)
- [foo: foo, foo_path: foo_path]
+ [foo: foo, foo_path: foo_path, bar: bar, bar_path: bar_path]
end
setup :with_lsp
@@ -42,6 +54,7 @@ defmodule NextLS.Extensions.ElixirExtension.CodeActionTest do
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}
did_open(context.client, context.foo_path, context.foo)
+ did_open(context.client, context.bar_path, context.bar)
context
end
@@ -60,7 +73,7 @@ defmodule NextLS.Extensions.ElixirExtension.CodeActionTest do
"data" => %{"namespace" => "elixir", "type" => "unused_variable"},
"message" =>
"variable \"foo\" is unused (if the variable is not meant to be used, prefix it with an underscore)",
- "range" => %{"end" => %{"character" => 999, "line" => 2}, "start" => %{"character" => 4, "line" => 2}},
+ "range" => %{"end" => %{"character" => 999, "line" => 3}, "start" => %{"character" => 4, "line" => 3}},
"severity" => 2,
"source" => "Elixir"
}
@@ -122,4 +135,50 @@ defmodule NextLS.Extensions.ElixirExtension.CodeActionTest do
},
500
end
+
+ test "sends back a remove inspect action", %{client: client, bar_path: bar} do
+ bar_uri = uri(bar)
+ id = 1
+
+ request client, %{
+ method: "textDocument/codeAction",
+ id: id,
+ jsonrpc: "2.0",
+ params: %{
+ context: %{
+ "diagnostics" => [
+ %{
+ "data" => %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.Dbg"},
+ "message" => "There should be no calls to `dbg/1`.",
+ "range" => %{"end" => %{"character" => 13, "line" => 3}, "start" => %{"character" => 8, "line" => 3}},
+ "severity" => 2,
+ "source" => "Elixir"
+ },
+ %{
+ "data" => %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.IoInspect"},
+ "message" => "There should be no calls to `IO.inspect/1`.",
+ "range" => %{"end" => %{"character" => 28, "line" => 3}, "start" => %{"character" => 20, "line" => 3}},
+ "severity" => 2,
+ "source" => "Elixir"
+ }
+ ]
+ },
+ range: %{start: %{line: 0, character: 0}, end: %{line: 7, character: 999}},
+ textDocument: %{uri: bar_uri}
+ }
+ }
+
+ assert_receive %{
+ "jsonrpc" => "2.0",
+ "id" => 1,
+ "result" => [
+ %{"edit" => %{"changes" => %{^bar_uri => [%{"newText" => "a", "range" => range1}]}}},
+ %{"edit" => %{"changes" => %{^bar_uri => [%{"newText" => "a", "range" => range2}]}}}
+ ]
+ },
+ 500
+
+ assert %{"start" => %{"character" => 8, "line" => 3}, "end" => %{"character" => 14, "line" => 3}} == range1
+ assert %{"start" => %{"character" => 16, "line" => 3}, "end" => %{"character" => 29, "line" => 3}} == range2
+ end
end
diff --git a/test/next_ls/helpers/ast_helpers/env_test.exs b/test/next_ls/helpers/ast_helpers/env_test.exs
deleted file mode 100644
index 1be03d13..00000000
--- a/test/next_ls/helpers/ast_helpers/env_test.exs
+++ /dev/null
@@ -1,217 +0,0 @@
-defmodule NextLS.ASTHelpers.EnvTest do
- use ExUnit.Case, async: true
-
- describe "build/2" do
- test "collects simple variables" do
- code = """
- defmodule Foo do
- def one do
- foo = :bar
-
- Enum.map([foo], fn ->
- bar = x
- __cursor__()
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["foo", "bar"]
- end
-
- test "collects variables from patterns" do
- code = """
- defmodule Foo do
- def one() do
- %{bar: [one, %{baz: two}]} = Some.thing()
-
- __cursor__()
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["two", "one"]
- end
-
- test "collects variables from 'formal' parameters" do
- code = """
- defmodule Foo do
- def zero(notme) do
- :error
- end
-
- def one(foo, bar, baz) do
-
- __cursor__()
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["baz", "bar", "foo"]
- end
-
- test "collects variables from stab parameters" do
- code = """
- defmodule Foo do
- def one() do
- Enum.map(Some.thing(), fn
- four ->
- :ok
-
- one, two, three ->
- __cursor__()
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["three", "two", "one"]
- end
-
- test "collects variables from left stab" do
- code = """
- defmodule Foo do
- def one() do
- with [foo] <- thing(),
- bar <- thang() do
- __cursor__()
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["foo", "bar"]
- end
-
- test "scopes variables lexically" do
- code = """
- defmodule Foo do
- def one() do
- baz = Some.thing()
- foo = Enum.map(two(), fn bar ->
- big_bar = bar * 2
- __cursor__()
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["baz", "bar", "big_bar"]
- end
-
- test "comprehension and with parameters do not leak" do
- code = """
- defmodule Foo do
- def one(entries) do
- with {:ok, entry} <- entries do
- :ok
- end
-
- for entry <- entries do
- :ok
- end
-
- __cursor__()
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["entries"]
- end
-
- test "comprehension lhs of generator do not leak into rhs " do
- code = """
- defmodule Foo do
- def one(entries) do
- for entry <- entries,
- not_me <- __cursor__() do
- :ok
- end
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["entries", "entry"]
- end
-
- test "multiple generators and filters in comprehension" do
- code = """
- defmodule Foo do
- def one(entries) do
- for entry <- entries,
- foo = do_something(),
- bar <- foo do
- __cursor__()
- :ok
- end
- end
-
- def two do
- baz = :bar
- end
- end
- """
-
- actual = run(code)
-
- assert actual.variables == ["entries", "entry", "foo", "bar"]
- end
- end
-
- defp run(code) do
- {:ok, zip} =
- code
- |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}})
- |> then(fn
- {:ok, ast} -> ast
- {:error, ast, _} -> ast
- end)
- |> NextLS.ASTHelpers.find_cursor()
-
- NextLS.ASTHelpers.Env.build(zip)
- end
-end
diff --git a/test/next_ls/helpers/ast_helpers_test.exs b/test/next_ls/helpers/ast_helpers_test.exs
index c378da58..343739f1 100644
--- a/test/next_ls/helpers/ast_helpers_test.exs
+++ b/test/next_ls/helpers/ast_helpers_test.exs
@@ -91,26 +91,26 @@ defmodule NextLS.ASTHelpersTest do
end
""")
- lines = 1..3
+ for {line, character} <- [{0, 2}, {1, 1}, {4, 0}, {5, 1}, {8, 2}] do
+ position = %Position{line: line, character: character}
- for line <- lines do
- position = %Position{line: line, character: 0}
+ assert {:ok, {:defmodule, _, [{:__aliases__, _, [:Test]} | _]}} =
+ ASTHelpers.get_surrounding_module(ast, position)
+ end
+
+ for {line, character} <- [{1, 2}, {1, 6}, {2, 5}, {3, 3}] do
+ position = %Position{line: line, character: character}
assert {:ok, {:defmodule, _, [{:__aliases__, _, [:Foo]} | _]}} =
ASTHelpers.get_surrounding_module(ast, position)
end
- lines = 5..7
-
- for line <- lines do
- position = %Position{line: line, character: 0}
+ for {line, character} <- [{5, 4}, {6, 1}, {7, 0}, {7, 3}] do
+ position = %Position{line: line, character: character}
assert {:ok, {:defmodule, _, [{:__aliases__, _, [:Bar]} | _]}} =
ASTHelpers.get_surrounding_module(ast, position)
end
-
- position = %Position{line: 0, character: 0}
- assert {:ok, {:defmodule, _, [{:__aliases__, _, [:Test]} | _]}} = ASTHelpers.get_surrounding_module(ast, position)
end
test "errors out when it can't find a module" do
@@ -120,25 +120,7 @@ defmodule NextLS.ASTHelpersTest do
""")
position = %Position{line: 0, character: 0}
- assert {:error, "no defmodule definition"} = ASTHelpers.get_surrounding_module(ast, position)
- end
-
- test "it finds the nearest surrounding module" do
- {:ok, ast} =
- Spitfire.parse("""
- defmodule Test do
- alias Foo
- alias Bar
- alias Baz
-
- defmodule Quix do
- defstruct [:key]
- end
- end
- """)
-
- position = %Position{line: 4, character: 0}
- assert {:ok, {:defmodule, _, [{:__aliases__, _, [:Test]} | _]}} = ASTHelpers.get_surrounding_module(ast, position)
+ assert {:error, :not_found} = ASTHelpers.get_surrounding_module(ast, position)
end
end
end
diff --git a/test/next_ls/helpers/docs_helpers_test.exs b/test/next_ls/helpers/docs_helpers_test.exs
deleted file mode 100644
index d2e45a77..00000000
--- a/test/next_ls/helpers/docs_helpers_test.exs
+++ /dev/null
@@ -1,264 +0,0 @@
-defmodule NextLS.DocsHelpersTest do
- use ExUnit.Case, async: true
-
- alias NextLS.DocsHelpers
-
- describe "converts erlang html format to markdown" do
- test "some divs and p and code" do
- html = [
- {:p, [],
- [
- "Suspends the process calling this function for ",
- {:code, [], ["Time"]},
- " milliseconds and then returns ",
- {:code, [], ["ok"]},
- ", or suspends the process forever if ",
- {:code, [], ["Time"]},
- " is the atom ",
- {:code, [], ["infinity"]},
- ". Naturally, this function does ",
- {:em, [], ["not"]},
- " return immediately."
- ]},
- {:div, [class: "note"],
- [
- {:p, [],
- [
- "Before OTP 25, ",
- {:code, [], ["timer:sleep/1"]},
- " did not accept integer timeout values greater than ",
- {:code, [], ["16#ffffffff"]},
- ", that is, ",
- {:code, [], ["2^32-1"]},
- ". Since OTP 25, arbitrarily high integer values are accepted."
- ]}
- ]}
- ]
-
- actual = DocsHelpers.to_markdown("application/erlang+html", html)
-
- assert actual ==
- String.trim("""
- Suspends the process calling this function for `Time` milliseconds and then returns `ok`, or suspends the process forever if `Time` is the atom `infinity`. Naturally, this function does _not_ return immediately.
-
- > Before OTP 25, `timer:sleep/1` did not accept integer timeout values greater than `16#ffffffff`, that is, `2^32-1`. Since OTP 25, arbitrarily high integer values are accepted.
- """)
- end
-
- test "some p and a and code" do
- html = [
- {:p, [],
- [
- "The same as ",
- {:a,
- [
- href: "erts:erlang#atom_to_binary/2",
- rel: "https://erlang.org/doc/link/seemfa"
- ], [{:code, [], ["atom_to_binary"]}, " "]},
- {:code, [], ["(Atom, utf8)"]},
- "."
- ]}
- ]
-
- actual = DocsHelpers.to_markdown("application/erlang+html", html)
-
- assert actual ==
- String.trim("""
- The same as [`atom_to_binary`](erts:erlang#atom_to_binary/2) `(Atom, utf8)`.
- """)
- end
-
- test "some code" do
- html = [
- {:p, [],
- [
- "Extracts the part of the binary described by ",
- {:code, [], ["PosLen"]},
- "."
- ]},
- {:p, [], ["Negative length can be used to extract bytes at the end of a binary, for example:"]},
- {:pre, [],
- [
- {:code, [],
- ["1> Bin = <<1,2,3,4,5,6,7,8,9,10>>.\n2> binary_part(Bin,{byte_size(Bin), -5}).\n<<6,7,8,9,10>>"]}
- ]},
- {:p, [],
- [
- "Failure: ",
- {:code, [], ["badarg"]},
- " if ",
- {:code, [], ["PosLen"]},
- " in any way references outside the binary."
- ]},
- {:p, [], [{:code, [], ["Start"]}, " is zero-based, that is:"]},
- {:pre, [], [{:code, [], ["1> Bin = <<1,2,3>>\n2> binary_part(Bin,{0,2}).\n<<1,2>>"]}]},
- {:p, [],
- [
- "For details about the ",
- {:code, [], ["PosLen"]},
- " semantics, see ",
- {:a, [href: "stdlib:binary", rel: "https://erlang.org/doc/link/seeerl"], [{:code, [], ["binary(3)"]}]},
- "."
- ]},
- {:p, [], ["Allowed in guard tests."]}
- ]
-
- actual = DocsHelpers.to_markdown("application/erlang+html", html)
-
- assert actual ==
- String.trim("""
- Extracts the part of the binary described by `PosLen`.
-
- Negative length can be used to extract bytes at the end of a binary, for example:
-
- ```erlang
- 1> Bin = <<1,2,3,4,5,6,7,8,9,10>>.
- 2> binary_part(Bin,{byte_size(Bin), -5}).
- <<6,7,8,9,10>>
- ```
-
- Failure: `badarg` if `PosLen` in any way references outside the binary.
-
- `Start` is zero-based, that is:
-
- ```erlang
- 1> Bin = <<1,2,3>>
- 2> binary_part(Bin,{0,2}).
- <<1,2>>
- ```
-
- For details about the `PosLen` semantics, see [`binary(3)`](stdlib:binary).
-
- Allowed in guard tests.
- """)
- end
-
- test "ul and li" do
- html = [
- {:ul, [],
- [
- {:li, [],
- [
- {:p, [],
- [
- "Find an arbitrary ",
- {:a,
- [
- href: "stdlib:digraph#simple_path",
- rel: "https://erlang.org/doc/link/seeerl"
- ], ["simple path"]},
- " v[1], v[2], ..., v[k] from ",
- {:code, [], ["V1"]},
- " to ",
- {:code, [], ["V2"]},
- " in ",
- {:code, [], ["G"]},
- "."
- ]}
- ]},
- {:li, [],
- [
- {:p, [],
- [
- "Remove all edges of ",
- {:code, [], ["G"]},
- " ",
- {:a,
- [
- href: "stdlib:digraph#emanate",
- rel: "https://erlang.org/doc/link/seeerl"
- ], ["emanating"]},
- " from v[i] and ",
- {:a,
- [
- href: "stdlib:digraph#incident",
- rel: "https://erlang.org/doc/link/seeerl"
- ], ["incident"]},
- " to v[i+1] for 1 <= i < k (including multiple edges)."
- ]}
- ]},
- {:li, [],
- [
- {:p, [],
- [
- "Repeat until there is no path between ",
- {:code, [], ["V1"]},
- " and ",
- {:code, [], ["V2"]},
- "."
- ]}
- ]}
- ]}
- ]
-
- actual = DocsHelpers.to_markdown("application/erlang+html", html)
-
- assert String.trim(actual) ==
- String.trim("""
- * Find an arbitrary [simple path](stdlib:digraph#simple_path) v[1], v[2], ..., v[k] from `V1` to `V2` in `G`.
- * Remove all edges of `G` [emanating](stdlib:digraph#emanate) from v[i] and [incident](stdlib:digraph#incident) to v[i+1] for 1 <= i < k (including multiple edges).
- * Repeat until there is no path between `V1` and `V2`.
- """)
- end
-
- test "dl, dt, and dd" do
- html = [
- {:dl, [],
- [
- {:dt, [], [{:code, [], ["root"]}]},
- {:dd, [],
- [
- {:p, [], ["The installation directory of Erlang/OTP, ", {:code, [], ["$ROOT"]}, ":"]},
- {:pre, [],
- [
- {:code, [],
- ["2> init:get_argument(root).\n{ok,[[\"/usr/local/otp/releases/otp_beam_solaris8_r10b_patched\"]]}"]}
- ]}
- ]},
- {:dt, [], [{:code, [], ["progname"]}]},
- {:dd, [],
- [
- {:p, [], ["The name of the program which started Erlang:"]},
- {:pre, [], [{:code, [], ["3> init:get_argument(progname).\n{ok,[[\"erl\"]]}"]}]}
- ]},
- {:dt, [], [{:a, [id: "home"], []}, {:code, [], ["home"]}]},
- {:dd, [],
- [
- {:p, [], ["The home directory (on Unix, the value of $HOME):"]},
- {:pre, [], [{:code, [], ["4> init:get_argument(home).\n{ok,[[\"/home/harry\"]]}"]}]}
- ]}
- ]},
- {:p, [], ["Returns ", {:code, [], ["error"]}, " if no value is associated with ", {:code, [], ["Flag"]}, "."]}
- ]
-
- actual = DocsHelpers.to_markdown("application/erlang+html", html)
-
- assert String.trim(actual) ==
- String.trim("""
- * `root`
- The installation directory of Erlang/OTP, `$ROOT`:
-
- ```erlang
- 2> init:get_argument(root).
- {ok,[[\"/usr/local/otp/releases/otp_beam_solaris8_r10b_patched\"]]}
- ```
- * `progname`
- The name of the program which started Erlang:
-
- ```erlang
- 3> init:get_argument(progname).
- {ok,[[\"erl\"]]}
- ```
- * []()`home`
- The home directory (on Unix, the value of $HOME):
-
- ```erlang
- 4> init:get_argument(home).
- {ok,[[\"/home/harry\"]]}
- ```
-
- Returns `error` if no value is associated with `Flag`.
- """)
- end
- end
-end
diff --git a/test/next_ls/hover_test.exs b/test/next_ls/hover_test.exs
index 61e27a32..aede5bce 100644
--- a/test/next_ls/hover_test.exs
+++ b/test/next_ls/hover_test.exs
@@ -195,7 +195,7 @@ defmodule NextLS.HoverTest do
"contents" => %{
"kind" => "markdown",
"value" =>
- "## Atom.to_string/1\n\n" <>
+ "## Atom.to_string/1\n" <>
_
},
"range" => %{
@@ -223,7 +223,7 @@ defmodule NextLS.HoverTest do
%{
"contents" => %{
"kind" => "markdown",
- "value" => "## Bar.Baz.q/0\n\nBar.Baz.q function"
+ "value" => "## Bar.Baz.q/0\n\n`q()`\n\nBar.Baz.q function"
},
"range" => %{
"start" => %{"character" => 13, "line" => 12},
@@ -246,7 +246,10 @@ defmodule NextLS.HoverTest do
}
}
- assert_result 9, nil, 500
+ assert_result 9, %{
+ "contents" => %{"kind" => "markdown", "value" => "## Bar.Fiz"},
+ "range" => %{"end" => %{"character" => 11, "line" => 13}, "start" => %{"character" => 9, "line" => 13}}
+ }
end
test "function without docs", %{client: client, example: example} do
@@ -282,7 +285,7 @@ defmodule NextLS.HoverTest do
%{
"contents" => %{
"kind" => "markdown",
- "value" => "## Kernel.to_string/1\n\nConverts the argument to a string" <> _
+ "value" => "## Kernel.to_string/1\n\n`to_string(term)`\n\nConverts the argument to a string" <> _
},
"range" => %{
"start" => %{"character" => 9, "line" => 15},
@@ -310,7 +313,7 @@ defmodule NextLS.HoverTest do
"contents" => %{
"kind" => "markdown",
"value" =>
- "## :timer.sleep/1\n\nSuspends the process" <>
+ "## :timer.sleep/1\n\n`sleep/1`\n\nSuspends the process" <>
_
},
"range" => %{
@@ -365,7 +368,9 @@ defmodule NextLS.HoverTest do
%{
"contents" => %{
"kind" => "markdown",
- "value" => "## Kernel.def/2\n\nDefines a public function with the given name and body" <> _
+ "value" =>
+ "## Kernel.def/2\n\n`def(call, expr \\\\ nil)`\n\nDefines a public function with the given name and body" <>
+ _
},
"range" => %{
"start" => %{"character" => 2, "line" => 9},
diff --git a/test/next_ls/runtime_test.exs b/test/next_ls/runtime_test.exs
index ff3e3d6f..3de28f2c 100644
--- a/test/next_ls/runtime_test.exs
+++ b/test/next_ls/runtime_test.exs
@@ -65,6 +65,7 @@ defmodule NextLs.RuntimeTest do
db: :some_db,
mix_env: "dev",
mix_target: "host",
+ mix_home: Path.join(cwd, ".mix"),
registry: RuntimeTest.Registry}
)
@@ -95,6 +96,7 @@ defmodule NextLs.RuntimeTest do
db: :some_db,
mix_env: "dev",
mix_target: "host",
+ mix_home: Path.join(cwd, ".mix"),
registry: RuntimeTest.Registry}
)
@@ -126,6 +128,7 @@ defmodule NextLs.RuntimeTest do
db: :some_db,
mix_env: "dev",
mix_target: "host",
+ mix_home: Path.join(cwd, ".mix"),
registry: RuntimeTest.Registry}
)
@@ -190,6 +193,7 @@ defmodule NextLs.RuntimeTest do
db: :some_db,
mix_env: "dev",
mix_target: "host",
+ mix_home: Path.join(cwd, ".mix"),
registry: RuntimeTest.Registry}
)
diff --git a/test/support/utils.ex b/test/support/utils.ex
index fb2a759c..b4ab007b 100644
--- a/test/support/utils.ex
+++ b/test/support/utils.ex
@@ -4,6 +4,10 @@ defmodule NextLS.Support.Utils do
import ExUnit.Callbacks
import GenLSP.Test
+ alias GenLSP.Structures.Position
+ alias GenLSP.Structures.Range
+ alias GenLSP.Structures.TextEdit
+
def mix_exs do
"""
defmodule Project.MixProject do
@@ -86,6 +90,10 @@ defmodule NextLS.Support.Utils do
capabilities: %{
workspace: %{
workspaceFolders: true
+ },
+ window: %{
+ work_done_progress: false,
+ showMessage: %{}
}
},
workspaceFolders:
@@ -175,18 +183,47 @@ defmodule NextLS.Support.Utils do
defmacro did_change(client, uri) do
quote do
- assert :ok == notify(unquote(client), %{
- method: "workspace/didChangeWatchedFiles",
- jsonrpc: "2.0",
- params: %{
- changes: [
- %{
- type: GenLSP.Enumerations.FileChangeType.changed(),
- uri: unquote(uri)
- }
- ]
- }
- })
+ assert :ok ==
+ notify(unquote(client), %{
+ method: "workspace/didChangeWatchedFiles",
+ jsonrpc: "2.0",
+ params: %{
+ changes: [
+ %{
+ type: GenLSP.Enumerations.FileChangeType.changed(),
+ uri: unquote(uri)
+ }
+ ]
+ }
+ })
+ end
+ end
+
+ def apply_edit(code, edit) when is_binary(code), do: apply_edit(String.split(code, "\n"), edit)
+
+ def apply_edit(lines, %TextEdit{} = edit) when is_list(lines) do
+ text = edit.new_text
+ %Range{start: %Position{line: startl, character: startc}, end: %Position{line: endl, character: endc}} = edit.range
+
+ startl_text = Enum.at(lines, startl)
+ prefix = String.slice(startl_text, 0, startc)
+
+ endl_text = Enum.at(lines, endl)
+ suffix = String.slice(endl_text, endc, String.length(endl_text) - endc)
+
+ replacement = prefix <> text <> suffix
+
+ new_lines = Enum.slice(lines, 0, startl) ++ [replacement] ++ Enum.slice(lines, endl + 1, Enum.count(lines))
+
+ new_lines
+ |> Enum.join("\n")
+ |> String.trim()
+ end
+
+ defmacro assert_is_text_edit(code, edit, expected) do
+ quote do
+ actual = unquote(__MODULE__).apply_edit(unquote(code), unquote(edit))
+ assert actual == unquote(expected)
end
end
end