65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AvalonEdit (WPF 文本编辑器)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (267投票s)

2009年10月5日

LGPL3

13分钟阅读

viewsIcon

2045974

downloadIcon

72685

AvalonEdit 是一个可扩展的开源文本编辑器,支持语法高亮和折叠。

AvalonEdit 的最新版本可以在 SharpDevelop 项目中找到。有关 AvalonEdit 的详细信息,请参阅 www.avalonedit.net。 

Sample Image

引言

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 的架构。

架构

Namespace Dependency Graph

正如您在此依赖关系图中看到的,AvalonEdit 由几个具有清晰分隔任务的子命名空间组成。大多数命名空间都有某种“主”类。

  • ICSharpCode.AvalonEdit.Utils:各种实用类
  • ICSharpCode.AvalonEdit.DocumentTextDocument — 文本模型
  • ICSharpCode.AvalonEdit.RenderingTextView — 文档的可扩展视图
  • ICSharpCode.AvalonEdit.EditingTextArea — 控制文本编辑(例如,光标、选择、处理用户输入)
  • ICSharpCode.AvalonEdit.FoldingFoldingManager — 启用代码折叠
  • ICSharpCode.AvalonEdit.HighlightingHighlightingManager — 高亮引擎
  • ICSharpCode.AvalonEdit.Highlighting.XshdHighlightingLoader — XML 语法高亮定义支持(.xshd 文件)
  • ICSharpCode.AvalonEdit.CodeCompletionCompletionWindow — 显示用于代码补全的下拉列表
  • ICSharpCode.AvalonEditTextEditor — 将所有内容整合在一起的主要控件

这是 TextEditor 控件的视觉树

Visual Tree

重要的是要理解 AvalonEdit 是一个复合控件,包含三个层:TextEditor(主控件)、TextArea(编辑)、TextView(渲染)。虽然主控件为常见任务提供了一些便捷方法,但对于大多数高级功能,您需要直接与内部控件进行交互。您可以通过 textEditor.TextAreatextEditor.TextArea.TextView 访问它们。

文档(文本模型)

模型的类是 ICSharpCode.AvalonEdit.Document.TextDocument。基本上,文档就是一个带有事件的 StringBuilder。然而,Document 命名空间还包含一些对处理文本编辑器的应用程序很有用的功能。

在文本编辑器中,所有三个控件(TextEditorTextAreaTextView)都有一个指向 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 StringStringBuilder 类中的方法使用的“索引”参数完全相同。

偏移量易于使用,但有时您需要行/列对。AvalonEdit 定义了一个名为 TextLocationstruct 来处理这些。

文档提供了 GetLocationGetOffset 方法来在偏移量和 TextLocation 之间进行转换。这些是基于 DocumentLine 类构建的便捷方法。

TextDocument.Lines 集合包含文档中每一行的 DocumentLine 实例。此集合对用户代码是只读的,并且会自动更新以反映当前文档内容。

渲染

在整个“文档”部分,没有提到可扩展性。文本渲染基础设施现在必须通过完全可扩展来弥补这一点。

ICSharpCode.AvalonEdit.Rendering.TextView 类是 AvalonEdit 的核心。它负责将文档显示到屏幕上。

为了以可扩展的方式实现这一点,TextView 使用自己的模型:VisualLine。视觉行仅为文档的可见部分创建。

渲染过程如下所示

rendering pipeline

管道中的最后一步是转换为一个或多个 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 可以帮助确定用户想要哪种类型的插入——根据触发插入的方式,它是 TextCompositionEventArgsKeyEventArgsMouseEventArgs 的实例。

历史

  • 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) 
  • 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 条款提供的。 

© . All rights reserved.