Browse Source

Add JumpToReferenceAsync() overload to allow detecting when the decompilation after the jump has finished.

pull/469/merge
Daniel Grunwald 11 years ago
parent
commit
9084ce2eb5
  1. 8
      ILSpy.BamlDecompiler/BamlResourceEntryNode.cs
  2. 8
      ILSpy/App.xaml.cs
  3. 2
      ILSpy/Commands/DecompileAllCommand.cs
  4. 1
      ILSpy/ILSpy.csproj
  5. 17
      ILSpy/MainWindow.xaml.cs
  6. 203
      ILSpy/TaskHelper.cs
  7. 128
      ILSpy/TextView/DecompilerTextView.cs
  8. 5
      ILSpy/TreeNodes/ResourceNodes/XamlResourceNode.cs
  9. 5
      ILSpy/TreeNodes/ResourceNodes/XmlResourceNode.cs

8
ILSpy.BamlDecompiler/BamlResourceEntryNode.cs

@ -26,12 +26,12 @@ namespace ILSpy.BamlDecompiler @@ -26,12 +26,12 @@ namespace ILSpy.BamlDecompiler
public override bool View(DecompilerTextView textView)
{
AvalonEditTextOutput output = new AvalonEditTextOutput();
IHighlightingDefinition highlighting = null;
textView.RunWithCancellation(
token => Task.Factory.StartNew(
() => {
AvalonEditTextOutput output = new AvalonEditTextOutput();
try {
if (LoadBaml(output))
highlighting = HighlightingManager.Instance.GetDefinitionByExtension(".xml");
@ -39,9 +39,9 @@ namespace ILSpy.BamlDecompiler @@ -39,9 +39,9 @@ namespace ILSpy.BamlDecompiler
output.Write(ex.ToString());
}
return output;
}, token),
t => textView.ShowNode(t.Result, this, highlighting)
);
}, token))
.Then(output => textView.ShowNode(output, this, highlighting))
.HandleExceptions();
return true;
}

8
ILSpy/App.xaml.cs

@ -23,6 +23,7 @@ using System.Diagnostics; @@ -23,6 +23,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Navigation;
@ -92,6 +93,7 @@ namespace ICSharpCode.ILSpy @@ -92,6 +93,7 @@ namespace ICSharpCode.ILSpy
AppDomain.CurrentDomain.UnhandledException += ShowErrorBox;
Dispatcher.CurrentDispatcher.UnhandledException += Dispatcher_UnhandledException;
}
TaskScheduler.UnobservedTaskException += DotNet40_UnobservedTaskException;
EventManager.RegisterClassHandler(typeof(Window),
Hyperlink.RequestNavigateEvent,
@ -111,6 +113,12 @@ namespace ICSharpCode.ILSpy @@ -111,6 +113,12 @@ namespace ICSharpCode.ILSpy
return argument;
}
}
void DotNet40_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
// On .NET 4.0, an unobserved exception in a task terminates the process unless we mark it as observed
e.SetObserved();
}
#region Exception Handling
static void Dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)

2
ILSpy/Commands/DecompileAllCommand.cs

@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy @@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy
}
});
return output;
}, ct), task => MainWindow.Instance.TextView.ShowText(task.Result));
}, ct)).Then(output => MainWindow.Instance.TextView.ShowText(output)).HandleExceptions();
}
}
}

1
ILSpy/ILSpy.csproj

@ -177,6 +177,7 @@ @@ -177,6 +177,7 @@
<SubType>Code</SubType>
</Compile>
<Compile Include="Commands\SimpleCommand.cs" />
<Compile Include="TaskHelper.cs" />
<Compile Include="TextView\FoldingCommands.cs" />
<Compile Include="TreeNodes\Analyzer\AnalyzeContextMenuEntry.cs" />
<Compile Include="TreeNodes\Analyzer\AnalyzedAssemblyTreeNode.cs" />

17
ILSpy/MainWindow.xaml.cs

@ -558,6 +558,19 @@ namespace ICSharpCode.ILSpy @@ -558,6 +558,19 @@ namespace ICSharpCode.ILSpy
public void JumpToReference(object reference)
{
JumpToReferenceAsync(reference).HandleExceptions();
}
/// <summary>
/// Jumps to the specified reference.
/// </summary>
/// <returns>
/// Returns a task that will signal completion when the decompilation of the jump target has finished.
/// The task will be marked as canceled if the decompilation is canceled.
/// </returns>
public Task JumpToReferenceAsync(object reference)
{
decompilationTask = TaskHelper.CompletedTask;
ILSpyTreeNode treeNode = FindTreeNode(reference);
if (treeNode != null) {
SelectNode(treeNode);
@ -569,6 +582,7 @@ namespace ICSharpCode.ILSpy @@ -569,6 +582,7 @@ namespace ICSharpCode.ILSpy
}
}
return decompilationTask;
}
#endregion
@ -627,6 +641,7 @@ namespace ICSharpCode.ILSpy @@ -627,6 +641,7 @@ namespace ICSharpCode.ILSpy
DecompileSelectedNodes();
}
Task decompilationTask;
bool ignoreDecompilationRequests;
void DecompileSelectedNodes(DecompilerTextViewState state = null, bool recordHistory = true)
@ -646,7 +661,7 @@ namespace ICSharpCode.ILSpy @@ -646,7 +661,7 @@ namespace ICSharpCode.ILSpy
if (node != null && node.View(decompilerTextView))
return;
}
decompilerTextView.Decompile(this.CurrentLanguage, this.SelectedNodes, new DecompilationOptions() { TextViewState = state });
decompilationTask = decompilerTextView.DecompileAsync(this.CurrentLanguage, this.SelectedNodes, new DecompilationOptions() { TextViewState = state });
}
void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e)

203
ILSpy/TaskHelper.cs

@ -0,0 +1,203 @@ @@ -0,0 +1,203 @@
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.ILSpy.TextView;
namespace ICSharpCode.ILSpy
{
public static class TaskHelper
{
public static readonly Task CompletedTask = FromResult<object>(null);
public static Task<T> FromResult<T>(T result)
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
tcs.SetResult(result);
return tcs.Task;
}
public static Task<T> FromException<T>(Exception ex)
{
var tcs = new TaskCompletionSource<T>();
tcs.SetException(ex);
return tcs.Task;
}
public static Task<T> FromCancellation<T>()
{
var tcs = new TaskCompletionSource<T>();
tcs.SetCanceled();
return tcs.Task;
}
/// <summary>
/// Sets the result of the TaskCompletionSource based on the result of the finished task.
/// </summary>
public static void SetFromTask<T>(this TaskCompletionSource<T> tcs, Task<T> task)
{
switch (task.Status) {
case TaskStatus.RanToCompletion:
tcs.SetResult(task.Result);
break;
case TaskStatus.Canceled:
tcs.SetCanceled();
break;
case TaskStatus.Faulted:
tcs.SetException(task.Exception.InnerExceptions);
break;
default:
throw new InvalidOperationException("The input task must have already finished");
}
}
/// <summary>
/// Sets the result of the TaskCompletionSource based on the result of the finished task.
/// </summary>
public static void SetFromTask(this TaskCompletionSource<object> tcs, Task task)
{
switch (task.Status) {
case TaskStatus.RanToCompletion:
tcs.SetResult(null);
break;
case TaskStatus.Canceled:
tcs.SetCanceled();
break;
case TaskStatus.Faulted:
tcs.SetException(task.Exception.InnerExceptions);
break;
default:
throw new InvalidOperationException("The input task must have already finished");
}
}
public static Task Then<T>(this Task<T> task, Action<T> action)
{
if (action == null)
throw new ArgumentNullException("action");
return task.ContinueWith(t => action(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext());
}
public static Task<U> Then<T, U>(this Task<T> task, Func<T, U> func)
{
if (func == null)
throw new ArgumentNullException("func");
return task.ContinueWith(t => func(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext());
}
public static Task Then<T>(this Task<T> task, Func<T, Task> asyncFunc)
{
if (asyncFunc == null)
throw new ArgumentNullException("asyncFunc");
return task.ContinueWith(t => asyncFunc(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
}
public static Task<U> Then<T, U>(this Task<T> task, Func<T, Task<U>> asyncFunc)
{
if (asyncFunc == null)
throw new ArgumentNullException("asyncFunc");
return task.ContinueWith(t => asyncFunc(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
}
public static Task Then(this Task task, Action action)
{
if (action == null)
throw new ArgumentNullException("action");
return task.ContinueWith(t => {
t.Wait();
action();
}, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext());
}
public static Task<U> Then<U>(this Task task, Func<U> func)
{
if (func == null)
throw new ArgumentNullException("func");
return task.ContinueWith(t => {
t.Wait();
return func();
}, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext());
}
public static Task Then(this Task task, Func<Task> asyncAction)
{
if (asyncAction == null)
throw new ArgumentNullException("asyncAction");
return task.ContinueWith(t => {
t.Wait();
return asyncAction();
}, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
}
public static Task<U> Then<U>(this Task task, Func<Task<U>> asyncFunc)
{
if (asyncFunc == null)
throw new ArgumentNullException("asyncFunc");
return task.ContinueWith(t => {
t.Wait();
return asyncFunc();
}, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
}
/// <summary>
/// If the input task fails, calls the action to handle the error.
/// </summary>
/// <returns>
/// Returns a task that finishes successfully when error handling has completed.
/// If the input task ran successfully, the returned task completes successfully.
/// If the input task was cancelled, the returned task is cancelled as well.
/// </returns>
public static Task Catch<TException>(this Task task, Action<TException> action) where TException : Exception
{
if (action == null)
throw new ArgumentNullException("action");
return task.ContinueWith(t => {
if (t.IsFaulted) {
Exception ex = t.Exception;
while (ex is AggregateException)
ex = ex.InnerException;
if (ex is TException)
action((TException)ex);
else
throw t.Exception;
}
}, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext());
}
/// <summary>
/// Ignore exceptions thrown by the task.
/// </summary>
public static void IgnoreExceptions(this Task task)
{
}
/// <summary>
/// Handle exceptions by displaying the error message in the text view.
/// </summary>
public static void HandleExceptions(this Task task)
{
task.Catch<Exception>(exception => MainWindow.Instance.Dispatcher.BeginInvoke(new Action(delegate {
AvalonEditTextOutput output = new AvalonEditTextOutput();
output.Write(exception.ToString());
MainWindow.Instance.TextView.ShowText(output);
}))).IgnoreExceptions();
}
}
}

128
ILSpy/TextView/DecompilerTextView.cs

@ -210,7 +210,18 @@ namespace ICSharpCode.ILSpy.TextView @@ -210,7 +210,18 @@ namespace ICSharpCode.ILSpy.TextView
/// When the task is cancelled before completing, the callback is not called; and any result
/// of the task (including exceptions) are ignored.
/// </summary>
[Obsolete("RunWithCancellation(taskCreation).ContinueWith(taskCompleted) instead")]
public void RunWithCancellation<T>(Func<CancellationToken, Task<T>> taskCreation, Action<Task<T>> taskCompleted)
{
RunWithCancellation(taskCreation).ContinueWith(taskCompleted, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext());
}
/// <summary>
/// Switches the GUI into "waiting" mode, then calls <paramref name="taskCreation"/> to create
/// the task.
/// If another task is started before the previous task finishes running, the previous task is cancelled.
/// </summary>
public Task<T> RunWithCancellation<T>(Func<CancellationToken, Task<T>> taskCreation)
{
if (waitAdorner.Visibility != Visibility.Visible) {
waitAdorner.Visibility = Visibility.Visible;
@ -223,7 +234,15 @@ namespace ICSharpCode.ILSpy.TextView @@ -223,7 +234,15 @@ namespace ICSharpCode.ILSpy.TextView
if (previousCancellationTokenSource != null)
previousCancellationTokenSource.Cancel();
var task = taskCreation(myCancellationTokenSource.Token);
var tcs = new TaskCompletionSource<T>();
Task<T> task;
try {
task = taskCreation(myCancellationTokenSource.Token);
} catch (OperationCanceledException) {
task = TaskHelper.FromCancellation<T>();
} catch (Exception ex) {
task = TaskHelper.FromException<T>(ex);
}
Action continuation = delegate {
try {
if (currentCancellationTokenSource == myCancellationTokenSource) {
@ -233,21 +252,17 @@ namespace ICSharpCode.ILSpy.TextView @@ -233,21 +252,17 @@ namespace ICSharpCode.ILSpy.TextView
AvalonEditTextOutput output = new AvalonEditTextOutput();
output.WriteLine("The operation was canceled.");
ShowOutput(output);
} else {
taskCompleted(task);
}
tcs.SetFromTask(task);
} else {
try {
task.Wait();
} catch (AggregateException) {
// observe the exception (otherwise the task's finalizer will shut down the AppDomain)
}
tcs.SetCanceled();
}
} finally {
myCancellationTokenSource.Dispose();
}
};
task.ContinueWith(delegate { Dispatcher.BeginInvoke(DispatcherPriority.Normal, continuation); });
return tcs.Task;
}
void cancelButton_Click(object sender, RoutedEventArgs e)
@ -281,7 +296,11 @@ namespace ICSharpCode.ILSpy.TextView @@ -281,7 +296,11 @@ namespace ICSharpCode.ILSpy.TextView
currentCancellationTokenSource.Cancel();
currentCancellationTokenSource = null; // prevent canceled task from producing output
}
this.nextDecompilationRun = null; // remove scheduled decompilation run
if (this.nextDecompilationRun != null) {
// remove scheduled decompilation run
this.nextDecompilationRun.TaskCompletionSource.TrySetCanceled();
this.nextDecompilationRun = null;
}
ShowOutput(textOutput, highlighting);
decompiledNodes = nodes;
}
@ -343,26 +362,40 @@ namespace ICSharpCode.ILSpy.TextView @@ -343,26 +362,40 @@ namespace ICSharpCode.ILSpy.TextView
DecompilationContext nextDecompilationRun;
[Obsolete("Use DecompileAsync() instead")]
public void Decompile(ILSpy.Language language, IEnumerable<ILSpyTreeNode> treeNodes, DecompilationOptions options)
{
DecompileAsync(language, treeNodes, options).HandleExceptions();
}
/// <summary>
/// Starts the decompilation of the given nodes.
/// The result is displayed in the text view.
/// If any errors occur, the error message is displayed in the text view, and the task returned by this method completes successfully.
/// If the operation is cancelled (by starting another decompilation action); the returned task is marked as cancelled.
/// </summary>
public void Decompile(ILSpy.Language language, IEnumerable<ILSpyTreeNode> treeNodes, DecompilationOptions options)
public Task DecompileAsync(ILSpy.Language language, IEnumerable<ILSpyTreeNode> treeNodes, DecompilationOptions options)
{
// Some actions like loading an assembly list cause several selection changes in the tree view,
// and each of those will start a decompilation action.
bool isDecompilationScheduled = this.nextDecompilationRun != null;
if (this.nextDecompilationRun != null)
this.nextDecompilationRun.TaskCompletionSource.TrySetCanceled();
this.nextDecompilationRun = new DecompilationContext(language, treeNodes.ToArray(), options);
var task = this.nextDecompilationRun.TaskCompletionSource.Task;
if (!isDecompilationScheduled) {
Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(
delegate {
var context = this.nextDecompilationRun;
this.nextDecompilationRun = null;
if (context != null)
DoDecompile(context, DefaultOutputLengthLimit);
DoDecompile(context, DefaultOutputLengthLimit)
.ContinueWith(t => context.TaskCompletionSource.SetFromTask(t)).HandleExceptions();
}
));
}
return task;
}
sealed class DecompilationContext
@ -370,6 +403,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -370,6 +403,7 @@ namespace ICSharpCode.ILSpy.TextView
public readonly ILSpy.Language Language;
public readonly ILSpyTreeNode[] TreeNodes;
public readonly DecompilationOptions Options;
public readonly TaskCompletionSource<object> TaskCompletionSource = new TaskCompletionSource<object>();
public DecompilationContext(ILSpy.Language language, ILSpyTreeNode[] treeNodes, DecompilationOptions options)
{
@ -379,33 +413,28 @@ namespace ICSharpCode.ILSpy.TextView @@ -379,33 +413,28 @@ namespace ICSharpCode.ILSpy.TextView
}
}
void DoDecompile(DecompilationContext context, int outputLengthLimit)
Task DoDecompile(DecompilationContext context, int outputLengthLimit)
{
RunWithCancellation(
return RunWithCancellation(
delegate (CancellationToken ct) { // creation of the background task
context.Options.CancellationToken = ct;
return DecompileAsync(context, outputLengthLimit);
},
delegate (Task<AvalonEditTextOutput> task) { // handling the result
try {
AvalonEditTextOutput textOutput = task.Result;
ShowOutput(textOutput, context.Language.SyntaxHighlighting, context.Options.TextViewState);
} catch (AggregateException aggregateException) {
textEditor.SyntaxHighlighting = null;
Debug.WriteLine("Decompiler crashed: " + aggregateException.ToString());
// Unpack aggregate exceptions as long as there's only a single exception:
// (assembly load errors might produce nested aggregate exceptions)
Exception ex = aggregateException;
while (ex is AggregateException && (ex as AggregateException).InnerExceptions.Count == 1)
ex = ex.InnerException;
AvalonEditTextOutput output = new AvalonEditTextOutput();
if (ex is OutputLengthExceededException) {
WriteOutputLengthExceededMessage(output, context, outputLengthLimit == DefaultOutputLengthLimit);
} else {
output.WriteLine(ex.ToString());
}
ShowOutput(output);
})
.Then(
delegate (AvalonEditTextOutput textOutput) { // handling the result
ShowOutput(textOutput, context.Language.SyntaxHighlighting, context.Options.TextViewState);
decompiledNodes = context.TreeNodes;
})
.Catch<Exception>(exception => {
textEditor.SyntaxHighlighting = null;
Debug.WriteLine("Decompiler crashed: " + exception.ToString());
AvalonEditTextOutput output = new AvalonEditTextOutput();
if (exception is OutputLengthExceededException) {
WriteOutputLengthExceededMessage(output, context, outputLengthLimit == DefaultOutputLengthLimit);
} else {
output.WriteLine(exception.ToString());
}
ShowOutput(output);
decompiledNodes = context.TreeNodes;
});
}
@ -435,7 +464,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -435,7 +464,7 @@ namespace ICSharpCode.ILSpy.TextView
} catch (OutputLengthExceededException ex) {
tcs.SetException(ex);
} catch (AggregateException ex) {
tcs.SetException(ex);
tcs.SetException(ex.InnerExceptions);
} catch (OperationCanceledException) {
tcs.SetCanceled();
}
@ -489,7 +518,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -489,7 +518,7 @@ namespace ICSharpCode.ILSpy.TextView
output.AddButton(
Images.ViewCode, "Display Code",
delegate {
DoDecompile(context, ExtendedOutputLengthLimit);
DoDecompile(context, ExtendedOutputLengthLimit).HandleExceptions();
});
output.WriteLine();
}
@ -589,24 +618,17 @@ namespace ICSharpCode.ILSpy.TextView @@ -589,24 +618,17 @@ namespace ICSharpCode.ILSpy.TextView
delegate (CancellationToken ct) {
context.Options.CancellationToken = ct;
return SaveToDiskAsync(context, fileName);
},
delegate (Task<AvalonEditTextOutput> task) {
try {
ShowOutput(task.Result);
} catch (AggregateException aggregateException) {
textEditor.SyntaxHighlighting = null;
Debug.WriteLine("Decompiler crashed: " + aggregateException.ToString());
// Unpack aggregate exceptions as long as there's only a single exception:
// (assembly load errors might produce nested aggregate exceptions)
Exception ex = aggregateException;
while (ex is AggregateException && (ex as AggregateException).InnerExceptions.Count == 1)
ex = ex.InnerException;
AvalonEditTextOutput output = new AvalonEditTextOutput();
output.WriteLine(ex.ToString());
ShowOutput(output);
}
decompiledNodes = context.TreeNodes;
});
})
.Then(output => ShowOutput(output))
.Catch((Exception ex) => {
textEditor.SyntaxHighlighting = null;
Debug.WriteLine("Decompiler crashed: " + ex.ToString());
// Unpack aggregate exceptions as long as there's only a single exception:
// (assembly load errors might produce nested aggregate exceptions)
AvalonEditTextOutput output = new AvalonEditTextOutput();
output.WriteLine(ex.ToString());
ShowOutput(output);
}).HandleExceptions();
}
Task<AvalonEditTextOutput> SaveToDiskAsync(DecompilationContext context, string fileName)

5
ILSpy/TreeNodes/ResourceNodes/XamlResourceNode.cs

@ -73,9 +73,8 @@ namespace ICSharpCode.ILSpy.Xaml @@ -73,9 +73,8 @@ namespace ICSharpCode.ILSpy.Xaml
output.Write(ex.ToString());
}
return output;
}, token),
t => textView.ShowNode(t.Result, this, highlighting)
);
}, token)
).Then(t => textView.ShowNode(t, this, highlighting)).HandleExceptions();
return true;
}
}

5
ILSpy/TreeNodes/ResourceNodes/XmlResourceNode.cs

@ -101,9 +101,8 @@ namespace ICSharpCode.ILSpy.Xaml @@ -101,9 +101,8 @@ namespace ICSharpCode.ILSpy.Xaml
output.Write(ex.ToString());
}
return output;
}, token),
t => textView.ShowNode(t.Result, this, highlighting)
);
}, token)
).Then(t => textView.ShowNode(t, this, highlighting)).HandleExceptions();
return true;
}
}

Loading…
Cancel
Save