callmekohei's blog

callmekoheiのひとりごと

単語補完をやってみた!

この記事はF# Advent Calendar2017 1日目の記事です ( see : F# Advent Calendar 2017 - Qiita )



f:id:callmekohei00:20171130201341g:plain

Summary

こんにちは!ポンド円で負けまくって死にそうなcallmekoheiです!

ここ一年ほどF#の単語補完できたらいいな〜とすこしコード書いてました。

そのことを書いてみたいと思いマフ (^_^)/

単語補完とは?

F#のコードを書くじゃないですか?

で、そのとき例えばList.と書いて候補が出ると嬉しいですよね?

その候補を文脈から引き出します。

上記の映像が単語補完のサンプルです。

単語補完をやっている様子がつかめると思います。

どうやってるか?

今回やった環境はVimです。

Vimというのはメモ帳の機能をアップさせたようなものです。

で、このVimで単語補完するときの仕組みをdeopleteが提供してくれます。

で、実際に文脈から単語候補を提供してくれるのは

FSharp.Compiler.Serviceというライブラリ群です。

実際にやってみよう!

実際やってみるのはとても簡単です。

まずFSharp.Compiler.Servieライブラリをナゲットします。

で、次のようなコードを書きます。

// sample code from : https://github.com/fsharp/FSharp.Compiler.Service/blob/master/fcs/samples/EditorService/Program.fs

// Open the namespace with InteractiveChecker type
#r "./packages/FSharp.Compiler.Service/lib/net45/FSharp.Compiler.Service.dll"
open System
open Microsoft.FSharp.Compiler
open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.FSharp.Compiler.QuickParse

// Create an interactive checker instance (ignore notifications)
let checker = FSharpChecker.Create()

let parseWithTypeInfo (file, input) = 
    let checkOptions, _errors = checker.GetProjectOptionsFromScript(file, input) |> Async.RunSynchronously
    let parsingOptions, _errors = checker.GetParsingOptionsFromProjectOptions(checkOptions)
    let untypedRes = checker.ParseFile(file, input, parsingOptions) |> Async.RunSynchronously
    
    match checker.CheckFileInProject(untypedRes, file, 0, input, checkOptions) |> Async.RunSynchronously with 
    | FSharpCheckFileAnswer.Succeeded(res) -> untypedRes, res
    | res -> failwithf "Parsing did not finish... (%A)" res

はい。なんとなくよくわからないですけど、気にしない気にしない。

で次のコードを書きます。

// sample code from : https://github.com/fsharp/FSharp.Compiler.Service/blob/master/fcs/samples/EditorService/Program.fs

let input = 
  """
  let foo() = 
    let msg = "Hello world"
    if true then 
      printfn "%s" msg.
  """
let inputLines = input.Split('\n')
let file = "./Test.fsx" // <--- このファイルは実際にあるかどうかは問題ない

let untyped, parsed = parseWithTypeInfo (file, input)

let partialName = GetPartialLongNameEx(inputLines.[4], 22)

// Get declarations (autocomplete) for a location
let decls = 
    parsed.GetDeclarationListInfo(Some untyped, 5, inputLines.[4], partialName, (fun () -> [])) 
    |> Async.RunSynchronously

for item in decls.Items do
    printfn " - %s" item.Name

結果

 - Length
 - Chars
 - Clone
 - CompareTo
 - Contains
 - CopyTo
 - EndsWith
 - Equals
 - GetEnumerator
 - GetHashCode
...

このコードで重要な関数はGetDeclarationListInfo です。これが単語候補を引き出します。





longname, partial nameで考える

まずpartial nameとはみたいな。。下記を見たらわかるはず・・。

QuickParse.GetPartialLongNameEx("System.Test.",11)
|> printfn "%A"
(*
    {QualifyingIdents = ["System"; "Test"];
     PartialIdent = "";
     EndColumn = 11;
     LastDotPos = Some 11;}
*)


QuickParse.GetPartialLongNameEx("System.Test",10)
|> printfn "%A"

(*
    {QualifyingIdents = ["System"];
     PartialIdent = "Test";
     EndColumn = 10;
     LastDotPos = Some 6;}
*)



ではでは、このGetDeclarationListInfoがかえす4つのケースを考えてみます。

CASE1

よみこんでる namespace すべてをかえす

row = 0, col = 0でやってます。

昔はbase1だったんでrow=0にするとエラーがでてたけど最近変わった????

let file = "./Test.fsx" // <--- このファイルは実際にあるかどうかは問題ない

let input = ""
let row = 0
let partialName = QuickParse.GetPartialLongNameEx(input,0)
let untyped, parsed = parseWithTypeInfo (file, input)

// Get declarations (autocomplete) for a location
let decls = 
    parsed.GetDeclarationListInfo(Some untyped, row, input, partialName,  (fun () -> []))
    |> Async.RunSynchronously

for item in decls.Items do
    printfn " - %s" item.Name

結果

 - Choice1Of2
 - Choice1Of3
 - Choice1Of4
 - Choice1Of5
 - Choice1Of6
 - Choice1Of7
...

CASE2

namespace を指定している場合、その補完リストをかえす

let file = "./Test.fsx" // <--- このファイルは実際にあるかどうかは問題ない

let input = "Microsoft.FSharp."
let row = 1
let partialName = QuickParse.GetPartialLongNameEx(input,16)
let untyped, parsed = parseWithTypeInfo (file, input)

// Get declarations (autocomplete) for a location
let decls = 
    parsed.GetDeclarationListInfo(Some untyped, row, input, partialName,  (fun () -> []))
    |> Async.RunSynchronously

for item in decls.Items do
    printfn " - %s" item.Name

結果

 - Compiler
 - Reflection
 - Quotations
 - NativeInterop
 - Linq
 - Data
 - Core
 - Control
 - Collections

CASE3

dotを入力したとき推論できる場合は、その分の補完リストをかえす

let file = "./Test.fsx" // <--- このファイルは実際にあるかどうかは問題ない

let input =
    """
    let x = "abc"
    x.
    """
let inputLines = input.Split('\n')
let row = 3
let partialName = QuickParse.GetPartialLongNameEx(inputLines.[2],5)
let untyped, parsed = parseWithTypeInfo (file, input)

// Get declarations (autocomplete) for a location
let decls = 
    parsed.GetDeclarationListInfo(Some untyped, row, inputLines.[2], partialName,  (fun () -> []))
    |> Async.RunSynchronously

for item in decls.Items do
    printfn " - %s" item.Name

結果

 - Length
 - Chars
 - Clone
 - CompareTo
 - Contains
 - CopyTo
 - EndsWith
...

CASE4

dotを入力したとき推論できない場合は、なにもかえさない

let file = "./Test.fsx" // <--- このファイルは実際にあるかどうかは問題ない

let input = "a."
let row = 1
let partialName = QuickParse.GetPartialLongNameEx(input,1)
let untyped, parsed = parseWithTypeInfo (file, input)

// Get declarations (autocomplete) for a location
let decls = 
    parsed.GetDeclarationListInfo(Some untyped, row, input, partialName,  (fun () -> []))
    |> Async.RunSynchronously

for item in decls.Items do
    printfn " - %s" item.Name

結果

nothing

こんな感じです!

参考

github.com

fsharp.github.io