Skip to content

Commit

Permalink
feat: spitfire (#368)
Browse files Browse the repository at this point in the history
Incorporates experimental usage of the [Spitfire](https://github.com/elixir-tools/spitfire) parser.

To enable, the server should be started with `NEXTLS_SPITFIRE_ENABLED=1`. 

`elixir-tools.nvim` and `elixir-tools.vscode` will have settings to enable this for you.
  • Loading branch information
mhanberg authored Feb 14, 2024
1 parent 77b92f8 commit bcb7e2e
Show file tree
Hide file tree
Showing 16 changed files with 217 additions and 125 deletions.
58 changes: 33 additions & 25 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,90 +7,97 @@ on:
jobs:
tests:
runs-on: ${{matrix.os}}
name: Test (${{matrix.os}})
name: Test (${{matrix.os}}) - spitfire=${{matrix.spitfire}}

strategy:
matrix:
spitfire: [0, 1]
os:
- ubuntu-latest
- macos-14

steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main

- uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ matrix.os }}-mix-${{ hashFiles('**/.mise.toml') }}-${{ hashFiles('**/mix.lock') }}
key: ${{ matrix.os }}-mix-${{ hashFiles('**/flake.nix') }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ matrix.os }}-mix-${{ hashFiles('**/.mise.toml') }}-
${{ matrix.os }}-mix-${{ hashFiles('**/flake.nix') }}-
- name: Install Dependencies
run: mix deps.get
run: nix develop --command bash -c 'mix deps.get'

- name: Start EPMD
run: epmd -daemon
run: nix develop --command bash -c 'epmd -daemon'

- name: Compile
env:
MIX_ENV: test
run: mix compile
run: nix develop --command bash -c 'mix compile'

- name: remove tmp dir
run: rm -rf tmp

- name: Run Tests
run: elixir --erl '-kernel prevent_overlapping_partitions false' -S mix test --max-cases 1
env:
NEXTLS_SPITFIRE_ENABLED: ${{ matrix.spitfire }}
run: nix develop --command bash -c "elixir --erl '-kernel prevent_overlapping_partitions false' -S mix test --max-cases 1"

formatter:
runs-on: ubuntu-latest
name: Formatter

steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('**/.mise.toml') }}-${{ hashFiles('**/mix.lock') }}
key: ${{ runner.os }}-mix-${{ hashFiles('**/flake.nix') }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ hashFiles('**/.mise.toml') }}-
${{ runner.os }}-mix-${{ hashFiles('**/flake.nix') }}-
- name: Install Dependencies
run: mix deps.get
run: nix develop --command bash -c 'mix deps.get'

- name: Run Formatter
run: mix format --check-formatted
run: nix develop --command bash -c 'mix format --check-formatted'

dialyzer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main

# Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones
# Cache key based on Elixir & Erlang version (also useful when running in matrix)
- name: Restore PLT cache
uses: actions/cache/restore@v4
id: plt_cache
with:
key: ${{ runner.os }}-mix-${{ hashFiles('**/.mise.toml') }}-${{ hashFiles('**/mix.lock') }}
key: ${{ runner.os }}-mix-${{ hashFiles('**/flake.nix') }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ hashFiles('**/.mise.toml') }}-
${{ runner.os }}-mix-${{ hashFiles('**/flake.nix') }}-
path: |
priv/plts
- name: Install Dependencies
run: mix deps.get
run: nix develop --command bash -c 'mix deps.get'

# Create PLTs if no cache was found
- name: Create PLTs
if: steps.plt_cache.outputs.cache-hit != 'true'
run: mix dialyzer --plt
run: nix develop --command bash -c 'mix dialyzer --plt'

# By default, the GitHub Cache action will only save the cache if all steps in the job succeed,
# so we separate the cache restore and save steps in case running dialyzer fails.
Expand All @@ -99,12 +106,12 @@ jobs:
if: steps.plt_cache.outputs.cache-hit != 'true'
id: plt_cache_save
with:
key: ${{ runner.os }}-mix-${{ hashFiles('**/.mise.toml') }}-${{ hashFiles('**/mix.lock') }}
key: ${{ runner.os }}-mix-${{ hashFiles('**/flake.nix') }}-${{ hashFiles('**/mix.lock') }}
path: |
priv/plts
- name: Run dialyzer
run: mix dialyzer --format github
run: nix develop --command bash -c 'mix dialyzer --format github'

release-test:
runs-on: ${{matrix.os.name}}
Expand All @@ -122,23 +129,24 @@ jobs:

steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: actions/cache@v4
with:
path: |
deps
key: ${{ matrix.os.name }}-mix-prod-${{ hashFiles('**/.mise.toml') }}-${{ hashFiles('**/mix.lock') }}
key: ${{ matrix.os.name }}-mix-prod-${{ hashFiles('**/flake.nix') }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ matrix.os.name }}-mix-prod-${{ hashFiles('**/.mise.toml') }}-
${{ matrix.os.name }}-mix-prod-${{ hashFiles('**/flake.nix') }}-
- name: Install Dependencies
run: mix deps.get --only prod
run: nix develop --command bash -c 'mix deps.get --only prod'

- name: Release
env:
MIX_ENV: prod
BURRITO_TARGET: ${{ matrix.os.target }}
run: mix release
run: nix develop --command bash -c 'mix release'

nix-build:
strategy:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: chmod +x "$PWD/bin/7z"
- run: brew install 7zip
- run: mix local.hex --force
Expand Down
5 changes: 2 additions & 3 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
KERL_BUILD_DOCS = "yes"

[tools]
erlang = "26.1.2"
elixir = "1.15.7-otp-26"
erlang = "26.2.2"
elixir = "ref:52eaf1456182d5d6cce22a4f5c3f6ec9f4dcbfd9"
zig = "0.11.0"

31 changes: 27 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
inputs = {nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";};
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};

nixConfig = {
extra-substituters = ["https://elixir-tools.cachix.org"];
Expand All @@ -19,10 +21,23 @@
lib.genAttrs (builtins.attrNames burritoExe) (system: let
pkgs = nixpkgs.legacyPackages.${system};
beamPackages = pkgs.beam_minimal.packages.erlang_26;
elixir = beamPackages.elixir_1_15;
beam = fetchTarball beams.${system};
rawmusl = musls.${system};
musl = lib.optionals nixpkgs.legacyPackages.${system}.stdenv.isLinux (builtins.fetchurl (nixpkgs.lib.attrsets.getAttrs ["url" "sha256"] musls.${system}));
otp = (pkgs.beam.packagesWith beamPackages.erlang).extend (final: prev: {
elixir_1_17 = prev.elixir_1_16.override {
rev = "52eaf1456182d5d6cce22a4f5c3f6ec9f4dcbfd9";
# You can discover this using Trust On First Use by filling in `lib.fakeHash`
sha256 = "sha256-fOsV+jVIzsa38hQDvAjhUqee36nt8kG6AOpOQJnSZ74=";
version = "1.17.0-dev";
};

elixir = final.elixir_1_17;
# This will get upstreamed into nix-beam-flakes at some point
rebar = prev.rebar.overrideAttrs (_old: {doCheck = false;});
rebar3 = prev.rebar3.overrideAttrs (_old: {doCheck = false;});
});
elixir = otp.elixir;
in
f {inherit system pkgs beamPackages elixir beam rawmusl musl;});

Expand Down Expand Up @@ -100,7 +115,7 @@
src = self.outPath;
inherit version elixir;
pname = "next-ls-deps";
hash = "sha256-U5d8DftG0i1c4JiutUentNlRsefFgR4Mfc3eKqnKR3U=";
hash = "sha256-RYPweYD1GD0D6A7ZkrtD3h7arCVimdStcOhrrlHFrnw=";
mixEnv = "prod";
};

Expand Down Expand Up @@ -149,7 +164,15 @@
beamPackages,
elixir,
...
}: {
}: let
aliased_7zz = pkgs.symlinkJoin {
name = "7zz-aliased";
paths = [pkgs._7zz];
postBuild = ''
ln -s ${pkgs._7zz}/bin/7zz $out/bin/7z
'';
};
in {
default = pkgs.mkShell {
# The Nix packages provided in the environment
packages = [
Expand Down
42 changes: 27 additions & 15 deletions lib/next_ls/document_symbol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,35 @@ defmodule NextLS.DocumentSymbol do
alias GenLSP.Structures.Position
alias GenLSP.Structures.Range

# we set the literal encoder so that we can know when atoms and strings start and end
# this makes it useful for knowing the exact locations of struct field definitions
@spec fetch(text :: String.t()) :: list(DocumentSymbol.t())
def fetch(text) do
text
|> Code.string_to_quoted!(
literal_encoder: fn literal, meta ->
if is_atom(literal) or is_binary(literal) do
{:ok, {:__literal__, meta, [literal]}}
else
{:ok, literal}
end
end,
unescape: false,
token_metadata: true,
columns: true
)
ast =
case NextLS.Parser.parse(
text,
# we set the literal encoder so that we can know when atoms and strings start and end
# this makes it useful for knowing the exact locations of struct field definitions
literal_encoder: fn literal, meta ->
if is_atom(literal) or is_binary(literal) do
{:ok, {:__literal__, meta, [literal]}}
else
{:ok, literal}
end
end,
unescape: false,
token_metadata: true,
columns: true
) do
{:error, ast, _errors} ->
ast

{:error, _} ->
raise "Failed to parse!"

{:ok, ast} ->
ast
end

ast
|> walker(nil)
|> List.wrap()
end
Expand Down
84 changes: 44 additions & 40 deletions lib/next_ls/helpers/ast_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ defmodule NextLS.ASTHelpers do
defp postwalk(ast, acc, _module), do: {ast, acc}

defp ast_from_file(file) do
file |> File.read!() |> Code.string_to_quoted!(columns: true)
file |> File.read!() |> NextLS.Parser.parse!(columns: true)
end
end

Expand All @@ -99,53 +99,57 @@ defmodule NextLS.ASTHelpers do
"""

def extract_alias_range(code, {start, stop}, ale) do
lines =
code
|> String.split("\n")
|> Enum.map(&String.split(&1, ""))
|> Enum.slice((start.line - 1)..(stop.line - 1))

code =
if start.line == stop.line do
[line] = lines

line
|> Enum.slice(start.col..stop.col)
|> Enum.join()
else
[first | rest] = lines
first = Enum.drop(first, start.col)

[last | rest] = Enum.reverse(rest)

length = Enum.count(last)
last = Enum.drop(last, -(length - stop.col - 1))

Enum.map_join([first | Enum.reverse([last | rest])], "\n", &Enum.join(&1, ""))
end

{_, range} =
code
|> Code.string_to_quoted!(columns: true, column: start.col, token_metadata: true)
|> Macro.prewalk(nil, fn ast, range ->
range =
case ast do
{:__aliases__, meta, aliases} ->
if ale == List.last(aliases) do
{{meta[:line] + start.line - 1, meta[:column]},
{meta[:last][:line] + start.line - 1, meta[:last][:column] + String.length(to_string(ale)) - 1}}
else
|> NextLS.Parser.parse!(columns: true, token_metadata: true)
|> Macro.prewalk(nil, fn
ast, nil = range ->
range =
case ast do
{:__aliases__, meta, aliases} ->
if ale == List.last(aliases) do
found_range =
{{meta[:line], meta[:column]},
{meta[:last][:line], meta[:last][:column] + String.length(to_string(ale)) - 1}}

if NextLS.ASTHelpers.inside?({{start.line, start.col}, {stop.line, stop.col}}, found_range) do
found_range
else
range
end
else
range
end

_ ->
range
end
end

_ ->
range
end
{ast, range}

{ast, range}
ast, range ->
{ast, range}
end)

range
end
end

def inside?(outer, {{_, _}, {_, _}} = target) do
{{outer_startl, outer_startc}, {outer_endl, outer_endc}} = outer
{target_start, target_end} = target

Enum.all?([target_start, target_end], fn {line, col} ->
if outer_startl <= line and line <= outer_endl do
cond do
outer_startl < line and line < outer_endl -> true
outer_startl == line and outer_startc <= col -> true
outer_endl == line and col <= outer_endc -> true
true -> false
end
else
false
end
end)
end
end
Loading

0 comments on commit bcb7e2e

Please sign in to comment.