Merge pull request #1089 from sharwell/handle-exception

Rewrite SyntaxVisualizerControl to catch exceptions
This commit is contained in:
Sam Harwell 2023-04-27 16:16:16 -05:00 коммит произвёл GitHub
Родитель de1cc8e585 1cfbb251d7
Коммит 64fdcf16db
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 183 добавлений и 29 удалений

Просмотреть файл

@ -0,0 +1,134 @@
// 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.
// NOTE: This code is derived from an implementation originally in dotnet/project-system:
// https://github.com/dotnet/project-system/blob/bdf69d5420ec8d894f5bf4c3d4692900b7f2479c/src/Microsoft.VisualStudio.ProjectSystem.Managed/Threading/Tasks/CancellationSeries.cs
//
// See the commentary in https://github.com/dotnet/roslyn/pull/50156 for notes on incorporating changes made to the
// reference implementation.
using System;
using System.Threading;
namespace Roslyn.Utilities
{
/// <summary>
/// Produces a series of <see cref="CancellationToken"/> objects such that requesting a new token
/// causes the previously issued token to be cancelled.
/// </summary>
/// <remarks>
/// <para>Consuming code is responsible for managing overlapping asynchronous operations.</para>
/// <para>This class has a lock-free implementation to minimise latency and contention.</para>
/// </remarks>
internal sealed class CancellationSeries : IDisposable
{
private CancellationTokenSource? _cts;
private readonly CancellationToken _superToken;
/// <summary>
/// Initializes a new instance of <see cref="CancellationSeries"/>.
/// </summary>
/// <param name="token">An optional cancellation token that, when cancelled, cancels the last
/// issued token and causes any subsequent tokens to be issued in a cancelled state.</param>
public CancellationSeries(CancellationToken token = default)
{
// Initialize with a pre-cancelled source to ensure HasActiveToken has the correct state
_cts = new CancellationTokenSource();
_cts.Cancel();
_superToken = token;
}
/// <summary>
/// Determines if the cancellation series has an active token which has not been cancelled.
/// </summary>
public bool HasActiveToken
=> _cts is { IsCancellationRequested: false };
/// <summary>
/// Creates the next <see cref="CancellationToken"/> in the series, ensuring the last issued
/// token (if any) is cancelled first.
/// </summary>
/// <param name="token">An optional cancellation token that, when cancelled, cancels the
/// returned token.</param>
/// <returns>
/// A cancellation token that will be cancelled when either:
/// <list type="bullet">
/// <item><see cref="CreateNext"/> is called again</item>
/// <item>The token passed to this method (if any) is cancelled</item>
/// <item>The token passed to the constructor (if any) is cancelled</item>
/// <item><see cref="Dispose"/> is called</item>
/// </list>
/// </returns>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
public CancellationToken CreateNext(CancellationToken token = default)
{
var nextSource = CancellationTokenSource.CreateLinkedTokenSource(token, _superToken);
// Obtain the token before exchange, as otherwise the CTS may be cancelled before
// we request the Token, which will result in an ObjectDisposedException.
// This way we would return a cancelled token, which is reasonable.
var nextToken = nextSource.Token;
// The following block is identical to Interlocked.Exchange, except no replacement is made if the current
// field value is null (latch on null). This ensures state is not corrupted if CreateNext is called after
// the object is disposed.
var priorSource = Volatile.Read(ref _cts);
while (priorSource is not null)
{
var candidate = Interlocked.CompareExchange(ref _cts, nextSource, priorSource);
if (candidate == priorSource)
{
break;
}
else
{
priorSource = candidate;
}
}
if (priorSource == null)
{
nextSource.Dispose();
throw new ObjectDisposedException(nameof(CancellationSeries));
}
try
{
priorSource.Cancel();
}
finally
{
// A registered action on the token may throw, which would surface here.
// Ensure we always dispose the prior CTS.
priorSource.Dispose();
}
return nextToken;
}
public void Dispose()
{
var source = Interlocked.Exchange(ref _cts, null);
if (source == null)
{
// Already disposed
return;
}
try
{
source.Cancel();
}
finally
{
source.Dispose();
}
}
}
}

Просмотреть файл

@ -30,6 +30,10 @@
<PackageReference Include="StreamJsonRpc" Version="$(StreamJsonRpcVersion)" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Roslyn.SyntaxVisualizer.Extension" />
</ItemGroup>
<ItemGroup>
<Resource Include="SyntaxGraph.ico" />
</ItemGroup>

Просмотреть файл

@ -6,6 +6,7 @@ using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
@ -332,20 +333,19 @@ namespace Roslyn.SyntaxVisualizer.Control
// the children for any given item are only populated when the item is selected. If lazy is
// false then the entire tree is populated at once (and this can result in bad performance when
// displaying large trees).
public void DisplaySyntaxTree(Document document, SyntaxTree tree, SemanticModel? model = null, bool lazy = true, Workspace? workspace = null)
public async Task DisplaySyntaxTreeAsync(Document document, SyntaxTree tree, SemanticModel? model, bool lazy, Workspace? workspace, CancellationToken cancellationToken)
{
if (tree != null)
{
IsLazy = lazy;
SyntaxTree = tree;
SemanticModel = model;
AddNode(null, SyntaxTree.GetRoot());
AddNode(null, await SyntaxTree.GetRootAsync(cancellationToken));
if (model != null && workspace != null)
{
var root = ThreadHelper.JoinableTaskFactory.Run(() => tree.GetRootAsync());
classifiedSpans = ThreadHelper.JoinableTaskFactory.Run(
() => Classifier.GetClassifiedSpansAsync(document, root.FullSpan, CancellationToken.None)).ToImmutableArray();
var root = await tree.GetRootAsync(cancellationToken);
classifiedSpans = (await Classifier.GetClassifiedSpansAsync(document, root.FullSpan, cancellationToken)).ToImmutableArray();
}
else
{

Просмотреть файл

@ -19,6 +19,7 @@ using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.SyntaxVisualizer.DgmlHelper;
using Roslyn.Utilities;
namespace Roslyn.SyntaxVisualizer.Extension
{
@ -27,6 +28,7 @@ namespace Roslyn.SyntaxVisualizer.Extension
internal partial class SyntaxVisualizerContainer : UserControl, IVsRunningDocTableEvents, IVsSolutionEvents, IDisposable
{
private readonly SyntaxVisualizerToolWindow parent;
private readonly CancellationSeries cancellationSeries = new();
private IWpfTextView? activeWpfTextView;
private IClassificationFormatMap? activeClassificationFormatMap;
private IEditorFormatMap? activeEditorFormatMap;
@ -242,6 +244,8 @@ namespace Roslyn.SyntaxVisualizer.Extension
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread
runningDocumentTableCookie = 0;
}
cancellationSeries.Dispose();
}
#endregion
@ -251,32 +255,44 @@ namespace Roslyn.SyntaxVisualizer.Extension
// that is currently active in the editor.
private void RefreshSyntaxVisualizer()
{
if (IsVisible && activeWpfTextView != null)
var cancellationToken = cancellationSeries.CreateNext();
if (!IsVisible || activeWpfTextView == null)
{
var snapshot = activeWpfTextView.TextBuffer.CurrentSnapshot;
var contentType = snapshot.ContentType;
if (contentType.IsOfType(VisualBasicContentType) ||
contentType.IsOfType(CSharpContentType))
{
// Get the Document corresponding to the currently active text snapshot.
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document != null)
{
// Get the SyntaxTree and SemanticModel corresponding to the Document.
activeSyntaxTree = ThreadHelper.JoinableTaskFactory.Run(() => document.GetSyntaxTreeAsync());
var activeSemanticModel = ThreadHelper.JoinableTaskFactory.Run(() => document.GetSemanticModelAsync());
// Display the SyntaxTree.
if ((contentType.IsOfType(VisualBasicContentType) || contentType.IsOfType(CSharpContentType)) && activeSyntaxTree is not null)
{
syntaxVisualizer.DisplaySyntaxTree(document, activeSyntaxTree, activeSemanticModel, workspace: document.Project.Solution.Workspace);
}
NavigateFromSource();
}
}
return;
}
var snapshot = activeWpfTextView.TextBuffer.CurrentSnapshot;
var contentType = snapshot.ContentType;
if (!contentType.IsOfType(VisualBasicContentType)
&& !contentType.IsOfType(CSharpContentType))
{
return;
}
// Get the Document corresponding to the currently active text snapshot.
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
{
return;
}
_ = ThreadHelper.JoinableTaskFactory.RunAsync(
async () =>
{
// Get the SyntaxTree and SemanticModel corresponding to the Document.
activeSyntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
var activeSemanticModel = await document.GetSemanticModelAsync(cancellationToken);
// Display the SyntaxTree.
if (activeSyntaxTree is not null)
{
await syntaxVisualizer.DisplaySyntaxTreeAsync(document, activeSyntaxTree, activeSemanticModel, lazy: true, document.Project.Solution.Workspace, cancellationToken);
}
NavigateFromSource();
});
}
// When user clicks / selects text in the editor select the corresponding item in the treeview.