Add EditorSettings management to workspaces.

- Built a design where there's a singleton `EditorSettingsManager` that handles the "current" settings state in the world. When it detects that settings have changed via an update method being called it dispatches a `Changed` event.
- Exposed editor settings on the document tracker. When the editor settings change the document tracker dispatches to any listeners that its context has changed.
- Added tests to validate all the various settings management.

#1718
This commit is contained in:
N. Taylor Mullen 2017-10-20 17:28:33 -07:00
parent 212d97e511
commit fb8aff12f1
12 changed files with 307 additions and 25 deletions

View File

@ -0,0 +1,54 @@
// 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;
namespace Microsoft.CodeAnalysis.Razor.Editor
{
internal class DefaultEditorSettingsManager : EditorSettingsManager
{
public override event EventHandler<EditorSettingsChangedEventArgs> Changed;
private readonly object SettingsAccessorLock = new object();
private EditorSettings _settings;
public DefaultEditorSettingsManager()
{
_settings = EditorSettings.Default;
}
public override EditorSettings Current
{
get
{
lock (SettingsAccessorLock)
{
return _settings;
}
}
}
public override void Update(EditorSettings updatedSettings)
{
if (updatedSettings == null)
{
throw new ArgumentNullException(nameof(updatedSettings));
}
lock (SettingsAccessorLock)
{
if (!_settings.Equals(updatedSettings))
{
_settings = updatedSettings;
OnChanged();
}
}
}
private void OnChanged()
{
var args = new EditorSettingsChangedEventArgs(Current);
Changed?.Invoke(this, args);
}
}
}

View File

@ -0,0 +1,25 @@
// 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.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.CodeAnalysis.Razor.Editor
{
[Shared]
[ExportLanguageServiceFactory(typeof(EditorSettingsManager), RazorLanguage.Name)]
internal class DefaultEditorSettingsManagerFactory : ILanguageServiceFactory
{
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
if (languageServices == null)
{
throw new ArgumentNullException(nameof(languageServices));
}
return new DefaultEditorSettingsManager();
}
}
}

View File

@ -0,0 +1,53 @@
// 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.Extensions.Internal;
namespace Microsoft.CodeAnalysis.Razor.Editor
{
public sealed class EditorSettings : IEquatable<EditorSettings>
{
public static readonly EditorSettings Default = new EditorSettings(indentWithTabs: false, indentSize: 4);
public EditorSettings(bool indentWithTabs, int indentSize)
{
if (indentSize < 0)
{
throw new ArgumentOutOfRangeException(nameof(indentSize));
}
IndentWithTabs = indentWithTabs;
IndentSize = indentSize;
}
public bool IndentWithTabs { get; }
public int IndentSize { get; }
public bool Equals(EditorSettings other)
{
if (other == null)
{
return false;
}
return IndentWithTabs == other.IndentWithTabs &&
IndentSize == other.IndentSize;
}
public override bool Equals(object other)
{
return Equals(other as EditorSettings);
}
public override int GetHashCode()
{
var combiner = HashCodeCombiner.Start();
combiner.Add(IndentWithTabs);
combiner.Add(IndentSize);
return combiner.CombinedHash;
}
}
}

View File

@ -0,0 +1,17 @@
// 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;
namespace Microsoft.CodeAnalysis.Razor.Editor
{
public sealed class EditorSettingsChangedEventArgs : EventArgs
{
public EditorSettingsChangedEventArgs(EditorSettings settings)
{
Settings = settings;
}
public EditorSettings Settings { get; }
}
}

View File

@ -0,0 +1,17 @@
// 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.Host;
namespace Microsoft.CodeAnalysis.Razor.Editor
{
public abstract class EditorSettingsManager : ILanguageService
{
public abstract event EventHandler<EditorSettingsChangedEventArgs> Changed;
public abstract EditorSettings Current { get; }
public abstract void Update(EditorSettings updateSettings);
}
}

View File

@ -10,6 +10,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
@ -376,21 +378,25 @@ namespace Microsoft.VisualStudio.Editor.Razor
private void ConfigureTemplateEngine(IRazorEngineBuilder builder)
{
builder.Features.Add(new VisualStudioParserOptionsFeature());
builder.Features.Add(new VisualStudioParserOptionsFeature(_documentTracker.EditorSettings));
builder.Features.Add(new VisualStudioTagHelperFeature(TextBuffer));
}
/// <summary>
/// This class will cease to be useful once we harvest/monitor settings from the editor.
/// </summary>
private class VisualStudioParserOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
{
private readonly EditorSettings _settings;
public VisualStudioParserOptionsFeature(EditorSettings settings)
{
_settings = settings;
}
public int Order { get; set; }
public void Configure(RazorCodeGenerationOptionsBuilder options)
{
options.IndentSize = 4;
options.IndentWithTabs = false;
options.IndentSize = _settings.IndentSize;
options.IndentWithTabs = _settings.IndentWithTabs;
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
@ -16,6 +17,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
internal abstract ProjectExtensibilityConfiguration Configuration { get; }
public abstract EditorSettings EditorSettings { get; }
public abstract bool IsSupportedProject { get; }
public abstract string FilePath { get; }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell.Interop;
@ -17,6 +18,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
private readonly string _filePath;
private readonly ProjectSnapshotManager _projectManager;
private readonly EditorSettingsManager _editorSettingsManager;
private readonly TextBufferProjectService _projectService;
private readonly ITextBuffer _textBuffer;
private readonly List<ITextView> _textViews;
@ -31,6 +33,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
string filePath,
ProjectSnapshotManager projectManager,
TextBufferProjectService projectService,
EditorSettingsManager editorSettingsManager,
Workspace workspace,
ITextBuffer textBuffer)
{
@ -49,6 +52,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
throw new ArgumentNullException(nameof(projectService));
}
if (editorSettingsManager == null)
{
throw new ArgumentNullException(nameof(editorSettingsManager));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
@ -62,6 +70,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
_filePath = filePath;
_projectManager = projectManager;
_projectService = projectService;
_editorSettingsManager = editorSettingsManager;
_textBuffer = textBuffer;
_workspace = workspace; // For now we assume that the workspace is the always default VS workspace.
@ -70,6 +79,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
internal override ProjectExtensibilityConfiguration Configuration => _project.Configuration;
public override EditorSettings EditorSettings => _editorSettingsManager.Current;
public override bool IsSupportedProject => _isSupportedProject;
public override Project Project => _workspace.CurrentSolution.GetProject(_project.UnderlyingProject.Id);
@ -163,6 +174,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
_projectPath = projectPath;
_project = _projectManager.GetProjectWithFilePath(projectPath);
_projectManager.Changed += ProjectManager_Changed;
_editorSettingsManager.Changed += EditorSettingsManager_Changed;
OnContextChanged(_project);
}
@ -170,6 +182,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
private void Unsubscribe()
{
_projectManager.Changed -= ProjectManager_Changed;
_editorSettingsManager.Changed -= EditorSettingsManager_Changed;
// Detached from project.
_isSupportedProject = false;
@ -196,5 +209,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
OnContextChanged(e.Project);
}
}
// Internal for testing
internal void EditorSettingsManager_Changed(object sender, EditorSettingsChangedEventArgs args)
{
OnContextChanged(_project);
}
}
}

View File

@ -6,6 +6,7 @@ using System.ComponentModel.Composition;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text;
@ -21,6 +22,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
private readonly Workspace _workspace;
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly ProjectSnapshotManager _projectManager;
private readonly EditorSettingsManager _editorSettingsManager;
[ImportingConstructor]
public DefaultVisualStudioDocumentTrackerFactory(
@ -48,7 +50,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
_workspace = workspace;
_foregroundDispatcher = workspace.Services.GetRequiredService<ForegroundDispatcher>();
_projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<ProjectSnapshotManager>();
var razorLanguageServices = workspace.Services.GetLanguageServices(RazorLanguage.Name);
_projectManager = razorLanguageServices.GetRequiredService<ProjectSnapshotManager>();
_editorSettingsManager = razorLanguageServices.GetRequiredService<EditorSettingsManager>();
}
public override VisualStudioDocumentTracker Create(ITextBuffer textBuffer)
@ -65,7 +69,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
}
var filePath = textDocument.FilePath;
var tracker = new DefaultVisualStudioDocumentTracker(filePath, _projectManager, _projectService, _workspace, textBuffer);
var tracker = new DefaultVisualStudioDocumentTracker(filePath, _projectManager, _projectService, _editorSettingsManager, _workspace, textBuffer);
return tracker;
}

View File

@ -0,0 +1,60 @@
// 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 Xunit;
namespace Microsoft.CodeAnalysis.Razor.Editor
{
public class DefaultEditorSettingsManagerTest
{
[Fact]
public void InitialSettingsAreDefault()
{
// Act
var manager = new DefaultEditorSettingsManager();
// Assert
Assert.Equal(EditorSettings.Default, manager.Current);
}
[Fact]
public void Update_TriggersChangedIfEditorSettingsAreDifferent()
{
// Arrange
var manager = new DefaultEditorSettingsManager();
var called = false;
manager.Changed += (caller, args) =>
{
called = true;
};
var settings = new EditorSettings(indentWithTabs: true, indentSize: 7);
// Act
manager.Update(settings);
// Assert
Assert.True(called);
Assert.Equal(settings, manager.Current);
}
[Fact]
public void Update_DoesNotTriggerChangedIfEditorSettingsAreSame()
{
// Arrange
var manager = new DefaultEditorSettingsManager();
var called = false;
manager.Changed += (caller, args) =>
{
called = true;
};
var originalSettings = manager.Current;
// Act
manager.Update(EditorSettings.Default);
// Assert
Assert.False(called);
Assert.Same(originalSettings, manager.Current);
}
}
}

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
@ -29,13 +30,33 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true &&
s.GetProjectPath(It.IsAny<IVsHierarchy>()) == "C:/Some/Path/TestProject.csproj");
private EditorSettingsManager EditorSettingsManager => new DefaultEditorSettingsManager();
private Workspace Workspace => new AdhocWorkspace();
[Fact]
public void EditorSettingsManager_Changed_TriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
called = true;
};
// Act
documentTracker.EditorSettingsManager_Changed(null, null);
// Assert
Assert.True(called);
}
[Fact]
public void AddTextView_AddsToTextViewCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Act
@ -49,7 +70,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void AddTextView_SubscribesAfterFirstTextViewAdded()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Assert - 1
@ -66,7 +87,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void AddTextView_DoesNotAddDuplicateTextViews()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Act
@ -81,7 +102,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void AddTextView_AddsMultipleTextViewsToCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
@ -100,7 +121,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void RemoveTextView_RemovesTextViewFromCollection_SingleItem()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
documentTracker.AddTextView(textView);
@ -115,7 +136,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
var textView3 = Mock.Of<ITextView>();
@ -137,7 +158,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
var textView2 = Mock.Of<ITextView>();
@ -153,7 +174,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void RemoveTextView_UnsubscribesAfterLastTextViewRemoved()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell.Interop;
@ -25,6 +26,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true &&
s.GetProjectPath(It.IsAny<IVsHierarchy>()) == "C:/Some/Path/TestProject.csproj");
private EditorSettingsManager EditorSettingsManager => new DefaultEditorSettingsManager();
private Workspace Workspace { get; } = new AdhocWorkspace();
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
@ -56,12 +59,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
VisualStudioDocumentTracker documentTracker = new DefaultVisualStudioDocumentTracker("AFile", ProjectManager, ProjectService, Workspace, buffers[0]);
VisualStudioDocumentTracker documentTracker = new DefaultVisualStudioDocumentTracker("AFile", ProjectManager, ProjectService, EditorSettingsManager, Workspace, buffers[0]);
var editorFactoryService = Mock.Of<RazorEditorFactoryService>(factoryService => factoryService.TryGetDocumentTracker(It.IsAny<ITextBuffer>(), out documentTracker) == true);
var factory = new RazorTextViewConnectionListener(Dispatcher, editorFactoryService, Workspace);
var textViewListener = new RazorTextViewConnectionListener(Dispatcher, editorFactoryService, Workspace);
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
textViewListener.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
@ -81,19 +84,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, Workspace, buffers[0]);
var tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, EditorSettingsManager, Workspace, buffers[0]);
tracker.AddTextView(textView1);
tracker.AddTextView(textView2);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, Workspace, buffers[1]);
tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, EditorSettingsManager, Workspace, buffers[1]);
tracker.AddTextView(textView1);
tracker.AddTextView(textView2);
buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
var factory = new RazorTextViewConnectionListener(Dispatcher, Mock.Of<RazorEditorFactoryService>(), Workspace);
var textViewListener = new RazorTextViewConnectionListener(Dispatcher, Mock.Of<RazorEditorFactoryService>(), Workspace);
// Act
factory.SubjectBuffersDisconnected(textView2, ConnectionReason.BufferGraphChange, buffers);
textViewListener.SubjectBuffersDisconnected(textView2, ConnectionReason.BufferGraphChange, buffers);
// Assert
tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
@ -107,7 +110,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersDisconnected_ForAnyTextBufferWithoutTracker_DoesNothing()
{
// Arrange
var factory = new RazorTextViewConnectionListener(Dispatcher, Mock.Of<RazorEditorFactoryService>(), Workspace);
var textViewListener = new RazorTextViewConnectionListener(Dispatcher, Mock.Of<RazorEditorFactoryService>(), Workspace);
var textView = Mock.Of<IWpfTextView>();
@ -117,7 +120,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
};
// Act
factory.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
textViewListener.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));