Skip to content

Commit 6c2221f

Browse files
brettfobaronfel
authored andcommitted
enable symbol completion for scripting (#7893)
1 parent 917eb4f commit 6c2221f

File tree

6 files changed

+153
-58
lines changed

6 files changed

+153
-58
lines changed

fcs/.paket/Paket.Restore.targets

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,82 +20,75 @@
2020
<PaketBootstrapperStyle Condition="Exists('$(PaketToolsPath)paket.bootstrapper.proj')">proj</PaketBootstrapperStyle>
2121
<PaketExeImage>assembly</PaketExeImage>
2222
<PaketExeImage Condition=" '$(PaketBootstrapperStyle)' == 'proj' ">native</PaketExeImage>
23-
<MonoPath Condition="'$(MonoPath)' == '' AND Exists('/Library/Frameworks/Mono.framework/Commands/mono')">/Library/Frameworks/Mono.framework/Commands/mono</MonoPath>
23+
<MonoPath Condition="'$(MonoPath)' == '' And Exists('/Library/Frameworks/Mono.framework/Commands/mono')">/Library/Frameworks/Mono.framework/Commands/mono</MonoPath>
2424
<MonoPath Condition="'$(MonoPath)' == ''">mono</MonoPath>
2525

2626
<!-- PaketBootStrapper -->
2727
<PaketBootStrapperExePath Condition=" '$(PaketBootStrapperExePath)' == '' AND Exists('$(PaketRootPath)paket.bootstrapper.exe')">$(PaketRootPath)paket.bootstrapper.exe</PaketBootStrapperExePath>
2828
<PaketBootStrapperExePath Condition=" '$(PaketBootStrapperExePath)' == '' ">$(PaketToolsPath)paket.bootstrapper.exe</PaketBootStrapperExePath>
2929
<PaketBootStrapperExeDir Condition=" Exists('$(PaketBootStrapperExePath)') " >$([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\</PaketBootStrapperExeDir>
3030

31-
<PaketBootStrapperCommand Condition=" '$(OS)' == 'Windows_NT' ">"$(PaketBootStrapperExePath)"</PaketBootStrapperCommand>
31+
<PaketBootStrapperCommand Condition=" '$(OS)' == 'Windows_NT'">"$(PaketBootStrapperExePath)"</PaketBootStrapperCommand>
3232
<PaketBootStrapperCommand Condition=" '$(OS)' != 'Windows_NT' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)"</PaketBootStrapperCommand>
3333

34+
<!-- Disable automagic references for F# dotnet sdk -->
35+
<!-- This will not do anything for other project types -->
36+
<!-- see https://github.com/fsharp/fslang-design/blob/master/tooling/FST-1002-fsharp-in-dotnet-sdk.md -->
37+
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
38+
<DisableImplicitSystemValueTupleReference>true</DisableImplicitSystemValueTupleReference>
39+
3440
<!-- Disable Paket restore under NCrunch build -->
3541
<PaketRestoreDisabled Condition="'$(NCrunch)' == '1'">True</PaketRestoreDisabled>
3642

37-
<!-- Disable test for CLI tool completely - overrideable via properties in projects or via environment variables -->
38-
<PaketDisableCliTest Condition=" '$(PaketDisableCliTest)' == '' ">False</PaketDisableCliTest>
39-
4043
<PaketIntermediateOutputPath Condition=" '$(PaketIntermediateOutputPath)' == '' ">$(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/'))</PaketIntermediateOutputPath>
4144
</PropertyGroup>
4245

43-
<!-- Resolve how paket should be called -->
44-
<!-- Current priority is: local (1: repo root, 2: .paket folder) => 3: as CLI tool => as bootstrapper (4: proj Bootstrapper style, 5: BootstrapperExeDir) => 6: global path variable -->
46+
<!-- Check if paket is available as local dotnet cli tool -->
4547
<Target Name="SetPaketCommand" >
46-
<!-- Test if paket is available in the standard locations. If so, that takes priority. Case 1/2 - non-windows specific -->
47-
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
48-
<!-- no windows, try native paket as default, root => tool -->
49-
<PaketExePath Condition=" '$(PaketExePath)' == '' AND Exists('$(PaketRootPath)paket') ">$(PaketRootPath)paket</PaketExePath>
50-
<PaketExePath Condition=" '$(PaketExePath)' == '' AND Exists('$(PaketToolsPath)paket') ">$(PaketToolsPath)paket</PaketExePath>
51-
</PropertyGroup>
48+
49+
<!-- Call 'dotnet paket' and see if it returns without an error. Mute all the output. -->
50+
<Exec Command="dotnet paket --version" IgnoreExitCode="true" StandardOutputImportance="low" StandardErrorImportance="low" >
51+
<Output TaskParameter="ExitCode" PropertyName="LocalPaketToolExitCode" />
52+
</Exec>
5253

53-
<!-- Test if paket is available in the standard locations. If so, that takes priority. Case 2/2 - same across platforms -->
54-
<PropertyGroup>
55-
<!-- root => tool -->
56-
<PaketExePath Condition=" '$(PaketExePath)' == '' AND Exists('$(PaketRootPath)paket.exe') ">$(PaketRootPath)paket.exe</PaketExePath>
57-
<PaketExePath Condition=" '$(PaketExePath)' == '' AND Exists('$(PaketToolsPath)paket.exe') ">$(PaketToolsPath)paket.exe</PaketExePath>
54+
<!-- If local paket tool is found, use that -->
55+
<PropertyGroup Condition=" '$(LocalPaketToolExitCode)' == '0' ">
56+
<InternalPaketCommand>dotnet paket</InternalPaketCommand>
5857
</PropertyGroup>
5958

60-
<!-- If paket hasn't be found in standard locations, test for CLI tool usage. -->
61-
<!-- First test: Is CLI configured to be used in "dotnet-tools.json"? - can result in a false negative; only a positive outcome is reliable. -->
62-
<PropertyGroup Condition=" '$(PaketExePath)' == '' ">
63-
<_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json"))</_DotnetToolsJson>
64-
<_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"'))</_ConfigContainsPaket>
65-
<_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false</_ConfigContainsPaket>
66-
</PropertyGroup>
59+
<!-- If not, then we go through our normal steps of setting the Paket command. -->
60+
<PropertyGroup Condition=" '$(LocalPaketToolExitCode)' != '0' ">
61+
<!-- windows, root => tool => proj style => bootstrapper => global -->
62+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' == 'Windows_NT' AND Exists('$(PaketRootPath)paket.exe') ">$(PaketRootPath)paket.exe</PaketExePath>
63+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' == 'Windows_NT' AND Exists('$(PaketToolsPath)paket.exe') ">$(PaketToolsPath)paket.exe</PaketExePath>
64+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' == 'Windows_NT' AND '$(PaketBootstrapperStyle)' == 'proj' ">$(PaketToolsPath)paket.exe</PaketExePath>
65+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' == 'Windows_NT' AND Exists('$(PaketBootStrapperExeDir)') ">$(_PaketBootStrapperExeDir)paket.exe</PaketExePath>
66+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' == 'Windows_NT' ">paket.exe</PaketExePath>
6767

68-
<!-- Second test: Call 'dotnet paket' and see if it returns without an error. Mute all the output. Only run if previous test failed and the test has not been disabled. -->
69-
<!-- WARNING: This method can lead to processes hanging forever, and should be used as little as possible. See https://github.com/fsprojects/Paket/issues/3705 for details. -->
70-
<Exec Condition=" '$(PaketExePath)' == '' AND !$(PaketDisableCliTest) AND !$(_ConfigContainsPaket)" Command="dotnet paket --version" IgnoreExitCode="true" StandardOutputImportance="low" StandardErrorImportance="low" >
71-
<Output TaskParameter="ExitCode" PropertyName="LocalPaketToolExitCode" />
72-
</Exec>
68+
<!-- no windows, try native paket as default, root => tool => proj style => mono paket => bootstrpper => global -->
69+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' != 'Windows_NT' AND Exists('$(PaketRootPath)paket') ">$(PaketRootPath)paket</PaketExePath>
70+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' != 'Windows_NT' AND Exists('$(PaketToolsPath)paket') ">$(PaketToolsPath)paket</PaketExePath>
71+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' != 'Windows_NT' AND '$(PaketBootstrapperStyle)' == 'proj' ">$(PaketToolsPath)paket</PaketExePath>
7372

74-
<!-- If paket is installed as CLI use that. Again, only if paket haven't already been found in standard locations. -->
75-
<PropertyGroup Condition=" '$(PaketExePath)' == '' AND ($(_ConfigContainsPaket) OR '$(LocalPaketToolExitCode)' == '0') ">
76-
<_PaketCommand>dotnet paket</_PaketCommand>
77-
</PropertyGroup>
73+
<!-- no windows, try mono paket -->
74+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' != 'Windows_NT' AND Exists('$(PaketRootPath)paket.exe') ">$(PaketRootPath)paket.exe</PaketExePath>
75+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' != 'Windows_NT' AND Exists('$(PaketToolsPath)paket.exe') ">$(PaketToolsPath)paket.exe</PaketExePath>
7876

79-
<!-- If neither local files nor CLI tool can be found, final attempt is searching for boostrapper config before falling back to global path variable. -->
80-
<PropertyGroup Condition=" '$(PaketExePath)' == '' AND '$(_PaketCommand)' == '' ">
81-
<!-- Test for bootstrapper setup -->
82-
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(PaketBootstrapperStyle)' == 'proj' ">$(PaketToolsPath)paket</PaketExePath>
83-
<PaketExePath Condition=" '$(PaketExePath)' == '' AND Exists('$(PaketBootStrapperExeDir)') ">$(PaketBootStrapperExeDir)paket</PaketExePath>
77+
<!-- no windows, try bootstrapper -->
78+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' != 'Windows_NT' AND Exists('$(PaketBootStrapperExeDir)') ">$(PaketBootStrapperExeDir)paket.exe</PaketExePath>
8479

85-
<!-- If all else fails, use global path approach. -->
86-
<PaketExePath Condition=" '$(PaketExePath)' == ''">paket</PaketExePath>
87-
</PropertyGroup>
80+
<!-- no windows, try global native paket -->
81+
<PaketExePath Condition=" '$(PaketExePath)' == '' AND '$(OS)' != 'Windows_NT' ">paket</PaketExePath>
8882

89-
<!-- If not using CLI, setup correct execution command. -->
90-
<PropertyGroup Condition=" '$(_PaketCommand)' == '' ">
9183
<_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)"))</_PaketExeExtension>
92-
<_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)"</_PaketCommand>
93-
<_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)"</_PaketCommand>
94-
<_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)"</_PaketCommand>
84+
<InternalPaketCommand Condition=" '$(InternalPaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)"</InternalPaketCommand>
85+
<InternalPaketCommand Condition=" '$(InternalPaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)"</InternalPaketCommand>
86+
<InternalPaketCommand Condition=" '$(InternalPaketCommand)' == '' ">"$(PaketExePath)"</InternalPaketCommand>
87+
9588
</PropertyGroup>
9689

9790
<!-- The way to get a property to be available outside the target is to use this task. -->
98-
<CreateProperty Value="$(_PaketCommand)">
91+
<CreateProperty Value="$(InternalPaketCommand)">
9992
<Output TaskParameter="Value" PropertyName="PaketCommand"/>
10093
</CreateProperty>
10194

src/fsharp/FSharp.Compiler.Private.Scripting/FSharpScript.fs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
namespace FSharp.Compiler.Scripting
44

55
open System
6+
=======
7+
open System.Threading
8+
open FSharp.Compiler
69
open FSharp.Compiler.Interactive.Shell
710

811
type FSharpScript(?captureInput: bool, ?captureOutput: bool, ?additionalArgs: string[]) as this =
@@ -48,6 +51,23 @@ type FSharpScript(?captureInput: bool, ?captureOutput: bool, ?additionalArgs: st
4851
| Choice1Of2 v -> Ok(v), errors
4952
| Choice2Of2 ex -> Error(ex), errors
5053

54+
/// Get the available completion symbols from the code at the specified location.
55+
///
56+
/// <param name="text">The input text on which completions will be calculated</param>
57+
/// <param name="line">The 1-based line index</param>
58+
/// <param name="column">The 0-based column index</param>
59+
member __.GetCompletionSymbols(text: string, line: int, column: int) =
60+
async {
61+
let! parseResults, checkResults, _projectResults = fsi.ParseAndCheckInteraction(text)
62+
let lineText = text.Split('\n').[line - 1]
63+
let partialName = QuickParse.GetPartialLongNameEx(lineText, column - 1)
64+
let! symbolUses = checkResults.GetDeclarationListSymbols(Some parseResults, line, lineText, partialName)
65+
let symbols = symbolUses
66+
|> List.concat
67+
|> List.map (fun s -> s.Symbol)
68+
return symbols
69+
}
70+
5171
interface IDisposable with
5272
member __.Dispose() =
5373
if captureInput then
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
namespace FSharp.Compiler.Scripting.UnitTests
4+
5+
open System
6+
open System.Threading.Tasks
7+
open FSharp.Compiler.Scripting
8+
open NUnit.Framework
9+
10+
[<TestFixture>]
11+
type CompletionTests() =
12+
13+
[<Test>]
14+
member _.``Instance completions in the same submission``() =
15+
async {
16+
use script = new FSharpScript()
17+
let lines = [ "let x = 1"
18+
"x." ]
19+
let! completions = script.GetCompletionSymbols(String.Join("\n", lines), 2, 2)
20+
let matchingCompletions = completions |> List.filter (fun s -> s.DisplayName = "CompareTo")
21+
Assert.AreEqual(1, List.length matchingCompletions)
22+
} |> Async.StartAsTask :> Task
23+
24+
[<Test>]
25+
member _.``Instance completions from a previous submission``() =
26+
async {
27+
use script = new FSharpScript()
28+
script.Eval("let x = 1") |> ignoreValue
29+
let! completions = script.GetCompletionSymbols("x.", 1, 2)
30+
let matchingCompletions = completions |> List.filter (fun s -> s.DisplayName = "CompareTo")
31+
Assert.AreEqual(1, List.length matchingCompletions)
32+
} |> Async.StartAsTask :> Task
33+
34+
[<Test>]
35+
member _.``Static member completions``() =
36+
async {
37+
use script = new FSharpScript()
38+
let! completions = script.GetCompletionSymbols("System.String.", 1, 14)
39+
let matchingCompletions = completions |> List.filter (fun s -> s.DisplayName = "Join")
40+
Assert.GreaterOrEqual(List.length matchingCompletions, 1)
41+
} |> Async.StartAsTask :> Task
42+
43+
[<Test>]
44+
member _.``Type completions from namespace``() =
45+
async {
46+
use script = new FSharpScript()
47+
let! completions = script.GetCompletionSymbols("System.", 1, 7)
48+
let matchingCompletions = completions |> List.filter (fun s -> s.DisplayName = "String")
49+
Assert.GreaterOrEqual(List.length matchingCompletions, 1)
50+
} |> Async.StartAsTask :> Task
51+
52+
[<Test>]
53+
member _.``Namespace completions``() =
54+
async {
55+
use script = new FSharpScript()
56+
let! completions = script.GetCompletionSymbols("System.", 1, 7)
57+
let matchingCompletions = completions |> List.filter (fun s -> s.DisplayName = "Collections")
58+
Assert.AreEqual(1, List.length matchingCompletions)
59+
} |> Async.StartAsTask :> Task
60+
61+
[<Test>]
62+
member _.``Extension method completions``() =
63+
async {
64+
use script = new FSharpScript()
65+
let lines = [ "open System.Linq"
66+
"let list = new System.Collections.Generic.List<int>()"
67+
"list." ]
68+
let! completions = script.GetCompletionSymbols(String.Join("\n", lines), 3, 5)
69+
let matchingCompletions = completions |> List.filter (fun s -> s.DisplayName = "Select")
70+
Assert.AreEqual(1, List.length matchingCompletions)
71+
} |> Async.StartAsTask :> Task

tests/FSharp.Compiler.Private.Scripting.UnitTests/FSharp.Compiler.Private.Scripting.UnitTests.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
</PropertyGroup>
1212

1313
<ItemGroup>
14+
<Compile Include="TestHelpers.fs" />
1415
<Compile Include="FSharpScriptTests.fs" />
16+
<Compile Include="CompletionTests.fs" />
1517
</ItemGroup>
1618

1719
<ItemGroup>

tests/FSharp.Compiler.Private.Scripting.UnitTests/FSharpScriptTests.fs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,11 @@ open System.IO
77
open System.Threading
88
open FSharp.Compiler.Interactive.Shell
99
open FSharp.Compiler.Scripting
10-
open FSharp.Compiler.SourceCodeServices
1110
open NUnit.Framework
1211

1312
[<TestFixture>]
1413
type InteractiveTests() =
1514

16-
let getValue ((value: Result<FsiValue option, exn>), (errors: FSharpErrorInfo[])) =
17-
if errors.Length > 0 then
18-
failwith <| sprintf "Evaluation returned %d errors:\r\n\t%s" errors.Length (String.Join("\r\n\t", errors))
19-
match value with
20-
| Ok(value) -> value
21-
| Error ex -> raise ex
22-
23-
let ignoreValue = getValue >> ignore
24-
2515
[<Test>]
2616
member __.``Eval object value``() =
2717
use script = new FSharpScript()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
namespace FSharp.Compiler.Scripting.UnitTests
4+
5+
open System
6+
open FSharp.Compiler.Interactive.Shell
7+
open FSharp.Compiler.SourceCodeServices
8+
9+
[<AutoOpen>]
10+
module TestHelpers =
11+
12+
let getValue ((value: Result<FsiValue option, exn>), (errors: FSharpErrorInfo[])) =
13+
if errors.Length > 0 then
14+
failwith <| sprintf "Evaluation returned %d errors:\r\n\t%s" errors.Length (String.Join("\r\n\t", errors))
15+
match value with
16+
| Ok(value) -> value
17+
| Error ex -> raise ex
18+
19+
let ignoreValue = getValue >> ignore

0 commit comments

Comments
 (0)