🙆‍♀️

AvaloniaEditの使い方

2024/03/30に公開

AvaloniaEditとは

AvalonEditという、SharpDevelopの一部として開発されたWPF用のエディタエンジンがある。そのAvaloniaUI版がAvaloniaEdit。(ややこしい)
AvaloniaUI用のエディタエンジンなのでLinux/Windows等のマルチプラットフォーム実装に使うことができるのが特徴。

使用環境

Windows 11 : Home 22H2 + Visual Studio Community 2022(64bit)
Linuxテスト環境 : 上記Windows上のWSL2 Ubuntsu 22.04.2 LTS
Visual Studio2022, AvaloniaUI 11.0.2
Avalonia.Edit 11.0.6

AvaloniaEdit.Demo

使い方を把握するにはまずは下記のAvaloniaEditのDemoを確認するのが分かりやすい。

https://github.com/AvaloniaUI/AvaloniaEdit

実装の詳細はAvaloniaEdit.Demo MainWindow.xaml.cs以下にあり、以下のような機能がサンプル実装されている。

  • Textmateによるsyntax highlight
    AvaloniaEdit.Textmateを使ってsyntax highlightをかけている。多様な言語に対応していて、特に実装を追加せずに各言語のSyntax Highlightができる。

  • Codeの折り畳み
    対象ファイルがxmlのときのみCode折り畳みが実装されている。FoldingManagerで実装している。

  • AutoCompletion
    "."キーを押したときにAutoCompleteが起動する。"("キーを押したとき関数呼び出しのヒントが出る。
    textEditor_TextArea_TextEntered以下で実装している。

  • 文字の色/装飾
    DemoではUnderlineAndStrikeThroughTransformer : DocumentColorizingTransformerを使ってunderlineと取り消し線を描画している。

    DocumentColorizingTransformerをうまく使うとTextmateによらないSytax highlihtを実装することもできる。

実装のやりかた

自分のProjectにAvaloniaEditを使うときはNugetからAvalonia.AvaloniaEditをインストールして使用する。Avalonia.Textmateを使う場合はこれもインストールする。

nuget.configに修正が必要

以下の通りavaloniaのnugetサーバが停止されている。
https://zenn.dev/ajkfds/articles/23316b6d084a28
https://github.com/AvaloniaUI/Avalonia/issues/14549
下記のとおりnuget.configを修正する必要がある。(GitHub Commit #9e0f46d)

axaml

下記のようにして使う

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:AvalonEdit="using:AvaloniaEdit"
...
            >
    <AvalonEdit:TextEditor Name="Editor"
                        FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
                        HorizontalScrollBarVisibility="Auto"
                        VerticalScrollBarVisibility="Visible"
                        FontWeight="Light"
                        FontSize="10"
                    >
    </AvalonEdit:TextEditor>
</UserControl>

SyleInclude定義が必要

App.xamlに下記のStyleIncludeをいれておかないと何も表示されない。

App.axaml
<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="TestApp.App"
             RequestedThemeVariant="Dark">
    <Application.Styles>
        <FluentTheme />
        <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
    </Application.Styles>
</Application>

文字の色付け、装飾

LineTransformerを実装することで文字の装飾を自由に設定できる。
AvaloniaEdit.Demoでいうと、UnderlineAndStrikeThroughTransformerにあたる箇所を書き換える。
LineTransformerは描画の修正が発生したときにlineごとに適宜呼び出され、結果はキャッシュされて再描画のときも保持される。

_textEditor.TextArea.TextView.Redraw();

を実行すると、キャッシュが破棄されて再度LineTransformerが呼び出される。LineTransformerの結果を即時反映したい場合は適宜これを呼び出して必要箇所を修正要求する。

色を付ける場合はこんな感じ

    ChangeLinePart(
        color.Offset,
        color.Offset + color.Length,
        visualLine =>
        {
            visualLine.TextRunProperties.SetForegroundBrush(new SolidColorBrush(color.DrawColor));
        }
    );

太めのアンダーラインを引く場合

    ChangeLinePart(
        effect.Offset,
        effect.Offset + effect.Length,
        visualLine =>
        {
            if (visualLine.TextRunProperties.TextDecorations == null)
            {
                visualLine.TextRunProperties.SetTextDecorations(TextDecorations.Underline);
            }

            TextDecoration underline = TextDecorations.Underline[0];
            underline.StrokeThickness = 2;
            underline.StrokeThicknessUnit = TextDecorationUnit.Pixel;
            underline.StrokeOffset = 2;
            underline.StrokeOffsetUnit = TextDecorationUnit.Pixel;
            underline.Stroke = new SolidColorBrush(effect.DrawColor);
            var textDecorations = new TextDecorationCollection(visualLine.TextRunProperties.TextDecorations) { underline };

            visualLine.TextRunProperties.SetTextDecorations(textDecorations);
        }
    );

Background Highlight

TextDelcolationでの装飾は文字のある箇所にしか効かないので、スペースを含む文字列領域のハイライトには使えない。そのような用途には、独自にBackGroundRendererを作るのがよさそう。
AvaloniaEdit.Search.SearchResultBackgroundRenderer
を参考に作成し、
_textEditor.TextArea.TextView.BackgroundRenderers
に登録すると動作する。
このやりかただと文字背景になんでも描けるので、コードの特定箇所に波線を引いたりできる。

TextDocumentへのマルチスレッドアクセス

TextDocumentにはアクセスできるスレッドを制限するThread Ownershipという機構がある。
通常は作成したスレッドからしかアクセスできない。

ほかのスレッドに渡す場合には、渡す元のスレッドからいったんOwnerThreadをnullにして、

textDocument.SetOwnerThread(null);

受け取るThreadをOwnerThreadに設定する。

textDocument.SetOwnerThread(System.Threading.Thread.CurrentThread);

AutoCompleteの機能追加

AvaloniaEdit.DemoのMyCompletionDataをいろいろ修正すると多様な機能が実装できる。

    public class MyCompletionData : ICompletionData
    {
        public MyCompletionData(string text)
        {
            Text = text;
        }

        public IImage Image => null;
        public string Text { get; }

        // Use this property if you want to show a fancy UIElement in the list.
        public object Content => Text;

        public object Description => "Description for " + Text;
        public double Priority { get; } = 0;
        public void Complete(TextArea textArea, ISegment completionSegment,
            EventArgs insertionRequestEventArgs)
        {
            textArea.Document.Replace(completionSegment, Text);
        }
    }

CompletionDataがCompleteWindowで選択されたとき、"Complete"が呼ばれるので、ここを変えるといろいろなSnippetが実装できる。
DescriptionはItemのList表示時に各要素に表示される説明文。
Contentを変えると表示メニューに表示する内容を変えることができる。

AutoCompleteメニューに色をつける

MainWindow.axaml.csの
MyCompletionDataを元にしてコピーして別クラスを実装する

2度目は動作しないコード
    public class AutocompleteItem : AvaloniaEdit.CodeCompletion.ICompletionData
    {
        public AutocompleteItem(string text, Avalonia.Media.Color color)
        {
            this.text = text;
            textBlock.Text = text;
            textBlock.Foreground = new SolidColorBrush(color);
        }

        private TextBlock textBlock = new TextBlock();

        public object Content => textBlock;// Text;
    ...
    }

1度目にCompleteWindowを開いたときはこれでうまくいくのだけど、2度目に開いたときに以下のSystem.InvalidOperationExceptionが出る

textBlockをVisualizationTreeから切り離さずに再利用されるのがまずいっぽいが、これを切り離す方法がわからない。そこでCompleteWindowに追加するたびにtextBlockを作り直す構造にすると動作した。

AutocompleteItem.cs
        private TextBlock textBlock = null;

        public object Content => textBlock;

        public void Initialize()
        {
            textBlock = new TextBlock();
            textBlock.Text = Text;
            textBlock.Foreground = new SolidColorBrush(Color);
        }
CompeltionWindow展開時のコード
        var data = _completionWindow.CompletionList.CompletionData;
        foreach (AutocompleteItem item in items)
        {
            item.Initialize();
            data.Add(item);
        }

AutoCompleteを入力文字の1文字目から動作させる

AvaloniaEdit.Demoの例では"."を入力した際にその次の文字からAutoCompleteが動作するようになっている。これを最初の文字も含めてAutoComplete対象に入れたい場合、下記のようにcompletionWindowのStartOffsetを操作する。

    int prevIndex = _textEditor.CaretOffset;
    if (prevIndex != 0)
    {
        prevIndex--;
    }
    _completionWindow.Show();
    _completionWindow.StartOffset = prevIndex;

合字(Ligature)の抑制

AvaloniaEdit.Demoのデフォルト設定では<-や<=などの文字が合字扱いされ、記号に自動的に置き換わってしまう。下記にある通り、これを抑制する設定はまだ実装されていないよう。
https://github.com/AvaloniaUI/Avalonia/discussions/9933

合字を抑制するにはフォントを合字のないものにするしかなさそう。defaultでは
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
という設定になっている。下記にあるようにCascadia Codeの合字なしのフォントがCascadia Mono。
https://learn.microsoft.com/ja-jp/visualstudio/ide/how-to-change-fonts-and-colors-in-visual-studio?view=vs-2022#use-the-cascadia-code-font

よって、FontFamily設定を以下に変えることで合字を抑制できる。
FontFamily="Cascadia Mono,Consolas,Menlo,Monospace"

Tabの位置がおかしくなる問題

Tabが常に固定幅スペースとして表示され、各行のTab位置がそろわない。
下記にあるように、Tab表示はAvaloniaUIの未対応機能とされているよう。AvaloniaEditの改変が必要になるが、下記チケットの方法で暫定的な回避を行うことは可能。
https://github.com/AvaloniaUI/AvaloniaEdit/issues/412

Discussion