You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1467 lines
48 KiB
1467 lines
48 KiB
// Copyright (c) 2011 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.
|
|
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection.Metadata;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Data;
|
|
using System.Windows.Documents;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Threading;
|
|
using System.Xml;
|
|
|
|
using ICSharpCode.AvalonEdit;
|
|
using ICSharpCode.AvalonEdit.Document;
|
|
using ICSharpCode.AvalonEdit.Editing;
|
|
using ICSharpCode.AvalonEdit.Folding;
|
|
using ICSharpCode.AvalonEdit.Highlighting;
|
|
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
|
|
using ICSharpCode.AvalonEdit.Rendering;
|
|
using ICSharpCode.AvalonEdit.Search;
|
|
using ICSharpCode.Decompiler;
|
|
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
|
|
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
|
|
using ICSharpCode.Decompiler.Documentation;
|
|
using ICSharpCode.Decompiler.TypeSystem;
|
|
using ICSharpCode.ILSpy.AssemblyTree;
|
|
using ICSharpCode.ILSpy.AvalonEdit;
|
|
using ICSharpCode.ILSpy.Options;
|
|
using ICSharpCode.ILSpy.Themes;
|
|
using ICSharpCode.ILSpy.TreeNodes;
|
|
using ICSharpCode.ILSpy.ViewModels;
|
|
using ICSharpCode.ILSpyX;
|
|
|
|
using Microsoft.Win32;
|
|
|
|
using TomsToolbox.Composition;
|
|
using TomsToolbox.Wpf;
|
|
|
|
using ResourceKeys = ICSharpCode.ILSpy.Themes.ResourceKeys;
|
|
|
|
namespace ICSharpCode.ILSpy.TextView
|
|
{
|
|
/// <summary>
|
|
/// Manages the TextEditor showing the decompiled code.
|
|
/// Contains all the threading logic that makes the decompiler work in the background.
|
|
/// </summary>
|
|
public sealed partial class DecompilerTextView : UserControl, IHaveState, IProgress<DecompilationProgress>
|
|
{
|
|
readonly IExportProvider exportProvider;
|
|
readonly SettingsService settingsService;
|
|
readonly LanguageService languageService;
|
|
readonly MainWindow mainWindow;
|
|
readonly ReferenceElementGenerator referenceElementGenerator;
|
|
readonly UIElementGenerator uiElementGenerator;
|
|
readonly List<VisualLineElementGenerator?> activeCustomElementGenerators = new List<VisualLineElementGenerator?>();
|
|
readonly BracketHighlightRenderer bracketHighlightRenderer;
|
|
RichTextColorizer? activeRichTextColorizer;
|
|
RichTextModel? activeRichTextModel;
|
|
FoldingManager? foldingManager;
|
|
ILSpyTreeNode[]? decompiledNodes;
|
|
Uri? currentAddress;
|
|
string? currentTitle;
|
|
bool expandMemberDefinitions;
|
|
|
|
DefinitionLookup? definitionLookup;
|
|
TextSegmentCollection<ReferenceSegment>? references;
|
|
CancellationTokenSource? currentCancellationTokenSource;
|
|
|
|
readonly TextMarkerService textMarkerService;
|
|
readonly List<ITextMarker> localReferenceMarks = new List<ITextMarker>();
|
|
|
|
#region Constructor
|
|
public DecompilerTextView(IExportProvider exportProvider)
|
|
{
|
|
this.exportProvider = exportProvider;
|
|
settingsService = exportProvider.GetExportedValue<SettingsService>();
|
|
languageService = exportProvider.GetExportedValue<LanguageService>();
|
|
mainWindow = exportProvider.GetExportedValue<MainWindow>();
|
|
|
|
RegisterHighlighting();
|
|
|
|
InitializeComponent();
|
|
|
|
this.referenceElementGenerator = new ReferenceElementGenerator(this.IsLink);
|
|
textEditor.TextArea.TextView.ElementGenerators.Add(referenceElementGenerator);
|
|
this.uiElementGenerator = new UIElementGenerator();
|
|
this.bracketHighlightRenderer = new BracketHighlightRenderer(textEditor.TextArea.TextView);
|
|
textEditor.TextArea.TextView.ElementGenerators.Add(uiElementGenerator);
|
|
textEditor.Options.RequireControlModifierForHyperlinkClick = false;
|
|
textEditor.TextArea.TextView.MouseHover += TextViewMouseHover;
|
|
textEditor.TextArea.TextView.MouseHoverStopped += TextViewMouseHoverStopped;
|
|
textEditor.TextArea.PreviewMouseDown += TextAreaMouseDown;
|
|
textEditor.TextArea.PreviewMouseUp += TextAreaMouseUp;
|
|
textEditor.TextArea.Caret.PositionChanged += HighlightBrackets;
|
|
textEditor.MouseMove += TextEditorMouseMove;
|
|
textEditor.MouseLeave += TextEditorMouseLeave;
|
|
textEditor.SetBinding(Control.FontFamilyProperty, new Binding { Source = settingsService.DisplaySettings, Path = new PropertyPath("SelectedFont") });
|
|
textEditor.SetBinding(Control.FontSizeProperty, new Binding { Source = settingsService.DisplaySettings, Path = new PropertyPath("SelectedFontSize") });
|
|
textEditor.SetBinding(TextEditor.WordWrapProperty, new Binding { Source = settingsService.DisplaySettings, Path = new PropertyPath("EnableWordWrap") });
|
|
|
|
// disable Tab editing command (useless for read-only editor); allow using tab for focus navigation instead
|
|
RemoveEditCommand(EditingCommands.TabForward);
|
|
RemoveEditCommand(EditingCommands.TabBackward);
|
|
|
|
textMarkerService = new TextMarkerService(textEditor.TextArea.TextView);
|
|
textEditor.TextArea.TextView.BackgroundRenderers.Add(textMarkerService);
|
|
textEditor.TextArea.TextView.LineTransformers.Add(textMarkerService);
|
|
textEditor.ShowLineNumbers = true;
|
|
|
|
MessageBus<SettingsChangedEventArgs>.Subscribers += Settings_Changed;
|
|
|
|
// SearchPanel
|
|
SearchPanel searchPanel = SearchPanel.Install(textEditor.TextArea);
|
|
searchPanel.RegisterCommands(mainWindow.CommandBindings);
|
|
searchPanel.SetResourceReference(SearchPanel.MarkerBrushProperty, ResourceKeys.SearchResultBackgroundBrush);
|
|
searchPanel.Loaded += (_, _) => {
|
|
// HACK: fix search text box
|
|
var textBox = searchPanel.Template.FindName("PART_searchTextBox", searchPanel) as TextBox;
|
|
if (textBox != null)
|
|
{
|
|
// the hardcoded but misaligned margin
|
|
textBox.Margin = new Thickness(3);
|
|
// the hardcoded height
|
|
textBox.Height = double.NaN;
|
|
}
|
|
};
|
|
|
|
ShowLineMargin();
|
|
SetHighlightCurrentLine();
|
|
|
|
ContextMenuProvider.Add(this);
|
|
|
|
textEditor.TextArea.TextView.SetResourceReference(ICSharpCode.AvalonEdit.Rendering.TextView.LinkTextForegroundBrushProperty, ResourceKeys.LinkTextForegroundBrush);
|
|
textEditor.TextArea.TextView.SetResourceReference(ICSharpCode.AvalonEdit.Rendering.TextView.CurrentLineBackgroundProperty, ResourceKeys.CurrentLineBackgroundBrush);
|
|
textEditor.TextArea.TextView.SetResourceReference(ICSharpCode.AvalonEdit.Rendering.TextView.CurrentLineBorderProperty, ResourceKeys.CurrentLineBorderPen);
|
|
|
|
DataObject.AddSettingDataHandler(textEditor.TextArea, OnSettingData);
|
|
|
|
this.DataContextChanged += DecompilerTextView_DataContextChanged;
|
|
}
|
|
|
|
private void DecompilerTextView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (this.DataContext is PaneModel model)
|
|
{
|
|
model.Title = currentTitle ?? ILSpy.Properties.Resources.Decompiling;
|
|
}
|
|
}
|
|
|
|
void RemoveEditCommand(RoutedUICommand command)
|
|
{
|
|
var handler = textEditor.TextArea.DefaultInputHandler.Editing;
|
|
var inputBinding = handler.InputBindings.FirstOrDefault(b => b.Command == command);
|
|
if (inputBinding != null)
|
|
handler.InputBindings.Remove(inputBinding);
|
|
var commandBinding = handler.CommandBindings.FirstOrDefault(b => b.Command == command);
|
|
if (commandBinding != null)
|
|
handler.CommandBindings.Remove(commandBinding);
|
|
}
|
|
#endregion
|
|
|
|
#region Line margin
|
|
|
|
private void Settings_Changed(object? sender, SettingsChangedEventArgs e)
|
|
{
|
|
Settings_PropertyChanged(sender, e);
|
|
}
|
|
|
|
private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (sender is not DisplaySettings)
|
|
return;
|
|
|
|
switch (e.PropertyName)
|
|
{
|
|
case nameof(DisplaySettings.ShowLineNumbers):
|
|
ShowLineMargin();
|
|
break;
|
|
case nameof(DisplaySettings.HighlightCurrentLine):
|
|
SetHighlightCurrentLine();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ShowLineMargin()
|
|
{
|
|
foreach (var margin in this.textEditor.TextArea.LeftMargins)
|
|
{
|
|
if (margin is LineNumberMargin || margin is System.Windows.Shapes.Line)
|
|
{
|
|
margin.Visibility = settingsService.DisplaySettings.ShowLineNumbers ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetHighlightCurrentLine()
|
|
{
|
|
textEditor.Options.HighlightCurrentLine = settingsService.DisplaySettings.HighlightCurrentLine;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Tooltip support
|
|
ToolTip? toolTip;
|
|
Popup? popupToolTip;
|
|
|
|
void TextViewMouseHover(object sender, MouseEventArgs e)
|
|
{
|
|
if (!TryCloseExistingPopup(false))
|
|
{
|
|
return;
|
|
}
|
|
TextViewPosition? position = GetPositionFromMousePosition();
|
|
if (position == null)
|
|
return;
|
|
int offset = textEditor.Document.GetOffset(position.Value.Location);
|
|
if (referenceElementGenerator.References == null)
|
|
return;
|
|
ReferenceSegment? seg = referenceElementGenerator.References.FindSegmentsContaining(offset).FirstOrDefault();
|
|
if (seg == null)
|
|
return;
|
|
object? content = GenerateTooltip(seg);
|
|
|
|
if (content != null)
|
|
{
|
|
popupToolTip = content as Popup;
|
|
|
|
if (popupToolTip != null)
|
|
{
|
|
var popupPosition = GetPopupPosition(e);
|
|
popupToolTip.Closed += ToolTipClosed;
|
|
popupToolTip.Placement = PlacementMode.Relative;
|
|
popupToolTip.PlacementTarget = this;
|
|
popupToolTip.HorizontalOffset = popupPosition.X;
|
|
popupToolTip.VerticalOffset = popupPosition.Y;
|
|
popupToolTip.StaysOpen = true; // We will close it ourselves
|
|
|
|
e.Handled = true;
|
|
popupToolTip.IsOpen = true;
|
|
distanceToPopupLimit = double.PositiveInfinity; // reset limit; we'll re-calculate it on the next mouse movement
|
|
}
|
|
else
|
|
{
|
|
if (toolTip == null)
|
|
{
|
|
toolTip = new ToolTip();
|
|
toolTip.Closed += ToolTipClosed;
|
|
}
|
|
toolTip.PlacementTarget = this; // required for property inheritance
|
|
|
|
if (content is string s)
|
|
{
|
|
toolTip.Content = new TextBlock {
|
|
Text = s,
|
|
TextWrapping = TextWrapping.Wrap
|
|
};
|
|
}
|
|
else
|
|
toolTip.Content = content;
|
|
|
|
e.Handled = true;
|
|
toolTip.IsOpen = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TryCloseExistingPopup(bool mouseClick)
|
|
{
|
|
if (popupToolTip != null)
|
|
{
|
|
if (popupToolTip.IsOpen && !mouseClick && popupToolTip is FlowDocumentTooltip t && !t.CloseWhenMouseMovesAway)
|
|
{
|
|
return false; // Popup does not want to be closed yet
|
|
}
|
|
popupToolTip.IsOpen = false;
|
|
popupToolTip = null;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary> Returns Popup position based on mouse position, in device independent units </summary>
|
|
Point GetPopupPosition(MouseEventArgs mouseArgs)
|
|
{
|
|
Point mousePos = mouseArgs.GetPosition(this);
|
|
// align Popup with line bottom
|
|
TextViewPosition? logicalPos = textEditor.GetPositionFromPoint(mousePos);
|
|
if (logicalPos.HasValue)
|
|
{
|
|
var textView = textEditor.TextArea.TextView;
|
|
return textView.GetVisualPosition(logicalPos.Value, VisualYPosition.LineBottom)
|
|
- textView.ScrollOffset
|
|
+ new Vector(-4, 0);
|
|
}
|
|
else
|
|
{
|
|
return mousePos + new Vector(-4, 6);
|
|
}
|
|
}
|
|
|
|
void TextViewMouseHoverStopped(object sender, MouseEventArgs e)
|
|
{
|
|
// Non-popup tooltips get closed as soon as the mouse starts moving again
|
|
if (toolTip != null)
|
|
{
|
|
toolTip.IsOpen = false;
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
double distanceToPopupLimit;
|
|
const double MaxMovementAwayFromPopup = 5;
|
|
|
|
void TextEditorMouseMove(object sender, MouseEventArgs e)
|
|
{
|
|
if (popupToolTip != null && PresentationSource.FromVisual(popupToolTip.Child) != null)
|
|
{
|
|
double distanceToPopup = GetDistanceToPopup(e);
|
|
if (distanceToPopup > distanceToPopupLimit)
|
|
{
|
|
// Close popup if mouse moved away, exceeding the limit
|
|
TryCloseExistingPopup(false);
|
|
}
|
|
else
|
|
{
|
|
// reduce distanceToPopupLimit
|
|
distanceToPopupLimit = Math.Min(distanceToPopupLimit, distanceToPopup + MaxMovementAwayFromPopup);
|
|
}
|
|
}
|
|
}
|
|
|
|
double GetDistanceToPopup(MouseEventArgs e)
|
|
{
|
|
Point p = popupToolTip!.Child.PointFromScreen(PointToScreen(e.GetPosition(this)));
|
|
Size size = popupToolTip.Child.RenderSize;
|
|
double x = 0;
|
|
if (p.X < 0)
|
|
x = -p.X;
|
|
else if (p.X > size.Width)
|
|
x = p.X - size.Width;
|
|
double y = 0;
|
|
if (p.Y < 0)
|
|
y = -p.Y;
|
|
else if (p.Y > size.Height)
|
|
y = p.Y - size.Height;
|
|
return Math.Sqrt(x * x + y * y);
|
|
}
|
|
|
|
void TextEditorMouseLeave(object sender, MouseEventArgs e)
|
|
{
|
|
if (popupToolTip != null && !popupToolTip.IsMouseOver)
|
|
{
|
|
// do not close popup if mouse moved from editor to popup
|
|
TryCloseExistingPopup(false);
|
|
}
|
|
}
|
|
|
|
void OnUnloaded(object sender, EventArgs e)
|
|
{
|
|
// Close popup when another document gets selected
|
|
// TextEditorMouseLeave is not sufficient for this because the mouse might be over the popup when the document switch happens (e.g. Ctrl+Tab)
|
|
TryCloseExistingPopup(true);
|
|
}
|
|
|
|
void ToolTipClosed(object? sender, EventArgs e)
|
|
{
|
|
if (toolTip == sender)
|
|
{
|
|
toolTip = null;
|
|
}
|
|
if (popupToolTip == sender)
|
|
{
|
|
// Because popupToolTip instances are created by the tooltip provider,
|
|
// they might be reused; so we should detach the event handler
|
|
if (popupToolTip != null)
|
|
{
|
|
popupToolTip.Closed -= ToolTipClosed;
|
|
}
|
|
popupToolTip = null;
|
|
}
|
|
}
|
|
|
|
object? GenerateTooltip(ReferenceSegment segment)
|
|
{
|
|
var fontSize = settingsService.DisplaySettings.SelectedFontSize;
|
|
|
|
if (segment.Reference is ICSharpCode.Decompiler.Disassembler.OpCodeInfo code)
|
|
{
|
|
XmlDocumentationProvider docProvider = XmlDocLoader.MscorlibDocumentation;
|
|
DocumentationUIBuilder renderer = new DocumentationUIBuilder(new CSharpAmbience(), languageService.Language.SyntaxHighlighting, settingsService.DisplaySettings, mainWindow);
|
|
renderer.AddSignatureBlock($"{code.Name} (0x{code.Code:x})");
|
|
if (docProvider != null)
|
|
{
|
|
string documentation = docProvider.GetDocumentation("F:System.Reflection.Emit.OpCodes." + code.EncodedName);
|
|
if (documentation != null)
|
|
{
|
|
renderer.AddXmlDocumentation(documentation, null, null);
|
|
}
|
|
}
|
|
return new FlowDocumentTooltip(renderer.CreateDocument(), fontSize, mainWindow.ActualWidth);
|
|
}
|
|
else if (segment.Reference is IEntity entity)
|
|
{
|
|
var document = CreateTooltipForEntity(entity);
|
|
if (document == null)
|
|
return null;
|
|
return new FlowDocumentTooltip(document, fontSize, mainWindow.ActualWidth);
|
|
}
|
|
else if (segment.Reference is EntityReference unresolvedEntity)
|
|
{
|
|
var assemblyList = exportProvider.GetExportedValue<AssemblyList>();
|
|
var module = unresolvedEntity.ResolveAssembly(assemblyList);
|
|
if (module == null)
|
|
return null;
|
|
var typeSystem = new DecompilerTypeSystem(module,
|
|
module.GetAssemblyResolver(),
|
|
TypeSystemOptions.Default | TypeSystemOptions.Uncached);
|
|
try
|
|
{
|
|
Handle handle = unresolvedEntity.Handle;
|
|
if (!handle.IsEntityHandle())
|
|
return null;
|
|
IEntity resolved = typeSystem.MainModule.ResolveEntity((EntityHandle)handle);
|
|
if (resolved == null)
|
|
return null;
|
|
var document = CreateTooltipForEntity(resolved);
|
|
if (document == null)
|
|
return null;
|
|
return new FlowDocumentTooltip(document, fontSize, mainWindow.ActualWidth);
|
|
}
|
|
catch (BadImageFormatException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
FlowDocument? CreateTooltipForEntity(IEntity resolved)
|
|
{
|
|
Language currentLanguage = languageService.Language;
|
|
DocumentationUIBuilder renderer = new DocumentationUIBuilder(new CSharpAmbience(), currentLanguage.SyntaxHighlighting, settingsService.DisplaySettings, mainWindow);
|
|
RichText richText = currentLanguage.GetRichTextTooltip(resolved);
|
|
if (richText == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
renderer.AddSignatureBlock(richText.Text, richText.ToRichTextModel());
|
|
try
|
|
{
|
|
if (resolved.ParentModule == null || resolved.ParentModule.MetadataFile == null)
|
|
return null;
|
|
var docProvider = XmlDocLoader.LoadDocumentation(resolved.ParentModule.MetadataFile);
|
|
if (docProvider != null)
|
|
{
|
|
string documentation = docProvider.GetDocumentation(resolved.GetIdString());
|
|
if (documentation != null)
|
|
{
|
|
renderer.AddXmlDocumentation(documentation, resolved, ResolveReference);
|
|
}
|
|
}
|
|
}
|
|
catch (XmlException)
|
|
{
|
|
// ignore
|
|
}
|
|
return renderer.CreateDocument();
|
|
|
|
IEntity? ResolveReference(string idString)
|
|
{
|
|
var assemblyList = exportProvider.GetExportedValue<AssemblyList>();
|
|
return AssemblyTreeModel.FindEntityInRelevantAssemblies(idString, assemblyList.GetAssemblies());
|
|
}
|
|
}
|
|
|
|
sealed class FlowDocumentTooltip : Popup
|
|
{
|
|
readonly FlowDocumentScrollViewer viewer;
|
|
|
|
public FlowDocumentTooltip(FlowDocument document, double fontSize, double maxWith)
|
|
{
|
|
TextOptions.SetTextFormattingMode(this, TextFormattingMode.Display);
|
|
viewer = new() {
|
|
Width = document.MinPageWidth + fontSize * 5,
|
|
MaxWidth = maxWith,
|
|
Document = document
|
|
};
|
|
Border border = new Border {
|
|
BorderThickness = new Thickness(1),
|
|
MaxHeight = 400,
|
|
Child = viewer
|
|
};
|
|
border.SetResourceReference(Border.BackgroundProperty, SystemColors.ControlBrushKey);
|
|
border.SetResourceReference(Border.BorderBrushProperty, SystemColors.ControlDarkBrushKey);
|
|
|
|
this.Child = border;
|
|
viewer.SetResourceReference(ForegroundProperty, SystemColors.InfoTextBrushKey);
|
|
document.TextAlignment = TextAlignment.Left;
|
|
document.FontSize = fontSize;
|
|
document.FontFamily = SystemFonts.SmallCaptionFontFamily;
|
|
}
|
|
|
|
public bool CloseWhenMouseMovesAway {
|
|
get { return !this.IsKeyboardFocusWithin; }
|
|
}
|
|
|
|
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
|
|
{
|
|
base.OnLostKeyboardFocus(e);
|
|
this.IsOpen = false;
|
|
}
|
|
|
|
protected override void OnMouseLeave(MouseEventArgs e)
|
|
{
|
|
base.OnMouseLeave(e);
|
|
// When the mouse is over the popup, it is possible for ILSpy to be minimized,
|
|
// or moved into the background, and yet the popup stays open.
|
|
// We don't have a good method here to check whether the mouse moved back into the text area
|
|
// or somewhere else, so we'll just close the popup.
|
|
if (CloseWhenMouseMovesAway)
|
|
this.IsOpen = false;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Highlight brackets
|
|
void HighlightBrackets(object? sender, EventArgs e)
|
|
{
|
|
if (settingsService.DisplaySettings.HighlightMatchingBraces)
|
|
{
|
|
var result = languageService.Language.BracketSearcher.SearchBracket(textEditor.Document, textEditor.CaretOffset);
|
|
bracketHighlightRenderer.SetHighlight(result);
|
|
}
|
|
else
|
|
{
|
|
bracketHighlightRenderer.SetHighlight(null);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region RunWithCancellation
|
|
public void Report(DecompilationProgress value)
|
|
{
|
|
double v = (double)value.UnitsCompleted / value.TotalUnits;
|
|
Dispatcher.BeginInvoke(DispatcherPriority.Normal, delegate {
|
|
progressBar.IsIndeterminate = !double.IsFinite(v);
|
|
progressBar.Value = v * 100.0;
|
|
progressTitle.Text = !string.IsNullOrWhiteSpace(value.Title) ? value.Title : Properties.Resources.Decompiling;
|
|
progressText.Text = value.Status;
|
|
progressText.Visibility = !string.IsNullOrWhiteSpace(progressText.Text) ? Visibility.Visible : Visibility.Collapsed;
|
|
var taskBar = mainWindow.TaskbarItemInfo;
|
|
if (taskBar != null)
|
|
{
|
|
taskBar.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
|
|
taskBar.ProgressValue = v;
|
|
}
|
|
if (this.DataContext is TabPageModel model)
|
|
{
|
|
model.Title = progressTitle.Text;
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <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, string? progressTitle = null)
|
|
{
|
|
if (waitAdorner.Visibility != Visibility.Visible)
|
|
{
|
|
waitAdorner.Visibility = Visibility.Visible;
|
|
// Work around a WPF bug by setting IsIndeterminate only while the progress bar is visible.
|
|
// https://github.com/icsharpcode/ILSpy/issues/593
|
|
this.progressTitle.Text = progressTitle == null ? Properties.Resources.Decompiling : progressTitle;
|
|
progressBar.IsIndeterminate = true;
|
|
progressText.Text = null;
|
|
progressText.Visibility = Visibility.Collapsed;
|
|
waitAdorner.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, new Duration(TimeSpan.FromSeconds(0.5)), FillBehavior.Stop));
|
|
var taskBar = mainWindow.TaskbarItemInfo;
|
|
if (taskBar != null)
|
|
{
|
|
taskBar.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Indeterminate;
|
|
}
|
|
}
|
|
CancellationTokenSource? previousCancellationTokenSource = currentCancellationTokenSource;
|
|
var myCancellationTokenSource = new CancellationTokenSource();
|
|
currentCancellationTokenSource = myCancellationTokenSource;
|
|
// cancel the previous only after current was set to the new one (avoid that the old one still finishes successfully)
|
|
if (previousCancellationTokenSource != null)
|
|
{
|
|
previousCancellationTokenSource.Cancel();
|
|
}
|
|
|
|
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)
|
|
{
|
|
currentCancellationTokenSource = null;
|
|
waitAdorner.Visibility = Visibility.Collapsed;
|
|
progressBar.IsIndeterminate = false;
|
|
progressText.Text = null;
|
|
progressText.Visibility = Visibility.Collapsed;
|
|
var taskBar = mainWindow.TaskbarItemInfo;
|
|
if (taskBar != null)
|
|
{
|
|
taskBar.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
|
|
}
|
|
if (task.IsCanceled)
|
|
{
|
|
AvalonEditTextOutput output = new AvalonEditTextOutput();
|
|
output.WriteLine("The operation was canceled.");
|
|
ShowOutput(output);
|
|
}
|
|
tcs.SetFromTask(task);
|
|
}
|
|
else
|
|
{
|
|
tcs.SetCanceled();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
myCancellationTokenSource.Dispose();
|
|
}
|
|
};
|
|
task.ContinueWith(delegate { Dispatcher.BeginInvoke(DispatcherPriority.Normal, continuation); });
|
|
return tcs.Task;
|
|
}
|
|
|
|
void CancelButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (currentCancellationTokenSource != null)
|
|
{
|
|
currentCancellationTokenSource.Cancel();
|
|
// Don't set to null: the task still needs to produce output and hide the wait adorner
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region ShowOutput
|
|
public void ShowText(AvalonEditTextOutput textOutput)
|
|
{
|
|
ShowNodes(textOutput, null);
|
|
}
|
|
|
|
public void ShowNode(AvalonEditTextOutput textOutput, ILSpyTreeNode node, IHighlightingDefinition? highlighting = null)
|
|
{
|
|
ShowNodes(textOutput, new[] { node }, highlighting);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the given output in the text view.
|
|
/// Cancels any currently running decompilation tasks.
|
|
/// </summary>
|
|
public void ShowNodes(AvalonEditTextOutput textOutput, ILSpyTreeNode[]? nodes, IHighlightingDefinition? highlighting = null)
|
|
{
|
|
// Cancel the decompilation task:
|
|
if (currentCancellationTokenSource != null)
|
|
{
|
|
currentCancellationTokenSource.Cancel();
|
|
currentCancellationTokenSource = null; // prevent canceled task from producing output
|
|
}
|
|
if (this.nextDecompilationRun != null)
|
|
{
|
|
// remove scheduled decompilation run
|
|
this.nextDecompilationRun.TaskCompletionSource.TrySetCanceled();
|
|
this.nextDecompilationRun = null;
|
|
}
|
|
if (nodes != null && (string.IsNullOrEmpty(textOutput.Title)
|
|
|| textOutput.Title == Properties.Resources.NewTab))
|
|
{
|
|
textOutput.Title = string.Join(", ", nodes.Select(n => n.Text));
|
|
}
|
|
|
|
decompiledNodes = nodes;
|
|
ShowOutput(textOutput, highlighting);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the given output in the text view.
|
|
/// </summary>
|
|
void ShowOutput(AvalonEditTextOutput textOutput, IHighlightingDefinition? highlighting = null, DecompilerTextViewState? state = null)
|
|
{
|
|
Debug.WriteLine("Showing {0} characters of output", textOutput.TextLength);
|
|
Stopwatch w = Stopwatch.StartNew();
|
|
|
|
ClearLocalReferenceMarks();
|
|
textEditor.ScrollToHome();
|
|
if (foldingManager != null)
|
|
{
|
|
FoldingManager.Uninstall(foldingManager);
|
|
foldingManager = null;
|
|
}
|
|
textEditor.Document = null; // clear old document while we're changing the highlighting
|
|
uiElementGenerator.UIElements = textOutput.UIElements;
|
|
referenceElementGenerator.References = textOutput.References;
|
|
references = textOutput.References;
|
|
definitionLookup = textOutput.DefinitionLookup;
|
|
textEditor.SyntaxHighlighting = highlighting;
|
|
textEditor.Options.EnableEmailHyperlinks = textOutput.EnableHyperlinks;
|
|
textEditor.Options.EnableHyperlinks = textOutput.EnableHyperlinks;
|
|
activeRichTextModel = null;
|
|
if (activeRichTextColorizer != null)
|
|
textEditor.TextArea.TextView.LineTransformers.Remove(activeRichTextColorizer);
|
|
if (textOutput.HighlightingModel != null)
|
|
{
|
|
activeRichTextModel = textOutput.HighlightingModel;
|
|
activeRichTextColorizer = new RichTextColorizer(textOutput.HighlightingModel);
|
|
textEditor.TextArea.TextView.LineTransformers.Insert(highlighting == null ? 0 : 1, activeRichTextColorizer);
|
|
}
|
|
|
|
// Change the set of active element generators:
|
|
foreach (var elementGenerator in activeCustomElementGenerators)
|
|
{
|
|
textEditor.TextArea.TextView.ElementGenerators.Remove(elementGenerator);
|
|
}
|
|
activeCustomElementGenerators.Clear();
|
|
|
|
foreach (var elementGenerator in textOutput.elementGenerators)
|
|
{
|
|
textEditor.TextArea.TextView.ElementGenerators.Add(elementGenerator);
|
|
activeCustomElementGenerators.Add(elementGenerator);
|
|
}
|
|
|
|
Debug.WriteLine(" Set-up: {0}", w.Elapsed);
|
|
w.Restart();
|
|
textEditor.Document = textOutput.GetDocument();
|
|
Debug.WriteLine(" Assigning document: {0}", w.Elapsed);
|
|
w.Restart();
|
|
if (textOutput.Foldings.Count > 0)
|
|
{
|
|
if (state != null)
|
|
{
|
|
state.RestoreFoldings(textOutput.Foldings, settingsService.DisplaySettings.ExpandMemberDefinitions);
|
|
textEditor.ScrollToVerticalOffset(state.VerticalOffset);
|
|
textEditor.ScrollToHorizontalOffset(state.HorizontalOffset);
|
|
}
|
|
foldingManager = FoldingManager.Install(textEditor.TextArea);
|
|
foldingManager.UpdateFoldings(textOutput.Foldings.OrderBy(f => f.StartOffset), -1);
|
|
Debug.WriteLine(" Updating folding: {0}", w.Elapsed);
|
|
w.Restart();
|
|
}
|
|
else if (highlighting?.Name == "XML")
|
|
{
|
|
foldingManager = FoldingManager.Install(textEditor.TextArea);
|
|
var foldingStrategy = new XmlFoldingStrategy();
|
|
foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);
|
|
Debug.WriteLine(" Updating folding: {0}", w.Elapsed);
|
|
w.Restart();
|
|
}
|
|
|
|
if (this.DataContext is PaneModel model)
|
|
{
|
|
model.Title = textOutput.Title;
|
|
}
|
|
currentAddress = textOutput.Address;
|
|
currentTitle = textOutput.Title;
|
|
expandMemberDefinitions = settingsService.DisplaySettings.ExpandMemberDefinitions;
|
|
}
|
|
#endregion
|
|
|
|
#region Decompile (for display)
|
|
// more than 5M characters is too slow to output (when user browses treeview)
|
|
public const int DefaultOutputLengthLimit = 5000000;
|
|
|
|
// more than 75M characters can get us into trouble with memory usage
|
|
public const int ExtendedOutputLengthLimit = 75000000;
|
|
|
|
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 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)
|
|
.ContinueWith(t => context.TaskCompletionSource.SetFromTask(t)).HandleExceptions();
|
|
}
|
|
));
|
|
}
|
|
return task;
|
|
}
|
|
|
|
sealed class DecompilationContext
|
|
{
|
|
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)
|
|
{
|
|
this.Language = language;
|
|
this.TreeNodes = treeNodes;
|
|
this.Options = options;
|
|
}
|
|
}
|
|
|
|
Task DoDecompile(DecompilationContext context, int outputLengthLimit)
|
|
{
|
|
return RunWithCancellation(
|
|
delegate (CancellationToken ct) { // creation of the background task
|
|
context.Options.CancellationToken = ct;
|
|
context.Options.Progress = this;
|
|
decompiledNodes = context.TreeNodes;
|
|
return DecompileAsync(context, outputLengthLimit);
|
|
})
|
|
.Then(
|
|
delegate (AvalonEditTextOutput textOutput) { // handling the result
|
|
ShowOutput(textOutput, context.Language.SyntaxHighlighting, context.Options.TextViewState);
|
|
})
|
|
.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);
|
|
});
|
|
}
|
|
|
|
Task<AvalonEditTextOutput> DecompileAsync(DecompilationContext context, int outputLengthLimit)
|
|
{
|
|
Debug.WriteLine("Start decompilation of {0} tree nodes", context.TreeNodes.Length);
|
|
|
|
TaskCompletionSource<AvalonEditTextOutput> tcs = new TaskCompletionSource<AvalonEditTextOutput>();
|
|
if (context.TreeNodes.Length == 0)
|
|
{
|
|
// If there's nothing to be decompiled, don't bother starting up a thread.
|
|
// (Improves perf in some cases since we don't have to wait for the thread-pool to accept our task)
|
|
tcs.SetResult(new AvalonEditTextOutput());
|
|
return tcs.Task;
|
|
}
|
|
|
|
Thread thread = new Thread(new ThreadStart(
|
|
delegate {
|
|
try
|
|
{
|
|
AvalonEditTextOutput textOutput = new AvalonEditTextOutput();
|
|
textOutput.LengthLimit = outputLengthLimit;
|
|
DecompileNodes(context, textOutput);
|
|
textOutput.PrepareDocument();
|
|
tcs.SetResult(textOutput);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
tcs.SetCanceled();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
tcs.SetException(ex);
|
|
}
|
|
}));
|
|
thread.Start();
|
|
return tcs.Task;
|
|
}
|
|
|
|
void DecompileNodes(DecompilationContext context, ITextOutput textOutput)
|
|
{
|
|
var nodes = context.TreeNodes;
|
|
if (textOutput is ISmartTextOutput smartTextOutput)
|
|
{
|
|
smartTextOutput.Title = string.Join(", ", nodes.Select(n => n.Text));
|
|
}
|
|
for (int i = 0; i < nodes.Length; i++)
|
|
{
|
|
if (i > 0)
|
|
textOutput.WriteLine();
|
|
|
|
context.Options.CancellationToken.ThrowIfCancellationRequested();
|
|
nodes[i].Decompile(context.Language, textOutput, context.Options);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region WriteOutputLengthExceededMessage
|
|
/// <summary>
|
|
/// Creates a message that the decompiler output was too long.
|
|
/// The message contains buttons that allow re-trying (with larger limit) or saving to a file.
|
|
/// </summary>
|
|
void WriteOutputLengthExceededMessage(ISmartTextOutput output, DecompilationContext context, bool wasNormalLimit)
|
|
{
|
|
if (wasNormalLimit)
|
|
{
|
|
output.WriteLine("You have selected too much code for it to be displayed automatically.");
|
|
}
|
|
else
|
|
{
|
|
output.WriteLine("You have selected too much code; it cannot be displayed here.");
|
|
}
|
|
output.WriteLine();
|
|
if (wasNormalLimit)
|
|
{
|
|
output.AddButton(
|
|
Images.ViewCode, Properties.Resources.DisplayCode,
|
|
delegate {
|
|
DoDecompile(context, ExtendedOutputLengthLimit).HandleExceptions();
|
|
});
|
|
output.WriteLine();
|
|
}
|
|
|
|
output.AddButton(
|
|
Images.Save, Properties.Resources.SaveCode,
|
|
delegate {
|
|
SaveToDisk(context.Language, context.TreeNodes, context.Options);
|
|
});
|
|
output.WriteLine();
|
|
}
|
|
#endregion
|
|
|
|
#region JumpToReference
|
|
/// <summary>
|
|
/// Jumps to the definition referred to by the <see cref="ReferenceSegment"/>.
|
|
/// </summary>
|
|
internal void JumpToReference(ReferenceSegment referenceSegment, bool openInNewTab)
|
|
{
|
|
object reference = referenceSegment.Reference;
|
|
if (referenceSegment.IsLocal)
|
|
{
|
|
ClearLocalReferenceMarks();
|
|
if (references != null)
|
|
{
|
|
foreach (var r in references)
|
|
{
|
|
if (reference.Equals(r.Reference))
|
|
{
|
|
|
|
var mark = textMarkerService.Create(r.StartOffset, r.Length);
|
|
mark.BackgroundColor = (Color)(r.IsDefinition ? FindResource(ResourceKeys.TextMarkerDefinitionBackgroundColor) : FindResource(ResourceKeys.TextMarkerBackgroundColor));
|
|
localReferenceMarks.Add(mark);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (definitionLookup != null)
|
|
{
|
|
int pos = definitionLookup.GetDefinitionPosition(reference);
|
|
if (pos >= 0)
|
|
{
|
|
textEditor.TextArea.Focus();
|
|
textEditor.Select(pos, 0);
|
|
textEditor.ScrollTo(textEditor.TextArea.Caret.Line, textEditor.TextArea.Caret.Column);
|
|
Dispatcher.Invoke(DispatcherPriority.Background, new Action(
|
|
delegate {
|
|
CaretHighlightAdorner.DisplayCaretHighlightAnimation(textEditor.TextArea);
|
|
}));
|
|
return;
|
|
}
|
|
}
|
|
MessageBus.Send(this, new NavigateToReferenceEventArgs(reference, openInNewTab));
|
|
}
|
|
|
|
Point? mouseDownPos;
|
|
|
|
void TextAreaMouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
mouseDownPos = e.GetPosition(this);
|
|
}
|
|
|
|
void TextAreaMouseUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (mouseDownPos == null)
|
|
return;
|
|
Vector dragDistance = e.GetPosition(this) - mouseDownPos.Value;
|
|
if (Math.Abs(dragDistance.X) < SystemParameters.MinimumHorizontalDragDistance
|
|
&& Math.Abs(dragDistance.Y) < SystemParameters.MinimumVerticalDragDistance
|
|
&& (e.ChangedButton == MouseButton.Left || e.ChangedButton == MouseButton.Middle))
|
|
{
|
|
// click without moving mouse
|
|
var referenceSegment = GetReferenceSegmentAtMousePosition();
|
|
if (referenceSegment == null)
|
|
{
|
|
ClearLocalReferenceMarks();
|
|
}
|
|
else if (referenceSegment.IsLocal || !referenceSegment.IsDefinition)
|
|
{
|
|
textEditor.TextArea.ClearSelection();
|
|
// cancel mouse selection to avoid AvalonEdit selecting between the new
|
|
// cursor position and the mouse position.
|
|
textEditor.TextArea.MouseSelectionMode = MouseSelectionMode.None;
|
|
|
|
JumpToReference(referenceSegment, e.ChangedButton == MouseButton.Middle || Keyboard.Modifiers.HasFlag(ModifierKeys.Shift));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClearLocalReferenceMarks()
|
|
{
|
|
foreach (var mark in localReferenceMarks)
|
|
{
|
|
textMarkerService.Remove(mark);
|
|
}
|
|
localReferenceMarks.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Filters all ReferenceSegments that are no real links.
|
|
/// </summary>
|
|
bool IsLink(ReferenceSegment referenceSegment)
|
|
{
|
|
return referenceSegment.IsLocal || !referenceSegment.IsDefinition;
|
|
}
|
|
#endregion
|
|
|
|
#region SaveToDisk
|
|
/// <summary>
|
|
/// Shows the 'save file dialog', prompting the user to save the decompiled nodes to disk.
|
|
/// </summary>
|
|
public void SaveToDisk(ILSpy.Language language, IEnumerable<ILSpyTreeNode> treeNodes, DecompilationOptions options)
|
|
{
|
|
if (!treeNodes.Any())
|
|
return;
|
|
|
|
SaveFileDialog dlg = new SaveFileDialog();
|
|
dlg.DefaultExt = language.FileExtension;
|
|
dlg.Filter = language.Name + "|*" + language.FileExtension + Properties.Resources.AllFiles;
|
|
dlg.FileName = WholeProjectDecompiler.CleanUpFileName(treeNodes.First().ToString(), language.FileExtension);
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
SaveToDisk(new DecompilationContext(language, treeNodes.ToArray(), options), dlg.FileName);
|
|
}
|
|
}
|
|
|
|
public void SaveToDisk(ILSpy.Language language, IEnumerable<ILSpyTreeNode> treeNodes, DecompilationOptions options, string fileName)
|
|
{
|
|
SaveToDisk(new DecompilationContext(language, treeNodes.ToArray(), options), fileName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the decompilation of the given nodes.
|
|
/// The result will be saved to the given file name.
|
|
/// </summary>
|
|
void SaveToDisk(DecompilationContext context, string fileName)
|
|
{
|
|
RunWithCancellation(
|
|
delegate (CancellationToken ct) {
|
|
context.Options.CancellationToken = ct;
|
|
return SaveToDiskAsync(context, fileName);
|
|
})
|
|
.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)
|
|
{
|
|
TaskCompletionSource<AvalonEditTextOutput> tcs = new TaskCompletionSource<AvalonEditTextOutput>();
|
|
Thread thread = new Thread(new ThreadStart(
|
|
delegate {
|
|
try
|
|
{
|
|
bool originalProjectFormatSetting = context.Options.DecompilerSettings.UseSdkStyleProjectFormat;
|
|
context.Options.EscapeInvalidIdentifiers = true;
|
|
context.Options.Progress = this;
|
|
AvalonEditTextOutput output = new AvalonEditTextOutput {
|
|
EnableHyperlinks = true,
|
|
Title = string.Join(", ", context.TreeNodes.Select(n => n.Text))
|
|
};
|
|
Stopwatch stopwatch = new Stopwatch();
|
|
stopwatch.Start();
|
|
try
|
|
{
|
|
using (StreamWriter w = new StreamWriter(fileName))
|
|
{
|
|
try
|
|
{
|
|
DecompileNodes(context, new PlainTextOutput(w));
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
w.WriteLine();
|
|
w.WriteLine(Properties.Resources.DecompilationWasCancelled);
|
|
throw;
|
|
}
|
|
catch (PathTooLongException pathTooLong) when (context.Options.SaveAsProjectDirectory != null)
|
|
{
|
|
output.WriteLine(Properties.Resources.ProjectExportPathTooLong, string.Join(", ", context.TreeNodes.Select(n => n.Text)));
|
|
output.WriteLine();
|
|
output.WriteLine(pathTooLong.ToString());
|
|
tcs.SetResult(output);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
stopwatch.Stop();
|
|
}
|
|
|
|
output.WriteLine(Properties.Resources.DecompilationCompleteInF1Seconds, stopwatch.Elapsed.TotalSeconds);
|
|
if (context.Options.SaveAsProjectDirectory != null)
|
|
{
|
|
output.WriteLine();
|
|
bool useSdkStyleProjectFormat = context.Options.DecompilerSettings.UseSdkStyleProjectFormat;
|
|
if (useSdkStyleProjectFormat)
|
|
{
|
|
output.WriteLine(Properties.Resources.ProjectExportFormatSDKHint);
|
|
}
|
|
else
|
|
{
|
|
output.WriteLine(Properties.Resources.ProjectExportFormatNonSDKHint);
|
|
}
|
|
output.WriteLine(Properties.Resources.ProjectExportFormatChangeSettingHint);
|
|
if (originalProjectFormatSetting != useSdkStyleProjectFormat)
|
|
{
|
|
output.WriteLine(Properties.Resources.CouldNotUseSdkStyleProjectFormat);
|
|
}
|
|
}
|
|
output.WriteLine();
|
|
output.AddButton(null, Properties.Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); });
|
|
output.WriteLine();
|
|
tcs.SetResult(output);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
tcs.SetCanceled();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
tcs.SetException(ex);
|
|
}
|
|
}));
|
|
thread.Start();
|
|
return tcs.Task;
|
|
}
|
|
#endregion
|
|
|
|
#region Clipboard
|
|
private void OnSettingData(object sender, DataObjectSettingDataEventArgs e)
|
|
{
|
|
if (e.Format == DataFormats.Html && e.DataObject is DataObject dataObject)
|
|
{
|
|
e.CancelCommand();
|
|
HtmlClipboard.SetHtml(dataObject, CreateHtmlFragmentFromSelection());
|
|
}
|
|
}
|
|
|
|
private string CreateHtmlFragmentFromSelection()
|
|
{
|
|
var options = new HtmlOptions(textEditor.TextArea.Options);
|
|
var highlighter = textEditor.TextArea.GetService(typeof(IHighlighter)) as IHighlighter;
|
|
var html = new StringBuilder();
|
|
|
|
foreach (var segment in textEditor.TextArea.Selection.Segments)
|
|
{
|
|
var line = textEditor.Document.GetLineByOffset(segment.StartOffset);
|
|
|
|
while (line != null && line.Offset < segment.EndOffset)
|
|
{
|
|
if (html.Length > 0)
|
|
html.AppendLine("<br>");
|
|
|
|
var s = GetOverlap(segment, line);
|
|
var highlightedLine = highlighter?.HighlightLine(line.LineNumber) ?? new HighlightedLine(textEditor.Document, line);
|
|
|
|
if (activeRichTextModel is not null)
|
|
{
|
|
var richTextHighlightedLine = new HighlightedLine(textEditor.Document, line);
|
|
foreach (HighlightedSection richTextSection in activeRichTextModel.GetHighlightedSections(s.Offset, s.Length))
|
|
richTextHighlightedLine.Sections.Add(richTextSection);
|
|
highlightedLine.MergeWith(richTextHighlightedLine);
|
|
}
|
|
|
|
html.Append(highlightedLine.ToHtml(s.Offset, s.Offset + s.Length, options));
|
|
line = line.NextLine;
|
|
}
|
|
}
|
|
|
|
return html.ToString();
|
|
|
|
static (int Offset, int Length) GetOverlap(ISegment segment1, ISegment segment2)
|
|
{
|
|
int start = Math.Max(segment1.Offset, segment2.Offset);
|
|
int end = Math.Min(segment1.EndOffset, segment2.EndOffset);
|
|
return (start, end - start);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
internal ReferenceSegment? GetReferenceSegmentAtMousePosition()
|
|
{
|
|
if (referenceElementGenerator.References == null)
|
|
return null;
|
|
TextViewPosition? position = GetPositionFromMousePosition();
|
|
if (position == null)
|
|
return null;
|
|
int offset = textEditor.Document.GetOffset(position.Value.Location);
|
|
return referenceElementGenerator.References.FindSegmentsContaining(offset).FirstOrDefault();
|
|
}
|
|
|
|
internal TextViewPosition? GetPositionFromMousePosition()
|
|
{
|
|
var position = textEditor.TextArea.TextView.GetPosition(Mouse.GetPosition(textEditor.TextArea.TextView) + textEditor.TextArea.TextView.ScrollOffset);
|
|
if (position == null)
|
|
return null;
|
|
var lineLength = textEditor.Document.GetLineByNumber(position.Value.Line).Length + 1;
|
|
if (position.Value.Column == lineLength)
|
|
return null;
|
|
return position;
|
|
}
|
|
|
|
public DecompilerTextViewState? GetState()
|
|
{
|
|
if (decompiledNodes == null && currentAddress == null)
|
|
return null;
|
|
|
|
var state = new DecompilerTextViewState();
|
|
if (foldingManager != null)
|
|
state.SaveFoldingsState(foldingManager.AllFoldings);
|
|
state.VerticalOffset = textEditor.VerticalOffset;
|
|
state.HorizontalOffset = textEditor.HorizontalOffset;
|
|
state.ExpandMemberDefinitions = expandMemberDefinitions;
|
|
state.DecompiledNodes = decompiledNodes == null ? null : new HashSet<ILSpyTreeNode>(decompiledNodes);
|
|
state.ViewedUri = currentAddress;
|
|
return state;
|
|
}
|
|
|
|
ViewState? IHaveState.GetState() => GetState();
|
|
|
|
public static void RegisterHighlighting()
|
|
{
|
|
HighlightingManager.Instance.RegisterHighlighting("ILAsm", new[] { ".il" }, "ILAsm-Mode");
|
|
HighlightingManager.Instance.RegisterHighlighting("C#", new[] { ".cs" }, "CSharp-Mode");
|
|
HighlightingManager.Instance.RegisterHighlighting("Asm", new[] { ".s", ".asm" }, "Asm-Mode");
|
|
HighlightingManager.Instance.RegisterHighlighting("xml", new[] { ".xml", ".baml" }, "XML-Mode");
|
|
}
|
|
|
|
#region Unfold
|
|
public void UnfoldAndScroll(int lineNumber)
|
|
{
|
|
if (lineNumber <= 0 || lineNumber > textEditor.Document.LineCount)
|
|
return;
|
|
if (foldingManager == null)
|
|
return;
|
|
|
|
var line = textEditor.Document.GetLineByNumber(lineNumber);
|
|
|
|
// unfold
|
|
var foldings = foldingManager.GetFoldingsContaining(line.Offset);
|
|
if (foldings != null)
|
|
{
|
|
foreach (var folding in foldings)
|
|
{
|
|
if (folding.IsFolded)
|
|
{
|
|
folding.IsFolded = false;
|
|
}
|
|
}
|
|
}
|
|
// scroll to
|
|
textEditor.ScrollTo(lineNumber, 0);
|
|
}
|
|
|
|
public FoldingManager? FoldingManager {
|
|
get {
|
|
return foldingManager;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
[DebuggerDisplay("Nodes = {DecompiledNodes}, ViewedUri = {ViewedUri}")]
|
|
public class ViewState : IEquatable<ViewState>
|
|
{
|
|
public HashSet<ILSpyTreeNode>? DecompiledNodes;
|
|
public Uri? ViewedUri;
|
|
|
|
public virtual bool Equals(ViewState? other)
|
|
{
|
|
return other != null
|
|
&& ViewedUri == other.ViewedUri
|
|
&& NullSafeSetEquals(DecompiledNodes, other.DecompiledNodes);
|
|
|
|
static bool NullSafeSetEquals(HashSet<ILSpyTreeNode>? a, HashSet<ILSpyTreeNode>? b)
|
|
{
|
|
if (a == b)
|
|
return true;
|
|
if (a == null || b == null)
|
|
return false;
|
|
return a.SetEquals(b);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class DecompilerTextViewState : ViewState
|
|
{
|
|
private List<(int StartOffset, int EndOffset)>? ExpandedFoldings;
|
|
private int FoldingsChecksum;
|
|
public bool ExpandMemberDefinitions;
|
|
public double VerticalOffset;
|
|
public double HorizontalOffset;
|
|
|
|
public void SaveFoldingsState(IEnumerable<FoldingSection> foldings)
|
|
{
|
|
ExpandedFoldings = foldings.Where(f => !f.IsFolded)
|
|
.Select(f => (f.StartOffset, f.EndOffset)).ToList();
|
|
FoldingsChecksum = unchecked(foldings.Select(f => f.StartOffset * 3 - f.EndOffset)
|
|
.DefaultIfEmpty()
|
|
.Aggregate((a, b) => a + b));
|
|
}
|
|
|
|
internal void RestoreFoldings(List<NewFolding> list, bool expandMemberDefinitions)
|
|
{
|
|
if (ExpandedFoldings == null)
|
|
return;
|
|
var checksum = unchecked(list.Select(f => f.StartOffset * 3 - f.EndOffset)
|
|
.DefaultIfEmpty()
|
|
.Aggregate((a, b) => a + b));
|
|
if (FoldingsChecksum == checksum)
|
|
{
|
|
foreach (var folding in list)
|
|
{
|
|
bool wasExpanded = ExpandedFoldings.Any(
|
|
f => f.StartOffset == folding.StartOffset
|
|
&& f.EndOffset == folding.EndOffset
|
|
);
|
|
bool isExpanded = !folding.DefaultClosed;
|
|
// State of the folding was changed
|
|
if (wasExpanded != isExpanded)
|
|
{
|
|
// The "ExpandMemberDefinitions" setting was not changed
|
|
if (expandMemberDefinitions == ExpandMemberDefinitions)
|
|
{
|
|
// restore fold state
|
|
folding.DefaultClosed = !wasExpanded;
|
|
}
|
|
else
|
|
{
|
|
// only restore fold state if fold was not a definition
|
|
if (!folding.IsDefinition)
|
|
{
|
|
folding.DefaultClosed = !wasExpanded;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override bool Equals(ViewState? other)
|
|
{
|
|
if (other is DecompilerTextViewState vs)
|
|
{
|
|
return base.Equals(vs)
|
|
&& FoldingsChecksum == vs.FoldingsChecksum
|
|
&& VerticalOffset == vs.VerticalOffset
|
|
&& HorizontalOffset == vs.HorizontalOffset;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static class ExtensionMethods
|
|
{
|
|
public static void RegisterHighlighting(
|
|
this HighlightingManager manager,
|
|
string name,
|
|
string[] extensions,
|
|
string resourceName)
|
|
{
|
|
Stream? resourceStream = typeof(DecompilerTextView).Assembly
|
|
.GetManifestResourceStream(typeof(DecompilerTextView), resourceName + ".xshd");
|
|
|
|
if (resourceStream != null)
|
|
{
|
|
IHighlightingDefinition highlightingDefinition;
|
|
|
|
using (resourceStream)
|
|
using (XmlTextReader reader = new XmlTextReader(resourceStream))
|
|
{
|
|
highlightingDefinition = HighlightingLoader.Load(reader, manager);
|
|
}
|
|
|
|
manager.RegisterHighlighting(
|
|
name, extensions,
|
|
delegate {
|
|
ThemeManager.Current.ApplyHighlightingColors(highlightingDefinition);
|
|
return highlightingDefinition;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|