@@ -49,7 +49,9 @@ defmodule ElixirLS.LanguageServer.Server do
49
49
:root_uri ,
50
50
:project_dir ,
51
51
:settings ,
52
- parser_diagnostics: [ ] ,
52
+ last_published_diagnostics_uris: [ ] ,
53
+ parser_diagnostics: % { } ,
54
+ document_versions_on_build: % { } ,
53
55
build_diagnostics: [ ] ,
54
56
dialyzer_diagnostics: [ ] ,
55
57
needs_build?: false ,
@@ -257,9 +259,8 @@ defmodule ElixirLS.LanguageServer.Server do
257
259
258
260
@ impl GenServer
259
261
def handle_cast ( { :parser_finished , diagnostics } , state = % __MODULE__ { } ) do
260
- old_diagnostics = current_diagnostics ( state )
261
262
state = % { state | parser_diagnostics: diagnostics }
262
- publish_diagnostics ( state , old_diagnostics )
263
+ |> publish_diagnostics ( )
263
264
264
265
{ :noreply , state }
265
266
end
@@ -1359,13 +1360,19 @@ defmodule ElixirLS.LanguageServer.Server do
1359
1360
end
1360
1361
end
1361
1362
1363
+ document_versions_on_build = Map . new ( state . source_files , fn { uri , source_file } -> { uri , source_file . version } end )
1364
+
1362
1365
% __MODULE__ {
1363
1366
state
1364
1367
| build_ref: build_ref ,
1365
1368
needs_build?: false ,
1366
1369
build_running?: true ,
1367
- analysis_ready?: false
1370
+ analysis_ready?: false ,
1371
+ build_diagnostics: [ ] ,
1372
+ dialyzer_diagnostics: [ ] ,
1373
+ document_versions_on_build: document_versions_on_build
1368
1374
}
1375
+ |> publish_diagnostics ( )
1369
1376
1370
1377
true ->
1371
1378
# build already running, schedule another one
@@ -1407,38 +1414,32 @@ defmodule ElixirLS.LanguageServer.Server do
1407
1414
defp handle_build_result ( status , diagnostics , state = % __MODULE__ { } ) do
1408
1415
do_sanity_check ( state )
1409
1416
1410
- old_diagnostics = current_diagnostics ( state )
1411
-
1412
- state = put_in ( state . build_diagnostics , diagnostics )
1413
-
1414
- state =
1415
- cond do
1416
- state . needs_build? ->
1417
- state
1418
-
1419
- status == :error or not dialyzer_enabled? ( state ) ->
1420
- put_in ( state . dialyzer_diagnostics , [ ] )
1421
-
1422
- true ->
1423
- dialyze ( state )
1424
- end
1417
+ state = if state . needs_build? or status == :error or not dialyzer_enabled? ( state ) do
1418
+ state
1419
+ else
1420
+ dialyze ( state )
1421
+ end
1425
1422
1426
- publish_diagnostics ( state , old_diagnostics )
1423
+ state = if state . needs_build? do
1424
+ # do not publish diagnostics if we need another build
1425
+ state
1426
+ else
1427
+ put_in ( state . build_diagnostics , diagnostics )
1428
+ |> publish_diagnostics ( )
1429
+ end
1427
1430
1428
1431
% { state | full_build_done?: if ( status == :ok , do: true , else: state . full_build_done? ) }
1429
1432
end
1430
1433
1431
1434
defp handle_dialyzer_result ( diagnostics , build_ref , state = % __MODULE__ { } ) do
1432
- old_diagnostics = current_diagnostics ( state )
1433
- state = put_in ( state . dialyzer_diagnostics , diagnostics )
1434
-
1435
- publish_diagnostics ( state , old_diagnostics )
1436
-
1437
1435
# If these results were triggered by the most recent build and files are not dirty, then we know
1438
1436
# we're up to date and can release spec suggestions to the code lens provider
1439
1437
if build_ref == state . build_ref do
1440
1438
Logger . info ( "Dialyzer analysis is up to date" )
1441
1439
1440
+ state = put_in ( state . dialyzer_diagnostics , diagnostics )
1441
+ |> publish_diagnostics ( )
1442
+
1442
1443
{ dirty , not_dirty } =
1443
1444
state . awaiting_contracts
1444
1445
|> Enum . split_with ( fn { _ , uri } ->
@@ -1461,6 +1462,7 @@ defmodule ElixirLS.LanguageServer.Server do
1461
1462
1462
1463
% { state | analysis_ready?: true , awaiting_contracts: dirty }
1463
1464
else
1465
+ # do not publish diagnostics if those results are not for the current build
1464
1466
state
1465
1467
end
1466
1468
end
@@ -1488,30 +1490,31 @@ defmodule ElixirLS.LanguageServer.Server do
1488
1490
end
1489
1491
end
1490
1492
1491
- defp current_diagnostics ( state = % __MODULE__ { } ) do
1492
- state . build_diagnostics ++
1493
- state . dialyzer_diagnostics ++ state . parser_diagnostics
1494
- end
1495
-
1496
- defp publish_diagnostics ( state = % __MODULE__ { project_dir: project_dir , source_files: source_files } , old_diagnostics ) do
1497
- new_diagnostics = current_diagnostics ( state )
1498
- # we need to publish diagnostics for all files in new_diagnostics
1499
- # to clear diagnostics we need to push empty sets for old_diagnostics
1500
- # in case we missed something or restarted and don't have old_diagnostics
1493
+ defp publish_diagnostics ( state = % __MODULE__ { project_dir: project_dir , source_files: source_files , last_published_diagnostics_uris: last_published_diagnostics_uris } ) do
1494
+ # we need to publish diagnostics for all uris in current diagnostics
1495
+ # to clear diagnostics we need to push empty sets for uris from last push
1496
+ # in case we missed something or restarted and don't have old diagnostics uris
1501
1497
# we also push for all open files
1502
- uris_from_old_diagnostics =
1503
- old_diagnostics
1504
- |> Enum . reject ( & is_nil ( & 1 . file ) )
1505
- |> Enum . map ( & ( & 1 . file |> SourceFile.Path . to_uri ( project_dir ) ) )
1506
1498
1507
- new_diagnostics_by_uri =
1508
- new_diagnostics
1509
- |> Enum . reject ( & is_nil ( & 1 . file ) )
1510
- |> Enum . group_by ( & ( & 1 . file |> SourceFile.Path . to_uri ( project_dir ) ) )
1499
+ uris_from_parser_diagnostics = Map . keys ( state . parser_diagnostics )
1500
+
1501
+ filter_diagnostics_with_known_location = fn
1502
+ % Mix.Task.Compiler.Diagnostic { file: file } when is_binary ( file ) ->
1503
+ file != "nofile"
1504
+ _ -> false
1505
+ end
1511
1506
1507
+ valid_build_and_dialyzer_diagnostics_by_uri = ( state . build_diagnostics ++ state . dialyzer_diagnostics )
1508
+ |> Enum . filter ( filter_diagnostics_with_known_location )
1509
+ |> Enum . group_by ( fn
1510
+ % Mix.Task.Compiler.Diagnostic { file: file } -> SourceFile.Path . to_uri ( file , project_dir )
1511
+ end )
1512
+
1513
+ uris_from_build_and_dialyzer_diagnostics = Map . keys ( valid_build_and_dialyzer_diagnostics_by_uri )
1514
+
1512
1515
invalid_diagnostics =
1513
- new_diagnostics
1514
- |> Enum . filter ( & is_nil ( & 1 . file ) )
1516
+ ( state . build_diagnostics ++ state . dialyzer_diagnostics )
1517
+ |> Enum . reject ( filter_diagnostics_with_known_location )
1515
1518
1516
1519
# TODO remove when we are sure diagnostic code is correct
1517
1520
if invalid_diagnostics != [ ] do
@@ -1528,21 +1531,39 @@ defmodule ElixirLS.LanguageServer.Server do
1528
1531
)
1529
1532
end
1530
1533
1531
- uris_from_new_diagnostics = Map . keys ( new_diagnostics_by_uri )
1532
-
1533
1534
uris_from_open_files = Map . keys ( source_files )
1534
1535
1535
1536
uris_to_publish_diagnostics =
1536
- Enum . uniq ( uris_from_old_diagnostics ++ uris_from_new_diagnostics ++ uris_from_open_files )
1537
+ Enum . uniq ( last_published_diagnostics_uris ++ uris_from_parser_diagnostics ++ uris_from_build_and_dialyzer_diagnostics ++ uris_from_open_files )
1537
1538
1538
1539
for uri <- uris_to_publish_diagnostics do
1539
- uri_diagnostics = Map . get ( new_diagnostics_by_uri , uri , [ ] )
1540
- # TODO store versions on compile/parse/dialyze?
1541
- version =
1542
- case source_files [ uri ] do
1543
- nil -> nil
1544
- file -> file . version
1545
- end
1540
+ { parser_diagnostics_document_version , parser_diagnostics } = Map . get ( state . parser_diagnostics , uri , { nil , [ ] } )
1541
+
1542
+ build_document_version = Map . get ( state . document_versions_on_build , uri )
1543
+ build_and_dialyzer_diagnostics = Map . get ( valid_build_and_dialyzer_diagnostics_by_uri , uri , [ ] )
1544
+
1545
+ { version , uri_diagnostics } = cond do
1546
+ is_integer ( parser_diagnostics_document_version ) and is_integer ( build_document_version ) and parser_diagnostics_document_version > build_document_version ->
1547
+ # document open and edited since build was triggered
1548
+ # parser diagnostics are more recent, discard build and dialyzer
1549
+ { parser_diagnostics_document_version , parser_diagnostics }
1550
+ is_integer ( parser_diagnostics_document_version ) and is_integer ( build_document_version ) and parser_diagnostics_document_version == build_document_version ->
1551
+ # document open and not edited since build was triggered
1552
+ # parser build and dialyzer diagnostics are recent
1553
+ # prefer them as they will likely contain the same warnings as parser ones
1554
+ { build_document_version , if ( build_and_dialyzer_diagnostics != [ ] , do: build_and_dialyzer_diagnostics , else: parser_diagnostics ) }
1555
+ is_integer ( parser_diagnostics_document_version ) and is_nil ( build_document_version ) ->
1556
+ # document open but was closed when build was triggered
1557
+ # document could have been edited
1558
+ # combine diagnostics
1559
+ # they can be duplicated but it is not trivial to deduplicate
1560
+ { parser_diagnostics_document_version , Enum . uniq ( parser_diagnostics ++ build_and_dialyzer_diagnostics ) }
1561
+ true ->
1562
+ # document closed
1563
+ # don't emit parser diagnostics
1564
+ # return build diagnostics with possibly nil version
1565
+ { build_document_version , build_and_dialyzer_diagnostics }
1566
+ end
1546
1567
1547
1568
Diagnostics . publish_file_diagnostics (
1548
1569
uri ,
@@ -1551,8 +1572,10 @@ defmodule ElixirLS.LanguageServer.Server do
1551
1572
version
1552
1573
)
1553
1574
1554
- # TODO dump diagnostics to file
1575
+ # TODO dump last_published_diagnostics_uris to file?
1555
1576
end
1577
+
1578
+ % { state | last_published_diagnostics_uris: uris_to_publish_diagnostics }
1556
1579
end
1557
1580
1558
1581
defp show_version_warnings do
0 commit comments