使用 AvalonEdit (WPF 文本编辑器)






4.97/5 (267投票s)
AvalonEdit 是一个可扩展的开源文本编辑器,支持语法高亮和折叠。
AvalonEdit 的最新版本可以在 SharpDevelop 项目中找到。有关 AvalonEdit 的详细信息,请参阅 www.avalonedit.net。
引言
ICSharpCode.AvalonEdit
是我为 SharpDevelop 4.0 编写的基于 WPF 的文本编辑器。它旨在取代 ICSharpCode.TextEditor,但应该
- 可扩展
- 易于使用
- 能够更好地处理大文件
可扩展意味着我希望 SharpDevelop 插件能够为文本编辑器添加功能。例如,插件应该能够允许在注释中插入图像——这样,您就可以将类图等内容直接放在源代码中!
就易于使用而言,我指的是编程 API。它应该就是能用™。例如,这意味着如果您更改文档文本,编辑器应该自动重绘,而无需调用 Invalidate()
。而且,如果您做错了什么,您应该会收到有意义的异常,而不是损坏的状态并在稍后不相关的位置崩溃。
更好地处理大文件意味着编辑器应该能够处理大文件(例如,mscorlib XML 文档文件,7 MB,74100 行),即使启用了折叠(代码折叠)等功能。
Using the Code
编辑器的主要类是 ICSharpCode.AvalonEdit.TextEditor
。您可以像使用普通 WPF TextBox
一样使用它。
<avalonEdit:TextEditor
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
Name="textEditor"
FontFamily="Consolas"
SyntaxHighlighting="C#"
FontSize="10pt"/>
AvalonEdit 内置了以下语言的语法高亮定义:ASP.NET、Boo、Coco/R 语法、C++、C#、HTML、Java、JavaScript、Patch 文件、PHP、TeX、VB 和 XML。
如果您需要比带语法高亮的简单文本框更多的 AvalonEdit 功能,您首先需要了解 AvalonEdit 的架构。
架构
正如您在此依赖关系图中看到的,AvalonEdit 由几个具有清晰分隔任务的子命名空间组成。大多数命名空间都有某种“主”类。
ICSharpCode.AvalonEdit.Utils
:各种实用类ICSharpCode.AvalonEdit.Document
:TextDocument
— 文本模型ICSharpCode.AvalonEdit.Rendering
:TextView
— 文档的可扩展视图ICSharpCode.AvalonEdit.Editing
:TextArea
— 控制文本编辑(例如,光标、选择、处理用户输入)ICSharpCode.AvalonEdit.Folding
:FoldingManager
— 启用代码折叠ICSharpCode.AvalonEdit.Highlighting
:HighlightingManager
— 高亮引擎ICSharpCode.AvalonEdit.Highlighting.Xshd
:HighlightingLoader
— XML 语法高亮定义支持(.xshd 文件)ICSharpCode.AvalonEdit.CodeCompletion
:CompletionWindow
— 显示用于代码补全的下拉列表ICSharpCode.AvalonEdit
:TextEditor
— 将所有内容整合在一起的主要控件
这是 TextEditor
控件的视觉树
重要的是要理解 AvalonEdit 是一个复合控件,包含三个层:TextEditor
(主控件)、TextArea
(编辑)、TextView
(渲染)。虽然主控件为常见任务提供了一些便捷方法,但对于大多数高级功能,您需要直接与内部控件进行交互。您可以通过 textEditor.TextArea
或 textEditor.TextArea.TextView
访问它们。
文档(文本模型)
模型的类是 ICSharpCode.AvalonEdit.Document.TextDocument
。基本上,文档就是一个带有事件的 StringBuilder
。然而,Document
命名空间还包含一些对处理文本编辑器的应用程序很有用的功能。
在文本编辑器中,所有三个控件(TextEditor
、TextArea
、TextView
)都有一个指向 TextDocument
实例的 Document
属性。您可以更改 Document
属性将编辑器绑定到另一个文档。可以将两个编辑器实例绑定到同一个文档;您可以使用此功能创建分屏视图。
这是 TextDocument
的简化定义
public sealed class TextDocument : ITextSource
{
public event EventHandler<DocumentChangeEventArgs> Changing;
public event EventHandler<DocumentChangeEventArgs> Changed;
public event EventHandler TextChanged;
public IList<DocumentLine> Lines { get; }
public DocumentLine GetLineByNumber(int number);
public DocumentLine GetLineByOffset(int offset);
public TextLocation GetLocation(int offset);
public int GetOffset(int line, int column);
public char GetCharAt(int offset);
public string GetText(int offset, int length);
public void Insert(int offset, string text);
public void Remove(int offset, int length);
public void Replace(int offset, int length, string text);
public string Text { get; set; }
public int LineCount { get; }
public int TextLength { get; }
public UndoStack UndoStack { get; }
}
在 AvalonEdit 中,文档中的索引称为偏移量。
偏移量通常表示两个字符之间的位置。文档开头的第一个偏移量是 0;文档中第一个 char
之后的偏移量是 1。最后一个有效偏移量是 document.TextLength
,表示文档的结尾。这与 .NET String
或 StringBuilder
类中的方法使用的“索引”参数完全相同。
偏移量易于使用,但有时您需要行/列对。AvalonEdit 定义了一个名为 TextLocation
的 struct
来处理这些。
文档提供了 GetLocation
和 GetOffset
方法来在偏移量和 TextLocation
之间进行转换。这些是基于 DocumentLine
类构建的便捷方法。
TextDocument.Lines
集合包含文档中每一行的 DocumentLine
实例。此集合对用户代码是只读的,并且会自动更新以反映当前文档内容。
渲染
在整个“文档”部分,没有提到可扩展性。文本渲染基础设施现在必须通过完全可扩展来弥补这一点。
ICSharpCode.AvalonEdit.Rendering.TextView
类是 AvalonEdit 的核心。它负责将文档显示到屏幕上。
为了以可扩展的方式实现这一点,TextView
使用自己的模型:VisualLine
。视觉行仅为文档的可见部分创建。
渲染过程如下所示
管道中的最后一步是转换为一个或多个 System.Windows.Media.TextFormatting.TextLine
实例。然后 WPF 负责实际的文本渲染。
“元素生成器”、“行转换器”和“背景渲染器”是扩展点;可以向 TextView
添加它们的自定义实现,以在编辑器中实现其他功能。
编辑
TextArea
类处理用户输入并执行相应的操作。光标和选择都由 TextArea
控制。
您可以通过修改 TextArea.DefaultInputHandler
来自定义文本区域,方法是添加新的或替换其中的现有 WPF 输入绑定。您还可以将 TextArea.ActiveInputHandler
设置为不同于默认值的值,以将文本区域切换到另一种模式。您可以使用此功能来实现“增量搜索”功能,甚至是一个 VI 模拟器。
文本区域具有 LeftMargins
属性——使用它可以在文本视图的左侧添加控件,这些控件看起来像是位于滚动查看器内部,但实际上不会滚动。AbstractMargin
基类包含一些有用的代码,用于检测边距何时附加/分离到文本视图;或何时活动文档更改。但是,您不必使用它;任何 UIElement
都可以用作边距。
折叠
折叠(代码折叠)实现为编辑器的扩展。它本可以在单独的程序集中实现,而无需修改 AvalonEdit 代码。VisualLineElementGenerator
负责处理文本文档中的折叠部分,自定义边距绘制加号和减号按钮。
您可以单独使用相关类;但是,为了使其更易于使用,静态 FoldingManager.Install
方法将自动创建和注册必要的组件。
您所要做的就是定期使用要提供的折叠列表调用 FoldingManager.UpdateFoldings
。您可以自己计算该列表,也可以使用内置的折叠策略来为您完成。
以下是启用折叠所需的完整代码
foldingManager = FoldingManager.Install(textEditor.TextArea);
foldingStrategy = new XmlFoldingStrategy();
foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);
如果您希望折叠标记在文本更改时更新,则必须定期重复 foldingStrategy.UpdateFoldings
调用。
目前,AvalonEdit 中仅内置了 XmlFoldingStrategy
。本文的示例应用程序还包含 BraceFoldingStrategy
,它使用 { 和 } 进行折叠。但是,这是一个非常简单的实现,不能正确处理字符串或注释中的 { 和 }。
语法高亮
AvalonEdit 中的高亮引擎实现在 DocumentHighlighter
类中。高亮是获取 DocumentLine
并为其构建 HighlightedLine
实例的过程,方法是将颜色分配给行的不同部分。
HighlightingColorizer
类是高亮和渲染之间的唯一联系。它使用 DocumentHighlighter
来实现一个行转换器,该转换器将高亮应用于渲染过程中的视觉行。
除了这个单独的调用之外,语法高亮独立于渲染命名空间。为了帮助其他潜在地使用高亮引擎,HighlightedLine
类有一个 ToHtml
方法,可以生成语法高亮的 HTML 源代码。
高亮的规则使用“可扩展语法高亮定义”(.xshd)文件定义。以下是 C# 子集的完整高亮定义
<SyntaxDefinition name="C#"
xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Comment" foreground="Green" />
<Color name="String" foreground="Blue" />
<!-- This is the main ruleset. -->
<RuleSet>
<Span color="Comment" begin="//" />
<Span color="Comment" multiline="true"
begin="/\*" end="\*/" />
<Span color="String">
<Begin>"</Begin>
<End>"</End>
<RuleSet>
<!-- nested span for escape sequences -->
<Span begin="\\" end="." />
</RuleSet>
</Span>
<Keywords fontWeight="bold" foreground="Blue">
<Word>if</Word>
<Word>else</Word>
<!-- ... -->
</Keywords>
<!-- Digits -->
<Rule foreground="DarkBlue">
\b0[xX][0-9a-fA-F]+ # hex number
| \b
( \d+(\.[0-9]+)? #number with optional floating point
| \.[0-9]+ #or just starting with floating point
)
([eE][+-]?[0-9]+)? # optional exponent
</Rule>
</RuleSet>
</SyntaxDefinition>
高亮引擎使用带有分配颜色的“跨度”和“规则”。在 XSHD 格式中,颜色既可以被引用(color="Comment"
),也可以被直接指定(fontWeight="bold" foreground="Blue"
)。
跨度由两个正则表达式(开始+结束)组成;而规则只是一个带有颜色的单个正则表达式。<Keywords>
元素只是定义一个匹配一组单词的高亮规则的便捷语法;在内部,将为整个关键字列表使用一个正则表达式。
高亮引擎的工作原理是先分析跨度:每当开始正则表达式匹配某些文本时,该跨度就会被推入堆栈。每当当前跨度的结束正则表达式匹配某些文本时,该跨度就会从堆栈中弹出。
每个跨度都有一个与之关联的嵌套规则集,默认情况下为空。这就是为什么关键字在注释中不会被高亮显示的原因:该跨度的空规则集在那里处于活动状态,因此关键字规则不适用。
此功能也用于字符串跨度:当遇到反斜杠时,嵌套跨度会匹配,并且反斜杠后面的字符将被嵌套跨度的结束正则表达式(.
匹配任何字符)所消耗。这确保了 \"
不表示字符串跨度的结束;但 \\"
仍然表示。
高亮引擎的优点是它仅按需高亮,增量工作,并且即使对于大型代码文件,通常也只需要几 KB 的内存。
按需意味着当打开文档时,只有最初可见的行会被高亮显示。当用户向下滚动时,高亮将从上次停止的地方继续。如果用户滚动得很快,使得第一行可见行远低于最后一行高亮显示,那么高亮引擎仍然必须处理之间的所有行——其中可能包含注释的开始。但是,它只会扫描该区域以查找跨度堆栈中的更改;高亮规则将不会被测试。
活动跨度的堆栈存储在每行的开头。如果用户向上滚动,进入视图的行可以立即高亮显示,因为所需上下文(跨度堆栈)仍然可用。
增量意味着即使文档被更改,只要可能,仍会重用存储的跨度堆栈。如果用户键入 /*
,理论上会导致文件剩余部分以注释颜色进行高亮显示。但是,由于引擎是按需工作的,它只会更新可见区域内的跨度堆栈,并保留一个通知“行 X 和行 X+1 之间的高亮状态不一致”,其中 X 是可见区域的最后一行。现在,如果用户向下滚动,高亮状态将更新,“不一致”通知将向下移动。但通常,用户会继续输入,并且只会在几行后键入 */
。现在,可见区域中的高亮状态将恢复到正常“只有主规则集在活动跨度的堆栈上”。当用户现在滚动到“不一致”标记所在的行下方时,引擎会注意到旧堆栈和新堆栈是相同的,并将删除“不一致”标记。这允许重用用户键入 /*
之前的缓存的跨度堆栈。
虽然活动跨度的堆栈在行内可能会频繁更改,但它很少从一行开头到下一行开头发生更改。对于大多数语言,这种更改只发生在多行注释的开始和结束处。高亮引擎利用此属性,将跨度堆栈列表存储在特殊数据结构(ICSharpCode.AvalonEdit.Utils.CompressingTreeList
)中。高亮引擎的内存使用量与跨度堆栈更改的数量成线性关系,而不是与总行数成线性关系。这使得高亮引擎能够使用少量内存存储大代码文件的跨度堆栈,特别是在 C# 等语言中,其中 //
或 ///
序列比 /* */
注释更受欢迎。
代码补全
AvalonEdit 带有一个代码补全下拉窗口。您只需处理文本输入事件即可确定何时要显示窗口;所有 UI 都已为您完成。
以下是如何使用它
// in the constructor:
textEditor.TextArea.TextEntering += textEditor_TextArea_TextEntering;
textEditor.TextArea.TextEntered += textEditor_TextArea_TextEntered;
}
CompletionWindow completionWindow;
void textEditor_TextArea_TextEntered(object sender, TextCompositionEventArgs e)
{
if (e.Text == ".") {
// Open code completion after the user has pressed dot:
completionWindow = new CompletionWindow(textEditor.TextArea);
IList<ICompletionData> data = completionWindow.CompletionList.CompletionData;
data.Add(new MyCompletionData("Item1"));
data.Add(new MyCompletionData("Item2"));
data.Add(new MyCompletionData("Item3"));
completionWindow.Show();
completionWindow.Closed += delegate {
completionWindow = null;
};
}
}
void textEditor_TextArea_TextEntering(object sender, TextCompositionEventArgs e)
{
if (e.Text.Length > 0 && completionWindow != null) {
if (!char.IsLetterOrDigit(e.Text[0])) {
// Whenever a non-letter is typed while the completion window is open,
// insert the currently selected element.
completionWindow.CompletionList.RequestInsertion(e);
}
}
// Do not set e.Handled=true.
// We still want to insert the character that was typed.
}
按下“.”时,此代码将打开代码补全窗口。默认情况下,CompletionWindow
仅处理 Tab 和 Enter 等按键以插入当前选定的项目。为了使其在按下“.”或“;”等按键时也完成,我们向 TextEntering
事件附加了另一个处理程序,并告诉补全窗口插入选定的项目。
CompletionWindow
实际上永远不会获得焦点——相反,它会劫持文本区域上的 WPF 键盘输入事件,并通过其 ListBox
传递它们。这允许使用键盘在补全列表中选择条目,同时在编辑器中正常键入。
为了完整起见,以下是上述代码中使用的 MyCompletionData
类的实现
/// Implements AvalonEdit ICompletionData interface to provide the entries in the
/// completion drop down.
public class MyCompletionData : ICompletionData
{
public MyCompletionData(string text)
{
this.Text = text;
}
public System.Windows.Media.ImageSource Image {
get { return null; }
}
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content {
get { return this.Text; }
}
public object Description {
get { return "Description for " + this.Text; }
}
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text);
}
}
显示的内容和描述可以是 WPF 中可接受的任何内容,包括自定义 UIElements。如果您想做比简单插入文本更多的事情,也可以在 Complete
方法中实现自定义逻辑。insertionRequestEventArgs
可以帮助确定用户想要哪种类型的插入——根据触发插入的方式,它是 TextCompositionEventArgs
、KeyEventArgs
或 MouseEventArgs
的实例。
历史
- 2008 年 8 月 13 日:AvalonEdit 开始开发
- 2008 年 11 月 7 日:AvalonEdit 的第一个版本添加到 SharpDevelop 4.0 主干
- 2009 年 6 月 14 日:SharpDevelop 团队切换到 SharpDevelop 4 作为他们开发 SharpDevelop 的 IDE;AvalonEdit 开始用于实际工作
- 2009 年 10 月 4 日:本文首次发布于 The Code Project
- 2010 年 6 月 13 日:下载更新至 AvalonEdit 4.0.0.5950 (SharpDevelop 4.0 Beta 1)
- 2011 年 9 月 13 日:下载更新至 AvalonEdit 4.1.0.7916 (SharpDevelop 4.1 RC)
- 修复了大量错误
- 提高了性能
- 现在目标是 .NET 4.0
- 2012 年 5 月 12 日:下载更新至 AvalonEdit 4.2.0.8783 (SharpDevelop 4.2)
- 添加了 SearchPanel
- 添加了对 虚拟空间的支持
- 一些 bug 修复
- 2013 年 3 月 3 日:下载更新至 AvalonEdit 4.3.0.9390 (SharpDevelop 4.3)
- 添加了对 输入法编辑器 (IME) 的支持
- 修复了一个主要 bug,该 bug 有时会在更新现有折叠时导致“InvalidOperationException: 尝试从折叠的行构建视觉行”。
- 2013 年 4 月 2 日:下载更新至 AvalonEdit 4.3.1.9430 (SharpDevelop 4.3.1)
- 修复了 IME 支持中的一个 bug——前一个版本如果被另一个 WPF 控件禁用,则没有正确重新启用 IME。
注意:虽然我的示例代码是在 MIT 许可证下提供的,但 ICSharpCode.AvalonEdit
本身是根据 GNU LGPL 条款提供的。