コンパイラサービス: エディタサービス
このチュートリアルはF#コンパイラによって公開されるエディタサービスの
使用方法についてのデモです。
このAPIにより、Visual StudioやXamarin Studio、EmacsなどのF#エディタ内において、
自動補完機能やツールチップ表示、引数情報のヘルプ表示、括弧の補完などの機能を
実装することができます
(詳細については fsharpbindings のプロジェクトを参照してください)。
型無しASTを使用するチュートリアル と同じく、
今回も FSharpChecker
オブジェクトを作成するところから始めます。
注意: 以下で使用しているAPIは試験的なもので、最新バージョンのnugetパッケージの 公開に伴って変更されることがあります。
サンプルソースコードの型チェック
前回の(型無しASTを使った)チュートリアル と同じく、
FSharp.Compiler.Service.dll
への参照を追加した後に特定の名前空間をオープンし、
FSharpChecker
のインスタンスを作成します:
1: 2: 3: 4: 5: 6: 7: 8: |
|
前回 同様、
コンパイラに渡されるファイルとしては特定の入力値だけであるという
コンテキストを想定するため、 GetCheckOptionsFromScriptRoot
を使います
(この入力値はコンパイラによってスクリプトファイル、
あるいはスタンドアロンのF#ソースコードとみなされます)。
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: |
|
型チェックを実行するには、まず ParseFile
を使って
入力値をパースする必要があります。
このメソッドを使うと 型無しAST にアクセスできるようになります。
しかし今回は完全な型チェックを実行するため、続けて CheckFileInProject
を呼び出す必要があります。
このメソッドは ParseFile
の結果も必要とするため、
たいていの場合にはこれら2つのメソッドをセットで呼び出すことになります。
1: 2: 3: 4: |
|
TypeCheckResults
に備えられた興味深い機能の紹介に入る前に、
サンプル入力に対して型チェッカーを実行する必要があります。
F#コードにエラーがあった場合も何らかの型チェックの結果が返されます
(ただし間違って「推測された」結果が含まれることがあります)。
1: 2: 3: 4: |
|
あるいは ParseAndCheckFileInProject
を使用すれば1つの操作で両方のチェックを行うことができます:
1: 2: 3: |
|
この返り値は CheckFileAnswer
型で、この型に機能的に興味深いものが揃えられています...
1: 2: 3: 4: |
|
今回は単に(状況に応じて)「Hello world」と表示するだけの
単純な関数の型をチェックしています。
最終行では値 msg
に対する補完リストを表示することができるように、
msg.
というようにドットを追加している点に注意してください
(今回の場合は文字列型に対する様々なメソッドが期待されます)。
型チェックの結果を使用する
では TypeCheckResults
型で公開されているAPIをいくつか見ていきましょう。
一般的に、F#ソースコードエディタサービスの実装に必要な機能は
ほとんどこの型に備えられています。
ツールチップの取得
ツールチップを取得するには GetToolTipTextAlternate
メソッドを使用します。
このメソッドには行数と文字オフセットを指定します。
いずれも0から始まる数値です。
サンプルコードでは3行目(0行目は空白行)、インデックス7にある文字 f
から始まる関数
foo
のツールチップを取得しています
(ツールチップは識別子の中であれば任意の位置で機能します)。
またこのメソッドにはトークンタグを指定する必要もあります。
トークンタグは一般的には IDENT
を指定して、識別子に対する
ツールチップが取得できるようにします
(あるいは #r "..."
を使用している場合にはアセンブリの完全パスを表示させるように
することもできるでしょう)。
1: 2: 3: 4: 5: 6: 7: |
|
注意:
GetToolTipTextAlternate
は古い関数GetToolTipText
に代わるものです。GetToolTipText
は0から始まる行番号を受け取るようになっていたため、非推奨になりました。
この関数には位置とトークンの種類の他にも、
(ソースコードの変更時に役立つように)特定行の現在の内容と、
現時点における完全修飾された 名前
を表す文字列のリストを指定する必要があります。
たとえば完全修飾名 System.Random
という名前を持った識別子 Random
に対する
ツールチップを取得する場合、 Random
という文字列が現れる場所の他に、
["System"; "Random"]
という値を指定する必要があります。
返り値の型は ToolTipText
で、この型には ToolTipElement
という
判別共用体が含まれます。
この共用体は、コンパイラによって返されたツールチップの種類に応じて異なります。
自動補完リストの取得
次に紹介する TypeCheckResults
のメソッドを使用すると、
特定の位置における自動補完機能を実装できます。
この機能は任意の識別子上、
あるいは(特定のスコープ内で利用可能な名前の一覧を取得する場合には)任意のスコープ、
あるいは特定のオブジェクトにおけるメンバーリストを取得する場合には
.
の直後で呼び出すことができます。
今回は文字列の値 msg
に対するメンバーリストを取得することにします。
そのためには最終行( printfn "%s" msg.
で終わっている行)にある
シンボル .
の位置を指定して GetDeclarationListInfo
を呼び出します。
オフセットは1から始まるため、位置は 7, 23
になります。
また、テキストが変更されていないことを表す関数と、
現時点において補完する必要がある識別子を指定する必要もあります。
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
注意:
GetDeclarationListInfo
は古い関数GetDeclarations
に代わるものです。GetDeclarations
は0から始まる行番号を受け取るようになっていたため、非推奨になりました。 また、将来的には現在のGetDeclarations
が削除され、GetDeclarationListInfo
がGetDeclarations
になる予定です。
コードを実行してみると、 Substring
や ToUpper
、 ToLower
といった
文字列に対するいつものメソッドのリストが取得できていることでしょう。
GetDeclarations
の5,6番目の引数( []
および "msg"
)には
自動補完用のコンテキストを指定します。
今回の場合は完全名 msg
に対する補完を行いましたが、
たとえば ["System"; "Collections"]
と "Generic"
というように
完全修飾された名前空間を指定して補完リストを取得することもできます。
引数の情報を取得する
次に一般的なエディタの機能としては、メソッドのオーバーロードに
関する情報を提供するというものでしょう。
サンプルコード中では多数のオーバーロードを持った String.Concat
を使っています。
このオーバーロード一覧は GetMethods
で取得できます。
先ほどと同じく、このメソッドには対象とする項目の位置を0基準のオフセットで指定し
(今回は String.Concat
識別子の右側の終端)、
識別子もやはり指定します
(これにより、コンパイラはソースコードが変更された場合でも最新の情報に追従できます):
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
ここでは Display
プロパティを使用することで各引数に対する
アノテーションを取得しています。
このプロパティは arg0: obj
あるいは params args: obj[]
、
str0: string, str1: string
といった情報を返します。
これらの引数を連結した後、メソッド名とメソッドの型情報とともに表示させています。
非同期操作と即時操作
CheckFileInProject
が非同期操作であることを気にされる人もいるかもしれません。
これはつまり、F#コードの型チェックにはある程度時間がかかることを示唆しています。
F#コンパイラは型チェックを(自動的に)バックグラウンドで処理を進めているため、
CheckFileInProject
メソッドを呼び出すと非同期操作が返されることになります。
また、 CheckFileInProjectIfReady
というメソッドもあります。
このメソッドは、型チェックの操作が即座に開始できない場合、
つまりプロジェクト内の他のファイルがまだ型チェックされていない場合には
処理が即座に返されます。
この場合、バックグラウンドワーカーは一定期間他の作業を進めるか、
FileTypeCheckStateIsDirty
イベントが発生するまでは
ファイルに対する型チェックを諦めるか、どちらか選択することになります。
fsharpbinding プロジェクトには 1つのF#エージェント経由ですべてのリクエストをバックグラウンドワークとして 処理するような、より複雑な具体例も含まれています。 エディタの機能を実装する方法としてはこちらのほうが適切です。
まとめ
CheckFileAnswer
にはチュートリアルで紹介していないような便利なメソッドが
多数揃えられています。
これらを使用すれば特定の識別子に対する宣言の位置を取得したり、
付加的な色情報を取得したりすることができます
(F# 3.1では式ビルダーの識別子やクエリ演算子も着色表示されます)。
最後に、直接.NET APIを呼び出すことができないようなエディタに対するサポート機能を 実装する場合、ここで紹介した様々な機能を FSharp.AutoComplete プロジェクトのコマンドラインインターフェイス経由で呼び出すこともできます。
Full name: Editor.checker
Full name: Editor.input
Full name: Editor.inputLines
String.Split(separator: string [], options: StringSplitOptions) : string []
String.Split(separator: char [], options: StringSplitOptions) : string []
String.Split(separator: char [], count: int) : string []
String.Split(separator: string [], count: int, options: StringSplitOptions) : string []
String.Split(separator: char [], count: int, options: StringSplitOptions) : string []
Full name: Editor.file
Full name: Editor.projOptions
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
Full name: Editor.parsingOptions
Full name: Editor.parseFileResults
Full name: Editor.checkFileAnswer
Full name: Editor.parseResults2
Full name: Editor.checkFileAnswer2
Full name: Editor.checkFileResults
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.failwithf
Full name: Editor.identToken
Full name: Editor.tip
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Full name: Editor.decls
Full name: Editor.methods
type String =
new : value:char -> string + 7 overloads
member Chars : int -> char
member Clone : unit -> obj
member CompareTo : value:obj -> int + 1 overload
member Contains : value:string -> bool
member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
member EndsWith : value:string -> bool + 2 overloads
member Equals : obj:obj -> bool + 2 overloads
member GetEnumerator : unit -> CharEnumerator
member GetHashCode : unit -> int
...
Full name: System.String
--------------------
String(value: nativeptr<char>) : unit
String(value: nativeptr<sbyte>) : unit
String(value: char []) : unit
String(c: char, count: int) : unit
String(value: nativeptr<char>, startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int) : unit
String(value: char [], startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : unit
Full name: Microsoft.FSharp.Core.String.concat