@@ -23,7 +23,8 @@ defmodule ElixirLS.Debugger.Server do
23
23
Variables ,
24
24
Utils ,
25
25
BreakpointCondition ,
26
- Binding
26
+ Binding ,
27
+ ModuleInfoCache
27
28
}
28
29
29
30
alias ElixirLS.Debugger.Stacktrace.Frame
@@ -84,6 +85,7 @@ defmodule ElixirLS.Debugger.Server do
84
85
@ impl GenServer
85
86
def init ( opts ) do
86
87
BreakpointCondition . start_link ( [ ] )
88
+ ModuleInfoCache . start_link ( % { } )
87
89
state = if opts [ :output ] , do: % __MODULE__ { output: opts [ :output ] } , else: % __MODULE__ { }
88
90
{ :ok , state }
89
91
end
@@ -269,17 +271,17 @@ defmodule ElixirLS.Debugger.Server do
269
271
for b <- breakpoints , do: { b [ "condition" ] , b [ "logMessage" ] , b [ "hitCondition" ] }
270
272
271
273
existing_bps = state . breakpoints [ path ] || [ ]
272
- existing_bp_lines = for { _module , line } <- existing_bps , do: line
274
+ existing_bp_lines = for { _modules , line } <- existing_bps , do: line
273
275
removed_lines = existing_bp_lines -- new_lines
274
276
removed_bps = Enum . filter ( existing_bps , fn { _ , line } -> line in removed_lines end )
275
277
276
- for { module , line } <- removed_bps do
278
+ for { modules , line } <- removed_bps , module <- modules do
277
279
:int . delete_break ( module , line )
278
280
BreakpointCondition . unregister_condition ( module , [ line ] )
279
281
end
280
282
281
283
result = set_breakpoints ( path , new_lines |> Enum . zip ( new_conditions ) )
282
- new_bps = for { :ok , module , line } <- result , do: { module , line }
284
+ new_bps = for { :ok , modules , line } <- result , do: { modules , line }
283
285
284
286
state =
285
287
if new_bps == [ ] do
@@ -333,19 +335,30 @@ defmodule ElixirLS.Debugger.Server do
333
335
result =
334
336
case current [ { m , f , a } ] do
335
337
nil ->
336
- case :int . ni ( m ) do
338
+ case interpret ( m , false ) do
337
339
{ :module , _ } ->
340
+ ModuleInfoCache . store ( m )
338
341
breaks_before = :int . all_breaks ( m )
339
- :ok = :int . break_in ( m , f , a )
340
- breaks_after = :int . all_breaks ( m )
341
- lines = for { { ^ m , line } , _ } <- breaks_after -- breaks_before , do: line
342
342
343
- # pass nil as log_message - not supported on function breakpoints as of DAP 1.51
344
- update_break_condition ( m , lines , condition , nil , hit_count )
343
+ Output . debugger_console (
344
+ "Setting function breakpoint in #{ inspect ( m ) } .#{ f } /#{ a } "
345
+ )
345
346
346
- { :ok , lines }
347
+ case :int . break_in ( m , f , a ) do
348
+ :ok ->
349
+ breaks_after = :int . all_breaks ( m )
350
+ lines = for { { ^ m , line } , _ } <- breaks_after -- breaks_before , do: line
347
351
348
- _ ->
352
+ # pass nil as log_message - not supported on function breakpoints as of DAP 1.51
353
+ update_break_condition ( m , lines , condition , nil , hit_count )
354
+
355
+ { :ok , lines }
356
+
357
+ { :error , :function_not_found } ->
358
+ { :error , "Function #{ inspect ( m ) } .#{ f } /#{ a } not found" }
359
+ end
360
+
361
+ :error ->
349
362
{ :error , "Cannot interpret module #{ inspect ( m ) } " }
350
363
end
351
364
@@ -1145,43 +1158,79 @@ defmodule ElixirLS.Debugger.Server do
1145
1158
defp save_and_reload ( module , beam_bin ) do
1146
1159
:ok = File . write ( Path . join ( @ temp_beam_dir , to_string ( module ) <> ".beam" ) , beam_bin )
1147
1160
true = :code . delete ( module )
1148
- { :module , _ } = :int . ni ( module )
1161
+ { :module , _ } = interpret ( module )
1162
+ ModuleInfoCache . store ( module )
1149
1163
end
1150
1164
1151
1165
defp set_breakpoints ( path , lines ) do
1152
1166
if Path . extname ( path ) == ".erl" do
1153
1167
module = String . to_atom ( Path . basename ( path , ".erl" ) )
1154
- for line <- lines , do: set_breakpoint ( module , line )
1168
+ for line <- lines , do: set_breakpoint ( [ module ] , path , line )
1155
1169
else
1156
- try do
1157
- metadata = ElixirSense.Core.Parser . parse_file ( path , false , false , nil )
1158
-
1159
- for line <- lines do
1160
- env = ElixirSense.Core.Metadata . get_env ( metadata , { line |> elem ( 0 ) , 1 } )
1161
-
1162
- if env . module == nil do
1163
- { :error , "Could not determine module at line" }
1170
+ loaded_elixir_modules =
1171
+ :code . all_loaded ( )
1172
+ |> Enum . map ( & elem ( & 1 , 0 ) )
1173
+ |> Enum . filter ( fn module -> String . starts_with? ( Atom . to_string ( module ) , "Elixir." ) end )
1174
+ |> Enum . group_by ( fn module ->
1175
+ module_info = ModuleInfoCache . get ( module ) || module . module_info ( )
1176
+ Path . expand ( to_string ( module_info [ :compile ] [ :source ] ) )
1177
+ end )
1178
+
1179
+ loaded_modules_from_path = Map . get ( loaded_elixir_modules , path , [ ] )
1180
+ metadata = ElixirSense.Core.Parser . parse_file ( path , false , false , nil )
1181
+
1182
+ for line <- lines do
1183
+ env = ElixirSense.Core.Metadata . get_env ( metadata , { line |> elem ( 0 ) , 1 } )
1184
+ metadata_modules = Enum . filter ( env . module_variants , & ( & 1 != Elixir ) )
1185
+
1186
+ modules_to_break =
1187
+ if metadata_modules != [ ] and
1188
+ Enum . all? ( metadata_modules , & ( & 1 in loaded_modules_from_path ) ) do
1189
+ # prefer metadata modules if valid and loaded
1190
+ metadata_modules
1164
1191
else
1165
- set_breakpoint ( env . module , line )
1192
+ # fall back to all loaded modules from file
1193
+ # this may create breakpoints outside module line range
1194
+ loaded_modules_from_path
1166
1195
end
1167
- end
1168
- rescue
1169
- error ->
1170
- for _line <- lines , do: { :error , Exception . format_exit ( error ) }
1196
+
1197
+ set_breakpoint ( modules_to_break , path , line )
1171
1198
end
1172
1199
end
1200
+ rescue
1201
+ error ->
1202
+ for _line <- lines , do: { :error , Exception . format_exit ( error ) }
1173
1203
end
1174
1204
1175
- defp set_breakpoint ( module , { line , { condition , log_message , hit_count } } ) do
1176
- case :int . ni ( module ) do
1177
- { :module , _ } ->
1178
- :int . break ( module , line )
1179
- update_break_condition ( module , line , condition , log_message , hit_count )
1205
+ defp set_breakpoint ( [ ] , path , { line , _ } ) do
1206
+ { :error , "Could not determine module at line #{ line } in #{ path } " }
1207
+ end
1180
1208
1181
- { :ok , module , line }
1209
+ defp set_breakpoint ( modules , path , { line , { condition , log_message , hit_count } } ) do
1210
+ modules_with_breakpoints =
1211
+ Enum . reduce ( modules , [ ] , fn module , added ->
1212
+ case interpret ( module , false ) do
1213
+ { :module , _ } ->
1214
+ ModuleInfoCache . store ( module )
1215
+ Output . debugger_console ( "Setting breakpoint in #{ inspect ( module ) } #{ path } :#{ line } " )
1216
+ # no need to handle errors here, it can fail only with {:error, :break_exists}
1217
+ :int . break ( module , line )
1218
+ update_break_condition ( module , line , condition , log_message , hit_count )
1182
1219
1183
- _ ->
1184
- { :error , "Cannot interpret module #{ inspect ( module ) } " }
1220
+ [ module | added ]
1221
+
1222
+ :error ->
1223
+ Output . debugger_console ( "Could not interpret module #{ inspect ( module ) } in #{ path } " )
1224
+ added
1225
+ end
1226
+ end )
1227
+
1228
+ if modules_with_breakpoints == [ ] do
1229
+ { :error ,
1230
+ "Could not interpret any of the modules #{ Enum . map_join ( modules , ", " , & inspect / 1 ) } in #{ path } " }
1231
+ else
1232
+ # return :ok if at least one breakpoint was set
1233
+ { :ok , modules_with_breakpoints , line }
1185
1234
end
1186
1235
end
1187
1236
@@ -1196,7 +1245,8 @@ defmodule ElixirLS.Debugger.Server do
1196
1245
1197
1246
defp interpret_module ( mod ) do
1198
1247
try do
1199
- { :module , _ } = :int . ni ( mod )
1248
+ { :module , _ } = interpret ( mod )
1249
+ ModuleInfoCache . store ( mod )
1200
1250
catch
1201
1251
_ , _ ->
1202
1252
Output . debugger_important (
@@ -1342,7 +1392,9 @@ defmodule ElixirLS.Debugger.Server do
1342
1392
function_breakpoints = Map . get ( state . function_breakpoints , frame_mfa , [ ] )
1343
1393
1344
1394
cond do
1345
- { first_frame . module , first_frame . line } in file_breakpoints ->
1395
+ Enum . any? ( file_breakpoints , fn { modules , line } ->
1396
+ line == first_frame . line and first_frame . module in modules
1397
+ end ) ->
1346
1398
"breakpoint"
1347
1399
1348
1400
first_frame . line in function_breakpoints ->
@@ -1352,4 +1404,43 @@ defmodule ElixirLS.Debugger.Server do
1352
1404
"step"
1353
1405
end
1354
1406
end
1407
+
1408
+ @ exclude_protocols_from_interpreting [
1409
+ Enumerable ,
1410
+ Collectable ,
1411
+ List.Chars ,
1412
+ String.Chars ,
1413
+ Inspect ,
1414
+ IEx.Info ,
1415
+ JasonV.Encoder
1416
+ ]
1417
+
1418
+ @ exclude_implementations_from_interpreting [ Inspect ]
1419
+
1420
+ defp interpret ( module , print_message? \\ true )
1421
+
1422
+ defp interpret ( module , _print_message? ) when module in @ exclude_protocols_from_interpreting do
1423
+ :error
1424
+ end
1425
+
1426
+ defp interpret ( module , print_message? ) do
1427
+ if Code . ensure_loaded? ( module ) do
1428
+ module_behaviours =
1429
+ module . module_info ( :attributes ) |> Keyword . get_values ( :behaviour ) |> Enum . concat ( )
1430
+
1431
+ if Enum . any? ( @ exclude_implementations_from_interpreting , & ( & 1 in module_behaviours ) ) do
1432
+ # debugger uses Inspect protocol and setting breakpoints in implementations leads to deadlocks
1433
+ # https://github.com/elixir-lsp/elixir-ls/issues/903
1434
+ :error
1435
+ else
1436
+ if print_message? do
1437
+ Output . debugger_console ( "Interpreting module #{ inspect ( module ) } " )
1438
+ end
1439
+
1440
+ :int . ni ( module )
1441
+ end
1442
+ else
1443
+ :error
1444
+ end
1445
+ end
1355
1446
end
0 commit comments