#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.
 
 
 
 
 
 

537 lines
17 KiB

// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.ObjectModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Indentation;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.Core;
using ICSharpCode.Core.Presentation;
using ICSharpCode.SharpDevelop;
using ICSharpCode.SharpDevelop.Bookmarks;
using ICSharpCode.SharpDevelop.Dom;
using ICSharpCode.SharpDevelop.Editor;
using ICSharpCode.SharpDevelop.Editor.AvalonEdit;
using ICSharpCode.SharpDevelop.Editor.CodeCompletion;
using ICSharpCode.SharpDevelop.Editor.Commands;
namespace ICSharpCode.AvalonEdit.AddIn
{
/// <summary>
/// Integrates AvalonEdit with SharpDevelop.
/// Also provides support for Split-View (showing two AvalonEdit instances using the same TextDocument)
/// </summary>
public class CodeEditor : Grid, IDisposable
{
const string contextMenuPath = "/SharpDevelop/ViewContent/AvalonEdit/ContextMenu";
QuickClassBrowser quickClassBrowser;
readonly CodeEditorView primaryTextEditor;
readonly CodeEditorAdapter primaryTextEditorAdapter;
CodeEditorView secondaryTextEditor;
CodeEditorView activeTextEditor;
CodeEditorAdapter secondaryTextEditorAdapter;
GridSplitter gridSplitter;
readonly IconBarManager iconBarManager;
readonly TextMarkerService textMarkerService;
ErrorPainter errorPainter;
public CodeEditorView PrimaryTextEditor {
get { return primaryTextEditor; }
}
public CodeEditorView ActiveTextEditor {
get { return activeTextEditor; }
private set {
if (activeTextEditor != value) {
activeTextEditor = value;
HandleCaretPositionChange();
}
}
}
TextDocument document;
public TextDocument Document {
get {
return document;
}
private set {
if (document != value) {
document = value;
if (DocumentChanged != null) {
DocumentChanged(this, EventArgs.Empty);
}
}
}
}
public event EventHandler DocumentChanged;
public IDocument DocumentAdapter {
get { return primaryTextEditorAdapter.Document; }
}
public ITextEditor PrimaryTextEditorAdapter {
get { return primaryTextEditorAdapter; }
}
public ITextEditor ActiveTextEditorAdapter {
get { return this.ActiveTextEditor.Adapter; }
}
public IconBarManager IconBarManager {
get { return iconBarManager; }
}
FileName fileName;
public FileName FileName {
get { return fileName; }
set {
if (fileName != value) {
fileName = value;
primaryTextEditorAdapter.FileNameChanged();
if (secondaryTextEditorAdapter != null)
secondaryTextEditorAdapter.FileNameChanged();
if (this.errorPainter == null) {
this.errorPainter = new ErrorPainter(primaryTextEditorAdapter);
} else {
this.errorPainter.UpdateErrors();
}
FetchParseInformation();
}
}
}
public void Redraw(ISegment segment, DispatcherPriority priority)
{
primaryTextEditor.TextArea.TextView.Redraw(segment, priority);
if (secondaryTextEditor != null) {
secondaryTextEditor.TextArea.TextView.Redraw(segment, priority);
}
}
const double minRowHeight = 40;
public CodeEditor()
{
this.CommandBindings.Add(new CommandBinding(SharpDevelopRoutedCommands.SplitView, OnSplitView));
textMarkerService = new TextMarkerService(this);
iconBarManager = new IconBarManager();
primaryTextEditor = CreateTextEditor();
primaryTextEditorAdapter = (CodeEditorAdapter)primaryTextEditor.TextArea.GetService(typeof(ITextEditor));
Debug.Assert(primaryTextEditorAdapter != null);
activeTextEditor = primaryTextEditor;
this.Document = primaryTextEditor.Document;
primaryTextEditor.SetBinding(TextEditor.DocumentProperty, new Binding("Document") { Source = this });
this.ColumnDefinitions.Add(new ColumnDefinition());
this.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
this.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star), MinHeight = minRowHeight });
SetRow(primaryTextEditor, 1);
this.Children.Add(primaryTextEditor);
}
protected virtual CodeEditorView CreateTextEditor()
{
CodeEditorView textEditor = new CodeEditorView();
CodeEditorAdapter adapter = new CodeEditorAdapter(this, textEditor);
textEditor.Adapter = adapter;
TextView textView = textEditor.TextArea.TextView;
textView.Services.AddService(typeof(ITextEditor), adapter);
textView.Services.AddService(typeof(CodeEditor), this);
textEditor.Background = Brushes.White;
textEditor.TextArea.TextEntering += TextAreaTextEntering;
textEditor.TextArea.TextEntered += TextAreaTextEntered;
textEditor.TextArea.Caret.PositionChanged += TextAreaCaretPositionChanged;
textEditor.TextArea.DefaultInputHandler.CommandBindings.Add(
new CommandBinding(CustomCommands.CtrlSpaceCompletion, OnCodeCompletion));
textView.BackgroundRenderers.Add(textMarkerService);
textView.LineTransformers.Add(textMarkerService);
textView.Services.AddService(typeof(ITextMarkerService), textMarkerService);
textView.Services.AddService(typeof(IBookmarkMargin), iconBarManager);
textEditor.TextArea.LeftMargins.Insert(0, new IconBarMargin(iconBarManager));
textView.Services.AddService(typeof(ISyntaxHighlighter), new AvalonEditSyntaxHighlighterAdapter(textView));
textEditor.TextArea.MouseRightButtonDown += TextAreaMouseRightButtonDown;
textEditor.TextArea.ContextMenuOpening += TextAreaContextMenuOpening;
textEditor.TextArea.TextCopied += textEditor_TextArea_TextCopied;
textEditor.GotFocus += textEditor_GotFocus;
return textEditor;
}
public event EventHandler<TextEventArgs> TextCopied;
void textEditor_TextArea_TextCopied(object sender, TextEventArgs e)
{
if (TextCopied != null)
TextCopied(this, e);
}
protected virtual void DisposeTextEditor(TextEditor textEditor)
{
// detach IconBarMargin from IconBarManager
textEditor.TextArea.LeftMargins.OfType<IconBarMargin>().Single().TextView = null;
}
void textEditor_GotFocus(object sender, RoutedEventArgs e)
{
Debug.Assert(sender is CodeEditorView);
this.ActiveTextEditor = (CodeEditorView)sender;
}
void TextAreaContextMenuOpening(object sender, ContextMenuEventArgs e)
{
ITextEditor adapter = GetAdapterFromSender(sender);
MenuService.CreateContextMenu(adapter, contextMenuPath).IsOpen = true;
}
void TextAreaMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
TextEditor textEditor = GetTextEditorFromSender(sender);
var position = textEditor.GetPositionFromPoint(e.GetPosition(textEditor));
if (position.HasValue) {
textEditor.TextArea.Caret.Position = position.Value;
}
}
// always use primary text editor for loading/saving
// (the file encoding is stored only there)
public void Load(Stream stream)
{
primaryTextEditor.Load(stream);
}
public void Save(Stream stream)
{
primaryTextEditor.Save(stream);
}
void OnSplitView(object sender, ExecutedRoutedEventArgs e)
{
if (secondaryTextEditor == null) {
// create secondary editor
this.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star), MinHeight = minRowHeight });
secondaryTextEditor = CreateTextEditor();
secondaryTextEditorAdapter = (CodeEditorAdapter)secondaryTextEditor.TextArea.GetService(typeof(ITextEditor));
Debug.Assert(primaryTextEditorAdapter != null);
secondaryTextEditor.SetBinding(TextEditor.DocumentProperty,
new Binding(TextEditor.DocumentProperty.Name) { Source = primaryTextEditor });
secondaryTextEditor.SyntaxHighlighting = primaryTextEditor.SyntaxHighlighting;
gridSplitter = new GridSplitter {
Height = 4,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Top
};
SetRow(gridSplitter, 2);
this.Children.Add(gridSplitter);
secondaryTextEditor.Margin = new Thickness(0, 4, 0, 0);
SetRow(secondaryTextEditor, 2);
this.Children.Add(secondaryTextEditor);
secondaryTextEditorAdapter.FileNameChanged();
FetchParseInformation();
} else {
// remove secondary editor
this.Children.Remove(secondaryTextEditor);
this.Children.Remove(gridSplitter);
DisposeTextEditor(secondaryTextEditor);
secondaryTextEditor = null;
secondaryTextEditorAdapter.Language.Detach();
secondaryTextEditorAdapter = null;
gridSplitter = null;
this.RowDefinitions.RemoveAt(this.RowDefinitions.Count - 1);
this.ActiveTextEditor = primaryTextEditor;
}
}
public event EventHandler CaretPositionChanged;
void TextAreaCaretPositionChanged(object sender, EventArgs e)
{
Debug.Assert(sender is Caret);
Debug.Assert(!document.IsInUpdate);
if (sender == this.ActiveTextEditor.TextArea.Caret) {
HandleCaretPositionChange();
}
}
void HandleCaretPositionChange()
{
if (quickClassBrowser != null) {
quickClassBrowser.SelectItemAtCaretPosition(this.ActiveTextEditorAdapter.Caret.Position);
}
CaretPositionChanged.RaiseEvent(this, EventArgs.Empty);
}
volatile static ReadOnlyCollection<ICodeCompletionBinding> codeCompletionBindings;
public static ReadOnlyCollection<ICodeCompletionBinding> CodeCompletionBindings {
get {
if (codeCompletionBindings == null) {
codeCompletionBindings = AddInTree.BuildItems<ICodeCompletionBinding>("/AddIns/DefaultTextEditor/CodeCompletion", null, false).AsReadOnly();
}
return codeCompletionBindings;
}
}
void TextAreaTextEntering(object sender, TextCompositionEventArgs e)
{
// don't start new code completion if there is still a completion window open
if (completionWindow != null)
return;
if (e.Handled)
return;
ITextEditor adapter = GetAdapterFromSender(sender);
foreach (char c in e.Text) {
foreach (ICodeCompletionBinding cc in CodeCompletionBindings) {
CodeCompletionKeyPressResult result = cc.HandleKeyPress(adapter, c);
if (result == CodeCompletionKeyPressResult.Completed) {
if (completionWindow != null) {
// a new CompletionWindow was shown, but does not eat the input
// tell it to expect the text insertion
completionWindow.ExpectInsertionBeforeStart = true;
}
if (insightWindow != null) {
insightWindow.ExpectInsertionBeforeStart = true;
}
return;
} else if (result == CodeCompletionKeyPressResult.CompletedIncludeKeyInCompletion) {
if (completionWindow != null) {
if (completionWindow.StartOffset == completionWindow.EndOffset) {
completionWindow.CloseWhenCaretAtBeginning = true;
}
}
return;
} else if (result == CodeCompletionKeyPressResult.EatKey) {
e.Handled = true;
return;
}
}
}
}
void TextAreaTextEntered(object sender, TextCompositionEventArgs e)
{
if (e.Text.Length > 0 && !e.Handled) {
ILanguageBinding languageBinding = GetAdapterFromSender(sender).Language;
if (languageBinding != null && languageBinding.FormattingStrategy != null) {
char c = e.Text[0];
// When entering a newline, AvalonEdit might use either "\r\n" or "\n", depending on
// what was passed to TextArea.PerformTextInput. We'll normalize this to '\n'
// so that formatting strategies don't have to handle both cases.
if (c == '\r')
c = '\n';
languageBinding.FormattingStrategy.FormatLine(GetAdapterFromSender(sender), c);
if (c == '\n') {
// Immediately parse on enter.
// This ensures we have up-to-date CC info about the method boundary when a user
// types near the end of a method.
ParserService.BeginParse(this.FileName, this.DocumentAdapter.CreateSnapshot());
}
}
}
}
ITextEditor GetAdapterFromSender(object sender)
{
ITextEditorComponent textArea = (ITextEditorComponent)sender;
ITextEditor textEditor = (ITextEditor)textArea.GetService(typeof(ITextEditor));
if (textEditor == null)
throw new InvalidOperationException("could not find TextEditor service");
return textEditor;
}
CodeEditorView GetTextEditorFromSender(object sender)
{
ITextEditorComponent textArea = (ITextEditorComponent)sender;
CodeEditorView textEditor = (CodeEditorView)textArea.GetService(typeof(TextEditor));
if (textEditor == null)
throw new InvalidOperationException("could not find TextEditor service");
return textEditor;
}
void OnCodeCompletion(object sender, ExecutedRoutedEventArgs e)
{
CloseExistingCompletionWindow();
CodeEditorView textEditor = GetTextEditorFromSender(sender);
foreach (ICodeCompletionBinding cc in CodeCompletionBindings) {
if (cc.CtrlSpace(textEditor.Adapter)) {
e.Handled = true;
break;
}
}
}
SharpDevelopCompletionWindow completionWindow;
SharpDevelopInsightWindow insightWindow;
void CloseExistingCompletionWindow()
{
if (completionWindow != null) {
completionWindow.Close();
}
}
void CloseExistingInsightWindow()
{
if (insightWindow != null) {
insightWindow.Close();
}
}
public SharpDevelopCompletionWindow ActiveCompletionWindow {
get { return completionWindow; }
}
public SharpDevelopInsightWindow ActiveInsightWindow {
get { return insightWindow; }
}
internal void ShowCompletionWindow(SharpDevelopCompletionWindow window)
{
CloseExistingCompletionWindow();
completionWindow = window;
window.Closed += delegate {
completionWindow = null;
};
Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(
delegate {
if (completionWindow == window) {
window.Show();
}
}
));
}
internal void ShowInsightWindow(SharpDevelopInsightWindow window)
{
CloseExistingInsightWindow();
insightWindow = window;
window.Closed += delegate {
insightWindow = null;
};
Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(
delegate {
if (insightWindow == window) {
window.Show();
}
}
));
}
public IHighlightingDefinition SyntaxHighlighting {
get { return primaryTextEditor.SyntaxHighlighting; }
set {
primaryTextEditor.SyntaxHighlighting = value;
if (secondaryTextEditor != null) {
secondaryTextEditor.SyntaxHighlighting = value;
}
}
}
void FetchParseInformation()
{
ParseInformationUpdated(ParserService.GetParseInformation(this.FileName));
}
public void ParseInformationUpdated(ParseInformation parseInfo)
{
if (parseInfo != null) {
// don't create quickClassBrowser for files that don't have any classes
// (but do keep the quickClassBrowser when the last class is removed from a file)
if (quickClassBrowser != null || parseInfo.CompilationUnit.Classes.Count > 0) {
if (quickClassBrowser == null) {
quickClassBrowser = new QuickClassBrowser();
quickClassBrowser.JumpAction = (line, col) => ActiveTextEditor.JumpTo(line, col);
SetRow(quickClassBrowser, 0);
this.Children.Add(quickClassBrowser);
}
quickClassBrowser.Update(parseInfo.CompilationUnit);
quickClassBrowser.SelectItemAtCaretPosition(this.ActiveTextEditorAdapter.Caret.Position);
}
} else {
if (quickClassBrowser != null) {
this.Children.Remove(quickClassBrowser);
quickClassBrowser = null;
}
}
iconBarManager.UpdateClassMemberBookmarks(parseInfo);
UpdateFolding(primaryTextEditorAdapter, parseInfo);
UpdateFolding(secondaryTextEditorAdapter, parseInfo);
}
void UpdateFolding(ITextEditor editor, ParseInformation parseInfo)
{
if (editor != null) {
IServiceContainer container = editor.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 = editor.GetService(typeof(TextArea)) as TextArea;
folding = new ParserFoldingStrategy(textArea);
container.AddService(typeof(ParserFoldingStrategy), folding);
}
folding.UpdateFoldings(parseInfo);
}
}
}
public void Dispose()
{
primaryTextEditorAdapter.Language.Detach();
if (secondaryTextEditorAdapter != null)
secondaryTextEditorAdapter.Language.Detach();
if (errorPainter != null)
errorPainter.Dispose();
this.Document = null;
}
}
}