// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) // This code is distributed under the GNU LGPL (for details please see \doc\license.txt) using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using CSharpBinding.Parser; using ICSharpCode.Core; using ICSharpCode.NRefactory; using ICSharpCode.NRefactory.CSharp; using ICSharpCode.NRefactory.CSharp.Refactoring; using ICSharpCode.NRefactory.Editor; using ICSharpCode.SharpDevelop.Editor; using ICSharpCode.SharpDevelop.Gui; using ICSharpCode.SharpDevelop.Parser; using ICSharpCode.SharpDevelop.Refactoring; namespace CSharpBinding.Refactoring { /// /// Performs code analysis in the background and creates text markers to show warnings. /// public class IssueManager : IDisposable, IContextActionProvider { static readonly Lazy> issueProviders = new Lazy>( () => AddInTree.BuildItems("/SharpDevelop/ViewContent/TextEditor/C#/IssueProviders", null, false) .Select(p => new IssueProvider(p)).ToList()); internal static IReadOnlyList IssueProviders { get { return issueProviders.Value; } } internal class IssueProvider { readonly ICodeIssueProvider provider; public readonly Type ProviderType; public readonly IssueDescriptionAttribute Attribute; public IssueProvider(ICodeIssueProvider provider) { if (provider == null) throw new ArgumentNullException("provider"); this.provider = provider; this.ProviderType = provider.GetType(); var attributes = ProviderType.GetCustomAttributes(typeof(IssueDescriptionAttribute), true); if (attributes.Length == 1) this.Attribute = (IssueDescriptionAttribute)attributes[0]; } public Severity DefaultSeverity { get { return Attribute != null ? Attribute.Severity : Severity.Hint; } } public IssueMarker DefaultMarker { get { return Attribute != null ? Attribute.IssueMarker : IssueMarker.Underline; } } public IEnumerable GetIssues(BaseRefactoringContext context) { return provider.GetIssues(context); } } public static IReadOnlyDictionary GetIssueSeveritySettings() { // TODO: cache the result var dict = new Dictionary(); var prop = PropertyService.Get("CSharpIssueSeveritySettings", new Properties()); foreach (var provider in issueProviders.Value) { dict[provider.ProviderType] = prop.Get(provider.ProviderType.FullName, provider.DefaultSeverity); } return dict; } public static void SetIssueSeveritySettings(IReadOnlyDictionary dict) { var prop = new Properties(); foreach (var pair in dict) { prop.Set(pair.Key.FullName, pair.Value); } PropertyService.Set("CSharpIssueSeveritySettings", prop); } readonly ITextEditor editor; readonly ITextMarkerService markerService; public IssueManager(ITextEditor editor) { this.editor = editor; this.markerService = editor.GetService(typeof(ITextMarkerService)) as ITextMarkerService; ParserService.ParserUpdateStepFinished += ParserService_ParserUpdateStepFinished; editor.ContextActionProviders.Add(this); } public void Dispose() { editor.ContextActionProviders.Remove(this); ParserService.ParserUpdateStepFinished -= ParserService_ParserUpdateStepFinished; if (cancellationTokenSource != null) cancellationTokenSource.Cancel(); Clear(); } sealed class InspectionTag { readonly IssueManager manager; public readonly IssueProvider Provider; public readonly ITextSourceVersion InspectedVersion; public readonly string Description; public readonly int StartOffset; public readonly int EndOffset; public readonly IReadOnlyList Actions; public readonly Severity Severity; public InspectionTag(IssueManager manager, IssueProvider provider, ITextSourceVersion inspectedVersion, string description, int startOffset, int endOffset, Severity severity, IEnumerable actions) { this.manager = manager; this.Provider = provider; this.InspectedVersion = inspectedVersion; this.Description = description; this.StartOffset = startOffset; this.EndOffset = endOffset; this.Severity = severity; this.Actions = actions.Select(Wrap).ToList(); } IContextAction Wrap(CodeAction actionToWrap, int index) { // Take care not to capture 'actionToWrap' in the lambda string actionDescription = actionToWrap.Description; return new CSharpContextActionWrapper( manager, actionToWrap, context => { // Look up the new issue position int newStart = InspectedVersion.MoveOffsetTo(context.Version, StartOffset, AnchorMovementType.Default); int newEnd = InspectedVersion.MoveOffsetTo(context.Version, EndOffset, AnchorMovementType.Default); // If the length changed, don't bother looking up the issue again if (newEnd - newStart != EndOffset - StartOffset) return null; // Now rediscover this issue in the new context var issue = this.Provider.GetIssues(context).FirstOrDefault( i => context.GetOffset(i.Start) == newStart && context.GetOffset(i.End) == newEnd && i.Desription == this.Description); if (issue == null) return null; // Now look up the action within that issue: if (issue.Action != null && issue.Action.Description == actionDescription) return issue.Action; else return null; }); } ITextMarker marker; public void CreateMarker(IDocument document, ITextMarkerService markerService) { int startOffset = InspectedVersion.MoveOffsetTo(document.Version, this.StartOffset, AnchorMovementType.Default); int endOffset = InspectedVersion.MoveOffsetTo(document.Version, this.EndOffset, AnchorMovementType.Default); if (startOffset >= endOffset) return; marker = markerService.Create(startOffset, endOffset - startOffset); marker.ToolTip = this.Description; switch (Provider.DefaultMarker) { case IssueMarker.Underline: Color underlineColor = GetColor(this.Severity); underlineColor.A = 186; marker.MarkerType = TextMarkerType.SquigglyUnderline; marker.MarkerColor = underlineColor; break; case IssueMarker.GrayOut: marker.ForegroundColor = SystemColors.GrayTextColor; break; } marker.Tag = this; } static Color GetColor(Severity severity) { switch (severity) { case Severity.Error: return Colors.Red; case Severity.Warning: return Colors.Orange; case Severity.Suggestion: return Colors.Green; default: return Colors.Blue; } } public void RemoveMarker() { if (marker != null) { marker.Delete(); marker = null; } } } CancellationTokenSource cancellationTokenSource; ITextSourceVersion analyzedVersion; List existingResults; void Clear() { if (existingResults != null) { foreach (var oldResult in existingResults) { oldResult.RemoveMarker(); } existingResults = null; } analyzedVersion = null; } void ParserService_ParserUpdateStepFinished(object sender, ParserUpdateStepEventArgs e) { var parseInfo = e.ParseInformation as CSharpFullParseInformation; ITextSourceVersion currentVersion = editor.Document.Version; ITextSourceVersion parsedVersion = e.Content.Version; if (parseInfo != null && parsedVersion != null && currentVersion != null && parsedVersion.BelongsToSameDocumentAs(currentVersion)) { if (analyzedVersion != null && analyzedVersion.CompareAge(parsedVersion) == 0) { // don't analyze the same version twice return; } RunAnalysis(e.Content, parseInfo); } } async void RunAnalysis(ITextSource textSource, CSharpFullParseInformation parseInfo) { if (markerService == null) return; if (cancellationTokenSource != null) cancellationTokenSource.Cancel(); cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; List results = new List(); try { await Task.Run( delegate { var compilation = ParserService.GetCompilationForFile(parseInfo.FileName); var resolver = parseInfo.GetResolver(compilation); var context = new SDRefactoringContext(textSource, resolver, new TextLocation(0, 0), 0, 0, cancellationToken); var settings = GetIssueSeveritySettings(); foreach (var issueProvider in issueProviders.Value) { Severity severity; if (!settings.TryGetValue(issueProvider.ProviderType, out severity)) severity = Severity.Hint; if (severity == Severity.None) continue; foreach (var issue in issueProvider.GetIssues(context)) { results.Add(new InspectionTag( this, issueProvider, textSource.Version, issue.Desription, context.GetOffset(issue.Start), context.GetOffset(issue.End), severity, issue.Action != null ? new [] { issue.Action } : new CodeAction[0])); } } }, cancellationToken); } catch (TaskCanceledException) { } if (!cancellationToken.IsCancellationRequested) { analyzedVersion = textSource.Version; Clear(); foreach (var newResult in results) { newResult.CreateMarker(editor.Document, markerService); } existingResults = results; } cancellationTokenSource.Dispose(); cancellationTokenSource = null; } #region IContextActionProvider implementation string IContextActionProvider.ID { get { return "C# IssueManager"; } } string IContextActionProvider.DisplayName { get { return "C# IssueManager"; } } string IContextActionProvider.Category { get { return string.Empty; } } bool IContextActionProvider.AllowHiding { get { return false; } } bool IContextActionProvider.IsVisible { get { return true; } set { } } Task IContextActionProvider.GetAvailableActionsAsync(EditorRefactoringContext context, CancellationToken cancellationToken) { List result = new List(); if (existingResults != null) { var markers = markerService.GetMarkersAtOffset(context.CaretOffset); foreach (var tag in markers.Select(m => m.Tag).OfType()) { result.AddRange(tag.Actions); } } return Task.FromResult(result.ToArray()); } #endregion } }