12
12
using System . Text ;
13
13
using System . Collections ;
14
14
using Microsoft . Extensions . Logging ;
15
+ using OmniSharp . Extensions . LanguageServer . Protocol . Models ;
16
+ using OmniSharp . Extensions . LanguageServer . Protocol . Server ;
17
+ using System . Threading ;
15
18
16
19
namespace Microsoft . PowerShell . EditorServices
17
20
{
@@ -51,6 +54,11 @@ public class AnalysisService : IDisposable
51
54
52
55
private static readonly string [ ] s_emptyGetRuleResult = new string [ 0 ] ;
53
56
57
+ private Dictionary < string , Dictionary < string , MarkerCorrection > > codeActionsPerFile =
58
+ new Dictionary < string , Dictionary < string , MarkerCorrection > > ( ) ;
59
+
60
+ private static CancellationTokenSource s_existingRequestCancellation ;
61
+
54
62
/// <summary>
55
63
/// The indentation to add when the logger lists errors.
56
64
/// </summary>
@@ -87,6 +95,9 @@ public class AnalysisService : IDisposable
87
95
/// </summary>
88
96
private PSModuleInfo _pssaModuleInfo ;
89
97
98
+ private readonly ILanguageServer _languageServer ;
99
+ private readonly ConfigurationService _configurationService ;
100
+
90
101
#endregion // Private Fields
91
102
92
103
#region Properties
@@ -126,12 +137,16 @@ private AnalysisService(
126
137
RunspacePool analysisRunspacePool ,
127
138
string pssaSettingsPath ,
128
139
IEnumerable < string > activeRules ,
140
+ ILanguageServer languageServer ,
141
+ ConfigurationService configurationService ,
129
142
ILogger logger ,
130
143
PSModuleInfo pssaModuleInfo = null )
131
144
{
132
145
_analysisRunspacePool = analysisRunspacePool ;
133
146
SettingsPath = pssaSettingsPath ;
134
147
ActiveRules = activeRules . ToArray ( ) ;
148
+ _languageServer = languageServer ;
149
+ _configurationService = configurationService ;
135
150
_logger = logger ;
136
151
_pssaModuleInfo = pssaModuleInfo ;
137
152
}
@@ -150,8 +165,9 @@ private AnalysisService(
150
165
/// A new analysis service instance with a freshly imported PSScriptAnalyzer module and runspace pool.
151
166
/// Returns null if problems occur. This method should never throw.
152
167
/// </returns>
153
- public static AnalysisService Create ( string settingsPath , ILogger logger )
168
+ public static AnalysisService Create ( ConfigurationService configurationService , ILanguageServer languageServer , ILogger logger )
154
169
{
170
+ string settingsPath = configurationService . CurrentSettings . ScriptAnalysis . SettingsPath ;
155
171
try
156
172
{
157
173
RunspacePool analysisRunspacePool ;
@@ -184,6 +200,8 @@ public static AnalysisService Create(string settingsPath, ILogger logger)
184
200
analysisRunspacePool ,
185
201
settingsPath ,
186
202
s_includedRules ,
203
+ languageServer ,
204
+ configurationService ,
187
205
logger ,
188
206
pssaModuleInfo ) ;
189
207
@@ -673,6 +691,232 @@ public PowerShellResult(
673
691
674
692
public bool HasErrors { get ; }
675
693
}
694
+
695
+ internal async Task RunScriptDiagnosticsAsync (
696
+ ScriptFile [ ] filesToAnalyze )
697
+ {
698
+ // If there's an existing task, attempt to cancel it
699
+ try
700
+ {
701
+ if ( s_existingRequestCancellation != null )
702
+ {
703
+ // Try to cancel the request
704
+ s_existingRequestCancellation . Cancel ( ) ;
705
+
706
+ // If cancellation didn't throw an exception,
707
+ // clean up the existing token
708
+ s_existingRequestCancellation . Dispose ( ) ;
709
+ s_existingRequestCancellation = null ;
710
+ }
711
+ }
712
+ catch ( Exception e )
713
+ {
714
+ // TODO: Catch a more specific exception!
715
+ _logger . LogError (
716
+ string . Format (
717
+ "Exception while canceling analysis task:\n \n {0}" ,
718
+ e . ToString ( ) ) ) ;
719
+
720
+ TaskCompletionSource < bool > cancelTask = new TaskCompletionSource < bool > ( ) ;
721
+ cancelTask . SetCanceled ( ) ;
722
+ return ;
723
+ }
724
+
725
+ // If filesToAnalzye is empty, nothing to do so return early.
726
+ if ( filesToAnalyze . Length == 0 )
727
+ {
728
+ return ;
729
+ }
730
+
731
+ // Create a fresh cancellation token and then start the task.
732
+ // We create this on a different TaskScheduler so that we
733
+ // don't block the main message loop thread.
734
+ // TODO: Is there a better way to do this?
735
+ s_existingRequestCancellation = new CancellationTokenSource ( ) ;
736
+ await Task . Factory . StartNew (
737
+ ( ) =>
738
+ DelayThenInvokeDiagnosticsAsync (
739
+ 750 ,
740
+ filesToAnalyze ,
741
+ _configurationService . CurrentSettings . ScriptAnalysis . Enable ?? false ,
742
+ s_existingRequestCancellation . Token ) ,
743
+ CancellationToken . None ,
744
+ TaskCreationOptions . None ,
745
+ TaskScheduler . Default ) ;
746
+ }
747
+
748
+ private async Task DelayThenInvokeDiagnosticsAsync (
749
+ int delayMilliseconds ,
750
+ ScriptFile [ ] filesToAnalyze ,
751
+ bool isScriptAnalysisEnabled ,
752
+ CancellationToken cancellationToken )
753
+ {
754
+ // First of all, wait for the desired delay period before
755
+ // analyzing the provided list of files
756
+ try
757
+ {
758
+ await Task . Delay ( delayMilliseconds , cancellationToken ) ;
759
+ }
760
+ catch ( TaskCanceledException )
761
+ {
762
+ // If the task is cancelled, exit directly
763
+ foreach ( var script in filesToAnalyze )
764
+ {
765
+ PublishScriptDiagnostics (
766
+ script ,
767
+ script . DiagnosticMarkers ) ;
768
+ }
769
+
770
+ return ;
771
+ }
772
+
773
+ // If we've made it past the delay period then we don't care
774
+ // about the cancellation token anymore. This could happen
775
+ // when the user stops typing for long enough that the delay
776
+ // period ends but then starts typing while analysis is going
777
+ // on. It makes sense to send back the results from the first
778
+ // delay period while the second one is ticking away.
779
+
780
+ // Get the requested files
781
+ foreach ( ScriptFile scriptFile in filesToAnalyze )
782
+ {
783
+ List < ScriptFileMarker > semanticMarkers = null ;
784
+ if ( isScriptAnalysisEnabled )
785
+ {
786
+ semanticMarkers = await GetSemanticMarkersAsync ( scriptFile ) ;
787
+ }
788
+ else
789
+ {
790
+ // Semantic markers aren't available if the AnalysisService
791
+ // isn't available
792
+ semanticMarkers = new List < ScriptFileMarker > ( ) ;
793
+ }
794
+
795
+ scriptFile . DiagnosticMarkers . AddRange ( semanticMarkers ) ;
796
+
797
+ PublishScriptDiagnostics (
798
+ scriptFile ,
799
+ // Concat script analysis errors to any existing parse errors
800
+ scriptFile . DiagnosticMarkers ) ;
801
+ }
802
+ }
803
+
804
+ internal void ClearMarkers ( ScriptFile scriptFile )
805
+ {
806
+ // send empty diagnostic markers to clear any markers associated with the given file
807
+ PublishScriptDiagnostics (
808
+ scriptFile ,
809
+ new List < ScriptFileMarker > ( ) ) ;
810
+ }
811
+
812
+ private void PublishScriptDiagnostics (
813
+ ScriptFile scriptFile ,
814
+ List < ScriptFileMarker > markers )
815
+ {
816
+ List < Diagnostic > diagnostics = new List < Diagnostic > ( ) ;
817
+
818
+ // Hold on to any corrections that may need to be applied later
819
+ Dictionary < string , MarkerCorrection > fileCorrections =
820
+ new Dictionary < string , MarkerCorrection > ( ) ;
821
+
822
+ foreach ( var marker in markers )
823
+ {
824
+ // Does the marker contain a correction?
825
+ Diagnostic markerDiagnostic = GetDiagnosticFromMarker ( marker ) ;
826
+ if ( marker . Correction != null )
827
+ {
828
+ string diagnosticId = GetUniqueIdFromDiagnostic ( markerDiagnostic ) ;
829
+ fileCorrections [ diagnosticId ] = marker . Correction ;
830
+ }
831
+
832
+ diagnostics . Add ( markerDiagnostic ) ;
833
+ }
834
+
835
+ codeActionsPerFile [ scriptFile . DocumentUri ] = fileCorrections ;
836
+
837
+ var uriBuilder = new UriBuilder ( )
838
+ {
839
+ Scheme = Uri . UriSchemeFile ,
840
+ Path = scriptFile . FilePath ,
841
+ Host = string . Empty ,
842
+ } ;
843
+
844
+ // Always send syntax and semantic errors. We want to
845
+ // make sure no out-of-date markers are being displayed.
846
+ _languageServer . Document . PublishDiagnostics ( new PublishDiagnosticsParams ( )
847
+ {
848
+ Uri = uriBuilder . Uri ,
849
+ Diagnostics = new Container < Diagnostic > ( diagnostics ) ,
850
+ } ) ;
851
+ }
852
+
853
+ // Generate a unique id that is used as a key to look up the associated code action (code fix) when
854
+ // we receive and process the textDocument/codeAction message.
855
+ private static string GetUniqueIdFromDiagnostic ( Diagnostic diagnostic )
856
+ {
857
+ Position start = diagnostic . Range . Start ;
858
+ Position end = diagnostic . Range . End ;
859
+
860
+ var sb = new StringBuilder ( 256 )
861
+ . Append ( diagnostic . Source ?? "?" )
862
+ . Append ( "_" )
863
+ . Append ( diagnostic . Code . ToString ( ) )
864
+ . Append ( "_" )
865
+ . Append ( diagnostic . Severity ? . ToString ( ) ?? "?" )
866
+ . Append ( "_" )
867
+ . Append ( start . Line )
868
+ . Append ( ":" )
869
+ . Append ( start . Character )
870
+ . Append ( "-" )
871
+ . Append ( end . Line )
872
+ . Append ( ":" )
873
+ . Append ( end . Character ) ;
874
+
875
+ var id = sb . ToString ( ) ;
876
+ return id ;
877
+ }
878
+
879
+ private static Diagnostic GetDiagnosticFromMarker ( ScriptFileMarker scriptFileMarker )
880
+ {
881
+ return new Diagnostic
882
+ {
883
+ Severity = MapDiagnosticSeverity ( scriptFileMarker . Level ) ,
884
+ Message = scriptFileMarker . Message ,
885
+ Code = scriptFileMarker . RuleName ,
886
+ Source = scriptFileMarker . Source ,
887
+ Range = new Range
888
+ {
889
+ Start = new Position
890
+ {
891
+ Line = scriptFileMarker . ScriptRegion . StartLineNumber - 1 ,
892
+ Character = scriptFileMarker . ScriptRegion . StartColumnNumber - 1
893
+ } ,
894
+ End = new Position
895
+ {
896
+ Line = scriptFileMarker . ScriptRegion . EndLineNumber - 1 ,
897
+ Character = scriptFileMarker . ScriptRegion . EndColumnNumber - 1
898
+ }
899
+ }
900
+ } ;
901
+ }
902
+
903
+ private static DiagnosticSeverity MapDiagnosticSeverity ( ScriptFileMarkerLevel markerLevel )
904
+ {
905
+ switch ( markerLevel )
906
+ {
907
+ case ScriptFileMarkerLevel . Error :
908
+ return DiagnosticSeverity . Error ;
909
+
910
+ case ScriptFileMarkerLevel . Warning :
911
+ return DiagnosticSeverity . Warning ;
912
+
913
+ case ScriptFileMarkerLevel . Information :
914
+ return DiagnosticSeverity . Information ;
915
+
916
+ default :
917
+ return DiagnosticSeverity . Error ;
918
+ }
919
+ }
676
920
}
677
921
678
922
/// <summary>
0 commit comments