|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Microsoft.VisualStudio.Shell.TableControl;
using Microsoft.VisualStudio.Shell.TableManager;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
{
internal partial class VisualStudioDiagnosticListTableWorkspaceEventListener
{
internal partial class VisualStudioDiagnosticListTable : VisualStudioBaseDiagnosticListTable
{
/// <summary>
/// Error list diagnostic source for "Build only" setting.
/// See <see cref="VisualStudioBaseDiagnosticListTable.LiveTableDataSource"/>
/// for error list diagnostic source for "Build + Intellisense" setting.
/// </summary>
private class BuildTableDataSource : AbstractTableDataSource<DiagnosticTableItem, object>
{
private readonly object _key = new();
private readonly ExternalErrorDiagnosticUpdateSource _buildErrorSource;
public BuildTableDataSource(Workspace workspace, IThreadingContext threadingContext, ExternalErrorDiagnosticUpdateSource errorSource)
: base(workspace, threadingContext)
{
_buildErrorSource = errorSource;
ConnectToBuildUpdateSource(errorSource);
}
private void ConnectToBuildUpdateSource(ExternalErrorDiagnosticUpdateSource errorSource)
{
if (errorSource == null)
{
// it can be null in unit test
return;
}
SetStableState(errorSource.IsInProgress);
errorSource.BuildProgressChanged += OnBuildProgressChanged;
}
private void OnBuildProgressChanged(object sender, ExternalErrorDiagnosticUpdateSource.BuildProgress progress)
{
SetStableState(progress == ExternalErrorDiagnosticUpdateSource.BuildProgress.Done);
if (progress != ExternalErrorDiagnosticUpdateSource.BuildProgress.Started)
{
OnDataAddedOrChanged(_key);
}
}
private void SetStableState(bool done)
{
IsStable = done;
ChangeStableState(IsStable);
}
public override string DisplayName => ServicesVSResources.CSharp_VB_Build_Table_Data_Source;
public override string SourceTypeIdentifier => StandardTableDataSources.ErrorTableDataSource;
public override string Identifier => IdentifierString;
public override object GetItemKey(object data) => data;
protected override object GetOrUpdateAggregationKey(object data)
=> data;
public override AbstractTableEntriesSource<DiagnosticTableItem> CreateTableEntriesSource(object data)
=> new TableEntriesSource(this);
public override AbstractTableEntriesSnapshot<DiagnosticTableItem> CreateSnapshot(AbstractTableEntriesSource<DiagnosticTableItem> source, int version, ImmutableArray<DiagnosticTableItem> items, ImmutableArray<ITrackingPoint> trackingPoints)
{
// Build doesn't support tracking point.
return new TableEntriesSnapshot(ThreadingContext, (DiagnosticTableEntriesSource)source, version, items);
}
public override IEqualityComparer<DiagnosticTableItem> GroupingComparer
=> DiagnosticTableItem.GroupingComparer.Instance;
public override IEnumerable<DiagnosticTableItem> Order(IEnumerable<DiagnosticTableItem> groupedItems)
{
// errors are already given in order. use it as it is.
return groupedItems;
}
private class TableEntriesSource : DiagnosticTableEntriesSource
{
private readonly BuildTableDataSource _source;
public TableEntriesSource(BuildTableDataSource source)
=> _source = source;
public override object Key => _source._key;
public override string BuildTool => PredefinedBuildTools.Build;
public override bool SupportSpanTracking => false;
public override DocumentId TrackingDocumentId => throw ExceptionUtilities.Unreachable();
public override ImmutableArray<DiagnosticTableItem> GetItems()
{
return _source.AggregateItems(
_source._buildErrorSource.GetBuildErrors().
GroupBy(d => d, d => DiagnosticTableItem.Create(_source.Workspace, d), DiagnosticTableItem.GroupingComparer.Instance));
}
public override ImmutableArray<ITrackingPoint> GetTrackingPoints(ImmutableArray<DiagnosticTableItem> items)
=> ImmutableArray<ITrackingPoint>.Empty;
}
private class TableEntriesSnapshot : AbstractTableEntriesSnapshot<DiagnosticTableItem>, IWpfTableEntriesSnapshot
{
private readonly DiagnosticTableEntriesSource _source;
public TableEntriesSnapshot(
IThreadingContext threadingContext,
DiagnosticTableEntriesSource source, int version, ImmutableArray<DiagnosticTableItem> items)
: base(threadingContext, version, items, ImmutableArray<ITrackingPoint>.Empty)
{
_source = source;
}
public override bool TryGetValue(int index, string columnName, [NotNullWhen(true)] out object? content)
{
// REVIEW: this method is too-chatty to make async, but otherwise, how one can implement it async?
// also, what is cancellation mechanism?
var item = GetItem(index);
if (item == null)
{
content = null;
return false;
}
var data = item.Data;
switch (columnName)
{
case StandardTableKeyNames.ErrorRank:
// build error gets highest rank
content = ValueTypeCache.GetOrCreate(ErrorRank.Lexical);
return content != null;
case StandardTableKeyNames.ErrorSeverity:
content = ValueTypeCache.GetOrCreate(GetErrorCategory(data.Severity));
return content != null;
case StandardTableKeyNames.ErrorCode:
content = data.Id;
return content != null;
case StandardTableKeyNames.ErrorCodeToolTip:
content = (data.GetValidHelpLinkUri() != null) ? string.Format(EditorFeaturesResources.Get_help_for_0, data.Id) : null;
return content != null;
case StandardTableKeyNames.HelpKeyword:
content = data.GetHelpKeyword();
return content != null;
case StandardTableKeyNames.HelpLink:
content = data.GetValidHelpLinkUri()?.AbsoluteUri;
return content != null;
case StandardTableKeyNames.ErrorCategory:
content = data.Category;
return content != null;
case StandardTableKeyNames.ErrorSource:
content = ValueTypeCache.GetOrCreate(ErrorSource.Build);
return content != null;
case StandardTableKeyNames.BuildTool:
content = _source.BuildTool;
return content != null;
case StandardTableKeyNames.Text:
content = data.Message;
return content != null;
case StandardTableKeyNames.DocumentName:
content = data.DataLocation.MappedFileSpan.Path;
return content != null;
case StandardTableKeyNames.Line:
content = data.DataLocation.MappedFileSpan.StartLinePosition.Line;
return true;
case StandardTableKeyNames.Column:
content = data.DataLocation.MappedFileSpan.StartLinePosition.Character;
return true;
case StandardTableKeyNames.ProjectName:
content = item.ProjectName;
return content != null;
case ProjectNames:
var names = item.ProjectNames;
content = names;
return names.Length > 0;
case StandardTableKeyNames.ProjectGuid:
content = ValueTypeCache.GetOrCreate(item.ProjectGuid);
return (Guid)content != Guid.Empty;
case ProjectGuids:
var guids = item.ProjectGuids;
content = guids;
return guids.Length > 0;
case StandardTableKeyNames.SuppressionState:
// Build doesn't support suppression.
Contract.ThrowIfTrue(data.IsSuppressed);
content = SuppressionState.NotApplicable;
return true;
default:
content = null;
return false;
}
}
public override bool TryNavigateTo(int index, NavigationOptions options, CancellationToken cancellationToken)
{
var item = GetItem(index);
if (item is null)
return false;
var documentId = GetProperDocumentId(ThreadingContext, item, cancellationToken);
if (documentId is null)
return false;
return TryNavigateTo(item.Workspace, documentId, item.GetOriginalPosition(), options, cancellationToken);
}
private static DocumentId? GetProperDocumentId(IThreadingContext threadingContext, DiagnosticTableItem item, CancellationToken cancellationToken)
{
var documentId = item.DocumentId;
var projectId = item.ProjectId;
// check whether documentId still exist. it might have changed if project it belong to has reloaded.
var solution = item.Workspace.CurrentSolution;
if (solution.GetDocument(documentId) != null)
{
return documentId;
}
if (solution.GetProject(projectId) is { } project)
{
// We couldn't find a document matching a known ID when the item was created, so it may be a
// source generator output.
var documents = threadingContext.JoinableTaskFactory.Run(() => project.GetSourceGeneratedDocumentsAsync(cancellationToken).AsTask());
if (documentId is not null)
{
if (documents.Any(document => document.Id == documentId))
return documentId;
}
else
{
var projectDirectory = Path.GetDirectoryName(project.FilePath);
documentId = documents.FirstOrDefault(document => Path.Combine(projectDirectory, document.FilePath) == item.GetOriginalFilePath())?.Id;
if (documentId is not null)
return documentId;
}
}
// okay, documentId no longer exist in current solution, find it by file path.
var filePath = item.GetOriginalFilePath();
var documentIds = solution.GetDocumentIdsWithFilePath(filePath);
foreach (var id in documentIds)
{
// found right project
if (id.ProjectId == projectId)
{
return id;
}
}
// okay, there is no right one, take the first one if there is any
return documentIds.FirstOrDefault();
}
#region IWpfTableEntriesSnapshot
public bool CanCreateDetailsContent(int index)
=> CanCreateDetailsContent(index, GetItem);
public bool TryCreateDetailsContent(int index, [NotNullWhen(returnValue: true)] out FrameworkElement? expandedContent)
=> TryCreateDetailsContent(index, GetItem, out expandedContent);
public bool TryCreateDetailsStringContent(int index, [NotNullWhen(returnValue: true)] out string? content)
=> TryCreateDetailsStringContent(index, GetItem, out content);
// unused ones
public bool TryCreateColumnContent(int index, string columnName, bool singleColumnView, [NotNullWhen(returnValue: true)] out FrameworkElement? content)
{
content = null;
return false;
}
public bool TryCreateImageContent(int index, string columnName, bool singleColumnView, out ImageMoniker content)
{
content = default;
return false;
}
public bool TryCreateStringContent(int index, string columnName, bool truncatedText, bool singleColumnView, [NotNullWhen(returnValue: true)] out string? content)
{
content = null;
return false;
}
public bool TryCreateToolTip(int index, string columnName, [NotNullWhen(returnValue: true)] out object? toolTip)
{
toolTip = null;
return false;
}
#endregion
}
}
}
}
}
|