Skip to content

Commit

Permalink
Merge pull request #1 from heejaechang/editorconfig3
Browse files Browse the repository at this point in the history
add editorconfig related changes
  • Loading branch information
Carol Hu authored Apr 26, 2018
2 parents 2c6dd7f + 7e0fd53 commit 3981411
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,81 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorLogger;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.VisualStudio.CodingConventions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Options
{
// NOTE: this type depends Microsoft.VisualStudio.CodingConventions, so for now it's living in EditorFeatures.Wpf as that assembly
// isn't yet available outside of Visual Studio.
internal sealed partial class EditorConfigDocumentOptionsProvider : IDocumentOptionsProvider
{
private const int EventDelayInMillisecond = 50;

// this lock guard _openDocumentContexts mutation
private readonly object _gate = new object();

// this lock guard _resettableDelay
private readonly object _eventGate = new object();

/// <summary>
/// The map of cached contexts for currently open documents. Should only be accessed if holding a monitor lock
/// on <see cref="_gate"/>.
/// </summary>
private readonly Dictionary<DocumentId, Task<ICodingConventionContext>> _openDocumentContexts = new Dictionary<DocumentId, Task<ICodingConventionContext>>();

private readonly Workspace _workspace;
private readonly ICodingConventionsManager _codingConventionsManager;
private readonly IErrorLoggerService _errorLogger;

internal EditorConfigDocumentOptionsProvider(Workspace workspace)
/// <summary>
/// this is used to aggregate OnCodingConventionsChangedAsync event
/// the event will be raised to all open documents that is affected by same editorconfig files
/// </summary>
private ResettableDelay _resettableDelay;

internal EditorConfigDocumentOptionsProvider(Workspace workspace, ICodingConventionsManager codingConventionsManager)
{
_codingConventionsManager = CodingConventionsManagerFactory.CreateCodingConventionsManager();
_workspace = workspace;

_codingConventionsManager = codingConventionsManager;
_errorLogger = workspace.Services.GetService<IErrorLoggerService>();

workspace.DocumentOpened += Workspace_DocumentOpened;
workspace.DocumentClosed += Workspace_DocumentClosed;
_resettableDelay = ResettableDelay.CompletedDelay;

workspace.DocumentOpened += OnDocumentOpened;
workspace.DocumentClosed += OnDocumentClosed;

// workaround until this is fixed.
// https://github.com/dotnet/roslyn/issues/26377
// otherwise, we will leak files in _openDocumentContexts
workspace.WorkspaceChanged += OnWorkspaceChanged;
}

private void Workspace_DocumentClosed(object sender, DocumentEventArgs e)
private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
switch (e.Kind)
{
case WorkspaceChangeKind.SolutionRemoved:
case WorkspaceChangeKind.SolutionCleared:
ClearOpenFileCache();
break;
case WorkspaceChangeKind.ProjectRemoved:
ClearOpenFileCache(e.ProjectId);
break;
default:
break;
}
}

private void OnDocumentClosed(object sender, DocumentEventArgs e)
{
lock (_gate)
{
Expand All @@ -46,23 +86,69 @@ private void Workspace_DocumentClosed(object sender, DocumentEventArgs e)
_openDocumentContexts.Remove(e.Document.Id);

// Ensure we dispose the context, which we'll do asynchronously
contextTask.ContinueWith(
t => t.Result.Dispose(),
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default);
EnsureContextCleanup(contextTask);
}
}
}

private void Workspace_DocumentOpened(object sender, DocumentEventArgs e)
private void OnDocumentOpened(object sender, DocumentEventArgs e)
{
lock (_gate)
{
_openDocumentContexts.Add(e.Document.Id, Task.Run(() => GetConventionContextAsync(e.Document.FilePath, CancellationToken.None)));
var contextTask = Task.Run(async () =>
{
var context = await GetConventionContextAsync(e.Document.FilePath, CancellationToken.None).ConfigureAwait(false);
context.CodingConventionsChangedAsync += OnCodingConventionsChangedAsync;
return context;
});

Contract.Requires(!_openDocumentContexts.ContainsKey(e.Document.Id));
_openDocumentContexts.Add(e.Document.Id, contextTask);
}
}

private void ClearOpenFileCache(ProjectId projectId = null)
{
var contextTasks = new List<Task<ICodingConventionContext>>();

lock (_gate)
{
if (projectId == null)
{
contextTasks.AddRange(_openDocumentContexts.Values);
_openDocumentContexts.Clear();
}
else
{
foreach (var kv in _openDocumentContexts.Where(kv => kv.Key.ProjectId == projectId).ToList())
{
_openDocumentContexts.Remove(kv.Key);
contextTasks.Add(kv.Value);
}
}
}

foreach (var contextTask in contextTasks)
{
EnsureContextCleanup(contextTask);
}
}

private void EnsureContextCleanup(Task<ICodingConventionContext> contextTask)
{
contextTask.ContinueWith(
t =>
{
var context = t.Result;

context.CodingConventionsChangedAsync -= OnCodingConventionsChangedAsync;
context.Dispose();
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default);
}

public async Task<IDocumentOptions> GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken)
{
Task<ICodingConventionContext> contextTask;
Expand Down Expand Up @@ -122,5 +208,40 @@ private Task<ICodingConventionContext> GetConventionContextAsync(string path, Ca
() => _codingConventionsManager.GetConventionContextAsync(path, cancellationToken),
defaultValue: EmptyCodingConventionContext.Instance);
}

private Task OnCodingConventionsChangedAsync(object sender, CodingConventionsChangedEventArgs arg)
{
// this is a temporary workaround. once we finish the work to put editorconfig file as a part of roslyn solution snapshot,
// that system will automatically pick up option changes and update snapshot. and it will work regardless
// whether a file is opened in editor or not.
//
// but until then, we need to explicitly touch workspace to update snapshot. and
// only works for open files. it is not easy to track option changes for closed files with current model.
// related tracking issue - https://github.com/dotnet/roslyn/issues/26250
//
// use its own lock to remove dead lock possibility
ResettableDelay delay;
lock (_eventGate)
{
if (!_resettableDelay.Task.IsCompleted)
{
_resettableDelay.Reset();
return Task.CompletedTask;
}

// since this event gets raised for all documents that are affected by 1 editconfig file,
// and since for now we make that event as whole solution changed event, we don't need to update
// snapshot for each events. aggregate all events to 1.
delay = new ResettableDelay(EventDelayInMillisecond);
_resettableDelay = delay;
}

delay.Task.ContinueWith(_ => _workspace.OnOptionChanged(),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);

return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.CodingConventions;

namespace Microsoft.CodeAnalysis.Editor.Options
{
[Export(typeof(IDocumentOptionsProviderFactory))]
class EditorConfigDocumentOptionsProviderFactory : IDocumentOptionsProviderFactory
{
private readonly ICodingConventionsManager _codingConventionsManager;

[ImportingConstructor]
[Obsolete("Never call this directly")]
public EditorConfigDocumentOptionsProviderFactory(ICodingConventionsManager codingConventionsManager)
{
_codingConventionsManager = codingConventionsManager;
}

public IDocumentOptionsProvider Create(Workspace workspace)
{
return new EditorConfigDocumentOptionsProvider(workspace);
return new EditorConfigDocumentOptionsProvider(workspace, _codingConventionsManager);
}
}
}
12 changes: 12 additions & 0 deletions src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities
{
internal class ResettableDelay
{
public static readonly ResettableDelay CompletedDelay = new ResettableDelay();

private readonly int _delayInMilliseconds;
private readonly TaskCompletionSource<object> _taskCompletionSource;

Expand Down Expand Up @@ -39,6 +41,16 @@ public ResettableDelay(int delayInMilliseconds, TaskScheduler foregroundTaskSche
}
}

private ResettableDelay()
{
// create resettableDelay with completed state
_delayInMilliseconds = 0;
_taskCompletionSource = new TaskCompletionSource<object>();
_taskCompletionSource.SetResult(null);

Reset();
}

public Task Task => _taskCompletionSource.Task;

public void Reset()
Expand Down
13 changes: 13 additions & 0 deletions src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,5 +1051,18 @@ public OptionSet Options
return this.Workspace.Options;
}
}

/// <summary>
/// Update current solution as a result of option changes.
///
/// this is a temporary workaround until editorconfig becomes real part of roslyn solution snapshot.
/// until then, this will explicitly fork current solution snapshot
/// </summary>
internal Solution WithOptionChanged()
{
// options are associated with solution snapshot. creating new snapshot
// will cause us to retrieve new options
return new Solution(_state);
}
}
}
22 changes: 22 additions & 0 deletions src/Workspaces/Core/Portable/Workspace/Workspace_Editor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -725,5 +725,27 @@ protected virtual Project AdjustReloadedProject(Project oldProject, Project relo

return newSolution.GetProject(oldProject.Id);
}

/// <summary>
/// Update current solution as a result of option changes.
///
/// this is a temporary workaround until editorconfig becomes real part of roslyn solution snapshot.
/// until then, this will explicitly move current solution forward when such event happened
/// </summary>
internal void OnOptionChanged()
{
using (_serializationLock.DisposableWait())
{
var oldSolution = this.CurrentSolution;
var newSolution = this.SetCurrentSolution(oldSolution.WithOptionChanged());

// for now, this says whole solution is changed.
// in future, we probably going to just raise additional file changed event (for editconfig file) and then
// let IOptionService.OptionChanged event to raise what option has changed.
// currently, since editorconfig file is not part of solution, we can't say which file is changed.
// 1 editorconfig file can affect multiple files in multiple projects so solution changed is easiest options for now.
this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionChanged, oldSolution, newSolution);
}
}
}
}

0 comments on commit 3981411

Please sign in to comment.