File: Diagnostics\VisualStudioVenusSpanMappingService.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_zthhlzqo_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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); }
        }
    }
}