#develop (short for SharpDevelop) is a free IDE for .NET programming languages.
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.
 
 
 
 
 
 

519 lines
17 KiB

// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.AddIn.ContextActions;
using ICSharpCode.AvalonEdit.AddIn.Options;
using ICSharpCode.AvalonEdit.AddIn.Snippets;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Folding;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.NRefactory.Editor;
using ICSharpCode.NRefactory.Semantics;
using ICSharpCode.NRefactory.TypeSystem;
using ICSharpCode.SharpDevelop;
using ICSharpCode.SharpDevelop.Editor;
using ICSharpCode.SharpDevelop.Editor.AvalonEdit;
using ICSharpCode.SharpDevelop.Editor.Commands;
using ICSharpCode.SharpDevelop.Gui;
using ICSharpCode.SharpDevelop.Parser;
using ICSharpCode.SharpDevelop.Refactoring;
namespace ICSharpCode.AvalonEdit.AddIn
{
/// <summary>
/// The text editor use inside the CodeEditor.
/// There can be two CodeEditorView instances in a single CodeEditor if split-view
/// is enabled.
/// </summary>
public class CodeEditorView : SharpDevelopTextEditor, IDisposable
{
public ITextEditor Adapter { get; set; }
BracketHighlightRenderer bracketRenderer;
//CaretReferencesRenderer caretReferencesRenderer;
ContextActionsRenderer contextActionsRenderer;
HiddenDefinition.HiddenDefinitionRenderer hiddenDefinitionRenderer;
public CodeEditorView()
{
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Help, OnHelpExecuted));
this.bracketRenderer = new BracketHighlightRenderer(this.TextArea.TextView);
//this.caretReferencesRenderer = new CaretReferencesRenderer(this);
this.contextActionsRenderer = new ContextActionsRenderer(this);
this.hiddenDefinitionRenderer = new HiddenDefinition.HiddenDefinitionRenderer(this);
UpdateCustomizedHighlighting();
this.MouseHover += TextEditorMouseHover;
this.MouseHoverStopped += TextEditorMouseHoverStopped;
this.MouseMove += TextEditorMouseMove;
this.MouseLeave += TextEditorMouseLeave;
this.Unloaded += OnUnloaded;
this.TextArea.TextView.MouseDown += TextViewMouseDown;
this.TextArea.TextView.MouseUp += TextViewMouseUp;
this.TextArea.Caret.PositionChanged += HighlightBrackets;
this.TextArea.TextView.VisualLinesChanged += CodeEditorView_VisualLinesChanged;
SetupTabSnippetHandler();
}
void CodeEditorView_VisualLinesChanged(object sender, EventArgs e)
{
// hide tooltip
if (this.toolTip != null)
this.toolTip.IsOpen = false;
}
public virtual void Dispose()
{
contextActionsRenderer.Dispose();
hiddenDefinitionRenderer.ClosePopup();
}
public IList<IContextActionProvider> ContextActionProviders {
get { return contextActionsRenderer.Providers; }
}
protected override ICSharpCode.Core.FileName FileName {
get { return this.Adapter.FileName; }
}
protected override void OnOptionChanged(PropertyChangedEventArgs e)
{
base.OnOptionChanged(e);
switch (e.PropertyName) {
case "HighlightBrackets":
HighlightBrackets(null, e);
break;
case "EnableFolding":
UpdateParseInformationForFolding();
break;
case "HighlightSymbol":
//if (this.caretReferencesRenderer != null)
// this.caretReferencesRenderer.ClearHighlight();
break;
}
}
#region CaretPositionChanged - Bracket Highlighting
/// <summary>
/// Highlights matching brackets.
/// </summary>
void HighlightBrackets(object sender, EventArgs e)
{
/*
* Special case: ITextEditor.Language guarantees that it never returns null.
* In this case however it can be null, since this code may be called while the document is loaded.
* ITextEditor.Language gets set in CodeEditorAdapter.FileNameChanged, which is called after
* loading of the document has finished.
* */
if (this.Adapter.Language != null) {
if (CodeEditorOptions.Instance.HighlightBrackets || CodeEditorOptions.Instance.ShowHiddenDefinitions) {
var bracketSearchResult = this.Adapter.Language.BracketSearcher.SearchBracket(this.Adapter.Document, this.TextArea.Caret.Offset);
if (CodeEditorOptions.Instance.HighlightBrackets) {
this.bracketRenderer.SetHighlight(bracketSearchResult);
} else {
this.bracketRenderer.SetHighlight(null);
}
if (CodeEditorOptions.Instance.ShowHiddenDefinitions) {
this.hiddenDefinitionRenderer.Show(bracketSearchResult);
} else {
this.hiddenDefinitionRenderer.ClosePopup();
}
} else {
this.bracketRenderer.SetHighlight(null);
this.hiddenDefinitionRenderer.ClosePopup();
}
}
}
#endregion
#region Custom Tab command (code snippet expansion)
void SetupTabSnippetHandler()
{
var editingKeyBindings = this.TextArea.DefaultInputHandler.Editing.InputBindings.OfType<KeyBinding>();
var tabBinding = editingKeyBindings.Single(b => b.Key == Key.Tab && b.Modifiers == ModifierKeys.None);
this.TextArea.DefaultInputHandler.Editing.InputBindings.Remove(tabBinding);
var newTabBinding = new KeyBinding(new CustomTabCommand(this, tabBinding.Command), tabBinding.Key, tabBinding.Modifiers);
this.TextArea.DefaultInputHandler.Editing.InputBindings.Add(newTabBinding);
}
sealed class CustomTabCommand : ICommand
{
CodeEditorView editor;
ICommand baseCommand;
public CustomTabCommand(CodeEditorView editor, ICommand baseCommand)
{
this.editor = editor;
this.baseCommand = baseCommand;
}
public event EventHandler CanExecuteChanged {
add {}
remove {}
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
if (editor.SelectionLength == 0) {
int wordStart = DocumentUtilities.FindPrevWordStart(editor.Adapter.Document, editor.CaretOffset);
if (wordStart > 0) {
string word = editor.Adapter.Document.GetText(wordStart, editor.CaretOffset - wordStart);
CodeSnippet snippet = SnippetManager.Instance.FindSnippet(Path.GetExtension(editor.Adapter.FileName),
word);
if (snippet != null) {
snippet.TrackUsage("CustomTabCommand");
using (editor.Document.RunUpdate()) {
editor.Adapter.Document.Remove(wordStart, editor.CaretOffset - wordStart);
snippet.CreateAvalonEditSnippet(editor.Adapter).Insert(editor.TextArea);
}
return;
}
}
}
baseCommand.Execute(parameter);
}
}
#endregion
#region OnKeyDown
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled && e.Key == Key.Escape && e.KeyboardDevice.Modifiers == ModifierKeys.None) {
if (this.SelectionLength > 0) {
this.SelectionLength = 0;
e.Handled = true;
}
}
}
#endregion
#region Help
void OnHelpExecuted(object sender, ExecutedRoutedEventArgs e)
{
ShowHelp();
}
public void ShowHelp()
{
// Resolve expression at cursor and show help
var compilation = SD.ParserService.GetCompilationForFile(FileName);
var result = SD.ParserService.Resolve(Adapter, TextArea.Caret.Location, compilation);
TypeResolveResult trr = result as TypeResolveResult;
if (trr != null && trr.Type.GetDefinition() != null) {
HelpProvider.ShowHelp(trr.Type.GetDefinition());
}
MemberResolveResult mrr = result as MemberResolveResult;
if (mrr != null) {
if ((mrr.Member.DeclaringType.Kind == TypeKind.Enum) &&
(mrr.Member.DeclaringType.GetDefinition() != null)) {
HelpProvider.ShowHelp(mrr.Member.DeclaringType.GetDefinition());
} else {
HelpProvider.ShowHelp(mrr.Member);
}
}
}
#endregion
#region Tooltip
ToolTip toolTip;
Popup popupToolTip;
void TextEditorMouseHover(object sender, MouseEventArgs e)
{
Debug.Assert(sender == this);
if (!TryCloseExistingPopup(false)) {
return;
}
ToolTipRequestEventArgs args = new ToolTipRequestEventArgs(this.Adapter);
var pos = this.TextArea.TextView.GetPositionFloor(e.GetPosition(this.TextArea.TextView) + this.TextArea.TextView.ScrollOffset);
args.InDocument = pos.HasValue;
if (pos.HasValue) {
args.LogicalPosition = pos.Value.Location;
}
if (!args.Handled) {
// if request wasn't handled by a marker, pass it to the ToolTipRequestService
ToolTipRequestService.RequestToolTip(args);
}
if (args.ContentToShow != null) {
popupToolTip = args.ContentToShow as Popup;
if (popupToolTip != null) {
var popupPosition = GetPopupPosition(e);
popupToolTip.Closed += ToolTipClosed;
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(args.ContentToShow is string) {
toolTip.Content = new TextBlock
{
Text = args.ContentToShow as string,
TextWrapping = TextWrapping.Wrap
};
}
else
toolTip.Content = args.ContentToShow;
e.Handled = true;
toolTip.IsOpen = true;
}
}
}
bool TryCloseExistingPopup(bool mouseClick)
{
if (popupToolTip != null) {
if (popupToolTip.IsOpen && !mouseClick && popupToolTip is ITooltip && !((ITooltip)popupToolTip).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);
Point positionInPixels;
// align Popup with line bottom
TextViewPosition? logicalPos = GetPositionFromPoint(mousePos);
if (logicalPos.HasValue) {
var textView = this.TextArea.TextView;
positionInPixels =
textView.PointToScreen(
textView.GetVisualPosition(logicalPos.Value, VisualYPosition.LineBottom) - textView.ScrollOffset);
positionInPixels.X -= 4;
} else {
positionInPixels = PointToScreen(mousePos + new Vector(-4, 6));
}
// use device independent units, because Popup Left/Top are in independent units
return positionInPixels.TransformFromDevice(this);
}
void TextEditorMouseHoverStopped(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) {
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
popupToolTip.Closed -= ToolTipClosed;
popupToolTip = null;
}
}
#endregion
#region Ctrl+Click Go To Definition
bool ctrlClickExecuted = false;
void TextViewMouseDown(object sender, MouseButtonEventArgs e)
{
// close existing debugger popup immediately on text editor mouse down
TryCloseExistingPopup(false);
ctrlClickExecuted = false;
if (options.CtrlClickGoToDefinition && e.ChangedButton == MouseButton.Left && Keyboard.Modifiers == ModifierKeys.Control) {
// Ctrl+Click Go to definition
var position = GetPositionFromPoint(e.GetPosition(this));
if (position == null)
return;
SD.AnalyticsMonitor.TrackFeature(typeof(GoToDefinition).FullName, "Ctrl+Click");
var resolveResult = SD.ParserService.Resolve(Adapter, position.Value.Location);
var goToDefinitionCommand = new GoToDefinition();
goToDefinitionCommand.Run(resolveResult);
e.Handled = true;
ctrlClickExecuted = true;
}
}
void TextViewMouseUp(object sender, MouseButtonEventArgs e)
{
e.Handled = options.CtrlClickGoToDefinition && ctrlClickExecuted;
}
#endregion
public void JumpTo(int line, int column)
{
// closes Debugger popup on debugger step
TryCloseExistingPopup(true);
// the adapter sets the caret position and takes care of scrolling
this.Adapter.JumpTo(line, column);
this.Focus();
if (CodeEditorOptions.Instance.EnableAnimations)
Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)DisplayCaretHighlightAnimation);
}
void DisplayCaretHighlightAnimation()
{
TextArea textArea = Adapter.GetService(typeof(TextArea)) as TextArea;
if (textArea == null)
return;
AdornerLayer layer = AdornerLayer.GetAdornerLayer(textArea.TextView);
if (layer == null)
return;
CaretHighlightAdorner adorner = new CaretHighlightAdorner(textArea);
layer.Add(adorner);
SD.MainThread.CallLater(TimeSpan.FromSeconds(1), () => layer.Remove(adorner));
}
#region UpdateParseInformation - Folding
void UpdateParseInformationForFolding()
{
UpdateParseInformationForFolding(null);
}
public void UpdateParseInformationForFolding(ParseInformation parseInfo)
{
if (!CodeEditorOptions.Instance.EnableFolding) {
parseInfo = null;
} else {
if (parseInfo == null || !parseInfo.IsFullParseInformation)
parseInfo = SD.ParserService.Parse(this.FileName, this.Document);
if (parseInfo != null && !parseInfo.SupportsFolding)
parseInfo = null;
}
IServiceContainer container = this.Adapter.GetService(typeof(IServiceContainer)) as IServiceContainer;
ParserFoldingStrategy folding = container.GetService(typeof(ParserFoldingStrategy)) as ParserFoldingStrategy;
if (parseInfo == null) {
if (folding != null) {
folding.Dispose();
container.RemoveService(typeof(ParserFoldingStrategy));
}
} else {
if (folding == null) {
TextArea textArea = this.Adapter.GetService(typeof(TextArea)) as TextArea;
folding = new ParserFoldingStrategy(textArea);
container.AddService(typeof(ParserFoldingStrategy), folding);
}
folding.UpdateFoldings(parseInfo);
}
}
#endregion
protected override IVisualLineTransformer CreateColorizer(IHighlightingDefinition highlightingDefinition)
{
return null;
}
public void UpdateCustomizedHighlighting()
{
string language = this.SyntaxHighlighting != null ? this.SyntaxHighlighting.Name : null;
var customizations = CustomizedHighlightingColor.FetchCustomizations(language);
CustomizingHighlighter.ApplyCustomizationsToDefaultElements(this, customizations);
BracketHighlightRenderer.ApplyCustomizationsToRendering(this.bracketRenderer, customizations);
HighlightingOptions.ApplyToRendering(this, customizations);
this.TextArea.TextView.Redraw(); // manually redraw if default elements didn't change but customized highlightings did
}
}
}