Fixes #1632 - track textviews in the document tracker

I've stripped out some of the dead code and complexity from the document
tracker in an attempt to simplify it. I will bring this back as part of
the multi-targeting work.
This commit is contained in:
Ryan Nowak 2017-08-23 09:01:50 -07:00
parent 8b4fc0cef6
commit d87e0f7fbd
21 changed files with 711 additions and 513 deletions

View File

@ -1,21 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor
{
public abstract class RazorDocumentTracker
{
public abstract event EventHandler ContextChanged;
public abstract bool IsSupportedDocument { get; }
public abstract ProjectId ProjectId { get; }
public abstract SourceTextContainer TextContainer { get; }
public abstract Workspace Workspace { get; }
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.ObjectModel;
using Microsoft.VisualStudio.Text.Projection;
namespace Microsoft.VisualStudio.Text
{
internal static class BufferGraphExtensions
{
public static Collection<ITextBuffer> GetRazorBuffers(this IBufferGraph bufferGraph)
{
if (bufferGraph == null)
{
throw new ArgumentNullException(nameof(bufferGraph));
}
return bufferGraph.GetTextBuffers(TextBufferExtensions.IsRazorBuffer);
}
}
}

View File

@ -1,267 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
internal class DefaultTextViewRazorDocumentTracker : RazorDocumentTracker
{
private readonly DefaultTextViewRazorDocumentTrackerService _service;
private readonly ITextView _textView;
private DocumentId _documentId;
private ITextBuffer _razorBuffer;
private bool _isSupportedProject;
private ITextBuffer _cSharpBuffer;
private SourceTextContainer _textContainer;
private Workspace _workspace;
private WorkspaceRegistration _workspaceRegistration;
public override event EventHandler ContextChanged;
public DefaultTextViewRazorDocumentTracker(DefaultTextViewRazorDocumentTrackerService service, ITextView textView)
{
if (service == null)
{
throw new ArgumentNullException(nameof(service));
}
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
_service = service;
_textView = textView;
Update();
_textView.Closed += TextView_Closed;
_textView.BufferGraph.GraphBufferContentTypeChanged += BufferGraph_GraphBufferContentTypeChanged;
_textView.BufferGraph.GraphBuffersChanged += BufferGraph_GraphBuffersChanged;
}
public override bool IsSupportedDocument => _isSupportedProject && _textContainer != null;
public override ProjectId ProjectId => _documentId?.ProjectId;
public override SourceTextContainer TextContainer => _textContainer;
public override Workspace Workspace => _workspace;
private bool Update()
{
// Update is called when the state of any of our surrounding systems changes. Here we want to examine the
// state of the world and then update properties as necessary.
//
// Fundamentally we have a Razor half of the world as as soon as the document is open - and then later
// the C# half of the world will be initialized. This code is in general pretty tolerant of
// unexpected /impossible states.
//
// We also want to successfully shut down when the buffer is renamed to something other .cshtml.
// If there's no Razor buffer, then there's really nothing to do. This isn't a Razor file.
var razorBuffer = _textView.BufferGraph.GetTextBuffers(IsRazorBuffer).FirstOrDefault();
// The source container is tied to the Razor buffer since that's what we want to watch for changes.
SourceTextContainer textContainer = null;
if (razorBuffer != null)
{
textContainer = razorBuffer.AsTextContainer();
}
bool isSupportedProject = false;
if (razorBuffer != null)
{
// The project can be null when the document is in the process of closing.
var project = _service.GetProject(razorBuffer);
if (project != null)
{
isSupportedProject = _service.IsSupportedProject(project);
}
}
// The C# buffer is the other buffer in the graph that is C# (only if we already found a Razor buffer).
// We're not currently validating that it's a child of the Razor buffer even though we expect that.
ITextBuffer cSharpBuffer = null;
if (razorBuffer != null)
{
cSharpBuffer = _textView.BufferGraph.GetTextBuffers(IsCSharpBuffer).FirstOrDefault();
}
// Now if we have a C# buffer we want to watch for it be attached to a workspace.
SourceTextContainer cSharpTextContainer = null;
WorkspaceRegistration workspaceRegistration = null;
if (cSharpBuffer != null)
{
cSharpTextContainer = cSharpBuffer.AsTextContainer();
Debug.Assert(cSharpTextContainer != null);
workspaceRegistration = Workspace.GetWorkspaceRegistration(cSharpTextContainer);
}
// Now finally we can see if we have a workspace
Workspace workspace = null;
if (workspaceRegistration != null)
{
workspace = workspaceRegistration.Workspace;
}
// Now we know if the Roslyn state for this document has been initialized, let's look for a project.
DocumentId documentId = null;
if (cSharpTextContainer != null && workspace != null)
{
documentId = workspace.GetDocumentIdInCurrentContext(cSharpTextContainer);
}
// As a special case, we want to default to the VisualStudioWorkspace until we find out otherwise
// This lets us start working before a project gets initialized.
if (isSupportedProject && workspace == null)
{
workspace = _service.Workspace;
}
var changed = false;
changed |= razorBuffer == _razorBuffer;
changed |= textContainer == _textContainer;
changed |= isSupportedProject == _isSupportedProject;
changed |= cSharpBuffer == _cSharpBuffer;
changed |= workspaceRegistration == _workspaceRegistration;
changed |= workspace == _workspace;
changed |= documentId == _documentId;
// Now if nothing has changed we're all done!
if (!changed)
{
return false;
}
// OK, so something did change, let's commit the changes.
//
// These are all of the straightforward ones.
_razorBuffer = razorBuffer;
_textContainer = textContainer;
_isSupportedProject = isSupportedProject;
_cSharpBuffer = cSharpBuffer;
_documentId = documentId;
// Now these ones we subscribe to events so it's a little tricky.
if (_workspaceRegistration != null)
{
_workspaceRegistration.WorkspaceChanged -= WorkspaceRegistration_WorkspaceChanged;
}
if (workspaceRegistration != null)
{
workspaceRegistration.WorkspaceChanged += WorkspaceRegistration_WorkspaceChanged;
}
if (_workspace != null)
{
_workspace.DocumentActiveContextChanged -= Workspace_DocumentActiveContextChanged;
}
if (workspace != null)
{
workspace.DocumentActiveContextChanged += Workspace_DocumentActiveContextChanged;
}
_workspaceRegistration = workspaceRegistration;
_workspace = workspace;
return true;
}
private static bool IsCSharpBuffer(ITextBuffer textBuffer)
{
return textBuffer.ContentType.IsOfType("CSharp");
}
private static bool IsRazorBuffer(ITextBuffer textBuffer)
{
return textBuffer.ContentType.IsOfType(RazorLanguage.ContentType);
}
private void BufferGraph_GraphBuffersChanged(object sender, GraphBuffersChangedEventArgs e)
{
if (Update())
{
OnContextChanged();
}
}
private void BufferGraph_GraphBufferContentTypeChanged(object sender, GraphBufferContentTypeChangedEventArgs e)
{
if (Update())
{
OnContextChanged();
}
}
private void WorkspaceRegistration_WorkspaceChanged(object sender, EventArgs e)
{
if (Update())
{
OnContextChanged();
}
}
private void Workspace_DocumentActiveContextChanged(object sender, DocumentActiveContextChangedEventArgs e)
{
var textBuffer = e.SourceTextContainer.GetTextBuffer();
if (textBuffer != null && (textBuffer == _cSharpBuffer || textBuffer == _razorBuffer))
{
if (Update())
{
OnContextChanged();
}
}
}
private void TextView_Closed(object sender, EventArgs e)
{
_textView.BufferGraph.GraphBufferContentTypeChanged -= BufferGraph_GraphBufferContentTypeChanged;
_textView.BufferGraph.GraphBuffersChanged -= BufferGraph_GraphBuffersChanged;
if (_workspaceRegistration != null)
{
_workspaceRegistration.WorkspaceChanged -= WorkspaceRegistration_WorkspaceChanged;
}
if (_workspace != null)
{
_workspace.DocumentActiveContextChanged -= Workspace_DocumentActiveContextChanged;
}
_textView.Closed -= TextView_Closed;
_razorBuffer = null;
_textContainer = null;
_isSupportedProject = false;
_cSharpBuffer = null;
_workspaceRegistration = null;
_workspace = null;
_documentId = null;
OnContextChanged();
}
private void OnContextChanged()
{
var handler = ContextChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
}

View File

@ -1,177 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
[ContentType(RazorLanguage.ContentType)]
[TextViewRole(PredefinedTextViewRoles.Editable)]
[Export(typeof(IWpfTextViewConnectionListener))]
[Export(typeof(TextViewRazorDocumentTrackerService))]
internal class DefaultTextViewRazorDocumentTrackerService : TextViewRazorDocumentTrackerService, IWpfTextViewConnectionListener
{
private readonly ITextDocumentFactoryService _documentFactory;
private readonly IServiceProvider _services;
private readonly Workspace _workspace;
private RunningDocumentTable _runningDocumentTable;
private IVsSolution _solution;
[ImportingConstructor]
public DefaultTextViewRazorDocumentTrackerService(
[Import(typeof(SVsServiceProvider))] IServiceProvider services,
ITextDocumentFactoryService documentFactory,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (documentFactory == null)
{
throw new ArgumentNullException(nameof(documentFactory));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_services = services;
_documentFactory = documentFactory;
_workspace = workspace;
}
// Lazy to avoid needing this in unit tests.
private RunningDocumentTable RunningDocumentTable
{
get
{
if (_runningDocumentTable == null)
{
_runningDocumentTable = new RunningDocumentTable(_services);
}
return _runningDocumentTable;
}
}
// Lazy to avoid needing this in unit tests.
private IVsSolution Solution
{
get
{
if (_solution == null)
{
_solution = (IVsSolution)_services.GetService(typeof(SVsSolution));
}
return _solution;
}
}
public Workspace Workspace => _workspace;
public override RazorDocumentTracker CreateTracker(ITextView textView)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
if (!textView.Properties.TryGetProperty<RazorDocumentTracker>(typeof(RazorDocumentTracker), out var tracker))
{
tracker = new DefaultTextViewRazorDocumentTracker(this, textView);
textView.Properties.AddProperty(typeof(RazorDocumentTracker), tracker);
}
return tracker;
}
public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
// This means a Razor buffer has been attached to this ITextView or the ITextView is just opening with Razor content.
//
// Call CreateTracker just for the side effect. The tracker will do all of the real work after it's initialized.
CreateTracker(textView);
}
public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
// This means a Razor buffer has be detached from this ITextView or the ITextView is closing.
//
// Do nothing, the tracker will update itself.
}
public virtual IVsHierarchy GetProject(ITextBuffer textBuffer)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
// If there's no document we can't find the FileName, or look for a matching hierarchy.
if (!_documentFactory.TryGetTextDocument(textBuffer, out var textDocument))
{
return null;
}
RunningDocumentTable.FindDocument(textDocument.FilePath, out var hierarchy, out uint itemId, out uint cookie);
// We don't currently try to look a Roslyn ProjectId at this point, we just want to know some
// basic things.
// See https://github.com/dotnet/roslyn/blob/4e3db2b7a0732d45a720e9ed00c00cd22ab67a14/src/VisualStudio/Core/SolutionExplorerShim/HierarchyItemToProjectIdMap.cs#L47
// for a more complete implementation.
return hierarchy;
}
public virtual bool IsSupportedProject(IVsHierarchy project)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
return project.IsCapabilityMatch("DotNetCoreWeb");
}
public static IEnumerable<ITextView> GetTextViews(ITextBuffer textBuffer)
{
// TODO: Extract text views from buffer
return new[] { (ITextView)null };
}
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
/// <summary>
/// Infrastructure methods to find project information from an <see cref="ITextBuffer"/>.
/// </summary>
[Export(typeof(TextBufferProjectService))]
internal class DefaultTextBufferProjectService : TextBufferProjectService
{
private readonly RunningDocumentTable _documentTable;
private readonly ITextDocumentFactoryService _documentFactory;
[ImportingConstructor]
public DefaultTextBufferProjectService(
[Import(typeof(SVsServiceProvider))] IServiceProvider services,
ITextDocumentFactoryService documentFactory,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (documentFactory == null)
{
throw new ArgumentNullException(nameof(documentFactory));
}
_documentFactory = documentFactory;
_documentTable = new RunningDocumentTable(services);
}
public override IVsHierarchy GetHierarchy(ITextBuffer textBuffer)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
// If there's no document we can't find the FileName, or look for a matching hierarchy.
if (!_documentFactory.TryGetTextDocument(textBuffer, out var textDocument))
{
return null;
}
_documentTable.FindDocument(textDocument.FilePath, out var hierarchy, out uint itemId, out uint cookie);
// We don't currently try to look a Roslyn ProjectId at this point, we just want to know some
// basic things.
// See https://github.com/dotnet/roslyn/blob/4e3db2b7a0732d45a720e9ed00c00cd22ab67a14/src/VisualStudio/Core/SolutionExplorerShim/HierarchyItemToProjectIdMap.cs#L47
// for a more complete implementation.
return hierarchy;
}
public override bool IsSupportedProject(IVsHierarchy hierarchy)
{
if (hierarchy == null)
{
throw new ArgumentNullException(nameof(hierarchy));
}
return hierarchy.IsCapabilityMatch("DotNetCoreWeb");
}
}
}

View File

@ -0,0 +1,108 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
internal class DefaultVisualStudioDocumentTracker : VisualStudioDocumentTracker
{
private readonly TextBufferProjectService _projectService;
private readonly ITextBuffer _textBuffer;
private readonly List<ITextView> _textViews;
private bool _isSupportedProject;
private Workspace _workspace;
public override event EventHandler ContextChanged;
public DefaultVisualStudioDocumentTracker(TextBufferProjectService projectService, Workspace workspace, ITextBuffer textBuffer)
{
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
_projectService = projectService;
_textBuffer = textBuffer;
_workspace = workspace;
_textViews = new List<ITextView>();
Update();
}
public override bool IsSupportedProject => _isSupportedProject;
public override ProjectId ProjectId => null;
public override ITextBuffer TextBuffer => _textBuffer;
public override IReadOnlyList<ITextView> TextViews => _textViews;
public IList<ITextView> TextViewsInternal => _textViews;
public override Workspace Workspace => _workspace;
private bool Update()
{
// Update is called when the state of any of our surrounding systems changes. Here we want to examine the
// state of the world and then update properties as necessary.
//
// Fundamentally we have a Razor half of the world as as soon as the document is open - and then later
// the C# half of the world will be initialized. This code is in general pretty tolerant of
// unexpected /impossible states.
//
// We also want to successfully shut down when the buffer is renamed to something other .cshtml.
IVsHierarchy project = null;
var isSupportedProject = false;
if (_textBuffer.ContentType.IsOfType(RazorLanguage.ContentType) &&
(project = _projectService.GetHierarchy(_textBuffer)) != null)
{
// We expect the document to have a hierarchy even if it's not a real 'project'.
// However the hierarchy can be null when the document is in the process of closing.
isSupportedProject = _projectService.IsSupportedProject(project);
}
// For now we temporarily assume that the workspace is the default VS workspace.
var workspace = _workspace;
var changed = false;
changed |= isSupportedProject == _isSupportedProject;
changed |= workspace == _workspace;
if (changed)
{
_isSupportedProject = isSupportedProject;
_workspace = workspace;
}
return changed;
}
private void OnContextChanged()
{
var handler = ContextChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
}

View File

@ -0,0 +1,139 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
[ContentType(RazorLanguage.ContentType)]
[TextViewRole(PredefinedTextViewRoles.Document)]
[Export(typeof(IWpfTextViewConnectionListener))]
[Export(typeof(VisualStudioDocumentTrackerFactory))]
internal class DefaultVisualStudioDocumentTrackerFactory : VisualStudioDocumentTrackerFactory, IWpfTextViewConnectionListener
{
private readonly TextBufferProjectService _projectService;
private readonly Workspace _workspace;
[ImportingConstructor]
public DefaultVisualStudioDocumentTrackerFactory(
TextBufferProjectService projectService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_projectService = projectService;
_workspace = workspace;
}
public Workspace Workspace => _workspace;
public override VisualStudioDocumentTracker GetTracker(ITextView textView)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
// While it's definitely possible to have multiple Razor text buffers attached to the same text view, there's
// no real scenario for it. This method always returns the tracker for the first Razor text buffer, but the
// other functionality for this class will maintain state correctly for the other buffers.
var textBuffer = textView.BufferGraph.GetRazorBuffers().FirstOrDefault();
if (textBuffer == null)
{
// No Razor buffer, nothing to track.
return null;
}
// A little bit of hardening here, to make sure our assumptions are correct.
DefaultVisualStudioDocumentTracker tracker;
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
Debug.Fail("The document tracker should be initialized");
}
Debug.Assert(tracker.TextViewsInternal.Contains(textView));
return tracker;
}
public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
if (!textBuffer.IsRazorBuffer())
{
continue;
}
DefaultVisualStudioDocumentTracker tracker;
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
tracker = new DefaultVisualStudioDocumentTracker(_projectService, _workspace, textBuffer);
textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
}
if (!tracker.TextViewsInternal.Contains(textView))
{
tracker.TextViewsInternal.Add(textView);
}
}
}
public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
// This means a Razor buffer has be detached from this ITextView or the ITextView is closing. Since we keep a
// list of all of the open text views for each text buffer, we need to update the tracker.
//
// Notice that this method is called *after* changes are applied to the text buffer(s). We need to check every
// one of them for a tracker because the content type could have changed.
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
DefaultVisualStudioDocumentTracker tracker;
if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
tracker.TextViewsInternal.Remove(textView);
}
}
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
internal abstract class TextBufferProjectService
{
public abstract IVsHierarchy GetHierarchy(ITextBuffer textBuffer);
public abstract bool IsSupportedProject(IVsHierarchy hierarchy);
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public abstract class VisualStudioDocumentTracker
{
public abstract event EventHandler ContextChanged;
public abstract bool IsSupportedProject { get; }
public abstract ProjectId ProjectId { get; }
public abstract Workspace Workspace { get; }
public abstract ITextBuffer TextBuffer { get; }
public abstract IReadOnlyList<ITextView> TextViews { get; }
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public abstract class VisualStudioDocumentTrackerFactory
{
public abstract VisualStudioDocumentTracker GetTracker(ITextView textView);
}
}

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
using Timer = System.Timers.Timer;
@ -145,7 +146,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
_foregroundThreadAffinitizedObject.AssertIsBackground();
var textViews = DefaultTextViewRazorDocumentTrackerService.GetTextViews(_textBuffer);
var textViews = Array.Empty<ITextView>();
foreach (var textView in textViews)
{

View File

@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="$(RoslynDevVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Editor" Version="$(VsShellVersion)" />
<PackageReference Include="Microsoft.VisualStudio.ComponentModelHost" Version="$(VsShellVersion)" />
<PackageReference Include="Microsoft.VisualStudio.LanguageServices" Version="$(RoslynDevVersion)" />
<PackageReference Include="Microsoft.VisualStudio.LanguageServices.Razor.RemoteClient" Version="$(RoslynDevVersion)" />

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.VisualStudio.Text
{
internal static class TextBufferExtensions
{
public static bool IsRazorBuffer(this ITextBuffer textBuffer)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
return textBuffer.ContentType.IsOfType(RazorLanguage.ContentType);
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
internal abstract class TextBufferProjectService
{
public abstract IVsHierarchy GetHierarchy(ITextBuffer textBuffer);
public abstract bool IsSupportedProject(IVsHierarchy hierarchy);
}
}

View File

@ -1,13 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public abstract class TextViewRazorDocumentTrackerService
{
public abstract RazorDocumentTracker CreateTracker(ITextView textView);
}
}

View File

@ -0,0 +1,258 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class DefaultVisualStudioDocumentTrackerFactoryTest
{
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true);
private Workspace Workspace { get; } = new AdhocWorkspace();
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
[Fact]
public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
[Fact]
public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
var tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
Assert.Equal(buffers[0], tracker.TextBuffer);
}
[Fact]
public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView_ForMultipleBuffers()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
var tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
Assert.Equal(buffers[0], tracker.TextBuffer);
Assert.False(buffers[1].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
tracker = buffers[2].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
Assert.Equal(buffers[2], tracker.TextBuffer);
}
[Fact]
public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_DoesNotAddDuplicateTextViewEntry()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.Same(tracker, buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker)));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
}
[Fact]
public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_AddsEntryForADifferentTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView1);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
factory.SubjectBuffersConnected(textView2, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.Same(tracker, buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker)));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1), v => Assert.Same(v, textView2));
}
[Fact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithTracker_RemovesTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView1);
tracker.TextViewsInternal.Add(textView2);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[1]);
tracker.TextViewsInternal.Add(textView1);
tracker.TextViewsInternal.Add(textView2);
buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
factory.SubjectBuffersDisconnected(textView2, ConnectionReason.BufferGraphChange, buffers);
// Assert
tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
tracker = buffers[1].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
}
[Fact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithoutTracker_DoesNothing()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
[Fact]
public void GetTracker_ForRazorTextBufferWithTracker_ReturnsTheFirstTracker()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
var bufferGraph = Mock.Of<IBufferGraph>(g => g.GetTextBuffers(It.IsAny<Predicate<ITextBuffer>>()) == buffers);
var textView = Mock.Of<IWpfTextView>(v => v.BufferGraph == bufferGraph);
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
var result = factory.GetTracker(textView);
// Assert
Assert.Same(tracker, result);
}
[Fact]
public void GetTracker_WithoutRazorBuffer_ReturnsNull()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var buffers = new Collection<ITextBuffer>();
var bufferGraph = Mock.Of<IBufferGraph>(g => g.GetTextBuffers(It.IsAny<Predicate<ITextBuffer>>()) == buffers);
var textView = Mock.Of<IWpfTextView>(v => v.BufferGraph == bufferGraph);
// Act
var result = factory.GetTracker(textView);
// Assert
Assert.Null(result);
}
}
}

View File

@ -3,6 +3,10 @@
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<!-- The binding redirects are needed due to a conflict between the test host and Roslyn regarding System.Collections.Immutable -->
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
@ -22,6 +26,8 @@
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="$(RoslynDevVersion)" NoWarn="NU1605" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynDevVersion)" NoWarn="NU1605" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />

View File

@ -7,15 +7,15 @@ using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Razor.Editor;
namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
{
public class RazorDocumentInfoViewModel : NotifyPropertyChanged
{
private readonly RazorDocumentTracker _documentTracker;
private readonly VisualStudioDocumentTracker _documentTracker;
public RazorDocumentInfoViewModel(RazorDocumentTracker documentTracker)
public RazorDocumentInfoViewModel(VisualStudioDocumentTracker documentTracker)
{
if (documentTracker == null)
{
@ -25,7 +25,7 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
_documentTracker = documentTracker;
}
public bool IsSupportedDocument => _documentTracker.IsSupportedDocument;
public bool IsSupportedDocument => _documentTracker.IsSupportedProject;
public Project Project
{
@ -42,8 +42,6 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
public ProjectId ProjectId => _documentTracker.ProjectId;
public SourceTextContainer TextContainer => _documentTracker.TextContainer;
public Workspace Workspace => _documentTracker.Workspace;
public HostLanguageServices RazorLanguageServices => Workspace?.Services.GetLanguageServices(RazorLanguage.Name);

View File

@ -9,7 +9,7 @@ using System.Windows;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServices.Razor;
using Microsoft.VisualStudio.LanguageServices.Razor.Editor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Editor;
@ -21,13 +21,13 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
internal class RazorDocumentInfoWindow : ToolWindowPane
{
private IVsEditorAdaptersFactoryService _adapterFactory;
private TextViewRazorDocumentTrackerService _documentTrackerService;
private VisualStudioDocumentTrackerFactory _documentTrackerService;
private IVsTextManager _textManager;
private IVsRunningDocumentTable _rdt;
private uint _cookie;
private ITextView _textView;
private RazorDocumentTracker _documentTracker;
private VisualStudioDocumentTracker _documentTracker;
public RazorDocumentInfoWindow()
: base(null)
@ -43,7 +43,7 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
var component = (IComponentModel)GetService(typeof(SComponentModel));
_adapterFactory = component.GetService<IVsEditorAdaptersFactoryService>();
_documentTrackerService = component.GetService<TextViewRazorDocumentTrackerService>();
_documentTrackerService = component.GetService<VisualStudioDocumentTrackerFactory>();
_textManager = (IVsTextManager)GetService(typeof(SVsTextManager));
_rdt = (IVsRunningDocumentTable)GetService(typeof(SVsRunningDocumentTable));
@ -78,7 +78,7 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
_documentTracker.ContextChanged -= DocumentTracker_ContextChanged;
}
_documentTracker = _documentTrackerService.CreateTracker(textView);
_documentTracker = _documentTrackerService.GetTracker(textView);
_documentTracker.ContextChanged += DocumentTracker_ContextChanged;
((FrameworkElement)Content).DataContext = new RazorDocumentInfoViewModel(_documentTracker);

View File

@ -19,34 +19,13 @@
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="Project"/>
<Label Grid.Column="1" Grid.Row="0" Content="{Binding Project.Name}"/>
<Label Grid.Column="0" Grid.Row="1" Content="Is Supported"/>
<Label Grid.Column="1" Grid.Row="1" Content="{Binding IsSupportedDocument}"/>
<Label Grid.Column="0" Grid.Row="2" Content="Text Container"/>
<Label Grid.Column="1" Grid.Row="2" Content="{Binding TextContainer}"/>
<Label Grid.Column="0" Grid.Row="3" Content="Workspace"/>
<Label Grid.Column="1" Grid.Row="3" Content="{Binding Workspace}"/>
<Label Grid.Column="0" Grid.Row="4" Content="Razor Lang"/>
<Label Grid.Column="1" Grid.Row="4" Content="{Binding RazorLanguageServices}"/>
<Label Grid.Column="0" Grid.Row="5" Content="Resolver"/>
<Label Grid.Column="1" Grid.Row="5" Content="{Binding TagHelperResolver}"/>
<Label Grid.Column="0" Grid.Row="6" Content="Syntax Facts"/>
<Label Grid.Column="1" Grid.Row="6" Content="{Binding RazorSyntaxFactsService}"/>
<Label Grid.Column="0" Grid.Row="7" Content="Template Eng"/>
<Label Grid.Column="1" Grid.Row="7" Content="{Binding RazorTemplateEngineFactoryService}"/>
<Label Grid.Column="0" Grid.Row="8" Content="Completion Service"/>
<Label Grid.Column="1" Grid.Row="8" Content="{Binding TagHelperCompletionService}"/>
<Label Grid.Column="0" Grid.Row="9" Content="TH Facts"/>
<Label Grid.Column="1" Grid.Row="9" Content="{Binding TagHelperFactsService}"/>
<Label Grid.Column="0" Grid.Row="2" Content="Workspace"/>
<Label Grid.Column="1" Grid.Row="2" Content="{Binding Workspace}"/>
</Grid>
</StackPanel>
</UserControl>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<VsixVersion>15.5</VsixVersion>
<VsixVersion Condition="'$(BuildNumber)'!=''">$(VsixVersion).$(BuildNumber)</VsixVersion>
<VsixVersion Condition="'$(BuildNumber)'==''">$(VsixVersion).999999</VsixVersion>
<MinimumVisualStudioVersion>15.0</MinimumVisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<NuGetPackageImportStamp>