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 of
brackets and other functions in F# editors including Visual Studio, Xamarin Studio and Emacs
(see fsharpbindings project for more information).
Similarly to the tutorial on using untyped AST, we start by
getting the InteractiveChecker
object.
NOTE: The FSharp.Compiler.Service API is subject to change when later versions of the nuget package are published
As in the previous tutorial (using untyped AST), we start by referencing
FSharp.Compiler.Service.dll
, opening the relevant namespace and creating an instance
of InteractiveChecker
:
// Reference F# compiler API
#r "FSharp.Compiler.Service.dll"
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
open FSharp.Compiler.Tokenization
// Create an interactive checker instance
let checker = FSharpChecker.Create()
As previously, we use GetProjectOptionsFromScriptRoot
to get a context
where the specified input is the only file passed to the compiler (and it is treated as a
script file or stand-alone F# source code).
// Sample input as a multi-line string
let input =
"""
open System
let foo() =
let msg = String.Concat("Hello"," ","world")
if true then
printfn "%s" msg.
"""
// Split the input & define file name
let inputLines = input.Split('\n')
let file = "/home/user/Test.fsx"
let projOptions, _diagnostics =
checker.GetProjectOptionsFromScript(file, SourceText.ofString input, assumeDotNetFramework=false)
|> Async.RunSynchronously
let parsingOptions, _diagnostics2 =
checker.GetParsingOptionsFromProjectOptions(projOptions)
To perform type checking, we first need to parse the input using
ParseFile
, which gives us access to the untyped AST. However,
then we need to call CheckFileInProject
to perform the full type checking. This function
also requires the result of ParseFileInProject
, so the two functions are often called
together.
// Perform parsing
let parseFileResults =
checker.ParseFile(file, SourceText.ofString input, parsingOptions)
|> Async.RunSynchronously
Before we look at the interesting operations provided by TypeCheckResults
, we
need to run the type checker on a sample input. On F# code with errors, you would get some type checking
result (but it may contain incorrectly "guessed" results).
// Perform type checking
let checkFileAnswer =
checker.CheckFileInProject(parseFileResults, file, 0, SourceText.ofString input, projOptions)
|> Async.RunSynchronously
Alternatively you can use ParseAndCheckFileInProject
to check both in one step:
let parseResults2, checkFileAnswer2 =
checker.ParseAndCheckFileInProject(file, 0, SourceText.ofString input, projOptions)
|> Async.RunSynchronously
The function returns both the untyped parse result (which we do not use in this
tutorial), but also a CheckFileAnswer
value, which gives us access to all
the interesting functionality...
let checkFileResults =
match checkFileAnswer with
| 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 in msg.
so that we can get the
completion list on the msg
value (we expect to see various methods on the string
type there).
Let's now look at some of the API that is exposed by the TypeCheckResults
type. In general,
this is the type that lets you implement most of the interesting F# source code editor services.
To get a tool tip, you can use the GetToolTip
method. The method takes a line number and character
offset. Both of the numbers are zero-based. In the sample code, we want to get a tooltip for the foo
function that is defined on line 3 (line 0 is blank) and the letter f
starts at index 7 (the tooltip
would work anywhere inside the identifier).
In addition, the method takes a tag of token which is typically IDENT
, when getting a tooltip for an
identifier (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 argument
let identToken = FSharpTokenTag.Identifier
// Get tool tip at the specified location
let tip =
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 a Names
value, which is a list of strings representing
the current long name. For example, to get a tooltip for the Random
identifier in a long name
System.Random
, you would use a location somewhere in the string Random
and you would pass
["System"; "Random"]
as the Names
value.
The returned value is of type ToolTipText
which contains a discriminated union ToolTipElement
.
The union represents different kinds of tool tips that you can get from the compiler.
The next method exposed by TypeCheckResults
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 visible
in the scope) or immediately after .
to get a list of members of some object. Here, we get a
list of members of the string value msg
.
To do this, we call GetDeclarationListInfo
with the location of the .
symbol on the last line
(ending with printfn "%s" msg.
). The offsets are one-based, so the location is 7, 23
.
We also need to specify a function that says that the text has not changed and the current identifier
where we need to perform the completion.
// Get declarations (autocomplete) for a location
let decls =
checkFileResults.GetDeclarationListInfo(
Some parseFileResults,
7,
inputLines.[6],
PartialLongName.Empty 23,
(fun () -> [])
)
// Print the names of available items
for item in decls.Items do
printfn " - %s" item.Name
NOTE: v
is an alternative name for the old GetDeclarations
. The old GetDeclarations
was
deprecated because it accepted zero-based line numbers. At some point it will be removed, and GetDeclarationListInfo
will be renamed back to GetDeclarations
.
When you run the code, you should get a list containing the usual string methods such as
Substring
, ToUpper
, ToLower
etc. The fourth argument of GetDeclarations
, here ([], "msg")
,
specifies the context for the auto-completion. Here, we want a completion on a complete name
msg
, but you could for example use (["System"; "Collections"], "Generic")
to get a completion list
for a fully qualified namespace.
The next common feature of editors is to provide information about overloads of a method. In our
sample code, we use String.Concat
which has a number of overloads. We can get the list using
GetMethods
operation. As previously, this takes the zero-indexed offset of the location that we are
interested in (here, right at the end of the String.Concat
identifier) and we also need to provide
the identifier again (so that the compiler can provide up-to-date information when the source code
changes):
// Get overloads of the String.Concat method
let methods =
checkFileResults.GetMethods(5, 27, inputLines.[4], Some [ "String"; "Concat" ])
// Print concatenated parameter lists
for mi in methods.Methods do
[ for p in mi.Parameters do
for tt in p.Display do
yield tt.Text ]
|> String.concat ", "
|> printfn "%s(%s)" methods.MethodName
The code uses the Display
property to get the annotation for each parameter. This returns information
such as arg0: obj
or params args: obj[]
or str0: string, str1: string
. We concatenate the parameters
and print a type annotation with the method name.
You may have noticed that CheckFileInProject
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 when
we call the CheckFileInProject
method, it returns an asynchronous operation.
There is also the CheckFileInProjectIfReady
method. This returns immediately if the
type checking operation can't be started immediately, e.g. if other files in the project
are not yet type-checked. In this case, a background worker might choose to do other
work in the meantime, or give up on type checking the file until the FileTypeCheckStateIsDirty
event
is raised.
The fsharpbinding project has a more advanced
example of handling the background work where all requests are sent through an F# agent.
This may be more appropriate for implementing editor support.
The CheckFileAnswer
object contains other useful methods that were not covered in this tutorial. You
can 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 involve
knowledge of the FSharpChecker operations queue and the FSharpChecker 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 the
FSharp.AutoComplete project.
Multiple items
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp.Compiler
namespace FSharp.Compiler.CodeAnalysis
namespace FSharp.Compiler.EditorServices
namespace FSharp.Compiler.Text
namespace FSharp.Compiler.Tokenization
val checker : FSharpChecker
type FSharpChecker =
member CheckFileInProject : parseResults:FSharpParseFileResults * fileName:string * fileVersion:int * sourceText:ISourceText * options:FSharpProjectOptions * ?userOpName:string -> Async<FSharpCheckFileAnswer>
member ClearCache : options:seq<FSharpProjectOptions> * ?userOpName:string -> unit
member ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients : unit -> unit
member Compile : argv:string [] * ?userOpName:string -> Async<FSharpDiagnostic [] * int> + 1 overload
member CompileToDynamicAssembly : otherFlags:string [] * execute:(TextWriter * TextWriter) option * ?userOpName:string -> Async<FSharpDiagnostic [] * int * Assembly option> + 1 overload
member FindBackgroundReferencesInFile : fileName:string * options:FSharpProjectOptions * symbol:FSharpSymbol * ?canInvalidateProject:bool * ?userOpName:string -> Async<seq<range>>
member GetBackgroundCheckResultsForFileInProject : fileName:string * options:FSharpProjectOptions * ?userOpName:string -> Async<FSharpParseFileResults * FSharpCheckFileResults>
member GetBackgroundParseResultsForFileInProject : fileName:string * options:FSharpProjectOptions * ?userOpName:string -> Async<FSharpParseFileResults>
member GetBackgroundSemanticClassificationForFile : fileName:string * options:FSharpProjectOptions * ?userOpName:string -> Async<SemanticClassificationView option>
member GetParsingOptionsFromCommandLineArgs : sourceFiles:string list * argv:string list * ?isInteractive:bool * ?isEditing:bool -> FSharpParsingOptions * FSharpDiagnostic list + 1 overload
...
<summary>
Used to parse and check F# source code.
</summary>
static member FSharpChecker.Create : ?projectCacheSize:int * ?keepAssemblyContents:bool * ?keepAllBackgroundResolutions:bool * ?legacyReferenceResolver:LegacyReferenceResolver * ?tryGetMetadataSnapshot:FSharp.Compiler.AbstractIL.ILBinaryReader.ILReaderTryGetMetadataSnapshot * ?suggestNamesForErrors:bool * ?keepAllBackgroundSymbolUses:bool * ?enableBackgroundItemKeyStoreAndSemanticClassification:bool * ?enablePartialTypeChecking:bool -> FSharpChecker
val input : string
val inputLines : string []
System.String.Split([<System.ParamArray>] separator: char []) : string []
System.String.Split(separator: string [], options: System.StringSplitOptions) : string []
System.String.Split(separator: string,?options: System.StringSplitOptions) : string []
System.String.Split(separator: char [], options: System.StringSplitOptions) : string []
System.String.Split(separator: char [], count: int) : string []
System.String.Split(separator: char,?options: System.StringSplitOptions) : string []
System.String.Split(separator: string [], count: int, options: System.StringSplitOptions) : string []
System.String.Split(separator: string, count: int,?options: System.StringSplitOptions) : string []
System.String.Split(separator: char [], count: int, options: System.StringSplitOptions) : string []
System.String.Split(separator: char, count: int,?options: System.StringSplitOptions) : string []
val file : string
val projOptions : FSharpProjectOptions
val _diagnostics : FSharp.Compiler.Diagnostics.FSharpDiagnostic list
member FSharpChecker.GetProjectOptionsFromScript : fileName:string * source:ISourceText * ?previewEnabled:bool * ?loadedTimeStamp:System.DateTime * ?otherFlags:string [] * ?useFsiAuxLib:bool * ?useSdkRefs:bool * ?assumeDotNetFramework:bool * ?sdkDirOverride:string * ?optionsStamp:int64 * ?userOpName:string -> Async<FSharpProjectOptions * FSharp.Compiler.Diagnostics.FSharpDiagnostic list>
module SourceText
from FSharp.Compiler.Text
<summary>
Functions related to ISourceText objects
</summary>
val ofString : string -> ISourceText
<summary>
Creates an ISourceText object from the given string
</summary>
Multiple items
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<'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:seq<Async<'T option>> -> Async<'T option>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T> + 3 overloads
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
...
<summary>Holds static members for creating and manipulating asynchronous computations.</summary>
<remarks>
See also <a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/asynchronous-workflows">F# Language Guide - Async Workflows</a>.
</remarks>
<category index="1">Async Programming</category>
--------------------
type Async<'T> =
<summary>
An asynchronous computation, which, when run, will eventually produce a value of type T, or else raises an exception.
</summary>
<remarks>
This type has no members. Asynchronous computations are normally specified either by using an async expression
or the static methods in the <see cref="T:Microsoft.FSharp.Control.Async" /> type.
See also <a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/asynchronous-workflows">F# Language Guide - Async Workflows</a>.
</remarks>
<namespacedoc><summary>
Library functionality for asynchronous programming, events and agents. See also
<a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/asynchronous-workflows">Asynchronous Programming</a>,
<a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/members/events">Events</a> and
<a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/lazy-expressions">Lazy Expressions</a> in the
F# Language Guide.
</summary></namespacedoc>
<category index="1">Async Programming</category>
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:System.Threading.CancellationToken -> 'T
val parsingOptions : FSharpParsingOptions
val _diagnostics2 : FSharp.Compiler.Diagnostics.FSharpDiagnostic list
member FSharpChecker.GetParsingOptionsFromProjectOptions : options:FSharpProjectOptions -> FSharpParsingOptions * FSharp.Compiler.Diagnostics.FSharpDiagnostic list
val parseFileResults : FSharpParseFileResults
member FSharpChecker.ParseFile : fileName:string * sourceText:ISourceText * options:FSharpParsingOptions * ?cache:bool * ?userOpName:string -> Async<FSharpParseFileResults>
val checkFileAnswer : FSharpCheckFileAnswer
member FSharpChecker.CheckFileInProject : parseResults:FSharpParseFileResults * fileName:string * fileVersion:int * sourceText:ISourceText * options:FSharpProjectOptions * ?userOpName:string -> Async<FSharpCheckFileAnswer>
val parseResults2 : FSharpParseFileResults
val checkFileAnswer2 : FSharpCheckFileAnswer
member FSharpChecker.ParseAndCheckFileInProject : fileName:string * fileVersion:int * sourceText:ISourceText * options:FSharpProjectOptions * ?userOpName:string -> Async<FSharpParseFileResults * FSharpCheckFileAnswer>
val checkFileResults : FSharpCheckFileResults
type FSharpCheckFileAnswer =
| Aborted
| Succeeded of FSharpCheckFileResults
<summary>
The result of calling TypeCheckResult including the possibility of abort and background compiler not caught up.
</summary>
union case FSharpCheckFileAnswer.Succeeded: FSharpCheckFileResults -> FSharpCheckFileAnswer
<summary>
Success
</summary>
val res : FSharpCheckFileResults
val res : FSharpCheckFileAnswer
val failwithf : format:Printf.StringFormat<'T,'Result> -> 'T
<summary>Print to a string buffer and raise an exception with the given
result. Helper printers must return strings.</summary>
<param name="format">The formatter.</param>
<returns>The formatted result.</returns>
val identToken : int
module FSharpTokenTag
from FSharp.Compiler.Tokenization
<summary>
Some of the values in the field FSharpTokenInfo.Tag
</summary>
val Identifier : int
<summary>
Indicates the token is an identifier
</summary>
val tip : ToolTipText
member FSharpCheckFileResults.GetToolTip : line:int * colAtEndOfNames:int * lineText:string * names:string list * tokenTag:int -> ToolTipText
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
<summary>Print to <c>stdout</c> using the given format, and add a newline.</summary>
<param name="format">The formatter.</param>
<returns>The formatted result.</returns>
val decls : DeclarationListInfo
member FSharpCheckFileResults.GetDeclarationListInfo : parsedFileResults:FSharpParseFileResults option * line:int * lineText:string * partialName:PartialLongName * ?getAllEntities:(unit -> AssemblySymbol list) * ?completionContextAtPos:(pos * CompletionContext option) -> DeclarationListInfo
union case Option.Some: Value: 'T -> Option<'T>
<summary>The representation of "Value of type 'T"</summary>
<param name="Value">The input value.</param>
<returns>An option representing the value.</returns>
type PartialLongName =
{ QualifyingIdents: string list
PartialIdent: string
EndColumn: int
LastDotPos: int option }
static member Empty : endColumn:int -> PartialLongName
<summary>
Qualified long name.
</summary>
static member PartialLongName.Empty : endColumn:int -> PartialLongName
val item : DeclarationListItem
property DeclarationListInfo.Items: DeclarationListItem [] with get
val methods : MethodGroup
member FSharpCheckFileResults.GetMethods : line:int * colAtEndOfNames:int * lineText:string * names:string list option -> MethodGroup
val mi : MethodGroupItem
property MethodGroup.Methods: MethodGroupItem [] with get
<summary>
The methods (or other items) in the group
</summary>
val p : MethodGroupItemParameter
property MethodGroupItem.Parameters: MethodGroupItemParameter [] with get
<summary>
The parameters of the method in the overload set
</summary>
val tt : TaggedText
property MethodGroupItemParameter.Display: TaggedText [] with get
<summary>
The representation for the parameter including its name, its type and visual indicators of other
information such as whether it is optional.
</summary>
property TaggedText.Text: string with get
<summary>
Gets the text
</summary>
module String
from Microsoft.FSharp.Core
<summary>Functional programming operators for string processing. Further string operations
are available via the member functions on strings and other functionality in
<a href="http://msdn2.microsoft.com/en-us/library/system.string.aspx">System.String</a>
and <a href="http://msdn2.microsoft.com/library/system.text.regularexpressions.aspx">System.Text.RegularExpressions</a> types.
</summary>
<category>Strings and Text</category>
val concat : sep:string -> strings:seq<string> -> string
<summary>Returns a new string made by concatenating the given strings
with separator <c>sep</c>, that is <c>a1 + sep + ... + sep + aN</c>.</summary>
<param name="sep">The separator string to be inserted between the strings
of the input sequence.</param>
<param name="strings">The sequence of strings to be concatenated.</param>
<returns>A new string consisting of the concatenated strings separated by
the separation string.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when <c>strings</c> is null.</exception>
property MethodGroup.MethodName: string with get
<summary>
The shared name of the methods (or other items) in the group
</summary>