// 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 { /// /// Manages the TextEditor showing the decompiled code. /// Contains all the threading logic that makes the decompiler work in the background. /// public sealed partial class DecompilerTextView : UserControl, IHaveState, IProgress { readonly IExportProvider exportProvider; readonly SettingsService settingsService; readonly LanguageService languageService; readonly MainWindow mainWindow; readonly ReferenceElementGenerator referenceElementGenerator; readonly UIElementGenerator uiElementGenerator; readonly List activeCustomElementGenerators = new List(); readonly BracketHighlightRenderer bracketHighlightRenderer; RichTextColorizer? activeRichTextColorizer; RichTextModel? activeRichTextModel; FoldingManager? foldingManager; ILSpyTreeNode[]? decompiledNodes; Uri? currentAddress; string? currentTitle; bool expandMemberDefinitions; DefinitionLookup? definitionLookup; TextSegmentCollection? references; CancellationTokenSource? currentCancellationTokenSource; readonly TextMarkerService textMarkerService; readonly List localReferenceMarks = new List(); #region Constructor public DecompilerTextView(IExportProvider exportProvider) { this.exportProvider = exportProvider; settingsService = exportProvider.GetExportedValue(); languageService = exportProvider.GetExportedValue(); mainWindow = exportProvider.GetExportedValue(); 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.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; } /// Returns Popup position based on mouse position, in device independent units 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(); 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(); 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; } }); } /// /// Switches the GUI into "waiting" mode, then calls to create /// the task. /// If another task is started before the previous task finishes running, the previous task is cancelled. /// public Task RunWithCancellation(Func> 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(); Task task; try { task = taskCreation(myCancellationTokenSource.Token); } catch (OperationCanceledException) { task = TaskHelper.FromCancellation(); } catch (Exception ex) { task = TaskHelper.FromException(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); } /// /// Shows the given output in the text view. /// Cancels any currently running decompilation tasks. /// 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); } /// /// Shows the given output in the text view. /// 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 treeNodes, DecompilationOptions options) { DecompileAsync(language, treeNodes, options).HandleExceptions(); } /// /// 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. /// public Task DecompileAsync(ILSpy.Language language, IEnumerable 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 TaskCompletionSource = new TaskCompletionSource(); 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 => { 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 DecompileAsync(DecompilationContext context, int outputLengthLimit) { Debug.WriteLine("Start decompilation of {0} tree nodes", context.TreeNodes.Length); TaskCompletionSource tcs = new TaskCompletionSource(); 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 /// /// 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. /// 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 /// /// Jumps to the definition referred to by the . /// 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(); } /// /// Filters all ReferenceSegments that are no real links. /// bool IsLink(ReferenceSegment referenceSegment) { return referenceSegment.IsLocal || !referenceSegment.IsDefinition; } #endregion #region SaveToDisk /// /// Shows the 'save file dialog', prompting the user to save the decompiled nodes to disk. /// public void SaveToDisk(ILSpy.Language language, IEnumerable 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 treeNodes, DecompilationOptions options, string fileName) { SaveToDisk(new DecompilationContext(language, treeNodes.ToArray(), options), fileName); } /// /// Starts the decompilation of the given nodes. /// The result will be saved to the given file name. /// 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 SaveToDiskAsync(DecompilationContext context, string fileName) { TaskCompletionSource tcs = new TaskCompletionSource(); 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("
"); 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(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 { public HashSet? DecompiledNodes; public Uri? ViewedUri; public virtual bool Equals(ViewState? other) { return other != null && ViewedUri == other.ViewedUri && NullSafeSetEquals(DecompiledNodes, other.DecompiledNodes); static bool NullSafeSetEquals(HashSet? a, HashSet? 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 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 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; }); } } } }