|
// 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.
#nullable disable
using System;
using System.Composition;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.Implementation.Venus;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
[ExportWorkspaceService(typeof(IWorkspaceVenusSpanMappingService), ServiceLayer.Default), Shared]
internal partial class VisualStudioVenusSpanMappingService : IWorkspaceVenusSpanMappingService
{
private readonly VisualStudioWorkspaceImpl _workspace;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VisualStudioVenusSpanMappingService(VisualStudioWorkspaceImpl workspace)
=> _workspace = workspace;
public void GetAdjustedDiagnosticSpan(
DocumentId documentId, Location location,
out TextSpan sourceSpan, out FileLinePositionSpan originalLineInfo, out FileLinePositionSpan mappedLineInfo)
{
sourceSpan = location.SourceSpan;
originalLineInfo = location.GetLineSpan();
mappedLineInfo = location.GetMappedLineSpan();
// check quick bail out case.
if (location == Location.None)
{
return;
}
// Update the original source span, if required.
if (!TryAdjustSpanIfNeededForVenus(documentId, originalLineInfo, mappedLineInfo, out var originalSpan, out var mappedSpan))
{
return;
}
if (originalSpan.Start != originalLineInfo.StartLinePosition || originalSpan.End != originalLineInfo.EndLinePosition)
{
originalLineInfo = new FileLinePositionSpan(originalLineInfo.Path, originalSpan.Start, originalSpan.End);
var textLines = GetTextLines(documentId, location);
if (textLines != null)
{
// adjust sourceSpan only if we could get text lines
var startPos = textLines.GetPosition(originalSpan.Start);
var endPos = textLines.GetPosition(originalSpan.End);
sourceSpan = TextSpan.FromBounds(startPos, Math.Max(startPos, endPos));
}
}
if (mappedSpan.Start != mappedLineInfo.StartLinePosition || mappedSpan.End != mappedLineInfo.EndLinePosition)
{
mappedLineInfo = new FileLinePositionSpan(mappedLineInfo.Path, mappedSpan.Start, mappedSpan.End);
}
}
private TextLineCollection GetTextLines(DocumentId currentDocumentId, Location location)
{
// normal case - all C# and VB should hit this
if (location.SourceTree != null)
{
return location.SourceTree.GetText().Lines;
}
// special case for typescript and etc that don't use our compilations.
var filePath = location.GetLineSpan().Path;
if (filePath != null)
{
// as a sanity check, make sure given location is on the current document
// we do the check down the stack for C# and VB using SyntaxTree in location
// but for typescript and other, we don't have the tree, so adding this as
// sanity check. later we could convert this to Contract to crash VS and
// know about the issue.
var solution = _workspace.CurrentSolution;
var documentIds = solution.GetDocumentIdsWithFilePath(filePath);
if (documentIds.Contains(currentDocumentId))
{
// text most likely already read in
return solution.GetDocument(currentDocumentId).State.GetTextSynchronously(CancellationToken.None).Lines;
}
}
// we don't know how to get text lines for the given location
return null;
}
private static bool TryAdjustSpanIfNeededForVenus(
DocumentId documentId, FileLinePositionSpan originalLineInfo, FileLinePositionSpan mappedLineInfo, out LinePositionSpan originalSpan, out LinePositionSpan mappedSpan)
{
var startChanged = true;
if (!TryAdjustSpanIfNeededForVenus(documentId, originalLineInfo.StartLinePosition.Line, originalLineInfo.StartLinePosition.Character, out var startLineColumn))
{
startChanged = false;
startLineColumn = new MappedSpan(originalLineInfo.StartLinePosition.Line, originalLineInfo.StartLinePosition.Character, mappedLineInfo.StartLinePosition.Line, mappedLineInfo.StartLinePosition.Character);
}
var endChanged = true;
if (!TryAdjustSpanIfNeededForVenus(documentId, originalLineInfo.EndLinePosition.Line, originalLineInfo.EndLinePosition.Character, out var endLineColumn))
{
endChanged = false;
endLineColumn = new MappedSpan(originalLineInfo.EndLinePosition.Line, originalLineInfo.EndLinePosition.Character, mappedLineInfo.EndLinePosition.Line, mappedLineInfo.EndLinePosition.Character);
}
// start and end position can be swapped when mapped between primary and secondary buffer if start position is within visible span (at the edge)
// but end position is outside of visible span. in that case, swap start and end position.
originalSpan = GetLinePositionSpan(startLineColumn.OriginalLinePosition, endLineColumn.OriginalLinePosition);
mappedSpan = GetLinePositionSpan(startLineColumn.MappedLinePosition, endLineColumn.MappedLinePosition);
return startChanged || endChanged;
}
private static LinePositionSpan GetLinePositionSpan(LinePosition position1, LinePosition position2)
{
if (position1 <= position2)
{
return new LinePositionSpan(position1, position2);
}
return new LinePositionSpan(position2, position1);
}
public static LinePosition GetAdjustedLineColumn(DocumentId documentId, int originalLine, int originalColumn, int mappedLine, int mappedColumn)
{
if (TryAdjustSpanIfNeededForVenus(documentId, originalLine, originalColumn, out var span))
{
return span.MappedLinePosition;
}
return new LinePosition(mappedLine, mappedColumn);
}
private static bool TryAdjustSpanIfNeededForVenus(DocumentId documentId, int originalLine, int originalColumn, out MappedSpan mappedSpan)
{
mappedSpan = default;
if (documentId == null)
{
return false;
}
var containedDocument = ContainedDocument.TryGetContainedDocument(documentId);
if (containedDocument == null)
{
return false;
}
var originalSpanOnSecondaryBuffer = new TextManager.Interop.TextSpan()
{
iStartLine = originalLine,
iStartIndex = originalColumn,
iEndLine = originalLine,
iEndIndex = originalColumn
};
var bufferCoordinator = containedDocument.BufferCoordinator;
var containedLanguageHost = containedDocument.ContainedLanguageHost;
var spansOnPrimaryBuffer = new TextManager.Interop.TextSpan[1];
if (VSConstants.S_OK == bufferCoordinator.MapSecondaryToPrimarySpan(originalSpanOnSecondaryBuffer, spansOnPrimaryBuffer))
{
// easy case, we can map span in subject buffer to surface buffer. no need to adjust any span
mappedSpan = new MappedSpan(originalLine, originalColumn, spansOnPrimaryBuffer[0].iStartLine, spansOnPrimaryBuffer[0].iStartIndex);
return true;
}
// we can't directly map span in subject buffer to surface buffer. see whether there is any visible span we can use from the subject buffer span
if (containedLanguageHost != null &&
VSConstants.S_OK != containedLanguageHost.GetNearestVisibleToken(originalSpanOnSecondaryBuffer, spansOnPrimaryBuffer))
{
// no visible span we can use.
return false;
}
// We need to map both the original and mapped location into visible code so that features such as error list, squiggle, etc. points to user visible area
// We have the mapped location in the primary buffer.
var nearestVisibleSpanOnPrimaryBuffer = new TextManager.Interop.TextSpan()
{
iStartLine = spansOnPrimaryBuffer[0].iStartLine,
iStartIndex = spansOnPrimaryBuffer[0].iStartIndex,
iEndLine = spansOnPrimaryBuffer[0].iStartLine,
iEndIndex = spansOnPrimaryBuffer[0].iStartIndex
};
// Map this location back to the secondary span to re-adjust the original location to be in user-code in secondary buffer.
var spansOnSecondaryBuffer = new TextManager.Interop.TextSpan[1];
if (VSConstants.S_OK != bufferCoordinator.MapPrimaryToSecondarySpan(nearestVisibleSpanOnPrimaryBuffer, spansOnSecondaryBuffer))
{
// we can't adjust original position but we can adjust mapped one
mappedSpan = new MappedSpan(originalLine, originalColumn, nearestVisibleSpanOnPrimaryBuffer.iStartLine, nearestVisibleSpanOnPrimaryBuffer.iStartIndex);
return true;
}
var nearestVisibleSpanOnSecondaryBuffer = spansOnSecondaryBuffer[0];
var originalLocationMovedAboveInFile = IsOriginalLocationMovedAboveInFile(originalLine, originalColumn, nearestVisibleSpanOnSecondaryBuffer.iStartLine, nearestVisibleSpanOnSecondaryBuffer.iStartIndex);
if (!originalLocationMovedAboveInFile)
{
mappedSpan = new MappedSpan(nearestVisibleSpanOnSecondaryBuffer.iStartLine, nearestVisibleSpanOnSecondaryBuffer.iStartIndex, nearestVisibleSpanOnPrimaryBuffer.iStartLine, nearestVisibleSpanOnPrimaryBuffer.iStartIndex);
return true;
}
if (TryFixUpNearestVisibleSpan(bufferCoordinator, nearestVisibleSpanOnSecondaryBuffer.iStartLine, nearestVisibleSpanOnSecondaryBuffer.iStartIndex, out var adjustedPosition))
{
// span has changed yet again, re-calculate span
return TryAdjustSpanIfNeededForVenus(documentId, adjustedPosition.Line, adjustedPosition.Character, out mappedSpan);
}
mappedSpan = new MappedSpan(nearestVisibleSpanOnSecondaryBuffer.iStartLine, nearestVisibleSpanOnSecondaryBuffer.iStartIndex, nearestVisibleSpanOnPrimaryBuffer.iStartLine, nearestVisibleSpanOnPrimaryBuffer.iStartIndex);
return true;
}
private static bool TryFixUpNearestVisibleSpan(
TextManager.Interop.IVsTextBufferCoordinator bufferCoordinator,
int originalLine, int originalColumn, out LinePosition adjustedPosition)
{
// GetNearestVisibleToken gives us the position right at the end of visible span.
// Move the position one position to the left so that squiggle can show up on last token.
if (originalColumn > 1)
{
adjustedPosition = new LinePosition(originalLine, originalColumn - 1);
return true;
}
if (originalLine > 1)
{
if (VSConstants.S_OK == bufferCoordinator.GetSecondaryBuffer(out var secondaryBuffer) &&
VSConstants.S_OK == secondaryBuffer.GetLengthOfLine(originalLine - 1, out var length))
{
adjustedPosition = new LinePosition(originalLine - 1, length);
return true;
}
}
adjustedPosition = LinePosition.Zero;
return false;
}
private static bool IsOriginalLocationMovedAboveInFile(int originalLine, int originalColumn, int movedLine, int movedColumn)
{
if (movedLine < originalLine)
{
return true;
}
if (movedLine == originalLine && movedColumn < originalColumn)
{
return true;
}
return false;
}
private readonly struct MappedSpan
{
private readonly int _originalLine;
private readonly int _originalColumn;
private readonly int _mappedLine;
private readonly int _mappedColumn;
public MappedSpan(int originalLine, int originalColumn, int mappedLine, int mappedColumn)
{
_originalLine = originalLine;
_originalColumn = originalColumn;
_mappedLine = mappedLine;
_mappedColumn = mappedColumn;
}
public LinePosition OriginalLinePosition
{
get { return new LinePosition(_originalLine, _originalColumn); }
}
public LinePosition MappedLinePosition
{
get { return new LinePosition(_mappedLine, _mappedColumn); }
}
}
}
|