Skip to content

Commit 29b91a6

Browse files
authored
Experimental project structure (#773)
* Experimental project structure This commit represents a new structure for the experimental project, and a path forward. With these changes the project now has: * A build option enabling the experimental server * Per-message routing for the experimental server. If enabled, it can either share messages with the existing server or take them over. Presently, the find references and formatting providers are implemented and "exclusive", meaning that they're handled solely by the experimental server * A consistent interface for building providers. * A consistent way to convert lsp messages into data structures and back again. This conversion is handled automatically for providers. * A genserver-like interface for providers to implement * Data structures representing LSP messages that are simple to define and build. * Fast and efficient conversion between utf8 and utf16. * A separation between what a provider does and how it responds to messages. This allows the work that underpins providers to be tested independently from the language server. * Add underscore code action Created a code action that prepends an underscore to unused variable names. * Notifications can be sent from the server * Properly handled spacing * Enforced required keys for jsonrpc messages * removed unused variable * Committed to pipeline * Added tests that check to ensure comments are preserved * Code modification framework First attempt at a standard interface for code modification. Code mod modules take the original text, the ast of the original text and arguments that they specify. They return a list of code edits or an error. * Simplified diff, change name of code action functions from appy to text_edits * Fixed off-by-one error that was vexing code unit conversions. The problem was that the character positions are _before_ the reported unit, so the 0th code unit is before the start of the line and the 1st code unit is the first character. The prior code added one to character counts to smooth this out, but you can't do that, because you could end up indexing into the middle of a multibyte character. * The code action needs to fix up the line numbers Code mods deal with snippets of code that need to have their line numbers fixed up by the code actions. * Fixed type spec The AST type is very complicated, and dialyzer was telling us I got it wrong. * Made type aliases a thing While working on the automatic protocol generators, it became clear that type aliases needed to be their own thing, as they operate quite differently from the other defined things in the jsonrpc protocol. Since they're just aliases, it makes sense to keep their definitions on hand and then spit them out when other things make use of them during encode and decode. This did require going back to encoding and ensuring all the encode functions return OK tuples. * Fixed unit tests When patches are unapplied, getting the beam file returned an empty path charlist, which dialyzer assumed was a real file name due to a weak assumption, which caused unit tests to fail. This was remedied by checking for a non-empty charlist, which allows tests to succeed. Also made patch a test only dependency for .formatter.exs, as this was causing formatters to fail. * removed unused module attribute * Added sourceror to ease ast to string conversion Under 1.12, Macro.to_string proudces wonky output, making `def` calls look like function calls by adding needless parenthesis. These parenthesis throw off the diff algorithm, and caused an off-by-one error in the code mod. Sourceror has backported the newer code generation so that it's compatible all the way back to 1.10, and produces the correct output. * Added patch as a dev dependency Patch's assertions will fail in CI due to `mix format --check-formatted` running in dev. Importing patch's deps in test will fix this. * Run check formatted in test so patch assertions work * Fixed dialyzer errors * Encapsulated sourceror
1 parent d9dc1b1 commit 29b91a6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+4910
-218
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,6 @@ jobs:
123123
mix deps.get
124124
- name: Restore timestamps to prevent unnecessary recompilation
125125
run: IFS=$'\n'; for f in $(git ls-files); do touch -d "$(git log -n 1 --pretty='%cI' -- $f)" "$f"; done
126-
- run: mix format --check-formatted
127-
- run: cd apps/language_server && mix format --check-formatted
126+
- run: MIX_ENV=test mix format --check-formatted
127+
- run: cd apps/language_server && MIX_ENV=test mix format --check-formatted
128128
- run: mix dialyzer_vendored

apps/language_server/.formatter.exs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@ impossible_to_format = [
33
"test/fixtures/project_with_tests/test/error_test.exs"
44
]
55

6+
deps =
7+
if Mix.env() == :test do
8+
[:patch]
9+
else
10+
[]
11+
end
12+
613
proto_dsl = [
14+
defalias: 1,
715
defenum: 1,
816
defnotification: 2,
9-
defrequest: 2,
17+
defnotification: 3,
18+
defrequest: 3,
1019
defresponse: 1,
1120
deftype: 1
1221
]
1322

1423
[
24+
import_deps: deps,
1525
export: [
1626
locals_without_parens: proto_dsl
1727
],

apps/language_server/lib/language_server.ex

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@ defmodule ElixirLS.LanguageServer do
77
alias ElixirLS.LanguageServer
88
alias ElixirLS.LanguageServer.Experimental
99

10-
# @maybe_experimental_server [Experimental.Server]
11-
@maybe_experimental_server []
12-
1310
@impl Application
1411
def start(_type, _args) do
15-
children = [
16-
Experimental.SourceFile.Store,
17-
{ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server},
18-
Experimental.Server,
19-
{ElixirLS.LanguageServer.PacketRouter,
20-
[LanguageServer.Server] ++ @maybe_experimental_server},
21-
{ElixirLS.LanguageServer.JsonRpc,
22-
name: ElixirLS.LanguageServer.JsonRpc, language_server: LanguageServer.PacketRouter},
23-
{ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []},
24-
{ElixirLS.LanguageServer.Tracer, []},
25-
{ElixirLS.LanguageServer.ExUnitTestTracer, []}
26-
]
12+
Experimental.LanguageServer.persist_enabled_state()
13+
14+
children =
15+
[
16+
maybe_experimental_supervisor(),
17+
{ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server},
18+
maybe_packet_router(),
19+
jsonrpc(),
20+
{ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []},
21+
{ElixirLS.LanguageServer.Tracer, []},
22+
{ElixirLS.LanguageServer.ExUnitTestTracer, []}
23+
]
24+
|> Enum.reject(&is_nil/1)
2725

2826
opts = [strategy: :one_for_one, name: LanguageServer.Supervisor, max_restarts: 0]
2927
Supervisor.start_link(children, opts)
@@ -42,4 +40,25 @@ defmodule ElixirLS.LanguageServer do
4240

4341
:ok
4442
end
43+
44+
defp maybe_experimental_supervisor do
45+
if Experimental.LanguageServer.enabled?() do
46+
Experimental.Supervisor
47+
end
48+
end
49+
50+
defp maybe_packet_router do
51+
if Experimental.LanguageServer.enabled?() do
52+
{ElixirLS.LanguageServer.PacketRouter, [LanguageServer.Server, Experimental.Server]}
53+
end
54+
end
55+
56+
defp jsonrpc do
57+
if Experimental.LanguageServer.enabled?() do
58+
{ElixirLS.LanguageServer.JsonRpc,
59+
name: ElixirLS.LanguageServer.JsonRpc, language_server: LanguageServer.PacketRouter}
60+
else
61+
{ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc}
62+
end
63+
end
4564
end

apps/language_server/lib/language_server/dialyzer/utils.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Utils do
44
@spec dialyzable?(module()) :: boolean()
55
def dialyzable?(module) do
66
file = get_beam_file(module)
7+
78
is_list(file) and match?({:ok, _}, :dialyzer_utils.get_core_from_beam(file))
89
end
910

1011
@spec get_beam_file(module()) :: charlist() | :preloaded | :non_existing | :cover_compiled
1112
def get_beam_file(module) do
1213
case :code.which(module) do
13-
file when is_list(file) ->
14+
[_ | _] = file ->
1415
file
1516

1617
other ->
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.CodeMod.Ast do
2+
alias ElixirLS.LanguageServer.Experimental.SourceFile
3+
4+
@type source :: SourceFile.t() | String.t()
5+
@type t ::
6+
atom()
7+
| binary()
8+
| [any()]
9+
| number()
10+
| {any(), any()}
11+
| {atom() | {any(), [any()], atom() | [any()]}, Keyword.t(), atom() | [any()]}
12+
13+
@spec from(source) :: t
14+
def from(%SourceFile{} = source_file) do
15+
source_file
16+
|> SourceFile.to_string()
17+
|> from()
18+
end
19+
20+
def from(s) when is_binary(s) do
21+
ElixirSense.string_to_quoted(s, 1, 6, token_metadata: true)
22+
end
23+
24+
@spec to_string(t()) :: String.t()
25+
def to_string(ast) do
26+
Sourceror.to_string(ast)
27+
end
28+
end
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.CodeMod.Diff do
2+
alias ElixirLS.LanguageServer.Experimental.CodeUnit
3+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Position
4+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Range
5+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.TextEdit
6+
7+
@spec diff(String.t(), String.t()) :: [TextEdit.t()]
8+
def diff(source, dest) do
9+
source
10+
|> String.myers_difference(dest)
11+
|> to_text_edits()
12+
end
13+
14+
defp to_text_edits(difference) do
15+
{_, {current_line, prev_lines}} =
16+
Enum.reduce(difference, {{0, 0}, {[], []}}, fn
17+
{diff_type, diff_string}, {position, edits} ->
18+
apply_diff(diff_type, position, diff_string, edits)
19+
end)
20+
21+
[current_line | prev_lines]
22+
|> Enum.flat_map(fn line_edits ->
23+
line_edits
24+
|> Enum.reduce([], &collapse/2)
25+
|> Enum.reverse()
26+
end)
27+
end
28+
29+
# This collapses a delete and an an insert that are adjacent to one another
30+
# into a single insert, changing the delete to insert the text from the
31+
# insert rather than ""
32+
# It's a small optimization, but it was in the original
33+
defp collapse(
34+
%TextEdit{
35+
new_text: "",
36+
range: %Range{
37+
end: %Position{character: same_character, line: same_line}
38+
}
39+
} = delete_edit,
40+
[
41+
%TextEdit{
42+
new_text: insert_text,
43+
range:
44+
%Range{
45+
start: %Position{character: same_character, line: same_line}
46+
} = _insert_edit
47+
}
48+
| rest
49+
]
50+
)
51+
when byte_size(insert_text) > 0 do
52+
collapsed_edit = %TextEdit{delete_edit | new_text: insert_text}
53+
[collapsed_edit | rest]
54+
end
55+
56+
defp collapse(%TextEdit{} = edit, edits) do
57+
[edit | edits]
58+
end
59+
60+
defp apply_diff(:eq, position, doc_string, edits) do
61+
advance(doc_string, position, edits)
62+
end
63+
64+
defp apply_diff(:del, {line, code_unit} = position, change, edits) do
65+
{after_pos, {current_line, prev_lines}} = advance(change, position, edits)
66+
{edit_end_line, edit_end_unit} = after_pos
67+
current_line = [edit("", line, code_unit, edit_end_line, edit_end_unit) | current_line]
68+
{after_pos, {current_line, prev_lines}}
69+
end
70+
71+
defp apply_diff(:ins, {line, code_unit} = position, change, {current_line, prev_lines}) do
72+
current_line = [edit(change, line, code_unit, line, code_unit) | current_line]
73+
advance(change, position, {current_line, prev_lines})
74+
end
75+
76+
defp advance(<<>>, position, edits) do
77+
{position, edits}
78+
end
79+
80+
for ending <- ["\r\n", "\r", "\n"] do
81+
defp advance(<<unquote(ending), rest::binary>>, {line, _unit}, {current_line, prev_lines}) do
82+
edits = {[], [current_line | prev_lines]}
83+
advance(rest, {line + 1, 0}, edits)
84+
end
85+
end
86+
87+
defp advance(<<c, rest::binary>>, {line, unit}, edits) when c < 128 do
88+
advance(rest, {line, unit + 1}, edits)
89+
end
90+
91+
defp advance(<<c::utf8, rest::binary>>, {line, unit}, edits) do
92+
increment = CodeUnit.count(:utf16, <<c::utf8>>)
93+
advance(rest, {line, unit + increment}, edits)
94+
end
95+
96+
defp edit(text, start_line, start_unit, end_line, end_unit) do
97+
TextEdit.new(
98+
new_text: text,
99+
range:
100+
Range.new(
101+
start: Position.new(line: start_line, character: start_unit),
102+
end: Position.new(line: end_line, character: end_unit)
103+
)
104+
)
105+
end
106+
end

0 commit comments

Comments
 (0)