@@ -20,7 +20,8 @@ defmodule ElixirLS.LanguageServer.Parser do
20
20
:ast ,
21
21
:diagnostics ,
22
22
:metadata ,
23
- :parsed_version
23
+ :parsed_version ,
24
+ :flag
24
25
]
25
26
end
26
27
@@ -32,14 +33,18 @@ defmodule ElixirLS.LanguageServer.Parser do
32
33
GenServer . cast ( __MODULE__ , { :closed , uri } )
33
34
end
34
35
35
- def parse_with_debounce ( uri , source_file ) do
36
+ def parse_with_debounce ( uri , source_file = % SourceFile { } ) do
36
37
GenServer . cast ( __MODULE__ , { :parse_with_debounce , uri , source_file } )
37
38
end
38
39
39
- def parse_immediate ( uri , source_file ) do
40
+ def parse_immediate ( uri , source_file = % SourceFile { } ) do
40
41
GenServer . call ( __MODULE__ , { :parse_immediate , uri , source_file } )
41
42
end
42
43
44
+ def parse_immediate ( uri , source_file = % SourceFile { } , position ) do
45
+ GenServer . call ( __MODULE__ , { :parse_immediate , uri , source_file , position } )
46
+ end
47
+
43
48
@ impl true
44
49
def init ( _args ) do
45
50
# TODO get source files on start?
@@ -59,7 +64,7 @@ defmodule ElixirLS.LanguageServer.Parser do
59
64
{ :noreply , % { state | files: updated_files , debounce_refs: updated_debounce_refs } }
60
65
end
61
66
62
- def handle_cast ( { :parse_with_debounce , uri , source_file } , state ) do
67
+ def handle_cast ( { :parse_with_debounce , uri , source_file = % SourceFile { } } , state ) do
63
68
state = if should_parse? ( uri , source_file ) do
64
69
state = update_in ( state . debounce_refs [ uri ] , fn old_ref ->
65
70
if old_ref do
@@ -89,7 +94,7 @@ defmodule ElixirLS.LanguageServer.Parser do
89
94
end
90
95
91
96
@ impl true
92
- def handle_call ( { :parse_immediate , uri , source_file } , _from , % { files: files , debounce_refs: debounce_refs } = state ) do
97
+ def handle_call ( { :parse_immediate , uri , source_file = % SourceFile { } } , _from , % { files: files , debounce_refs: debounce_refs } = state ) do
93
98
{ reply , state } = if should_parse? ( uri , source_file ) do
94
99
{ maybe_ref , updated_debounce_refs } = Map . pop ( debounce_refs , uri )
95
100
if maybe_ref do
@@ -132,6 +137,54 @@ defmodule ElixirLS.LanguageServer.Parser do
132
137
{ :reply , reply , state }
133
138
end
134
139
140
+ def handle_call ( { :parse_immediate , uri , source_file = % SourceFile { } , position } , _from , % { files: files , debounce_refs: debounce_refs } = state ) do
141
+ { reply , state } = if should_parse? ( uri , source_file ) do
142
+ { maybe_ref , updated_debounce_refs } = Map . pop ( debounce_refs , uri )
143
+ if maybe_ref do
144
+ Process . cancel_timer ( maybe_ref , info: false )
145
+ end
146
+
147
+ current_version = source_file . version
148
+
149
+ case files [ uri ] do
150
+ % Context { parsed_version: ^ current_version } = file ->
151
+ Logger . debug ( "#{ uri } already parsed for cursor position #{ inspect ( position ) } " )
152
+ file = maybe_fix_missing_env ( file , position )
153
+
154
+ updated_files = Map . put ( files , uri , file )
155
+ # no change to diagnostics, only update stored metadata
156
+ state = % { state | files: updated_files , debounce_refs: updated_debounce_refs }
157
+ { file , state }
158
+
159
+ _other ->
160
+ Logger . debug ( "Parsing #{ uri } immediately: languageId #{ source_file . language_id } " )
161
+ # overwrite everything
162
+ file = % Context {
163
+ source_file: source_file ,
164
+ path: get_path ( uri )
165
+ }
166
+ |> do_parse ( position )
167
+
168
+ updated_files = Map . put ( files , uri , file )
169
+
170
+ notify_diagnostics_updated ( updated_files )
171
+
172
+ state = % { state | files: updated_files , debounce_refs: updated_debounce_refs }
173
+ { file , state }
174
+ end
175
+ else
176
+ Logger . debug ( "Not parsing #{ uri } immediately: languageId #{ source_file . language_id } " )
177
+ # not parsing - respond with empty struct
178
+ reply = % Context {
179
+ source_file: source_file ,
180
+ path: get_path ( uri )
181
+ }
182
+ { reply , state }
183
+ end
184
+
185
+ { :reply , reply , state }
186
+ end
187
+
135
188
@ impl GenServer
136
189
def handle_info (
137
190
{ :parse_file , uri } ,
@@ -156,22 +209,112 @@ defmodule ElixirLS.LanguageServer.Parser do
156
209
String . ends_with? ( uri , [ ".ex" , ".exs" , ".eex" ] ) or source_file . language_id in [ "elixir" , "eex" , "html-eex" ]
157
210
end
158
211
159
- defp do_parse ( % Context { source_file: source_file , path: path } = file ) do
212
+ @ dummy_source ""
213
+ @ dummy_ast Code . string_to_quoted! ( @ dummy_source )
214
+ @ dummy_metadata ElixirSense.Core.Metadata . fill ( @ dummy_source , MetadataBuilder . build ( @ dummy_ast ) )
215
+
216
+ defp maybe_fix_missing_env ( % Context { metadata: metadata , flag: flag , source_file: source_file = % SourceFile { } } = file , { line , _character } = cursor_position ) do
217
+ if Map . has_key? ( metadata . lines_to_env , line ) do
218
+ file
219
+ else
220
+ case flag do
221
+ { _ , ^ cursor_position } ->
222
+ # give up - we already tried
223
+ file
224
+ { :exact , _ } ->
225
+ # file does not have parse errors, try to parse again with marker
226
+ metadata = case ElixirSense.Core.Parser . try_fix_line_not_found_by_inserting_marker ( source_file . text , cursor_position ) do
227
+ { :ok , acc } ->
228
+ Logger . debug ( "Fixed missing env" )
229
+ ElixirSense.Core.Metadata . fill ( source_file . text , acc )
230
+ _ ->
231
+ Logger . debug ( "Not able to fix missing env" )
232
+ metadata
233
+ end
234
+
235
+ % Context { file |
236
+ metadata: metadata ,
237
+ flag: { :exact , cursor_position }
238
+ }
239
+
240
+ :not_parsable ->
241
+ # give up - no support in fault tolerant parser
242
+ file
243
+ { f , _cursor_position } when f in [ :not_parsable , :fixed ] ->
244
+ # reparse with cursor position
245
+ { flag , ast , metadata } = fault_tolerant_parse ( source_file , cursor_position )
246
+ % Context { file |
247
+ ast: ast ,
248
+ metadata: metadata ,
249
+ flag: flag
250
+ }
251
+ end
252
+ end
253
+ end
254
+
255
+ defp do_parse ( % Context { source_file: source_file = % SourceFile { } , path: path } = file , cursor_position \\ nil ) do
160
256
{ ast , diagnostics } = parse_file ( source_file . text , path , source_file . language_id )
161
257
162
- metadata = if ast do
163
- acc = MetadataBuilder . build ( ast )
164
- ElixirSense.Core.Metadata . fill ( source_file . text , acc )
258
+ { flag , ast , metadata } = if ast do
259
+ # no syntax errors
260
+ metadata = MetadataBuilder . build ( ast )
261
+ |> fix_missing_env ( source_file . text , cursor_position )
262
+ { { :exact , cursor_position } , ast , metadata }
263
+ else
264
+ if elixir? ( path , source_file . language_id ) do
265
+ fault_tolerant_parse ( source_file , cursor_position )
266
+ else
267
+ # no support for eex in ElixirSense.Core.Parser
268
+ { :not_parsable , @ dummy_ast , @ dummy_metadata }
269
+ end
165
270
end
166
271
167
272
% Context { file |
168
273
ast: ast ,
169
274
diagnostics: diagnostics ,
170
275
metadata: metadata ,
171
- parsed_version: source_file . version
276
+ parsed_version: source_file . version ,
277
+ flag: flag
172
278
}
173
279
end
174
280
281
+ defp fault_tolerant_parse ( source_file = % SourceFile { } , cursor_position ) do
282
+ # attempt to parse with fixing syntax errors
283
+ options = [
284
+ errors_threshold: 3 ,
285
+ cursor_position: cursor_position ,
286
+ fallback_to_container_cursor_to_quoted: true
287
+ ]
288
+ case ElixirSense.Core.Parser . string_to_ast ( source_file . text , options ) do
289
+ { :ok , ast , modified_source , _error } ->
290
+ Logger . debug ( "Syntax error fixed" )
291
+ metadata = MetadataBuilder . build ( ast )
292
+ |> fix_missing_env ( modified_source , cursor_position )
293
+ { { :fixed , cursor_position } , ast , metadata }
294
+ _ ->
295
+ Logger . debug ( "Not able to fix syntax error" )
296
+ # we can't fix it
297
+ { { :not_parsable , cursor_position } , @ dummy_ast , @ dummy_metadata }
298
+ end
299
+ end
300
+
301
+ defp fix_missing_env ( acc , source , nil ) , do: ElixirSense.Core.Metadata . fill ( source , acc )
302
+ defp fix_missing_env ( acc , source , { line , _ } = cursor_position ) do
303
+ acc = if Map . has_key? ( acc . lines_to_env , line ) do
304
+ acc
305
+ else
306
+ case ElixirSense.Core.Parser . try_fix_line_not_found_by_inserting_marker ( source , cursor_position ) do
307
+ { :ok , acc } ->
308
+ Logger . debug ( "Fixed missing env" )
309
+ acc
310
+ _ ->
311
+ Logger . debug ( "Not able to fix missing env" )
312
+ acc
313
+ end
314
+ end
315
+ ElixirSense.Core.Metadata . fill ( source , acc )
316
+ end
317
+
175
318
defp get_path ( uri ) do
176
319
case uri do
177
320
"file:" <> _ -> SourceFile.Path . from_uri ( uri )
@@ -186,16 +329,25 @@ defmodule ElixirLS.LanguageServer.Parser do
186
329
|> Server . parser_finished ( )
187
330
end
188
331
332
+ defp elixir? ( file , language_id ) do
333
+ is_binary ( file ) and ( String . ends_with? ( file , ".ex" ) or String . ends_with? ( file , ".exs" ) ) or language_id in [ "elixir" ]
334
+ end
335
+
336
+ defp eex? ( file , language_id ) do
337
+ is_binary ( file ) and String . ends_with? ( file , ".eex" ) or language_id in [ "eex" , "html-eex" ]
338
+ end
339
+
189
340
defp parse_file ( text , file , language_id ) do
190
341
{ result , raw_diagnostics } =
191
342
Build . with_diagnostics ( [ log: false ] , fn ->
192
343
try do
193
344
parser_options = [
194
345
file: file ,
195
- columns: true
346
+ columns: true ,
347
+ token_metadata: true
196
348
]
197
349
198
- ast = if is_binary ( file ) and String . ends_with ?( file , ".eex" ) or language_id in [ "eex" , "html-eex" ] do
350
+ ast = if eex ?( file , language_id ) do
199
351
EEx . compile_string ( text ,
200
352
file: file ,
201
353
parser_options: parser_options
0 commit comments