File: IntelliSense\AbstractController.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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 Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense;
 
internal abstract class AbstractController<TSession, TModel, TPresenterSession, TEditorSession> : IController<TModel>
    where TSession : class, ISession<TModel>
    where TPresenterSession : IIntelliSensePresenterSession
{
    protected readonly IThreadingContext ThreadingContext;
    protected readonly IGlobalOptionService GlobalOptions;
    protected readonly ITextView TextView;
    protected readonly ITextBuffer SubjectBuffer;
    protected readonly IIntelliSensePresenter<TPresenterSession, TEditorSession> Presenter;
    protected readonly IDocumentProvider DocumentProvider;
 
    private readonly IAsynchronousOperationListener _asyncListener;
    private readonly string _asyncOperationId;
 
    // Null when we absolutely know we don't have any sort of item computation going on. Non
    // null the moment we think we start computing state. Null again once we decide we can
    // stop.
    protected TSession sessionOpt;
 
    protected bool IsSessionActive => sessionOpt != null;
 
    protected AbstractController(
        IGlobalOptionService globalOptions,
        IThreadingContext threadingContext,
        ITextView textView,
        ITextBuffer subjectBuffer,
        IIntelliSensePresenter<TPresenterSession, TEditorSession> presenter,
        IAsynchronousOperationListener asyncListener,
        IDocumentProvider documentProvider,
        string asyncOperationId)
    {
        this.GlobalOptions = globalOptions;
        ThreadingContext = threadingContext;
        this.TextView = textView;
        this.SubjectBuffer = subjectBuffer;
        this.Presenter = presenter;
        _asyncListener = asyncListener;
        this.DocumentProvider = documentProvider;
        _asyncOperationId = asyncOperationId;
 
        this.TextView.Closed += OnTextViewClosed;
 
        // Caret position changed only fires if the caret is explicitly moved.  It doesn't fire
        // when the caret is moved because of text change events.
        this.TextView.Caret.PositionChanged += this.OnCaretPositionChanged;
        this.TextView.TextBuffer.PostChanged += this.OnTextViewBufferPostChanged;
    }
 
    internal abstract void OnModelUpdated(TModel result, bool updateController);
    internal abstract void OnTextViewBufferPostChanged(object sender, EventArgs e);
    internal abstract void OnCaretPositionChanged(object sender, EventArgs e);
 
    private void OnTextViewClosed(object sender, EventArgs e)
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
        DismissSessionIfActive();
 
        this.TextView.Closed -= OnTextViewClosed;
        this.TextView.Caret.PositionChanged -= this.OnCaretPositionChanged;
        this.TextView.TextBuffer.PostChanged -= this.OnTextViewBufferPostChanged;
    }
 
    public TModel WaitForController()
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
        VerifySessionIsActive();
        return sessionOpt.WaitForController();
    }
 
    void IController<TModel>.OnModelUpdated(TModel result, bool updateController)
    {
        // This is only called from the model computation if it was not cancelled.  And if it was 
        // not cancelled then we must have a pointer to it (as well as the presenter session).
        this.ThreadingContext.ThrowIfNotOnUIThread();
        VerifySessionIsActive();
 
        this.OnModelUpdated(result, updateController);
    }
 
    IAsyncToken IController<TModel>.BeginAsyncOperation(string name, object tag, string filePath, int lineNumber)
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
        VerifySessionIsActive();
        name = String.IsNullOrEmpty(name)
            ? _asyncOperationId
            : $"{_asyncOperationId} - {name}";
        return _asyncListener.BeginAsyncOperation(name, tag, filePath: filePath, lineNumber: lineNumber);
    }
 
    protected void VerifySessionIsActive()
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
        Contract.ThrowIfFalse(IsSessionActive);
    }
 
    protected void VerifySessionIsInactive()
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
        Contract.ThrowIfTrue(IsSessionActive);
    }
 
    protected void DismissSessionIfActive()
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
        if (IsSessionActive)
        {
            this.StopModelComputation();
        }
    }
 
    public void StopModelComputation()
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
        VerifySessionIsActive();
 
        // Make a local copy so that we won't do anything that causes us to recurse and try to
        // dismiss this again.
        var localSession = sessionOpt;
        sessionOpt = null;
        localSession.Stop();
    }
 
    public bool TryHandleEscapeKey()
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
 
        // Escape simply dismissed a session if it's up. Otherwise let the next thing in the
        // chain handle us.
        if (!IsSessionActive)
        {
            return false;
        }
 
        // If we haven't even computed a model yet, then also send this command to anyone
        // listening.  It's unlikely that the command was intended for us (as we wouldn't
        // have even shown ui yet.
        var handledCommand = sessionOpt.InitialUnfilteredModel != null;
 
        // In the presence of an escape, we always stop what we're doing.
        this.StopModelComputation();
 
        return handledCommand;
    }
}