Skip to content

Commit 98c6041

Browse files
committed
refactor - move parser to separate process
1 parent 27e7e83 commit 98c6041

File tree

4 files changed

+229
-169
lines changed

4 files changed

+229
-169
lines changed

apps/language_server/lib/language_server/application.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule ElixirLS.LanguageServer.Application do
1515
{LanguageServer.Providers.WorkspaceSymbols, []},
1616
{LanguageServer.Tracer, []},
1717
{LanguageServer.MixProjectCache, []},
18+
{LanguageServer.Parser, []},
1819
{LanguageServer.ExUnitTestTracer, []}
1920
]
2021
|> Enum.reject(&is_nil/1)

apps/language_server/lib/language_server/diagnostics.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
defmodule ElixirLS.LanguageServer.Diagnostics do
2+
@moduledoc """
3+
This module provides utility functions for normalizing diagnostics
4+
from various sources
5+
"""
26
alias ElixirLS.LanguageServer.{SourceFile, JsonRpc}
37

48
def normalize(diagnostics, root_path, mixfile) do
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
defmodule ElixirLS.LanguageServer.Parser do
2+
@moduledoc """
3+
This server parses source files and maintains cache of AST and metadata
4+
"""
5+
use GenServer
6+
alias ElixirLS.LanguageServer.JsonRpc
7+
alias ElixirLS.LanguageServer.Diagnostics
8+
alias ElixirLS.LanguageServer.Build
9+
alias ElixirLS.LanguageServer.Server
10+
alias ElixirLS.LanguageServer.SourceFile
11+
require Logger
12+
13+
@debounce_timeout 300
14+
15+
defstruct [
16+
:source_file,
17+
:path,
18+
:ast,
19+
:diagnostics
20+
]
21+
22+
def start_link(args) do
23+
GenServer.start_link(__MODULE__, args, name: __MODULE__)
24+
end
25+
26+
def notify_closed(uri) do
27+
GenServer.cast(__MODULE__, {:closed, uri})
28+
end
29+
30+
def parse_with_debounce(uri, source_file) do
31+
GenServer.cast(__MODULE__, {:parse_with_debounce, uri, source_file})
32+
end
33+
34+
@impl true
35+
def init(_args) do
36+
# TODO get source files on start?
37+
{:ok, %{files: %{}, debounce_refs: %{}}}
38+
end
39+
40+
@impl true
41+
def handle_cast({:closed, uri}, state = %{files: files, debounce_refs: debounce_refs}) do
42+
{maybe_ref, updated_debounce_refs} = Map.pop(debounce_refs, uri)
43+
if maybe_ref do
44+
Process.cancel_timer(maybe_ref, info: false)
45+
end
46+
updated_files = Map.delete(files, uri)
47+
notify_diagnostics_updated(updated_files)
48+
{:noreply, %{state | files: updated_files, debounce_refs: updated_debounce_refs}}
49+
end
50+
51+
def handle_cast({:parse_with_debounce, uri, source_file}, state) do
52+
state = if String.ends_with?(uri, [".ex", ".exs", ".eex"]) do
53+
state = update_in(state.debounce_refs[uri], fn old_ref ->
54+
if old_ref do
55+
Process.cancel_timer(old_ref, info: false)
56+
end
57+
58+
Process.send_after(self(), {:parse_file, uri}, @debounce_timeout)
59+
end)
60+
61+
update_in(state.files[uri], fn _old_file ->
62+
%__MODULE__{
63+
source_file: source_file,
64+
path: get_path(uri),
65+
ast: nil,
66+
diagnostics: []
67+
}
68+
end)
69+
else
70+
state
71+
end
72+
73+
{:noreply, state}
74+
end
75+
76+
@impl GenServer
77+
def handle_info(
78+
{:parse_file, uri},
79+
%{files: files, debounce_refs: debounce_refs} = state
80+
) do
81+
Logger.debug("Parsing #{uri}")
82+
%__MODULE__{source_file: source_file, path: path} = file = Map.fetch!(files, uri)
83+
{ast, diagnostics} = parse_file(source_file.text, path)
84+
updated_file = %__MODULE__{file |
85+
ast: ast,
86+
diagnostics: diagnostics
87+
}
88+
89+
updated_files = Map.put(files, uri, updated_file)
90+
91+
state = %{state | files: updated_files, debounce_refs: Map.delete(debounce_refs, uri)}
92+
93+
notify_diagnostics_updated(updated_files)
94+
95+
{:noreply, state}
96+
end
97+
98+
defp get_path(uri) do
99+
case uri do
100+
"file:" <> _ -> SourceFile.Path.from_uri(uri)
101+
_ ->
102+
extension = uri
103+
|> String.split(".")
104+
|> List.last
105+
"nofile." <> extension
106+
end
107+
end
108+
109+
defp notify_diagnostics_updated(updated_files) do
110+
updated_files
111+
|> Enum.map(fn {_uri, %__MODULE__{diagnostics: diagnostics}} -> diagnostics end)
112+
|> List.flatten
113+
|> Server.parser_finished()
114+
end
115+
116+
117+
# TODO uri instead of file?
118+
# defp parse_file(_text, nil), do: {nil, []}
119+
defp parse_file(text, file) do
120+
{result, raw_diagnostics} =
121+
Build.with_diagnostics([log: false], fn ->
122+
try do
123+
parser_options = [
124+
file: file,
125+
columns: true
126+
]
127+
128+
ast = if String.ends_with?(file, ".eex") do
129+
EEx.compile_string(text,
130+
file: file,
131+
parser_options: parser_options
132+
)
133+
else
134+
Code.string_to_quoted!(text, parser_options)
135+
end
136+
137+
{:ok, ast}
138+
rescue
139+
e in [EEx.SyntaxError, SyntaxError, TokenMissingError, MismatchedDelimiterError] ->
140+
message = Exception.message(e)
141+
142+
diagnostic = %Mix.Task.Compiler.Diagnostic{
143+
compiler_name: "ElixirLS",
144+
file: file,
145+
position: {e.line, e.column},
146+
message: message,
147+
severity: :error
148+
}
149+
150+
{:error, diagnostic}
151+
152+
e ->
153+
message = Exception.message(e)
154+
155+
diagnostic = %Mix.Task.Compiler.Diagnostic{
156+
compiler_name: "ElixirLS",
157+
file: file,
158+
position: {1, 1},
159+
message: message,
160+
severity: :error
161+
}
162+
163+
# e.g. https://github.com/elixir-lang/elixir/issues/12926
164+
Logger.warning(
165+
"Unexpected parser error, please report it to elixir project https://github.com/elixir-lang/elixir/issues\n" <>
166+
Exception.format(:error, e, __STACKTRACE__)
167+
)
168+
169+
JsonRpc.telemetry(
170+
"parser_error",
171+
%{"elixir_ls.parser_error" => Exception.format(:error, e, __STACKTRACE__)},
172+
%{}
173+
)
174+
175+
{:error, diagnostic}
176+
end
177+
end)
178+
179+
warning_diagnostics =
180+
raw_diagnostics
181+
|> Enum.map(&Diagnostics.code_diagnostic/1)
182+
183+
case result do
184+
{:ok, ast} -> {ast, warning_diagnostics}
185+
{:error, diagnostic} -> {nil, [diagnostic | warning_diagnostics]}
186+
end
187+
end
188+
end

0 commit comments

Comments
 (0)