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 -Christopher GraingerMikkel HøghEthan GundersonSebastian HenaoAlexander KoutmosSimon WolfParker SelbertNoah BetzenShannon SelbertDorian KarterAndré Luiz da Fonsêca Paesjackson millsapsLeon QadirieDamir VandicEric OestrichBrett WiseDavid BernheiselDavid BaldwinQdentityNFIBrokerageRudolf ManusadzhianClark Lindsayv1d3rm3Stephen BusseySuperedKeith GautreauxJean-Luc GeeringJonathan YankovichJamie WrightZach NorrisJoe MartinezMarcøsDan DresselhausMarcel FahleMichael KummAmplifiedThibaut BarrèreVictor RodriguesDave Lucia0x7f +Christopher GraingerMikkel HøghEthan GundersonSebastian HenaoAlexander KoutmosSimon WolfParker SelbertNoah BetzenShannon SelbertDorian KarterAndré Luiz da Fonsêca Paesjackson millsapsLeon QadirieDamir VandicEric OestrichBrett WiseDavid BernheiselDavid BaldwinQdentityNFIBrokerageRudolf ManusadzhianClark Lindsayv1d3rm3Stephen BusseySuperedKeith GautreauxJean-Luc GeeringJonathan YankovichJamie WrightZach NorrisJoe MartinezMarcøsDan DresselhausMarcel FahleMichael KummAmplifiedThibaut BarrèreVictor RodriguesDave Lucia0x7fMarcelo DominguezChristoph Schmatzler ## 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