|
// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Shell.TableManager;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
{
/// <summary>
/// Base implementation of ITableEntriesSnapshot
/// </summary>
internal abstract class AbstractTableEntriesSnapshot<TItem> : ITableEntriesSnapshot
where TItem : TableItem
{
// TODO : remove these once we move to new drop which contains API change from editor team
protected const string ProjectNames = StandardTableKeyNames.ProjectName + "s";
protected const string ProjectGuids = StandardTableKeyNames.ProjectGuid + "s";
private readonly int _version;
private readonly ImmutableArray<TItem> _items;
private ImmutableArray<ITrackingPoint> _trackingPoints;
private FrameworkElement[]? _descriptions;
protected AbstractTableEntriesSnapshot(IThreadingContext threadingContext, int version, ImmutableArray<TItem> items, ImmutableArray<ITrackingPoint> trackingPoints)
{
ThreadingContext = threadingContext;
_version = version;
_items = items;
_trackingPoints = trackingPoints;
}
public abstract bool TryNavigateTo(int index, NavigationOptions options, CancellationToken cancellationToken);
public abstract bool TryGetValue(int index, string columnName, [NotNullWhen(true)] out object? content);
public int VersionNumber
{
get
{
return _version;
}
}
public int Count
{
get
{
return _items.Length;
}
}
protected IThreadingContext ThreadingContext { get; }
public int IndexOf(int index, ITableEntriesSnapshot newerSnapshot)
{
var item = GetItem(index);
if (item == null)
{
return -1;
}
if (newerSnapshot is not AbstractTableEntriesSnapshot<TItem> ourSnapshot || ourSnapshot.Count == 0)
{
// not ours, we don't know how to track index
return -1;
}
// quick path - this will deal with a case where we update data without any actual change
if (Count == ourSnapshot.Count)
{
var newItem = ourSnapshot.GetItem(index);
if (newItem != null && newItem.Equals(item))
{
return index;
}
}
// slow path.
for (var i = 0; i < ourSnapshot.Count; i++)
{
var newItem = ourSnapshot.GetItem(i);
// GetItem only returns null for index out of range
RoslynDebug.AssertNotNull(newItem);
if (item.EqualsIgnoringLocation(newItem))
{
return i;
}
}
// no similar item exist. table control itself will try to maintain selection
return -1;
}
public void StopTracking()
{
// remove tracking points
_trackingPoints = default;
}
public void Dispose()
=> StopTracking();
internal TItem? GetItem(int index)
{
if (index < 0 || _items.Length <= index)
{
return null;
}
return _items[index];
}
protected LinePosition GetTrackingLineColumn(Document document, int index)
{
if (_trackingPoints.IsDefaultOrEmpty)
{
return LinePosition.Zero;
}
var trackingPoint = _trackingPoints[index];
if (!document.TryGetText(out var text))
{
return LinePosition.Zero;
}
var snapshot = text.FindCorrespondingEditorTextSnapshot();
if (snapshot != null)
{
return GetLinePosition(snapshot, trackingPoint);
}
var textBuffer = text.Container.TryGetTextBuffer();
if (textBuffer == null)
{
return LinePosition.Zero;
}
var currentSnapshot = textBuffer.CurrentSnapshot;
return GetLinePosition(currentSnapshot, trackingPoint);
}
private static LinePosition GetLinePosition(ITextSnapshot snapshot, ITrackingPoint trackingPoint)
{
var point = trackingPoint.GetPoint(snapshot);
var line = point.GetContainingLine();
return new LinePosition(line.LineNumber, point.Position - line.Start);
}
protected bool TryNavigateTo(Workspace workspace, DocumentId documentId, LinePosition position, NavigationOptions options, CancellationToken cancellationToken)
{
var navigationService = workspace.Services.GetService<IDocumentNavigationService>();
if (navigationService == null)
return false;
return this.ThreadingContext.JoinableTaskFactory.Run(() =>
navigationService.TryNavigateToLineAndOffsetAsync(
this.ThreadingContext, workspace, documentId, position.Line, position.Character, options, cancellationToken));
}
protected bool TryNavigateToItem(int index, NavigationOptions options, CancellationToken cancellationToken)
{
var item = GetItem(index);
if (item is null)
return false;
var workspace = item.Workspace;
var solution = workspace.CurrentSolution;
var documentId = item.DocumentId;
if (documentId is null)
{
if (solution.GetProject(item.ProjectId) is { } project)
{
// We couldn't find a document ID when the item was created, so it may be a source generator output.
var documents = ThreadingContext.JoinableTaskFactory.Run(() => project.GetSourceGeneratedDocumentsAsync(cancellationToken).AsTask());
var projectDirectory = Path.GetDirectoryName(project.FilePath);
documentId = documents.FirstOrDefault(document => Path.Combine(projectDirectory, document.FilePath) == item.GetOriginalFilePath())?.Id;
if (documentId is null)
return false;
}
else
{
return false;
}
}
LinePosition position;
var document = solution.GetDocument(documentId);
if (document is not null
&& workspace.IsDocumentOpen(documentId)
&& GetTrackingLineColumn(document, index) is { } trackingLinePosition
&& trackingLinePosition != LinePosition.Zero)
{
// For normal documents already open, try to map the diagnostic location to its current position in a
// potentially-edited document.
position = trackingLinePosition;
}
else
{
// Otherwise navigate to the original reported location.
position = item.GetOriginalPosition();
}
return TryNavigateTo(workspace, documentId, position, options, cancellationToken);
}
// we don't use these
#pragma warning disable IDE0060 // Remove unused parameter - Implements interface method for sub-type
public object? Identity(int index)
#pragma warning restore IDE0060 // Remove unused parameter
=> null;
public void StartCaching()
{
}
public void StopCaching()
{
}
protected static bool CanCreateDetailsContent(int index, Func<int, DiagnosticTableItem?> getDiagnosticTableItem)
{
var item = getDiagnosticTableItem(index)?.Data;
if (item == null)
{
return false;
}
return !string.IsNullOrWhiteSpace(item.Description);
}
protected bool TryCreateDetailsContent(int index, Func<int, DiagnosticTableItem?> getDiagnosticTableItem, [NotNullWhen(returnValue: true)] out FrameworkElement? expandedContent)
{
var item = getDiagnosticTableItem(index)?.Data;
if (item == null)
{
expandedContent = null;
return false;
}
expandedContent = GetOrCreateTextBlock(ref _descriptions, this.Count, index, item, i => GetDescriptionTextBlock(i));
return true;
}
protected static bool TryCreateDetailsStringContent(int index, Func<int, DiagnosticTableItem?> getDiagnosticTableItem, [NotNullWhen(returnValue: true)] out string? content)
{
var item = getDiagnosticTableItem(index)?.Data;
if (item == null)
{
content = null;
return false;
}
if (string.IsNullOrWhiteSpace(item.Description))
{
content = null;
return false;
}
content = item.Description;
return content != null;
}
private static FrameworkElement GetDescriptionTextBlock(DiagnosticData item)
{
return new TextBlock()
{
Background = null,
Padding = new Thickness(10, 6, 10, 8),
TextWrapping = TextWrapping.Wrap,
Text = item.Description
};
}
private static FrameworkElement GetOrCreateTextBlock(
[NotNull] ref FrameworkElement[]? caches, int count, int index, DiagnosticData item, Func<DiagnosticData, FrameworkElement> elementCreator)
{
caches ??= new FrameworkElement[count];
if (caches[index] == null)
{
caches[index] = elementCreator(item);
}
return caches[index];
}
}
}
|