C#スクリプト エディタに入力補完機能を実装する
この記事は、以下の記事の続きとなります。
- CSVのクラスマッピングの定義をC#スクリプトで記述する - pierre3のブログ
- CSVのクラスマッピングの定義をC#スクリプトで記述する (その2:Converter) - pierre3のブログ
- CSVのクラスマッピングの定義をC#スクリプトで記述する (その3: ValidationRule) - pierre3のブログ
この記事のサンプルコードはこちら github.com
これまでは、読み込んだ設定スクリプトをTextBoxコントロールに表示していたのですが、
今回、もう少し"コードエディタっぽい" エディタに変更して、ソースコードの表示・編集が出来るようにしたいと思います。
目次
C#スクリプトエディタにAvalonEditを使う
AvalonEditは、WPF用の高機能テキストエディタライブラリで、ソースコードのシンタックスハイライトや入力補完機能を備えています。
今回は設定スクリプトの表示、編集用にこのライブラリを使用したいと思います。
NuGet からインストール可能です。
PM> Install-Package AvalonEdit
XAML での使用例は以下の通りです。
AvalonEdit:TextEditor
というコントロールを配置します。シンタックスハイライトや、行番号の表示有無などが指定可能です。
<Window x:Class="CsvEditor.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:CsvEditor" xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" mc:Ignorable="d" Title="MainWindow" Height="580" Width="960"> <Grid> <!-- 中略 --> <avalonEdit:TextEditor x:Name="configEdit" Document="{Binding ConfigurationDoc,Mode=TwoWay}" SyntaxHighlighting="C#" ShowLineNumbers="True" FontFamily="Consolas" FontSize="10Pt"> </avalonEdit:TextEditor> <!-- 中略 --> </Grid> </Window>
C#スクリプトエディタに入力補完機能を付ける
AvalonEdit の入力補完ウィンドウ
AvalonEdit には、入力補完ウィンドウを表示する仕組みが備わっています。
公式ドキュメント(http://avalonedit.net/documentation/ の"Code Completion")のサンプルを参考に以下のような記述を行うだけで、補完ウィンドウの表示自体は簡単に実装することが可能です。
//MainWindowのコードビハインド public partial class MainWindow : Window { private CompletionWindow completionWindow; public MainWindow() { InitializeComponent(); //イベントハンドラを登録 configEdit.TextArea.TextEntered += TextArea_TextEntered; configEdit.TextArea.TextEntering += TextArea_TextEntering; } //文字入力中 private void TextArea_TextEntering(object sender, System.Windows.Input.TextCompositionEventArgs e) { //補完Windowが開いている間 if (e.Text.Length > 0 && completionWindow != null) { //英数字以外が入力された場合 if (!char.IsLetterOrDigit(e.Text[0])) { //選択中のリストの項目をエディタに挿入する completionWindow.CompletionList.RequestInsertion(e); } } // HandledはTrueにしない // e.Handled=true; } //文字入力後 private async void TextArea_TextEntered(object sender, System.Windows.Input.TextCompositionEventArgs e) { // ピリオドを入力 if (e.Text == ".") { //入力補完Windowを生成 completionWindow = new CompletionWindow(configEdit.TextArea); //補完リストに表示するアイテムをコレクションに追加する //---> ここは、編集内容に応じて適切な入力候補を追加するように書き換える! IList<ICompletionData> data = completionWindow.CompletionList.CompletionData; data.Add(new CompletionData("item1")); data.Add(new CompletionData("item2")); data.Add(new CompletionData("item3")); //<--- //Windowを表示 completionWindow.Show(); completionWindow.Closed += delegate { completionWindow = null; }; } } }
public class CompletionData : ICompletionData { //入力候補一覧(UI)に表示する内容 public object Content { get; set; } //ツールチップに表示する説明文 public object Description { get; set; } //入力候補の左側に表示するアイコン public ImageSource Image { get; set; } //表示順の優先度? public double Priority { get; set; } //アイテム選択時に挿入される文字列 public string Text { get; set; } //アイテム選択後の処理 public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) { textArea.Document.Replace(completionSegment, Text); } }
Roslyn Workspaces API の Recommender を利用して入力候補一覧を取得する
AvalonEdit を利用することで、入力補完Window 自体を表示することができるのは分かったのですが、肝心の入力候補の一覧はどうやって作ればよいのでしょうか?
Roslyn の Workspaces API 辺りにコード補完の機能があったような。。。
と調べてみると、どうやら Microsoft.CodeAnalysis.Recommendations.Recommender
クラスのGetRecommendedSymbolsAtPositionAsync
というメソッドが使えそうだという事が分かりました。
Roslyn Workspases API もNuGetで追加します。
PM> Install-Package Microsoft.CodeAnalysis.Workspaces
Recommender.GetRecommendedSymbolsAtPositionAsync()
(このメソッドに関する情報が非常に少ないため、メソッド名と戻り値等から予測すると)
このメソッドは、ソースコード上の指定した場所(position)におけるコード補完候補の一覧を IEnumerable<ISymbol>
で返してくれるメソッドのようです。
シグネチャは以下の通り。引数にはposition のほかに、SemanticModel と Workspace を渡す必要があるようです。
public static Task<IEnumerable<ISymbol>> GetRecommendedSymbolsAtPositionAsync( SemanticModel semanticModel, int position, Workspace workspace, OptionSet options = null, CancellationToken cancellationToken = default(CancellationToken));
(引数1) semanticModel
SemanticModelは、C#スクリプトを実行する際に作成する Script<T>
オブジェクトから取得できるCompilation
オブジェクトから取得可能なので、これを指定すれば良さそうです。
private void CompileScript(string code) { Script<object> script = CSharpScript.Create(code, ScriptOptions.Default .WithReferences( typeof(object).Assembly, typeof(CsvReader).Assembly, typeof(ICsvEditorConfigurationHost).Assembly) .WithImports( "System", "System.Collections.Generic", "System.Linq"), typeof(ICsvEditorConfigurationHost)); //SemanticModel の取得 var compilation = script.GetCompilation(); var syntaxTree = compilation.SyntaxTrees.First(); var semanticModel = compilation.GetSemanticModel(syntaxTree); //Workspaceの作成 //...(略) }
(引数2) positon
ソースコード上の位置(エディタで補完機能を呼び出した際のキャレット位置)を指定すれば良さそう。
(引数3) workspace
ソースコードはWorkspace の形で渡してあげる必要があります。
Workspace を一から作成するには AdhocWorkspace
クラスを使用します。
//Workspaceの作成 Workspace workspace = new AdhocWorkspace(); //空のprojectを作成 string projectName = "CsvEditorConfig"; ProjectId projectId = ProjectId.CreateNewId(); VersionStamp versionStamp = VersionStamp.Create(); ProjectInfo projectInfo = ProjectInfo.Create(projectId, versionStamp, projectName, projectName, LanguageNames.CSharp); Project project = workspace.AddProject(projectInfo); //Documentを追加 SourceText sourcetext = Microsoft.CodeAnalysis.Text.SourceText.From(code); workspace.AddDocument(project.Id, "Config.csx", sourcetext);
- AdhocWorkspace について、詳しくは以下をご参照ください
Learn Roslyn Now: Part 6 Working with Workspacesjoshvarty.wordpress.com
ICompletionData のコレクションを作成する
AvalonEdit の CompletionWindow に渡す入力候補は ICompletionData
インターフェースを実装したオブジェクトである必要がありました。
次は、GetRecommendedSymbolsAtPositionAsync()
メソッドから取得したISymbol
のコレクション を ICompletionData
のコレクションに変換して返すメソッドを作ります。
public class MainWindowViewModel : BindableBase { private Script<object> script; private SemanticModel semanticModel; private AdhocWorkspace workspace = new AdhocWorkspace(); public async Task<IEnumerable<CompletionData>> GetCompletionListAsync(int position,string code) { CompileScript(code); if (semanticModel == null || workspace == null) { return Enumerable.Empty<CompletionData>(); } var items = await Recommender.GetRecommendedSymbolsAtPositionAsync( semanticModel, position, workspace); return items.Select(symbol => new CompletionData() { //ListBoxに表示する内容 Content = symbol.Name, //エディタに挿入する文字列 Text = symbol.Name, //ツールチップに表示する文字列 Description = symbol.ToMinimalDisplayString(semanticModel, position) }); } }
これをViewModelに実装して、View側 (コードビハインド) に記述した TextArea_TextEntered
イベントハンドラで呼び出すようにします。
また、ピリオド(.
)のほかに[Ctrl]+[Space]キーでも入力補完ウィンドウを表示するようにOnKeyDown()
メソッドをオーバーライドします。
public partial class MainWindow : Window { private CompletionWindow completionWindow; private MainWindowViewModel VM { get { return DataContext as MainWindowViewModel; } } private async void TextArea_TextEntered(object sender, System.Windows.Input.TextCompositionEventArgs e) { if (VM == null) { return; } // Open code completion after the user has pressed dot: if (e.Text == ".") { await ShowCompletionWindow(); } } protected override async void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); //Ctrl+Space でも表示するように if (e.Key == Key.Space && e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Control)) { e.Handled = true; await ShowCompletionWindow(); } } private async Task ShowCompletionWindow() { if (VM == null) { return; } completionWindow = new CompletionWindow(configEdit.TextArea); IList<ICompletionData> data = completionWindow.CompletionList.CompletionData; var completionItems = await VM.GetCompletionListAsync( configEdit.TextArea.Caret.Offset + 1, configEdit.Document.Text + " "); foreach (var item in completionItems) { data.Add(item); } completionWindow.Show(); completionWindow.Closed += delegate { completionWindow = null; }; } }
実行してみよう
長くなってしまいましたが、これで一通りの実装が完了しました。
それでは、実行結果を見てみましょう。
中々いい感じに動いていますね。
※ あとは、以下の2点を何とかすれば完璧なのですが、今回はあきらめました。
入力候補の種類(クラス、メソッド、プロパティ等)に応じたアイコンを表示したい
Download Visual Studio Image Library from Official Microsoft Download Center でXAML形式のアイコンが取得できるので、これを利用すればよさそう。メソッドのオーバーロードは「まとめて表示 ⇒ 上下キーで選択」のようなUIにしたい
ICSharpCode.AvalonEdit.CodeCompletion.OverloadInsightWindow
というので実現できそう。