File: DebuggerIntelliSense\AbstractDebuggerIntelliSenseContext.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_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.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.DebuggerIntelliSense;
 
internal abstract class AbstractDebuggerIntelliSenseContext : IDisposable
{
    private readonly IWpfTextView _textView;
    private readonly IContentType _originalContentType;
    protected readonly IProjectionBufferFactoryService ProjectionBufferFactoryService;
    protected readonly TextManager.Interop.TextSpan CurrentStatementSpan;
    private IProjectionBuffer _projectionBuffer;
    private DebuggerTextView _debuggerTextView;
    private DebuggerIntelliSenseWorkspace _workspace;
    private ImmediateWindowContext _immediateWindowContext;
    private readonly IBufferGraphFactoryService _bufferGraphFactoryService;
    private readonly bool _isImmediateWindow;
 
    private class ImmediateWindowContext
    {
        public int CurrentLineIndex = -1;
        public int QuestionIndex = -2;
        public IProjectionBuffer ElisionBuffer;
        public IProjectionBuffer ProjectionBuffer;
    }
 
    protected AbstractDebuggerIntelliSenseContext(
        IWpfTextView wpfTextView,
        IVsTextView vsTextView,
        IVsTextLines vsDebuggerTextLines,
        ITextBuffer contextBuffer,
        TextManager.Interop.TextSpan[] currentStatementSpan,
        IComponentModel componentModel,
        IServiceProvider serviceProvider,
        IContentType contentType)
    {
        _textView = wpfTextView;
        DebuggerTextLines = vsDebuggerTextLines;
        this.ContextBuffer = contextBuffer;
        this.CurrentStatementSpan = currentStatementSpan[0];
        ContentType = contentType;
        _originalContentType = _textView.TextBuffer.ContentType;
        this.ProjectionBufferFactoryService = componentModel.GetService<IProjectionBufferFactoryService>();
        _bufferGraphFactoryService = componentModel.GetService<IBufferGraphFactoryService>();
        _isImmediateWindow = IsImmediateWindow((IVsUIShell)serviceProvider.GetService(typeof(SVsUIShell)), vsTextView);
    }
 
    // Constructor for testing
    protected AbstractDebuggerIntelliSenseContext(
        IWpfTextView wpfTextView,
        ITextBuffer contextBuffer,
        Microsoft.VisualStudio.TextManager.Interop.TextSpan[] currentStatementSpan,
        IComponentModel componentModel,
        IContentType contentType,
        bool isImmediateWindow)
    {
        _textView = wpfTextView;
        this.ContextBuffer = contextBuffer;
        this.CurrentStatementSpan = currentStatementSpan[0];
        ContentType = contentType;
        this.ProjectionBufferFactoryService = componentModel.GetService<IProjectionBufferFactoryService>();
        _bufferGraphFactoryService = componentModel.GetService<IBufferGraphFactoryService>();
        _isImmediateWindow = isImmediateWindow;
    }
 
    public IVsTextLines DebuggerTextLines { get; }
 
    public ITextView DebuggerTextView { get { return _debuggerTextView; } }
 
    public ITextBuffer Buffer { get { return _projectionBuffer; } }
 
    public IContentType ContentType { get; }
 
    protected bool InImmediateWindow { get { return _immediateWindowContext != null; } }
 
    internal ITextBuffer ContextBuffer { get; private set; }
 
    public abstract bool CompletionStartsOnQuestionMark { get; }
 
    protected abstract string StatementTerminator { get; }
 
    protected abstract int GetAdjustedContextPoint(int contextPoint, Document document);
 
    protected abstract ITrackingSpan GetPreviousStatementBufferAndSpan(int lastTokenEndPoint, Document document);
 
    // Since the immediate window doesn't actually tell us when we change lines, we'll have to
    // determine ourselves when to rebuild our tracking spans to include only the last (input)
    // line of the buffer.
    public void RebuildSpans()
    {
        // Not in the immediate window, no work to do.
        if (!this.InImmediateWindow)
        {
            return;
        }
 
        // Reset the question mark location, since we may have to search for one again.
        _immediateWindowContext.QuestionIndex = -2;
        SetupImmediateWindowProjectionBuffer();
    }
 
    internal bool TryInitialize()
        => this.TrySetContext(_isImmediateWindow);
 
    private bool TrySetContext(
        bool isImmediateWindow)
    {
        // Get the workspace, and from there, the solution and document containing this buffer.
        // If there's an ExternalSource, we won't get a document. Give up in that case.
        var document = ContextBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        if (document == null)
        {
            _projectionBuffer = null;
            _debuggerTextView = null;
            _workspace = null;
            _immediateWindowContext = null;
            return false;
        }
 
        var solution = document.Project.Solution;
 
        // Get the appropriate ITrackingSpan for the window the user is typing in
        var viewSnapshot = _textView.TextSnapshot;
        _immediateWindowContext = null;
        var debuggerMappedSpan = isImmediateWindow
            ? CreateImmediateWindowProjectionMapping(out _immediateWindowContext)
            : viewSnapshot.CreateFullTrackingSpan(SpanTrackingMode.EdgeInclusive);
 
        // Wrap the original ContextBuffer in a projection buffer that we can make read-only
        this.ContextBuffer = this.ProjectionBufferFactoryService.CreateProjectionBuffer(null,
            [this.ContextBuffer.CurrentSnapshot.CreateFullTrackingSpan(SpanTrackingMode.EdgeInclusive)], ProjectionBufferOptions.None, ContentType);
 
        // Make projection readonly so we can't edit it by mistake.
        using (var regionEdit = this.ContextBuffer.CreateReadOnlyRegionEdit())
        {
            regionEdit.CreateReadOnlyRegion(new Span(0, this.ContextBuffer.CurrentSnapshot.Length), SpanTrackingMode.EdgeInclusive, EdgeInsertionMode.Deny);
            regionEdit.Apply();
        }
 
        // Adjust the context point to ensure that the right information is in scope.
        // For example, we may need to move the point to the end of the last statement in a method body
        // in order to be able to access all local variables.
        var contextPoint = this.ContextBuffer.CurrentSnapshot.GetLineFromLineNumber(CurrentStatementSpan.iEndLine).Start + CurrentStatementSpan.iEndIndex;
        var adjustedContextPoint = GetAdjustedContextPoint(contextPoint, document);
 
        // Get the previous span/text. We might have to insert another newline or something.
        var previousStatementSpan = GetPreviousStatementBufferAndSpan(adjustedContextPoint, document);
 
        // Build the tracking span that includes the rest of the file
        var restOfFileSpan = ContextBuffer.CurrentSnapshot.CreateTrackingSpanFromIndexToEnd(adjustedContextPoint, SpanTrackingMode.EdgePositive);
 
        // Put it all into a projection buffer
        _projectionBuffer = this.ProjectionBufferFactoryService.CreateProjectionBuffer(null,
            [previousStatementSpan, debuggerMappedSpan, this.StatementTerminator, restOfFileSpan], ProjectionBufferOptions.None, ContentType);
 
        // Fork the solution using this new primary buffer for the document and all of its linked documents.
        var forkedSolution = solution.WithDocumentText(document.Id, _projectionBuffer.CurrentSnapshot.AsText(), PreservationMode.PreserveIdentity);
        foreach (var link in document.GetLinkedDocumentIds())
        {
            forkedSolution = forkedSolution.WithDocumentText(link, _projectionBuffer.CurrentSnapshot.AsText(), PreservationMode.PreserveIdentity);
        }
 
        // Put it into a new workspace, and open it and its related documents
        // with the projection buffer as the text.
        _workspace = new DebuggerIntelliSenseWorkspace(forkedSolution);
        _workspace.OpenDocument(document.Id, _projectionBuffer.AsTextContainer());
        foreach (var link in document.GetLinkedDocumentIds())
        {
            _workspace.OpenDocument(link, _projectionBuffer.AsTextContainer());
        }
 
        // Start getting the compilation so the PartialSolution will be ready when the user starts typing in the window
        document.Project.GetCompilationAsync(System.Threading.CancellationToken.None);
 
        _textView.TextBuffer.ChangeContentType(ContentType, null);
 
        var bufferGraph = _bufferGraphFactoryService.CreateBufferGraph(_projectionBuffer);
 
        _debuggerTextView = new DebuggerTextView(_textView, bufferGraph, DebuggerTextLines, InImmediateWindow);
        return true;
    }
 
    internal void SetContentType(bool install)
    {
        var contentType = install ? ContentType : _originalContentType;
        _textView.TextBuffer.ChangeContentType(contentType, null);
    }
 
    private ITrackingSpan CreateImmediateWindowProjectionMapping(out ImmediateWindowContext immediateWindowContext)
    {
        var caretLine = _textView.Caret.ContainingTextViewLine.Extent;
        var currentLineIndex = _textView.TextSnapshot.GetLineNumberFromPosition(caretLine.Start.Position);
 
        var debuggerMappedSpan = _textView.TextSnapshot.CreateFullTrackingSpan(SpanTrackingMode.EdgeInclusive);
        var projectionBuffer = this.ProjectionBufferFactoryService.CreateProjectionBuffer(null,
            [debuggerMappedSpan], ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans, ContentType);
 
        // There's currently a bug in the editor (515925) where an elision buffer can't be projected into
        // another projection buffer.  So workaround by using a second projection buffer that only 
        // projects the text we care about
        var elisionProjectionBuffer = this.ProjectionBufferFactoryService.CreateProjectionBuffer(null,
            [projectionBuffer.CurrentSnapshot.CreateFullTrackingSpan(SpanTrackingMode.EdgeInclusive)],
            ProjectionBufferOptions.None, ContentType);
 
        immediateWindowContext = new ImmediateWindowContext()
        {
            ProjectionBuffer = projectionBuffer,
            ElisionBuffer = elisionProjectionBuffer
        };
 
        _textView.TextBuffer.PostChanged += TextBuffer_PostChanged;
 
        SetupImmediateWindowProjectionBuffer();
 
        return elisionProjectionBuffer.CurrentSnapshot.CreateFullTrackingSpan(SpanTrackingMode.EdgeInclusive);
    }
 
    private void TextBuffer_PostChanged(object sender, EventArgs e)
        => SetupImmediateWindowProjectionBuffer();
 
    /// <summary>
    /// If there's a ? mark, we want to skip the ? mark itself, and include the text that follows it
    /// </summary>
    private void SetupImmediateWindowProjectionBuffer()
    {
        var caretLine = _textView.Caret.ContainingTextViewLine.Extent;
        var currentLineIndex = _textView.TextSnapshot.GetLineNumberFromPosition(caretLine.Start.Position);
        var questionIndex = GetQuestionIndex(caretLine.GetText());
 
        if (_immediateWindowContext.QuestionIndex != questionIndex ||
            _immediateWindowContext.CurrentLineIndex != currentLineIndex)
        {
            _immediateWindowContext.QuestionIndex = questionIndex;
            _immediateWindowContext.CurrentLineIndex = currentLineIndex;
            _immediateWindowContext.ProjectionBuffer.DeleteSpans(0, _immediateWindowContext.ProjectionBuffer.CurrentSnapshot.SpanCount);
            _immediateWindowContext.ProjectionBuffer.InsertSpan(0, _textView.TextSnapshot.CreateTrackingSpanFromIndexToEnd(caretLine.Start.Position + questionIndex + 1, SpanTrackingMode.EdgeInclusive));
        }
    }
 
    private static int GetQuestionIndex(string text)
    {
        for (var i = 0; i < text.Length; i++)
        {
            if (!char.IsWhiteSpace(text[i]))
            {
                // Assume that the ? will be the first non-whitespace if it's being used as a
                // command
                return text[i] == '?' ? i : -1;
            }
        }
 
        return -1;
    }
 
    private static bool IsImmediateWindow(IVsUIShell shellService, IVsTextView textView)
    {
        Marshal.ThrowExceptionForHR(shellService.GetToolWindowEnum(out var windowEnum));
        Marshal.ThrowExceptionForHR(textView.GetBuffer(out _));
 
        var frame = new IVsWindowFrame[1];
        var immediateWindowGuid = Guid.Parse(ToolWindowGuids80.ImmediateWindow);
 
        while (windowEnum.Next(1, frame, out _) == VSConstants.S_OK)
        {
            Marshal.ThrowExceptionForHR(frame[0].GetGuidProperty((int)__VSFPROPID.VSFPROPID_GuidPersistenceSlot, out var toolWindowGuid));
            if (toolWindowGuid == immediateWindowGuid)
            {
                Marshal.ThrowExceptionForHR(frame[0].QueryViewInterface(typeof(IVsTextView).GUID, out var frameTextView));
                try
                {
                    var immediateWindowTextView = Marshal.GetObjectForIUnknown(frameTextView) as IVsTextView;
                    return textView == immediateWindowTextView;
                }
                finally
                {
                    Marshal.Release(frameTextView);
                }
            }
        }
 
        return false;
    }
 
    public void Dispose()
    {
        // Unsubscribe from events
        _textView.TextBuffer.PostChanged -= TextBuffer_PostChanged;
        _debuggerTextView.Cleanup();
 
        // The buffer graph subscribes to events of its source buffers, we're no longer interested
        _projectionBuffer.DeleteSpans(0, _projectionBuffer.CurrentSnapshot.SpanCount);
 
        // The next request will use a new workspace
        _workspace.Dispose();
    }
}