Compiler Services: Editor services
This tutorial demonstrates how to use the editor services provided by the F# compiler.This API is used to provide auto-complete, tool-tips, parameter info help, matching ofbrackets and other functions in F# editors including Visual Studio, Xamarin Studio and Emacs(seefsharpbindings project for more information).Similarly tothe tutorial on using untyped AST, we start bygetting theInteractiveChecker
object.
NOTE: The FSharp.Compiler.Service API is subject to change when later versions of the nuget package are published
Type checking sample source code
As in theprevious tutorial (using untyped AST), we start by referencingFSharp.Compiler.Service.dll
, opening the relevant namespace and creating an instanceofInteractiveChecker
:
// Reference F# compiler API#r"FSharp.Compiler.Service.dll"openFSharp.Compiler.CodeAnalysisopenFSharp.Compiler.EditorServicesopenFSharp.Compiler.TextopenFSharp.Compiler.Tokenization// Create an interactive checker instanceletchecker=FSharpChecker.Create()
Aspreviously, we useGetProjectOptionsFromScriptRoot
to get a contextwhere the specified input is the only file passed to the compiler (and it is treated as ascript file or stand-alone F# source code).
// Sample input as a multi-line stringletinput=""" open System let foo() = let msg = String.Concat("Hello"," ","world") if true then printfn "%s" msg. """// Split the input & define file nameletinputLines=input.Split('\n')letfile="/home/user/Test.fsx"letprojOptions,_diagnostics=checker.GetProjectOptionsFromScript(file,SourceText.ofStringinput,assumeDotNetFramework=false)|>Async.RunSynchronouslyletparsingOptions,_diagnostics2=checker.GetParsingOptionsFromProjectOptions(projOptions)
To perform type checking, we first need to parse the input usingParseFile
, which gives us access to theuntyped AST. However,then we need to callCheckFileInProject
to perform the full type checking. This functionalso requires the result ofParseFileInProject
, so the two functions are often calledtogether.
// Perform parsingletparseFileResults=checker.ParseFile(file,SourceText.ofStringinput,parsingOptions)|>Async.RunSynchronously
Before we look at the interesting operations provided byTypeCheckResults
, weneed to run the type checker on a sample input. On F# code with errors, you would get some type checkingresult (but it may contain incorrectly "guessed" results).
// Perform type checkingletcheckFileAnswer=checker.CheckFileInProject(parseFileResults,file,0,SourceText.ofStringinput,projOptions)|>Async.RunSynchronously
Alternatively you can useParseAndCheckFileInProject
to check both in one step:
letparseResults2,checkFileAnswer2=checker.ParseAndCheckFileInProject(file,0,SourceText.ofStringinput,projOptions)|>Async.RunSynchronously
The function returns both the untyped parse result (which we do not use in thistutorial), but also aCheckFileAnswer
value, which gives us access to allthe interesting functionality...
letcheckFileResults=matchcheckFileAnswerwith|FSharpCheckFileAnswer.Succeeded(res)->res|res->failwithf"Parsing did not finish... (%A)"res
Here, we type check a simple function that (conditionally) prints "Hello world".On the last line, we leave an additional dot inmsg.
so that we can get thecompletion list on themsg
value (we expect to see various methods on the stringtype there).
Using type checking results
Let's now look at some of the API that is exposed by theTypeCheckResults
type. In general,this is the type that lets you implement most of the interesting F# source code editor services.
Getting a tool tip
To get a tool tip, you can use theGetToolTip
method. The method takes a line number and characteroffset. Both of the numbers are zero-based. In the sample code, we want to get a tooltip for thefoo
function that is defined on line 3 (line 0 is blank) and the letterf
starts at index 7 (the tooltipwould work anywhere inside the identifier).
In addition, the method takes a tag of token which is typicallyIDENT
, when getting a tooltip for anidentifier (the other option lets you get a tooltip with full assembly location when using#r "..."
).
// Get tag of the IDENT token to be used as the last argumentletidentToken=FSharpTokenTag.Identifier// Get tool tip at the specified locationlettip=checkFileResults.GetToolTip(4,7,inputLines.[1],["foo"],identToken)printfn"%A"tip
Aside from the location and token kind, the function also requires the current contents of the line(useful when the source code changes) and aNames
value, which is a list of strings representingthe current long name. For example, to get a tooltip for theRandom
identifier in a long nameSystem.Random
, you would use a location somewhere in the stringRandom
and you would pass["System"; "Random"]
as theNames
value.
The returned value is of typeToolTipText
which contains a discriminated unionToolTipElement
.The union represents different kinds of tool tips that you can get from the compiler.
Getting auto-complete lists
The next method exposed byTypeCheckResults
lets us perform auto-complete on a given location.This can be called on any identifier or in any scope (in which case you get a list of names visiblein the scope) or immediately after.
to get a list of members of some object. Here, we get alist of members of the string valuemsg
.
To do this, we callGetDeclarationListInfo
with the location of the.
symbol on the last line(ending withprintfn "%s" msg.
). The offsets are one-based, so the location is7, 23
.We also need to specify a function that says that the text has not changed and the current identifierwhere we need to perform the completion.
// Get declarations (autocomplete) for a locationletdecls=checkFileResults.GetDeclarationListInfo(SomeparseFileResults,7,inputLines.[6],PartialLongName.Empty23,(fun()->[]))// Print the names of available itemsforitemindecls.Itemsdoprintfn" -%s"item.NameInList
NOTE:
v
is an alternative name for the oldGetDeclarations
. The oldGetDeclarations
wasdeprecated because it accepted zero-based line numbers. At some point it will be removed, andGetDeclarationListInfo
will be renamed back toGetDeclarations
.
When you run the code, you should get a list containing the usual string methods such asSubstring
,ToUpper
,ToLower
etc. The fourth argument ofGetDeclarations
, here([], "msg")
,specifies the context for the auto-completion. Here, we want a completion on a complete namemsg
, but you could for example use(["System"; "Collections"], "Generic")
to get a completion listfor a fully qualified namespace.
Getting parameter information
The next common feature of editors is to provide information about overloads of a method. In oursample code, we useString.Concat
which has a number of overloads. We can get the list usingGetMethods
operation. As previously, this takes the zero-indexed offset of the location that we areinterested in (here, right at the end of theString.Concat
identifier) and we also need to providethe identifier again (so that the compiler can provide up-to-date information when the source codechanges):
// Get overloads of the String.Concat methodletmethods=checkFileResults.GetMethods(5,27,inputLines.[4],Some["String";"Concat"])// Print concatenated parameter listsformiinmethods.Methodsdo[forpinmi.Parametersdoforttinp.Displaydoyieldtt.Text]|>String.concat", "|>printfn"%s(%s)"methods.MethodName
The code uses theDisplay
property to get the annotation for each parameter. This returns informationsuch asarg0: obj
orparams args: obj[]
orstr0: string, str1: string
. We concatenate the parametersand print a type annotation with the method name.
Asynchronous and immediate operations
You may have noticed thatCheckFileInProject
is an asynchronous operation.This indicates that type checking of F# code can take some time.The F# compiler performs the work in the background (automatically) and whenwe call theCheckFileInProject
method, it returns an asynchronous operation.
There is also theCheckFileInProjectIfReady
method. This returns immediately if thetype checking operation can't be started immediately, e.g. if other files in the projectare not yet type-checked. In this case, a background worker might choose to do otherwork in the meantime, or give up on type checking the file until theFileTypeCheckStateIsDirty
eventis raised.
Thefsharpbinding project has a more advancedexample of handling the background work where all requests are sent through an F# agent.This may be more appropriate for implementing editor support.
Summary
TheCheckFileAnswer
object contains other useful methods that were not covered in this tutorial. Youcan use it to get location of a declaration for a given identifier, additional colorization information(the F# 3.1 colorizes computation builder identifiers & query operators) and others.
Using the FSharpChecker component in multi-project, incremental and interactive editing situations may involveknowledge of theFSharpChecker operations queue and theFSharpChecker caches.
Finally, if you are implementing an editor support for an editor that cannot directly call the .NET API,you can call many of the methods discussed here via a command line interface that is available in theFSharp.AutoComplete project.
namespace FSharp
--------------------
namespace Microsoft.FSharp
<summary> Used to parse and check F# source code.</summary>
(+0 other overloads)
System.String.Split([<System.ParamArray>] separator: char array) : string array
(+0 other overloads)
System.String.Split(separator: string array, options: System.StringSplitOptions) : string array
(+0 other overloads)
System.String.Split(separator: string, ?options: System.StringSplitOptions) : string array
(+0 other overloads)
System.String.Split(separator: char array, options: System.StringSplitOptions) : string array
(+0 other overloads)
System.String.Split(separator: char array, count: int) : string array
(+0 other overloads)
System.String.Split(separator: char, ?options: System.StringSplitOptions) : string array
(+0 other overloads)
System.String.Split(separator: string array, count: int, options: System.StringSplitOptions) : string array
(+0 other overloads)
System.String.Split(separator: string, count: int, ?options: System.StringSplitOptions) : string array
(+0 other overloads)
System.String.Split(separator: char array, count: int, options: System.StringSplitOptions) : string array
(+0 other overloads)
<summary> Functions related to ISourceText objects</summary>
<summary> Creates an ISourceText object from the given string</summary>
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> 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<'T> -> Async<'T> + 1 overload 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: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * objnull -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...
--------------------
type Async<'T>
member FSharpChecker.ParseFile: fileName: string * sourceText: ISourceText * options: FSharpParsingOptions * ?cache: bool * ?userOpName: string -> Async<FSharpParseFileResults>
member FSharpChecker.ParseAndCheckFileInProject: fileName: string * fileVersion: int * sourceText: ISourceText * options: FSharpProjectOptions * ?userOpName: string -> Async<FSharpParseFileResults * FSharpCheckFileAnswer>
<summary> The result of calling TypeCheckResult including the possibility of abort and background compiler not caught up.</summary>
<summary> Success</summary>
<summary> Some of the values in the field FSharpTokenInfo.Tag</summary>
<summary> Indicates the token is an identifier</summary>
<summary> Qualified long name.</summary>
<summary> Get the text to display in the declaration list for the declaration. This is a display name without backticks.</summary>
<summary> The methods (or other items) in the group</summary>
<summary> The parameters of the method in the overload set</summary>
<summary> The representation for the parameter including its name, its type and visual indicators of other information such as whether it is optional.</summary>
<summary> Gets the text</summary>
<summary> The shared name of the methods (or other items) in the group</summary>