diff --git a/VisualFSharp.slnx b/VisualFSharp.slnx
index caa059a34ee..42f9b328796 100644
--- a/VisualFSharp.slnx
+++ b/VisualFSharp.slnx
@@ -108,6 +108,9 @@
+
+
+
diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md
index 2357fbcdff4..5bbba9104bd 100644
--- a/docs/release-notes/.VisualStudio/18.vNext.md
+++ b/docs/release-notes/.VisualStudio/18.vNext.md
@@ -3,6 +3,7 @@
* Suggest active-pattern cases from unopened modules and offer valid Add Open and qualification edits. ([PR #20719](https://github.com/dotnet/fsharp/pull/20719))
* Add maintainer-requested F# Apex Visual Studio integration tests on DartLab-1ES CloudTest, using PR pipeline definitions pinned to an explicitly requested commit SHA. ([PR #20625](https://github.com/dotnet/fsharp/pull/20625))
* Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
+* The F# Interactive window is rebuilt on the Interactive Window package and talks to `dotnet fsi` over its JSON-RPC server mode: the input and output are coloured, Enter submits only a complete interaction, diagnostics carry positions in the submitted file, and the session starts in the solution folder, so the SDK that `global.json` names supplies the compiler; until the .NET SDK includes the server (11.0.200), Visual Studio installs an F# Interactive of its own for the window to fall back to. ([PR #20565](https://github.com/dotnet/fsharp/pull/20565))
* Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188))
* Added a **Tools > Options > F# Tools > Compiler** option, **"Use the .NET SDK F# compiler for builds"**, on by default. Visual Studio uses the .NET SDK F# compiler when the project supplies SDK paths, matching `dotnet build`. Otherwise, it uses the bundled .NET Framework compiler. Turn the option off to use the bundled compiler for all projects. Changes are saved on Apply or OK; Cancel discards unsaved changes. Command-line builds are unaffected. ([Issue #20484](https://github.com/dotnet/fsharp/issues/20484), [PR #20485](https://github.com/dotnet/fsharp/pull/20485))
* F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ([PR #20409](https://github.com/dotnet/fsharp/pull/20409))
@@ -22,6 +23,7 @@
* Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421))
* Fix doubled F# diagnostics in tooltips. ([Issue #16360](https://github.com/dotnet/fsharp/issues/16360))
* Fix `NotSupportedException` in the memory-mapped-file optimization when copying `ReadOnlyMemory` into `MemoryMappedFileViewStream`. ([Issue #20263](https://github.com/dotnet/fsharp/issues/20263))
+* Handle unavailable document and classification information in the F# Interactive window with the updated Visual Studio SDK. ([PR #20565](https://github.com/dotnet/fsharp/pull/20565))
* Reduce allocations in the VS project options reactor: the command-line options and project options caches and the mailbox reply payloads now hold struct tuples, and `IProjectSite.CompilationBinOutputPath` returns `string voption` picked with a new `Array.tryPickV`. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413))
* Build a single-file project's `OtherOptions` reference flags with one array comprehension instead of two `Array.ofSeq` calls and an `Array.append`. ([PR #20499](https://github.com/dotnet/fsharp/pull/20499))
* Keep concurrent completion requests from mixing their suggestion lists. ([PR #20719](https://github.com/dotnet/fsharp/pull/20719))
diff --git a/eng/Packages.props b/eng/Packages.props
index 40d6eacd576..9df1b009c2c 100644
--- a/eng/Packages.props
+++ b/eng/Packages.props
@@ -76,6 +76,7 @@
+
+
+
diff --git a/eng/Versions.props b/eng/Versions.props
index 1a2c3909716..61624df7119 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -101,7 +101,7 @@
via Microsoft.CodeAnalysis.EditorFeatures / .LanguageServices.ExternalAccess 5.11.0). Full NU1109
rationale lives there, next to the 18.9.x pins. -->
18.12.46
- 0.1.800-beta
+ 5.6.0-2.26151.2
diff --git a/setup/Swix/Microsoft.FSharp.Compiler.MSBuild/Microsoft.FSharp.Compiler.MSBuild.csproj b/setup/Swix/Microsoft.FSharp.Compiler.MSBuild/Microsoft.FSharp.Compiler.MSBuild.csproj
index a6cf0324ca9..a712da4bbda 100644
--- a/setup/Swix/Microsoft.FSharp.Compiler.MSBuild/Microsoft.FSharp.Compiler.MSBuild.csproj
+++ b/setup/Swix/Microsoft.FSharp.Compiler.MSBuild/Microsoft.FSharp.Compiler.MSBuild.csproj
@@ -9,6 +9,11 @@
+
@@ -40,8 +45,41 @@
+
+ <_InteractiveBinaries>$(BinariesFolder)fsi\$(Configuration)\$(FSharpNetCoreProductTargetFramework)\
+ <_InteractiveInstallDir>InstallDir:Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools\Interactive
+ <_SwrNewLine>
+
+
+
+
+
+ <_InteractiveFile Include="$(_InteractiveBinaries)*.dll;$(_InteractiveBinaries)fsi.runtimeconfig.json" />
+
+
+ <_InteractiveBlock>folder "$(_InteractiveInstallDir)"$(_SwrNewLine)@(_InteractiveFile->' file source="%(FullPath)"', '$(_SwrNewLine)')$(_SwrNewLine)
+
+
+ <_BuiltSwrLines Include="$(_InteractiveBlock)" />
+
+
+
+
+ <_Language>%(_XlfLanguages.Identity)
+
+
+ <_InteractiveSatellite Remove="@(_InteractiveSatellite)" />
+ <_InteractiveSatellite Include="$(_InteractiveBinaries)$(_Language)\*.resources.dll" />
+
+
+ <_InteractiveSatelliteBlock>folder "$(_InteractiveInstallDir)\$(_Language)"$(_SwrNewLine)@(_InteractiveSatellite->' file source="%(FullPath)"', '$(_SwrNewLine)')$(_SwrNewLine)
+
+
+ <_BuiltSwrLines Include="$(_InteractiveSatelliteBlock)" Condition="'@(_InteractiveSatellite)' != ''" />
+
<_Line>
-
+
<_Lines>
diff --git a/src/fsi/interactiveProtocol.fs b/src/fsi/interactiveProtocol.fs
index ebc4aa050c9..2058df38e2c 100644
--- a/src/fsi/interactiveProtocol.fs
+++ b/src/fsi/interactiveProtocol.fs
@@ -38,6 +38,10 @@ module Methods =
[]
let Shutdown = "fsi/shutdown"
+module CommandLine =
+ []
+ let ServerOption = "--fsi-server-jsonrpc:"
+
[]
type InitializeResult =
{
@@ -67,7 +71,7 @@ type ExecuteRequest =
/// with startLine this makes diagnostics point at the user's own source rather than
/// at a position within the submission.
///
- sourcePath: string
+ sourcePath: string | null
startLine: System.Nullable
}
@@ -78,7 +82,7 @@ type ExecuteFileRequest = { path: string }
[]
type SetPathsRequest =
{
- includePaths: string[]
+ includePaths: string[] | null
workingDirectory: string
}
@@ -123,8 +127,8 @@ type ExecutionResult =
success: bool
cancelled: bool
- diagnostics: DiagnosticInfo[]
- ``exception``: ExceptionInfo
+ diagnostics: DiagnosticInfo[] | null
+ ``exception``: ExceptionInfo | null
values: ValueInfo[]
/// Reported after every interaction so that the host can keep its own view of the session
diff --git a/tests/FSharp.Compiler.Interactive.Server.Tests/FSharp.Compiler.Interactive.Server.Tests.fsproj b/tests/FSharp.Compiler.Interactive.Server.Tests/FSharp.Compiler.Interactive.Server.Tests.fsproj
index 6c96a8f7e7d..641be95627b 100644
--- a/tests/FSharp.Compiler.Interactive.Server.Tests/FSharp.Compiler.Interactive.Server.Tests.fsproj
+++ b/tests/FSharp.Compiler.Interactive.Server.Tests/FSharp.Compiler.Interactive.Server.Tests.fsproj
@@ -24,6 +24,20 @@
interactiveProtocol.fs
+
+ LexicalScanning.fs
+
+
+ InteractiveLexicalScanner.fs
+
+
+ SubmissionAnalysis.fs
+
+
+ SubmissionOrigins.fs
+
+
+
diff --git a/tests/FSharp.Compiler.Interactive.Server.Tests/FsiJsonRpcServerTests.fs b/tests/FSharp.Compiler.Interactive.Server.Tests/FsiJsonRpcServerTests.fs
index 47476113f6f..8eca9f1eb6c 100644
--- a/tests/FSharp.Compiler.Interactive.Server.Tests/FsiJsonRpcServerTests.fs
+++ b/tests/FSharp.Compiler.Interactive.Server.Tests/FsiJsonRpcServerTests.fs
@@ -667,6 +667,22 @@ let ``the session exits when its host process exits`` () =
Assert.True(session.WaitForExit 30_000, "the session outlived its host process")
+[]
+let ``the last owner named on the command line is the one that counts`` () =
+ use earlier = new FsiServerHarness()
+
+ use session =
+ new FsiServerHarness(
+ serverSwitches = fun pipeName -> [ $"--fsi-server-client-pid:{earlier.ProcessId}"; $"--fsi-server-jsonrpc:{pipeName}" ]
+ )
+
+ session.Initialize() |> ignore
+
+ (earlier :> IDisposable).Dispose()
+
+ Assert.False(session.WaitForExit 5_000, "the session followed the owner named first")
+ Assert.True(succeeded (session.Execute "1 + 1"))
+
//-------------------------------------------------------------------------
// What ships
//-------------------------------------------------------------------------
diff --git a/tests/FSharp.Compiler.Interactive.Server.Tests/FsiServerHarness.fs b/tests/FSharp.Compiler.Interactive.Server.Tests/FsiServerHarness.fs
index 5763454c980..1acf0809b66 100644
--- a/tests/FSharp.Compiler.Interactive.Server.Tests/FsiServerHarness.fs
+++ b/tests/FSharp.Compiler.Interactive.Server.Tests/FsiServerHarness.fs
@@ -111,7 +111,7 @@ type FsiServerHarness
locateFsi (defaultArg fsiDirectory (fsiOutputDirectory ()))
let serverSwitches =
- defaultArg serverSwitches (fun pipeName -> [ $"--fsi-server-jsonrpc:{pipeName}" ])
+ defaultArg serverSwitches (fun pipeName -> [ $"{CommandLine.ServerOption}{pipeName}" ])
let arguments =
[
@@ -229,7 +229,7 @@ type FsiServerHarness
let deadline = DateTime.UtcNow + defaultArg timeout (TimeSpan.FromSeconds 30.0)
let rec wait () =
- if this.StandardOutput.Contains text then true
+ if this.StandardOutput.Contains(text, StringComparison.Ordinal) then true
elif DateTime.UtcNow > deadline then false
else
Thread.Sleep 50
@@ -331,10 +331,10 @@ let diagnostics (result: ExecutionResult) =
| items -> items
let errors result =
- diagnostics result |> Array.filter (fun d -> d.severity = "error")
+ diagnostics result |> Array.filter (fun d -> String.Equals(d.severity, "error", StringComparison.Ordinal))
let warnings result =
- diagnostics result |> Array.filter (fun d -> d.severity = "warning")
+ diagnostics result |> Array.filter (fun d -> String.Equals(d.severity, "warning", StringComparison.Ordinal))
let succeeded (result: ExecutionResult) = result.success
diff --git a/tests/FSharp.Compiler.Interactive.Server.Tests/SubmissionAnalysisTests.fs b/tests/FSharp.Compiler.Interactive.Server.Tests/SubmissionAnalysisTests.fs
new file mode 100644
index 00000000000..076ae12a8c0
--- /dev/null
+++ b/tests/FSharp.Compiler.Interactive.Server.Tests/SubmissionAnalysisTests.fs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+module FSharp.Compiler.Interactive.Server.Tests.SubmissionAnalysisTests
+
+open Xunit
+
+open Microsoft.VisualStudio.FSharp.Editor
+open Microsoft.VisualStudio.FSharp.Interactive
+
+let private scanners = FSharpLexicalScannerFactory() :> ILexicalScannerFactory
+
+let private scenario (completes: bool) (name: string) (texts: string list) : obj[] list =
+ texts |> List.map (fun text -> [| box name; box completes; box text |])
+
+let private submits = scenario true
+let private takesAnotherLine = scenario false
+
+let scenarios: obj[] seq =
+ [
+ submits "an explicit terminator always submits, even mid-construct" [ "1 + 1;;"; "let f x =\n x + 1\n;;"; "let xs = [1; 2];;" ]
+ submits "a finished expression submits without a terminator" [ "1 + 1"; "printfn \"hi\""; "let x = 40"; "let f a b =\n a + b" ]
+ takesAnotherLine "a line that visibly continues" [ "let x ="; "fun x ->"; "1 +"; "if true then" ]
+ takesAnotherLine "an unclosed bracket" [ "printfn (\"a\""; "let xs = [1; 2"; "let xs = [| 1; 2"; "let r = {| A = 1" ]
+ submits "closed brackets do not hold the submission open" [ "printfn (\"a\")"; "let xs = [1; 2]"; "let xs = [| 1; 2 |]" ]
+ takesAnotherLine "an unterminated string or comment" [ "let s = \"abc"; "(* comment" ]
+ submits "brackets and terminators inside a string do not count" [ "let s = \"([{\""; "let s = \"a;;b\"" ]
+ submits "a string literal can end a submission: the last token is the literal, not the '=' before it" [ "let greeting = \"hello\"" ]
+ takesAnotherLine "a terminator inside a string is not a terminator" [ "let s = \";;" ]
+ submits "a comment does not change what the last token was" [ "let x = 1 // ([\n"; "(* c *) let x = 1" ]
+ takesAnotherLine "a comment does not change what the last token was" [ "let x = // nothing yet" ]
+ submits "an empty submission, which the window uses to start a session that has gone away" [ ""; " \n " ]
+ ]
+ |> Seq.concat
+
+[]
+[]
+let ``Enter submits only what is visibly finished`` (scenario: string) (completes: bool) (text: string) =
+ Assert.True((SubmissionAnalysis.isComplete scanners text = completes), $"{scenario}: <<{text}>>")
+
+[]
+[]
+[]
+[]
+let ``the terminator is added only when it is missing`` (text: string) (expected: string) =
+ Assert.Equal(expected, SubmissionAnalysis.withTerminator scanners text)
diff --git a/tests/FSharp.Compiler.Interactive.Server.Tests/SubmissionOriginsTests.fs b/tests/FSharp.Compiler.Interactive.Server.Tests/SubmissionOriginsTests.fs
new file mode 100644
index 00000000000..17d0efb3051
--- /dev/null
+++ b/tests/FSharp.Compiler.Interactive.Server.Tests/SubmissionOriginsTests.fs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+module FSharp.Compiler.Interactive.Server.Tests.SubmissionOriginsTests
+
+open Xunit
+
+open Microsoft.VisualStudio.FSharp.Interactive
+
+let private originOf (path: string, line: int) = ValueSome(struct (path, line))
+
+[]
+let ``submissions sent in a row each take their own origin`` () =
+ let origins = SubmissionOrigins()
+ origins.Add("a", "A.fsx", 10)
+ origins.Add("b", "B.fsx", 20)
+
+ Assert.Equal(originOf ("A.fsx", 10), origins.Take "a")
+ Assert.Equal(originOf ("B.fsx", 20), origins.Take "b")
+
+[]
+let ``text typed into the window takes no origin and leaves the sent ones alone`` () =
+ let origins = SubmissionOrigins()
+ origins.Add("let x =\r\n 1\r\n", "A.fsx", 3)
+
+ Assert.Equal(ValueNone, origins.Take "typed by hand")
+ Assert.Equal(originOf ("A.fsx", 3), origins.Take "let x =\n 1")
+
+[]
+let ``a submission that never ran does not keep a later one from its origin`` () =
+ let origins = SubmissionOrigins()
+ origins.Add("never ran", "A.fsx", 1)
+ origins.Add("ran", "B.fsx", 2)
+
+ Assert.Equal(originOf ("B.fsx", 2), origins.Take "ran")
+ Assert.Equal(ValueNone, origins.Take "never ran")
+
+[]
+let ``the same text sent twice takes the origins in the order it was sent`` () =
+ let origins = SubmissionOrigins()
+ origins.Add("go", "A.fsx", 1)
+ origins.Add("go", "B.fsx", 2)
+
+ Assert.Equal(originOf ("A.fsx", 1), origins.Take "go")
+ Assert.Equal(originOf ("B.fsx", 2), origins.Take "go")
diff --git a/vsintegration/Vsix/Directory.Build.props b/vsintegration/Vsix/Directory.Build.props
index d81bbf2613f..9260319bb57 100644
--- a/vsintegration/Vsix/Directory.Build.props
+++ b/vsintegration/Vsix/Directory.Build.props
@@ -8,6 +8,7 @@
$(VSRootSuffix)
$(ArtifactsDir)bin\fscAnyCpu\$(Configuration)\net472\
+ $(ArtifactsDir)bin\fsi\$(Configuration)\$(FSharpNetCoreProductTargetFramework)\fsi.dll
true
publish\
true
diff --git a/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json b/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json
index 7638dfe6832..f96bd2e9b42 100644
--- a/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json
+++ b/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json
@@ -12,7 +12,8 @@
"FSharpPreferAnyCpuTools": "true",
"Fsc_NetFramework_ToolPath": "$(FSharpCompilerPathForDebuggingLocally)",
"Fsc_NetFramework_AnyCpu_ToolExe": "fscAnyCpu.exe",
- "FSHARP_OTEL_EXPORT": ""
+ "FSHARP_OTEL_EXPORT": "",
+ "FSHARP_INTERACTIVE_PATH": "$(FSharpInteractivePathForDebuggingLocally)"
}
},
"OTEL Export": {
@@ -27,7 +28,8 @@
"FSharpPreferAnyCpuTools": "true",
"Fsc_NetFramework_ToolPath": "$(FSharpCompilerPathForDebuggingLocally)",
"Fsc_NetFramework_AnyCpu_ToolExe": "fscAnyCpu.exe",
- "FSHARP_OTEL_EXPORT": "http://127.0.0.1:4317"
+ "FSHARP_OTEL_EXPORT": "http://127.0.0.1:4317",
+ "FSHARP_INTERACTIVE_PATH": "$(FSharpInteractivePathForDebuggingLocally)"
}
}
}
diff --git a/vsintegration/Vsix/VisualFSharpFull/Source.extension.vsixmanifest b/vsintegration/Vsix/VisualFSharpFull/Source.extension.vsixmanifest
index f805558824b..528529c35a8 100644
--- a/vsintegration/Vsix/VisualFSharpFull/Source.extension.vsixmanifest
+++ b/vsintegration/Vsix/VisualFSharpFull/Source.extension.vsixmanifest
@@ -34,6 +34,7 @@
+
@@ -48,10 +49,12 @@
+
+
@@ -63,6 +66,7 @@
+
diff --git a/vsintegration/Vsix/VisualFSharpFull/VisualFSharp.Core.targets b/vsintegration/Vsix/VisualFSharpFull/VisualFSharp.Core.targets
index db4b3097d66..cce7556255f 100644
--- a/vsintegration/Vsix/VisualFSharpFull/VisualFSharp.Core.targets
+++ b/vsintegration/Vsix/VisualFSharpFull/VisualFSharp.Core.targets
@@ -127,6 +127,15 @@
True
+
+ FSharp.Interactive.Window
+ BuiltProjectOutputGroup%3bGetCopyToOutputDirectoryItems%3bPkgDefProjectOutputGroup%3bSatelliteDllsProjectOutputGroup%3b
+ DebugSymbolsProjectOutputGroup%3b
+ true
+ 2
+ True
+
+
{c4586a06-1402-48bc-8e35-a1b8642f895b}
FSharp.UIResources
diff --git a/vsintegration/Vsix/VisualFSharpFull/VisualFSharpDebug.csproj b/vsintegration/Vsix/VisualFSharpFull/VisualFSharpDebug.csproj
index 5a6671c4d24..4c9f00f8257 100644
--- a/vsintegration/Vsix/VisualFSharpFull/VisualFSharpDebug.csproj
+++ b/vsintegration/Vsix/VisualFSharpFull/VisualFSharpDebug.csproj
@@ -82,6 +82,25 @@
Tools
true
+
+
+
+ Tools/Interactive
+ true
+
+
+
+ Tools/Interactive/%(_XlfLanguages.Identity)
+ true
+
+
+
+
+
diff --git a/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs b/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs
index 0dd36a301d2..5870a4c0f29 100644
--- a/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs
+++ b/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs
@@ -38,43 +38,18 @@ type internal FsiCommandFilter(serviceProvider: System.IServiceProvider) =
interface IOleCommandTarget with
member x.Exec(pguidCmdGroup, nCmdId, nCmdexecopt, pvaIn, pvaOut) =
if
- pguidCmdGroup = VSConstants.VsStd11
- && nCmdId = uint32 VSConstants.VSStd11CmdID.ExecuteSelectionInInteractive
- then
- Hooks.OnMLSend fsiPackage.Value FsiEditorSendAction.ExecuteSelection null null
- VSConstants.S_OK
- elif
- pguidCmdGroup = VSConstants.VsStd11
- && nCmdId = uint32 VSConstants.VSStd11CmdID.ExecuteLineInInteractive
- then
- Hooks.OnMLSend fsiPackage.Value FsiEditorSendAction.ExecuteLine null null
- VSConstants.S_OK
- elif
pguidCmdGroup = Guids.guidInteractive
&& nCmdId = uint32 Guids.cmdIDDebugSelection
then
Hooks.OnMLSend fsiPackage.Value FsiEditorSendAction.DebugSelection null null
VSConstants.S_OK
- elif not (isNull nextTarget) then
- nextTarget.Exec(&pguidCmdGroup, nCmdId, nCmdexecopt, pvaIn, pvaOut)
else
- VSConstants.E_FAIL
+ match nextTarget with
+ | null -> VSConstants.E_FAIL
+ | target -> target.Exec(&pguidCmdGroup, nCmdId, nCmdexecopt, pvaIn, pvaOut)
member x.QueryStatus(pguidCmdGroup, cCmds, prgCmds, pCmdText) =
- if pguidCmdGroup = VSConstants.VsStd11 then
- for i = 0 to int cCmds - 1 do
- if prgCmds.[i].cmdID = uint32 VSConstants.VSStd11CmdID.ExecuteSelectionInInteractive then
- prgCmds.[i].cmdf <- uint32 (OLECMDF.OLECMDF_SUPPORTED ||| OLECMDF.OLECMDF_ENABLED)
- elif prgCmds.[i].cmdID = uint32 VSConstants.VSStd11CmdID.ExecuteLineInInteractive then
- prgCmds.[i].cmdf <-
- uint32 (
- OLECMDF.OLECMDF_SUPPORTED
- ||| OLECMDF.OLECMDF_ENABLED
- ||| OLECMDF.OLECMDF_DEFHIDEONCTXTMENU
- )
-
- VSConstants.S_OK
- elif pguidCmdGroup = Guids.guidInteractive then
+ if pguidCmdGroup = Guids.guidInteractive then
for i = 0 to int cCmds - 1 do
if prgCmds.[i].cmdID = uint32 Guids.cmdIDDebugSelection then
let dbgState = Hooks.GetDebuggerState fsiPackage.Value
@@ -85,10 +60,10 @@ type internal FsiCommandFilter(serviceProvider: System.IServiceProvider) =
prgCmds.[i].cmdf <- uint32 (OLECMDF.OLECMDF_SUPPORTED ||| OLECMDF.OLECMDF_ENABLED)
VSConstants.S_OK
- elif not (isNull nextTarget) then
- nextTarget.QueryStatus(&pguidCmdGroup, cCmds, prgCmds, pCmdText)
else
- VSConstants.E_FAIL
+ match nextTarget with
+ | null -> VSConstants.E_FAIL
+ | target -> target.QueryStatus(&pguidCmdGroup, cCmds, prgCmds, pCmdText)
[)>]
[]
diff --git a/vsintegration/src/FSharp.Editor/Commands/InteractiveLexicalScanner.fs b/vsintegration/src/FSharp.Editor/Commands/InteractiveLexicalScanner.fs
new file mode 100644
index 00000000000..a0100d6ec81
--- /dev/null
+++ b/vsintegration/src/FSharp.Editor/Commands/InteractiveLexicalScanner.fs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Editor
+
+open FSharp.Compiler.Tokenization
+
+open Microsoft.VisualStudio.FSharp.Interactive
+
+type internal FSharpLexicalScanner() =
+
+ static let tokenizer = FSharpSourceTokenizer([], Some "stdin.fsx", None)
+
+ static let probeIdentifier = "__fsharp_interactive_probe__"
+
+ let mutable state = FSharpTokenizerLexState.Initial
+
+ static let kindOf color =
+ match color with
+ | FSharpTokenColorKind.Keyword -> LexicalKind.Keyword
+ | FSharpTokenColorKind.Comment -> LexicalKind.Comment
+ | FSharpTokenColorKind.String -> LexicalKind.String
+ | FSharpTokenColorKind.Number -> LexicalKind.Number
+ | FSharpTokenColorKind.Operator -> LexicalKind.Operator
+ | FSharpTokenColorKind.Identifier
+ | FSharpTokenColorKind.UpperIdentifier -> LexicalKind.Identifier
+ | FSharpTokenColorKind.PreprocessorKeyword -> LexicalKind.PreprocessorKeyword
+ | FSharpTokenColorKind.InactiveCode -> LexicalKind.InactiveCode
+ | _ -> LexicalKind.Other
+
+ interface ILexicalScanner with
+
+ member _.ScanLine(line, tokens) =
+ let lineTokenizer = tokenizer.CreateLineTokenizer line
+ let mutable scanning = true
+
+ while scanning do
+ let token, nextState = lineTokenizer.ScanToken state
+ state <- nextState
+
+ match token with
+ | Some token ->
+ tokens.Add
+ {
+ Kind = kindOf token.ColorClass
+ Start = token.LeftColumn
+ Length = token.FullMatchedLength
+ }
+ | None -> scanning <- false
+
+ member _.EndsInsideMultiLineConstruct =
+ match (tokenizer.CreateLineTokenizer probeIdentifier).ScanToken state with
+ | Some token, _ ->
+ match token.ColorClass with
+ | FSharpTokenColorKind.String
+ | FSharpTokenColorKind.Comment
+ | FSharpTokenColorKind.InactiveCode -> true
+ | _ -> false
+ | None, _ -> false
+
+type internal FSharpLexicalScannerFactory() =
+
+ interface ILexicalScannerFactory with
+ member _.CreateScanner() =
+ FSharpLexicalScanner() :> ILexicalScanner
diff --git a/vsintegration/src/FSharp.Editor/Commands/InteractiveLexicalScannerExport.fs b/vsintegration/src/FSharp.Editor/Commands/InteractiveLexicalScannerExport.fs
new file mode 100644
index 00000000000..29144bcca64
--- /dev/null
+++ b/vsintegration/src/FSharp.Editor/Commands/InteractiveLexicalScannerExport.fs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Editor
+
+open System.ComponentModel.Composition
+
+open Microsoft.VisualStudio.FSharp.Interactive
+
+[)>]
+type internal FSharpLexicalScannerFactoryExport() =
+ inherit FSharpLexicalScannerFactory()
diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
index f5d6a8e0b04..a671aa6d421 100644
--- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
+++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
@@ -103,6 +103,8 @@
+
+
@@ -168,11 +170,13 @@
+
+
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
index ab876679a77..007b6bfddd8 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
@@ -360,8 +360,15 @@ type internal FSharpPackage() as this =
let! commandService = this.GetServiceAsync(typeof)
let commandService = commandService :?> OleMenuCommandService
- // FSI-LINKAGE-POINT: sited init
- FSharp.Interactive.Hooks.fsiConsoleWindowPackageInitializeSited (this :> Package) commandService
+ let interactiveWindow =
+ exportProvider.GetExport().Value
+
+ let openInteractiveWindow =
+ CommandID(FSharp.Interactive.Guids.guidFsiPackageCmdSet, int FSharp.Interactive.Guids.cmdIDLaunchFsiToolWindow)
+
+ commandService.AddCommand(
+ MenuCommand((fun _ _ -> interactiveWindow.Open(0, focus = true) |> ignore), openInteractiveWindow)
+ )
}
|> CancellableTask.startAsTask cancellationToken)
)
diff --git a/vsintegration/src/FSharp.Interactive.Window/FSharp.Interactive.Window.fsproj b/vsintegration/src/FSharp.Interactive.Window/FSharp.Interactive.Window.fsproj
new file mode 100644
index 00000000000..f836d7d99ea
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/FSharp.Interactive.Window.fsproj
@@ -0,0 +1,59 @@
+
+
+
+
+
+ Library
+ true
+ $(OtherFlags) --subsystemversion:6.00
+ enable
+
+ true
+
+
+
+
+
+
+
+
+ interactiveProtocol.fs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FSharp.Interactive.Window
+ $(VSAssemblyVersion)
+ $PackageFolder$\FSharp.Interactive.Window.dll
+
+
+
+
+
+
+
+
diff --git a/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveClassifier.fs b/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveClassifier.fs
new file mode 100644
index 00000000000..62633ac4598
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveClassifier.fs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+open System.Collections.Generic
+open System.ComponentModel.Composition
+
+open Microsoft.VisualStudio.InteractiveWindow
+open Microsoft.VisualStudio.Language.StandardClassification
+open Microsoft.VisualStudio.Text
+open Microsoft.VisualStudio.Text.Classification
+open Microsoft.VisualStudio.Utilities
+
+type internal FSharpInteractiveClassifier
+ (buffer: ITextBuffer, registry: IClassificationTypeRegistryService, scanners: ILexicalScannerFactory, carriesStateAcrossLines: bool) =
+
+ let classificationFor kind =
+ match kind with
+ | LexicalKind.Keyword -> ValueSome PredefinedClassificationTypeNames.Keyword
+ | LexicalKind.Comment -> ValueSome PredefinedClassificationTypeNames.Comment
+ | LexicalKind.String -> ValueSome PredefinedClassificationTypeNames.String
+ | LexicalKind.Number -> ValueSome PredefinedClassificationTypeNames.Number
+ | LexicalKind.Operator -> ValueSome PredefinedClassificationTypeNames.Operator
+ | LexicalKind.Identifier -> ValueSome PredefinedClassificationTypeNames.Identifier
+ | LexicalKind.PreprocessorKeyword -> ValueSome PredefinedClassificationTypeNames.PreprocessorKeyword
+ | LexicalKind.InactiveCode -> ValueSome PredefinedClassificationTypeNames.ExcludedCode
+ | LexicalKind.Other -> ValueNone
+ |> ValueOption.bind (fun name ->
+ match registry.GetClassificationType name with
+ | null -> ValueNone
+ | classification -> ValueSome classification)
+
+ let mutable ownershipConfirmed = false
+
+ let isOurs () =
+ if not ownershipConfirmed then
+ ownershipConfirmed <-
+ match InteractiveWindowExtensions.GetInteractiveWindow buffer with
+ | null -> FSharpInteractiveWindows.ownsOutputBuffer buffer
+ | window -> window.Evaluator :? FSharpInteractiveEvaluator
+
+ ownershipConfirmed
+
+ let changed =
+ Event, ClassificationChangedEventArgs>()
+
+ do
+ buffer.Changed.Add(fun args ->
+ if args.Changes.Count > 0 && isOurs () then
+ let snapshot = args.After
+ let start = snapshot.GetLineFromPosition(args.Changes[0].NewPosition).Start
+
+ let last =
+ if carriesStateAcrossLines then
+ SnapshotPoint(snapshot, snapshot.Length)
+ else
+ snapshot.GetLineFromPosition(min args.Changes[args.Changes.Count - 1].NewEnd snapshot.Length).End
+
+ changed.Trigger(null, ClassificationChangedEventArgs(SnapshotSpan(start, last))))
+
+ interface IClassifier with
+
+ []
+ member _.ClassificationChanged = changed.Publish
+
+ member _.GetClassificationSpans(span: SnapshotSpan) =
+ let result = List()
+
+ if isOurs () then
+ let snapshot = span.Snapshot
+
+ let firstLine =
+ if carriesStateAcrossLines then
+ 0
+ else
+ snapshot.GetLineNumberFromPosition span.Start.Position
+
+ let lastLine = snapshot.GetLineNumberFromPosition span.End.Position
+ let tokens = ResizeArray()
+ let mutable scanner = scanners.CreateScanner()
+
+ for lineNumber in firstLine..lastLine do
+ if not carriesStateAcrossLines then
+ scanner <- scanners.CreateScanner()
+
+ let line = snapshot.GetLineFromLineNumber lineNumber
+ let text = line.GetText()
+ tokens.Clear()
+ scanner.ScanLine(text, tokens)
+
+ for token in tokens do
+ if token.IsWithin text then
+ let tokenSpan =
+ SnapshotSpan(snapshot, line.Start.Position + token.Start, token.Length)
+
+ if tokenSpan.IntersectsWith span then
+ classificationFor token.Kind
+ |> ValueOption.iter (fun classification -> result.Add(ClassificationSpan(tokenSpan, classification)))
+
+ result :> IList<_>
+
+[)>]
+[]
+type internal FSharpInteractiveInputClassifierProvider
+ []
+ (registry: IClassificationTypeRegistryService, scanners: ILexicalScannerFactory) =
+
+ interface IClassifierProvider with
+ member _.GetClassifier buffer =
+ buffer.Properties.GetOrCreateSingletonProperty(fun () -> FSharpInteractiveClassifier(buffer, registry, scanners, carriesStateAcrossLines = true))
+
+[)>]
+[]
+type internal FSharpInteractiveOutputClassifierProvider
+ []
+ (registry: IClassificationTypeRegistryService, scanners: ILexicalScannerFactory) =
+
+ interface IClassifierProvider with
+ member _.GetClassifier buffer =
+ buffer.Properties.GetOrCreateSingletonProperty(fun () -> FSharpInteractiveClassifier(buffer, registry, scanners, carriesStateAcrossLines = false))
diff --git a/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveCommandFilter.fs b/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveCommandFilter.fs
new file mode 100644
index 00000000000..a64eaa1c43d
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveCommandFilter.fs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+open System.ComponentModel.Composition
+
+open Microsoft.VisualStudio
+open Microsoft.VisualStudio.Editor
+open Microsoft.VisualStudio.OLE.Interop
+open Microsoft.VisualStudio.Text
+open Microsoft.VisualStudio.Text.Editor
+open Microsoft.VisualStudio.TextManager.Interop
+open Microsoft.VisualStudio.Utilities
+
+type internal SendToInteractiveKind =
+ | Selection
+ | Line
+
+ member kind.Status =
+ match kind with
+ | Selection -> OLECMDF.OLECMDF_SUPPORTED ||| OLECMDF.OLECMDF_ENABLED
+ | Line ->
+ OLECMDF.OLECMDF_SUPPORTED
+ ||| OLECMDF.OLECMDF_ENABLED
+ ||| OLECMDF.OLECMDF_DEFHIDEONCTXTMENU
+
+[]
+module private InteractiveCommand =
+
+ []
+ let (|SendToInteractive|_|) (group: Guid, commandId: uint32) =
+ match group = VSConstants.VsStd11, enum (int commandId) with
+ | true, VSConstants.VSStd11CmdID.ExecuteSelectionInInteractive -> ValueSome Selection
+ | true, VSConstants.VSStd11CmdID.ExecuteLineInInteractive -> ValueSome Line
+ | _ -> ValueNone
+
+type internal EditorSubmission =
+ {
+ Text: string
+ SourcePath: string
+ OneBasedStartLine: int
+ }
+
+module internal EditorSubmission =
+
+ let readAndAdvanceCaret (documentFactory: ITextDocumentFactoryService) (view: ITextView) (kind: SendToInteractiveKind) =
+ let selection = view.Selection
+ let sendsLine = kind.IsLine || selection.IsEmpty
+
+ let span =
+ if sendsLine then
+ view.Caret.Position.BufferPosition.GetContainingLine().Extent
+ else
+ SnapshotSpan(selection.Start.Position, selection.End.Position)
+
+ let text = span.GetText()
+
+ if String.IsNullOrWhiteSpace text then
+ ValueNone
+ else
+ let sourcePath =
+ match documentFactory.TryGetTextDocument view.TextBuffer with
+ | true, document ->
+ match document with
+ | null -> ""
+ | document -> document.FilePath
+ | _ -> ""
+
+ let snapshot = view.TextSnapshot
+ let line = span.Start.GetContainingLine().LineNumber
+
+ if sendsLine && line + 1 < snapshot.LineCount then
+ view.Caret.MoveTo(snapshot.GetLineFromLineNumber(line + 1).Start) |> ignore
+ selection.Clear()
+
+ ValueSome
+ {
+ Text = text
+ SourcePath = sourcePath
+ OneBasedStartLine = line + 1
+ }
+
+type internal FSharpInteractiveCommandFilter
+ (provider: FSharpVsInteractiveWindowProvider, documentFactory: ITextDocumentFactoryService, view: ITextView) =
+
+ let mutable nextTarget: IOleCommandTarget | null = null
+
+ member this.AttachToViewAdapter(viewAdapter: IVsTextView) =
+ match viewAdapter.AddCommandFilter this with
+ | VSConstants.S_OK, next -> nextTarget <- next
+ | errorCode, _ -> ErrorHandler.ThrowOnFailure errorCode |> ignore
+
+ interface IOleCommandTarget with
+
+ member _.Exec(pguidCmdGroup, nCmdId, nCmdexecopt, pvaIn, pvaOut) =
+ match pguidCmdGroup, nCmdId with
+ | SendToInteractive kind ->
+ EditorSubmission.readAndAdvanceCaret documentFactory view kind
+ |> ValueOption.iter (fun submission ->
+ provider.SubmitFromEditor(submission.Text, submission.SourcePath, submission.OneBasedStartLine))
+
+ VSConstants.S_OK
+ | _ ->
+ match nextTarget with
+ | null -> VSConstants.E_FAIL
+ | target -> target.Exec(&pguidCmdGroup, nCmdId, nCmdexecopt, pvaIn, pvaOut)
+
+ member _.QueryStatus(pguidCmdGroup, cCmds, prgCmds, pCmdText) =
+ let group = pguidCmdGroup
+
+ let statuses =
+ Array.init (int cCmds) (fun i ->
+ match group, prgCmds[i].cmdID with
+ | SendToInteractive kind -> ValueSome kind.Status
+ | _ -> ValueNone)
+
+ let result =
+ if statuses.Length > 0 && statuses |> Array.forall _.IsSome then
+ VSConstants.S_OK
+ else
+ match nextTarget with
+ | null -> VSConstants.E_FAIL
+ | target -> target.QueryStatus(&pguidCmdGroup, cCmds, prgCmds, pCmdText)
+
+ statuses
+ |> Array.iteri (fun i status -> status |> ValueOption.iter (fun flags -> prgCmds[i].cmdf <- uint32 flags))
+
+ result
+
+[)>]
+[]
+[]
+type internal FSharpInteractiveCommandFilterProvider
+ []
+ (
+ provider: FSharpVsInteractiveWindowProvider,
+ documentFactory: ITextDocumentFactoryService,
+ editorFactory: IVsEditorAdaptersFactoryService
+ ) =
+
+ interface IWpfTextViewCreationListener with
+ member _.TextViewCreated(view) =
+ match editorFactory.GetViewAdapter view with
+ | null -> ()
+ | adapter -> FSharpInteractiveCommandFilter(provider, documentFactory, view).AttachToViewAdapter adapter
diff --git a/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveEvaluator.fs b/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveEvaluator.fs
new file mode 100644
index 00000000000..96126d54c81
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/FSharpInteractiveEvaluator.fs
@@ -0,0 +1,167 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+open System.IO
+open System.Threading.Tasks
+
+open Microsoft.VisualStudio.InteractiveWindow
+open Microsoft.VisualStudio.Text
+
+/// The window package tags its input buffers with the window, but not its output buffer.
+module internal FSharpInteractiveWindows =
+
+ let private windows = ResizeArray()
+
+ let add (window: IInteractiveWindow) =
+ lock windows (fun () ->
+ if not (windows.Contains window) then
+ windows.Add window)
+
+ let remove (window: IInteractiveWindow) =
+ lock windows (fun () -> windows.Remove window |> ignore)
+
+ let ownsOutputBuffer (buffer: ITextBuffer) =
+ lock windows (fun () ->
+ windows
+ |> Seq.exists (fun window -> obj.ReferenceEquals(window.OutputBuffer, buffer)))
+
+module internal ResultRendering =
+
+ let formatDiagnostic (diagnostic: FSharp.Compiler.Interactive.Protocol.DiagnosticInfo) =
+ $"{diagnostic.fileName}({diagnostic.startLine},{diagnostic.startColumn + 1}): {diagnostic.severity} FS%04d{diagnostic.errorNumber}: {diagnostic.message}"
+
+ let formatSessionStart (session: RemoteSession) =
+ let handshake = session.Initialization
+ $"F# Interactive {handshake.fsiVersion} from {session.Origin.Description} on {handshake.frameworkDescription}, in {handshake.workingDirectory}"
+
+[]
+type internal FSharpInteractiveEvaluator
+ (host: InteractiveHostClient, getOptions: unit -> InteractiveHostOptions, scanners: ILexicalScannerFactory) =
+
+ let mutable currentWindow: IInteractiveWindow | null = null
+ let mutable subscriptions: IDisposable list = []
+ let mutable disposed = false
+
+ let origins = SubmissionOrigins()
+
+ let output () =
+ match currentWindow with
+ | null -> TextWriter.Null
+ | window -> window.OutputWriter
+
+ let errorOutput () =
+ match currentWindow with
+ | null -> TextWriter.Null
+ | window -> window.ErrorOutputWriter
+
+ let writeErrorLine (text: string) = errorOutput().WriteLine text
+
+ let reportDiagnostics (result: FSharp.Compiler.Interactive.Protocol.ExecutionResult) =
+ match result.diagnostics with
+ | null -> ()
+ | diagnostics ->
+ for diagnostic in diagnostics do
+ writeErrorLine (ResultRendering.formatDiagnostic diagnostic)
+
+ match result.``exception`` with
+ | null -> ()
+ | failure ->
+ writeErrorLine failure.message
+
+ if not (String.IsNullOrWhiteSpace failure.stackTrace) then
+ writeErrorLine failure.stackTrace
+
+ let startedAsync (start: InteractiveHostOptions -> Task>) =
+ task {
+ match! start (getOptions ()) with
+ | Result.Ok _ -> return ExecutionResult true
+ | Result.Error message ->
+ writeErrorLine message
+ return ExecutionResult false
+ }
+
+ let unsubscribe () =
+ for subscription in subscriptions do
+ subscription.Dispose()
+
+ subscriptions <- []
+
+ member _.AddSubmissionOrigin(text: string, sourcePath: string, startLine: int) =
+ if not (String.IsNullOrEmpty sourcePath) then
+ origins.Add(text, sourcePath, startLine)
+
+ interface IInteractiveEvaluator with
+
+ member _.CurrentWindow
+ with get () = currentWindow
+ and set window =
+ currentWindow <- window
+ unsubscribe ()
+
+ match window with
+ | null -> ()
+ | window ->
+ FSharpInteractiveWindows.add window
+
+ subscriptions <-
+ [
+ host.OutputReceived.Subscribe(fun text -> output().Write text)
+ host.ErrorOutputReceived.Subscribe(fun text -> errorOutput().Write text)
+ host.SessionExitedUnexpectedly.Subscribe(fun exitCode ->
+ writeErrorLine $"{VFSIstrings.SR.sessionTerminationDetected ()} (exit code {exitCode})")
+ host.SessionStarted.Subscribe(fun session -> output().WriteLine(ResultRendering.formatSessionStart session))
+ ]
+
+ member _.InitializeAsync() =
+ startedAsync (fun options -> host.EnsureStartedAsync options)
+
+ member _.ResetAsync(_initialize) =
+ startedAsync (fun options -> host.ResetAsync options)
+
+ member _.CanExecuteCode(text) =
+ SubmissionAnalysis.isComplete scanners text
+
+ member _.ExecuteCodeAsync(text) =
+ task {
+ let origin = origins.Take text
+
+ let! started = startedAsync (fun options -> host.EnsureStartedAsync options)
+
+ if not started.IsSuccessful || String.IsNullOrWhiteSpace text then
+ return started
+ else
+ match! host.ExecuteAsync(SubmissionAnalysis.withTerminator scanners text, origin) with
+ | Result.Error message ->
+ writeErrorLine message
+ return ExecutionResult false
+ | Result.Ok result ->
+ reportDiagnostics result
+ return ExecutionResult result.success
+ }
+
+ member _.AbortExecution() = host.InterruptAsync() |> ignore
+
+ member _.FormatClipboard() = null
+
+ member _.GetPrompt() =
+ match currentWindow with
+ | null -> "> "
+ | window ->
+ match window.CurrentLanguageBuffer with
+ | null -> "> "
+ | buffer when buffer.CurrentSnapshot.LineCount > 1 -> "- "
+ | _ -> "> "
+
+ interface IDisposable with
+ member _.Dispose() =
+ if not disposed then
+ disposed <- true
+
+ match currentWindow with
+ | null -> ()
+ | window -> FSharpInteractiveWindows.remove window
+
+ unsubscribe ()
+ (host :> IDisposable).Dispose()
diff --git a/vsintegration/src/FSharp.Interactive.Window/FSharpVsInteractiveWindowPackage.fs b/vsintegration/src/FSharp.Interactive.Window/FSharpVsInteractiveWindowPackage.fs
new file mode 100644
index 00000000000..c387f521b27
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/FSharpVsInteractiveWindowPackage.fs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+open System.Runtime.InteropServices
+
+open Microsoft.VisualStudio
+open Microsoft.VisualStudio.ComponentModelHost
+open Microsoft.VisualStudio.InteractiveWindow.Shell
+open Microsoft.VisualStudio.Shell
+open Microsoft.VisualStudio.Shell.Interop
+
+module internal InteractiveWindowPackageGuids =
+
+ []
+ let PackageIdString = "F5C1B3D2-8E47-4A96-9C0B-1D7E4A2F6B39"
+
+ []
+ let OutputWindowIdString = "34E76E81-EE4A-11D0-AE2E-00A0C90FFFC3"
+
+[]
+[]
+[]
+type internal FSharpVsInteractiveWindowPackage() =
+ inherit AsyncPackage()
+
+ interface IVsToolWindowFactory with
+
+ member this.CreateToolWindow(toolWindowType: byref, id: uint32) =
+ if toolWindowType <> InteractiveWindowGuids.ToolWindowId then
+ VSConstants.E_FAIL
+ else
+ match this.GetService typeof with
+ | :? IComponentModel as components ->
+ components.DefaultExportProvider.GetExportedValue().Create(int id)
+ |> ignore
+
+ VSConstants.S_OK
+ | _ -> VSConstants.E_FAIL
diff --git a/vsintegration/src/FSharp.Interactive.Window/FSharpVsInteractiveWindowProvider.fs b/vsintegration/src/FSharp.Interactive.Window/FSharpVsInteractiveWindowProvider.fs
new file mode 100644
index 00000000000..18ddf6f543c
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/FSharpVsInteractiveWindowProvider.fs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+open System.ComponentModel.Composition
+open System.Diagnostics
+open System.IO
+open System.Threading
+
+open Microsoft.VisualStudio
+open Microsoft.VisualStudio.InteractiveWindow.Shell
+open Microsoft.VisualStudio.Shell
+open Microsoft.VisualStudio.Shell.Interop
+open Microsoft.VisualStudio.Utilities
+
+open Microsoft.VisualStudio.FSharp.Interactive.Session
+
+module InteractiveWindowGuids =
+
+ /// Persisted in users' layouts: never change.
+ []
+ let ToolWindowIdString = "6B0F0D9E-1B4A-4C4E-9E2D-6F3B2A5C7D18"
+
+ let ToolWindowId = Guid ToolWindowIdString
+
+ let FSharpLanguageServiceId = Guids.guidFsharpLanguageService
+
+ /// Must equal FSharpConstants.FSharpContentTypeName (FSharp.Editor).
+ []
+ let FSharpContentTypeName = "F#"
+
+module internal InteractiveHostOptionsFactory =
+
+ let private solutionDirectory () =
+ ThreadHelper.ThrowIfNotOnUIThread()
+
+ try
+ match ServiceProvider.GlobalProvider.GetService typeof with
+ | :? IVsSolution as solution ->
+ match solution.GetSolutionInfo() with
+ | hr, directory, _, _ when ErrorHandler.Succeeded hr && Directory.Exists directory -> ValueSome directory
+ | _ -> ValueNone
+ | _ -> ValueNone
+ with _ ->
+ ValueNone
+
+ let create () =
+ {
+ InitialWorkingDirectory =
+ solutionDirectory ()
+ |> ValueOption.defaultWith (fun () -> Environment.GetFolderPath Environment.SpecialFolder.UserProfile)
+ UserArguments = SessionsProperties.fsiArgs
+ DebugMode = SessionsProperties.fsiDebugMode
+ LanguageVersionPreview = SessionsProperties.fsiPreview
+ UICultureLcid = Thread.CurrentThread.CurrentUICulture.LCID
+ }
+
+[)>]
+[]
+type internal FSharpVsInteractiveWindowProvider
+ []
+ (windowFactory: IVsInteractiveWindowFactory, contentTypeRegistry: IContentTypeRegistryService, scanners: ILexicalScannerFactory) =
+
+ let mutable window: IVsInteractiveWindow | null = null
+ let mutable evaluator: FSharpInteractiveEvaluator voption = ValueNone
+
+ member _.Create(instanceId: int) =
+ let host = new InteractiveHostClient(Process.GetCurrentProcess().Id)
+
+ let created =
+ new FSharpInteractiveEvaluator(host, InteractiveHostOptionsFactory.create, scanners)
+
+ evaluator <- ValueSome created
+
+ let toolWindow =
+ windowFactory.Create(
+ InteractiveWindowGuids.ToolWindowId,
+ instanceId,
+ VFSIstrings.SR.fsharpInteractive (),
+ created,
+ __VSCREATETOOLWIN.CTW_fForceCreate
+ )
+
+ window <- toolWindow
+
+ toolWindow.SetLanguage(
+ InteractiveWindowGuids.FSharpLanguageServiceId,
+ contentTypeRegistry.GetContentType InteractiveWindowGuids.FSharpContentTypeName
+ )
+
+ let interactiveWindow = toolWindow.InteractiveWindow
+
+ interactiveWindow.TextView.Closed.Add(fun _ ->
+ (created :> IDisposable).Dispose()
+
+ if obj.ReferenceEquals(window, toolWindow) then
+ window <- null
+
+ match evaluator with
+ | ValueSome current when obj.ReferenceEquals(current, created) -> evaluator <- ValueNone
+ | _ -> ())
+
+ interactiveWindow.InitializeAsync() |> ignore
+ toolWindow
+
+ member this.Open(instanceId: int, focus: bool) =
+ let toolWindow =
+ match window with
+ | null -> this.Create instanceId
+ | existing -> existing
+
+ toolWindow.Show focus
+ toolWindow
+
+ member this.SubmitFromEditor(text: string, sourcePath: string, startLine: int) =
+ let toolWindow = this.Open(0, focus = false)
+
+ evaluator |> ValueOption.iter _.AddSubmissionOrigin(text, sourcePath, startLine)
+
+ toolWindow.InteractiveWindow.SubmitAsync [| text |] |> ignore
diff --git a/vsintegration/src/FSharp.Interactive.Window/InteractiveHost.fs b/vsintegration/src/FSharp.Interactive.Window/InteractiveHost.fs
new file mode 100644
index 00000000000..c1d90f8e9ee
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/InteractiveHost.fs
@@ -0,0 +1,525 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+open System.Collections.Generic
+open System.Diagnostics
+open System.IO
+open System.IO.Pipes
+open System.Text
+open System.Threading
+open System.Threading.Tasks
+
+open StreamJsonRpc
+
+open FSharp.Compiler.Interactive.Protocol
+
+type InteractiveHostOptions =
+ {
+ /// `dotnet fsi` takes its SDK from the `global.json` nearest this folder.
+ InitialWorkingDirectory: string
+ UserArguments: string
+ DebugMode: bool
+ LanguageVersionPreview: bool
+ UICultureLcid: int
+ }
+
+[]
+type internal FsiOrigin =
+ | Override
+ | Sdk
+ | VisualStudio
+
+ static member OverrideVariable = "FSHARP_INTERACTIVE_PATH"
+
+ member this.Description =
+ match this with
+ | Override -> FsiOrigin.OverrideVariable
+ | Sdk -> ".NET SDK"
+ | VisualStudio -> "Visual Studio"
+
+type internal FsiCandidate =
+ {
+ Origin: FsiOrigin
+ Executable: string
+ LeadingArguments: string list
+ }
+
+ member this.DescribeEarlyExit(output: string, exitCode: int) =
+ if output.IndexOf("install or update .NET", StringComparison.Ordinal) >= 0 then
+ $"The F# Interactive from {this.Origin.Description} needs a .NET runtime that is not installed; see the message above."
+ else
+ let commandLine = String.Join(" ", this.Executable :: this.LeadingArguments)
+
+ $"The F# Interactive from {this.Origin.Description} ({commandLine}) exited with code {exitCode} before the session was established."
+
+module internal FsiLocator =
+
+ let private hostExecutable = "dotnet.exe"
+
+ let private hostsOnSearchPath () =
+ match Environment.GetEnvironmentVariable "PATH" with
+ | null -> Seq.empty
+ | searchPath ->
+ searchPath.Split([| Path.PathSeparator |], StringSplitOptions.RemoveEmptyEntries)
+ |> Seq.choose (fun directory ->
+ try
+ Some(Path.Combine(directory.Trim(' ', '"'), hostExecutable))
+ with :? ArgumentException ->
+ None)
+
+ let private machineWideHost () =
+ let programFiles =
+ match Environment.GetEnvironmentVariable "ProgramW6432" with
+ | path when not (String.IsNullOrEmpty path) -> path
+ | _ -> Environment.GetFolderPath Environment.SpecialFolder.ProgramFiles
+
+ Path.Combine(programFiles, "dotnet", hostExecutable)
+
+ let private findDotnetHost () =
+ seq {
+ Environment.GetEnvironmentVariable "DOTNET_HOST_PATH"
+ yield! hostsOnSearchPath ()
+ }
+ |> Seq.tryFind File.Exists
+ |> Option.defaultWith machineWideHost
+
+ /// Laid out by Microsoft.FSharp.Compiler.MSBuild.csproj and VisualFSharpDebug.csproj.
+ let private bundledRelativePath = Path.Combine("Tools", "Interactive", "fsi.dll")
+
+ let private repositoryHostOrDefault (fsiPath: string) =
+ let rec search (directory: DirectoryInfo | null) =
+ match directory with
+ | null -> findDotnetHost ()
+ | directory ->
+ let candidate = Path.Combine(directory.FullName, ".dotnet", hostExecutable)
+
+ if File.Exists candidate then
+ candidate
+ else
+ search directory.Parent
+
+ search (DirectoryInfo(Path.GetDirectoryName fsiPath))
+
+ let private candidate origin (executable: string) leadingArguments =
+ {
+ Origin = origin
+ Executable = executable
+ LeadingArguments = leadingArguments
+ }
+
+ let private onHost origin (host: string) leadingArguments =
+ if File.Exists host then
+ Result.Ok(candidate origin host leadingArguments)
+ else
+ Result.Error(VFSIstrings.SR.couldNotFindFsiExe host)
+
+ let private tryOverride () =
+ match Environment.GetEnvironmentVariable FsiOrigin.OverrideVariable with
+ | path when not (String.IsNullOrWhiteSpace path) && File.Exists path ->
+ if Path.GetExtension(path).Equals(".dll", StringComparison.OrdinalIgnoreCase) then
+ ValueSome(onHost FsiOrigin.Override (repositoryHostOrDefault path) [ "exec"; path ])
+ else
+ ValueSome(Result.Ok(candidate FsiOrigin.Override path []))
+ | _ -> ValueNone
+
+ let private tryBundled (host: string) =
+ match Path.GetDirectoryName(typeof.Assembly.Location) with
+ | null -> ValueNone
+ | directory ->
+ let fsi = Path.Combine(directory, bundledRelativePath)
+
+ if File.Exists fsi then
+ ValueSome(candidate FsiOrigin.VisualStudio host [ "exec"; fsi ])
+ else
+ ValueNone
+
+ let candidates () =
+ match tryOverride () with
+ | ValueSome overridden -> overridden |> Result.map List.singleton
+ | ValueNone ->
+ let host = findDotnetHost ()
+
+ onHost FsiOrigin.Sdk host [ "fsi" ]
+ |> Result.map (fun sdk -> [ sdk; yield! tryBundled host |> ValueOption.toList ])
+
+module private SessionProcess =
+
+ let quietly (action: unit -> unit) =
+ try
+ action ()
+ with _ ->
+ ()
+
+ let kill (session: Process) =
+ quietly (fun () ->
+ if not session.HasExited then
+ session.Kill())
+
+ let pumpDrainMilliseconds = 200
+
+ let pumpWithoutWaitingForNewline (reader: StreamReader) (report: string -> unit) =
+ let read () =
+ let buffer = Array.zeroCreate 1024
+ let mutable count = reader.Read(buffer, 0, buffer.Length)
+
+ while count > 0 do
+ report (String(buffer, 0, count))
+ count <- reader.Read(buffer, 0, buffer.Length)
+
+ Thread((fun () -> quietly read), IsBackground = true).Start()
+
+ let private quoteIfNeeded (argument: string) =
+ if
+ argument.IndexOf ' ' >= 0
+ && not (argument.StartsWith("\"", StringComparison.Ordinal))
+ then
+ $"\"{argument}\""
+ else
+ argument
+
+ let startInfo (options: InteractiveHostOptions) (candidate: FsiCandidate) (pipeName: string) (clientProcessId: int) =
+ let arguments =
+ [
+ yield! candidate.LeadingArguments |> List.map quoteIfNeeded
+ "--nologo"
+ $"--fsi-server-output-codepage:{Encoding.UTF8.CodePage}"
+ $"--fsi-server-input-codepage:{Encoding.UTF8.CodePage}"
+ $"--fsi-server-lcid:{options.UICultureLcid}"
+
+ if not (String.IsNullOrWhiteSpace options.UserArguments) then
+ options.UserArguments.Trim()
+
+ if options.DebugMode then
+ "--optimize-"
+ "--debug+"
+
+ if options.LanguageVersionPreview then
+ "--langversion:preview"
+
+ // After the user's arguments: fsi takes the last occurrence of a --fsi-server-* switch.
+ $"{CommandLine.ServerOption}{pipeName}"
+ $"--fsi-server-client-pid:{clientProcessId}"
+ ]
+
+ let startInfo =
+ ProcessStartInfo(
+ FileName = candidate.Executable,
+ Arguments = String.Join(" ", arguments),
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ StandardOutputEncoding = Encoding.UTF8,
+ StandardErrorEncoding = Encoding.UTF8
+ )
+
+ if Directory.Exists options.InitialWorkingDirectory then
+ startInfo.WorkingDirectory <- options.InitialWorkingDirectory
+
+ startInfo
+
+[]
+type internal RemoteSession(origin: FsiOrigin, session: Process, pipe: Stream, rpc: JsonRpc, initialization: InitializeResult) =
+
+ member _.Origin = origin
+ member _.Rpc = rpc
+ member _.Initialization = initialization
+
+ member _.ExitCode =
+ try
+ session.ExitCode
+ with _ ->
+ 0
+
+ member _.IsAlive =
+ try
+ not session.HasExited
+ with _ ->
+ false
+
+ member _.Dispose() =
+ SessionProcess.quietly rpc.Dispose
+ SessionProcess.quietly pipe.Dispose
+ SessionProcess.kill session
+ SessionProcess.quietly session.Dispose
+
+[]
+type private OutputHeldUntilHandshake(forward: bool -> string -> unit) =
+
+ let held = ResizeArray()
+ let mutable holding = true
+
+ member _.Report (isError: bool) (text: string) =
+ let passes =
+ lock held (fun () ->
+ if holding then
+ held.Add(struct (isError, text))
+
+ not holding)
+
+ if passes then
+ forward isError text
+
+ member _.Release() =
+ let pending =
+ lock held (fun () ->
+ holding <- false
+ let pending = held.ToArray()
+ held.Clear()
+ pending)
+
+ for struct (isError, text) in pending do
+ forward isError text
+
+ member _.Text =
+ lock held (fun () -> String.Join("", held |> Seq.map (fun struct (_, text) -> text)))
+
+[]
+type internal InteractiveHostClient(clientProcessId: int) =
+
+ let stateLock = obj ()
+ let startGate = new SemaphoreSlim(1, 1)
+ let mutable current: RemoteSession voption = ValueNone
+ let mutable disposed = false
+
+ let outputReceived = Event()
+ let errorOutputReceived = Event()
+ let sessionExitedUnexpectedly = Event()
+ let sessionStarted = Event()
+
+ let lastWorkingOrigin = Dictionary(StringComparer.OrdinalIgnoreCase)
+
+ let forward isError (text: string) =
+ if isError then
+ errorOutputReceived.Trigger text
+ else
+ outputReceived.Trigger text
+
+ let tryCurrent () =
+ lock stateLock (fun () -> current |> ValueOption.filter _.IsAlive)
+
+ let takeCurrent () =
+ lock stateLock (fun () ->
+ let previous = current
+ current <- ValueNone
+ previous)
+
+ let reportExit (remote: RemoteSession) =
+ let wasCurrent =
+ lock stateLock (fun () ->
+ match current with
+ | ValueSome running when obj.ReferenceEquals(running, remote) ->
+ current <- ValueNone
+ true
+ | _ -> false)
+
+ if wasCurrent then
+ sessionExitedUnexpectedly.Trigger remote.ExitCode
+
+ let startAttemptAsync (options: InteractiveHostOptions) (candidate: FsiCandidate) (cancellationToken: CancellationToken) =
+ task {
+ let unguessablePipeName = $"FSharpInteractive.{Guid.NewGuid():N}"
+ let output = OutputHeldUntilHandshake forward
+
+ let session =
+ new Process(StartInfo = SessionProcess.startInfo options candidate unguessablePipeName clientProcessId, EnableRaisingEvents = true)
+
+ // Before Start: Exited is not raised for handlers added after the exit.
+ use exitedDuringConnect = new CancellationTokenSource()
+ session.Exited.Add(fun _ -> SessionProcess.quietly exitedDuringConnect.Cancel)
+
+ let started =
+ try
+ session.Start()
+ with _ ->
+ false
+
+ if not started then
+ return Result.Error(false, VFSIstrings.SR.couldNotFindFsiExe candidate.Executable, ignore)
+ else
+ SessionProcess.pumpWithoutWaitingForNewline session.StandardOutput (output.Report false)
+ SessionProcess.pumpWithoutWaitingForNewline session.StandardError (output.Report true)
+
+ use connectCancellation =
+ CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, exitedDuringConnect.Token)
+
+ // No CurrentUserOnly on net472: the unguessable pipe name is the only guard.
+ let pipe =
+ new NamedPipeClientStream(".", unguessablePipeName, PipeDirection.InOut, PipeOptions.Asynchronous)
+
+ try
+ do! pipe.ConnectAsync connectCancellation.Token
+
+ let rpc =
+ new JsonRpc(new HeaderDelimitedMessageHandler(pipe, new JsonMessageFormatter()))
+
+ rpc.StartListening()
+
+ let! handshake =
+ rpc.InvokeWithCancellationAsync(Methods.Initialize, cancellationToken = cancellationToken)
+
+ let remote = RemoteSession(candidate.Origin, session, pipe, rpc, handshake)
+ session.Exited.Add(fun _ -> reportExit remote)
+ output.Release()
+ return Result.Ok remote
+ with e ->
+ pipe.Dispose()
+ SessionProcess.kill session
+
+ let exitedEarly = session.HasExited
+
+ if exitedEarly then
+ do! Task.Delay SessionProcess.pumpDrainMilliseconds
+
+ let detail =
+ if exitedEarly then
+ candidate.DescribeEarlyExit(output.Text, session.ExitCode)
+ else
+ e.Message
+
+ return Result.Error(exitedEarly, detail, output.Release)
+ }
+
+ let startAsync (options: InteractiveHostOptions) (cancellationToken: CancellationToken) =
+ task {
+ match FsiLocator.candidates () with
+ | Result.Error message -> return Result.Error message
+ | Result.Ok candidates ->
+ let key = options.InitialWorkingDirectory
+
+ let mutable remaining =
+ match lock lastWorkingOrigin (fun () -> lastWorkingOrigin.TryGetValue key) with
+ | true, origin -> candidates |> List.sortBy (fun candidate -> candidate.Origin <> origin)
+ | _ -> candidates
+
+ // A loop because `let rec` inside `task` is FS3511.
+ let failures = ResizeArray()
+ let mutable started = ValueNone
+
+ while started.IsNone && not remaining.IsEmpty do
+ let candidate = remaining.Head
+ remaining <- remaining.Tail
+
+ match! startAttemptAsync options candidate cancellationToken with
+ | Result.Ok remote ->
+ lock lastWorkingOrigin (fun () -> lastWorkingOrigin[key] <- candidate.Origin)
+ sessionStarted.Trigger remote
+ started <- ValueSome remote
+ | Result.Error(exitedEarly, detail, release) ->
+ lock lastWorkingOrigin (fun () -> lastWorkingOrigin.Remove key |> ignore)
+ failures.Add detail
+
+ if not exitedEarly || remaining.IsEmpty then
+ release ()
+ remaining <- []
+
+ return
+ match started with
+ | ValueSome remote -> Result.Ok remote
+ | ValueNone -> Result.Error(String.Join(Environment.NewLine, failures))
+ }
+
+ let installUnlessExited (started: RemoteSession) =
+ let struct (previous, failure) =
+ lock stateLock (fun () ->
+ if disposed then
+ struct (ValueNone, ValueSome "The interactive window was closed while the session was starting.")
+ else
+ let previous = takeCurrent ()
+
+ if started.IsAlive then
+ current <- ValueSome started
+ struct (previous, ValueNone)
+ else
+ struct (previous, ValueSome $"F# Interactive exited with code {started.ExitCode} as the session started."))
+
+ previous |> ValueOption.iter _.Dispose()
+
+ match failure with
+ | ValueNone -> Result.Ok started
+ | ValueSome message ->
+ started.Dispose()
+ Result.Error message
+
+ member _.OutputReceived = outputReceived.Publish
+
+ member _.ErrorOutputReceived = errorOutputReceived.Publish
+
+ member _.SessionExitedUnexpectedly = sessionExitedUnexpectedly.Publish
+
+ member _.SessionStarted = sessionStarted.Publish
+
+ member _.EnsureStartedAsync(options, ?cancellationToken) : Task> =
+ let cancellationToken = defaultArg cancellationToken CancellationToken.None
+
+ task {
+ match tryCurrent () with
+ | ValueSome running -> return Result.Ok running
+ | ValueNone ->
+ do! startGate.WaitAsync cancellationToken
+
+ try
+ match tryCurrent () with
+ | ValueSome running -> return Result.Ok running
+ | ValueNone ->
+ let! outcome = startAsync options cancellationToken
+ return outcome |> Result.bind installUnlessExited
+ finally
+ startGate.Release() |> ignore
+ }
+
+ member this.ResetAsync(options, ?cancellationToken) =
+ takeCurrent () |> ValueOption.iter _.Dispose()
+ this.EnsureStartedAsync(options, ?cancellationToken = cancellationToken)
+
+ member _.ExecuteAsync(code: string, origin: struct (string * int) voption, ?cancellationToken) =
+ let request =
+ {
+ code = code
+ sourcePath = origin |> ValueOption.map (fun struct (path, _) -> path) |> ValueOption.toObj
+ startLine =
+ origin
+ |> ValueOption.map (fun struct (_, line) -> line)
+ |> ValueOption.toNullable
+ }
+
+ task {
+ match tryCurrent () with
+ | ValueNone -> return Result.Error "No F# Interactive session is running."
+ | ValueSome session ->
+ try
+ let! result =
+ session.Rpc.InvokeWithParameterObjectAsync(
+ Methods.Execute,
+ request,
+ defaultArg cancellationToken CancellationToken.None
+ )
+
+ return Result.Ok result
+ with e ->
+ return Result.Error e.Message
+ }
+
+ member _.InterruptAsync() =
+ task {
+ match tryCurrent () with
+ | ValueNone -> return false
+ | ValueSome session ->
+ try
+ let! result = session.Rpc.InvokeAsync(Methods.Interrupt)
+ return result.interrupted
+ with _ ->
+ return false
+ }
+
+ interface IDisposable with
+ member _.Dispose() =
+ lock stateLock (fun () ->
+ disposed <- true
+ takeCurrent ())
+ |> ValueOption.iter _.Dispose()
+
+ startGate.Dispose()
diff --git a/vsintegration/src/FSharp.Interactive.Window/LexicalScanning.fs b/vsintegration/src/FSharp.Interactive.Window/LexicalScanning.fs
new file mode 100644
index 00000000000..c7874a569dc
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/LexicalScanning.fs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+[]
+type internal LexicalKind =
+ | Keyword
+ | Comment
+ | String
+ | Number
+ | Operator
+ | Identifier
+ | PreprocessorKeyword
+ | InactiveCode
+ | Other
+
+[]
+type internal LexicalToken =
+ {
+ Kind: LexicalKind
+ Start: int
+ Length: int
+ }
+
+ member token.IsWithin(line: string) =
+ token.Start >= 0 && token.Start + token.Length <= line.Length
+
+type internal ILexicalScanner =
+
+ abstract ScanLine: line: string * tokens: ResizeArray -> unit
+
+ abstract EndsInsideMultiLineConstruct: bool
+
+type internal ILexicalScannerFactory =
+ abstract CreateScanner: unit -> ILexicalScanner
diff --git a/vsintegration/src/FSharp.Interactive.Window/SubmissionAnalysis.fs b/vsintegration/src/FSharp.Interactive.Window/SubmissionAnalysis.fs
new file mode 100644
index 00000000000..40fc4b4d29b
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/SubmissionAnalysis.fs
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+open System.Collections.Generic
+
+module internal SubmissionAnalysis =
+
+ let private ordinalSet (items: string seq) =
+ HashSet(items, StringComparer.Ordinal)
+
+ let private continuationTokens =
+ [
+ "= -> <- : , ; | || && + - * / % ** @ ^ |> <| >> << ->>"
+ "then else elif do try with finally function fun begin match if let use and or in when as of new"
+ "static member override abstract type module namespace open rec mutable yield return"
+ ]
+ |> Seq.collect _.Split(' ')
+ |> ordinalSet
+
+ let private opening = ordinalSet [ "("; "["; "{"; "[|"; "[<"; "{|" ]
+
+ let private closing = ordinalSet [ ")"; "]"; "}"; "|]"; ">]"; "|}" ]
+
+ let private literalPlaceholder = "\"\""
+
+ let private (|Opening|Closing|Ordinary|) token =
+ if opening.Contains token then Opening
+ elif closing.Contains token then Closing
+ else Ordinary
+
+ type private Scan =
+ {
+ OpenBrackets: int
+ InsideMultiLineConstruct: bool
+ LastToken: string voption
+ }
+
+ member scanned.EndsWithTerminator =
+ scanned.LastToken
+ |> ValueOption.exists (fun token -> String.Equals(token, ";;", StringComparison.Ordinal))
+
+ let private scan (scanners: ILexicalScannerFactory) (text: string) =
+ let scanner = scanners.CreateScanner()
+ let tokens = ResizeArray()
+ let mutable openBrackets = 0
+ let mutable lastToken = ValueNone
+
+ for line in text.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n') do
+ tokens.Clear()
+ scanner.ScanLine(line, tokens)
+
+ for token in tokens do
+ match token.Kind with
+ | LexicalKind.Comment
+ | LexicalKind.InactiveCode -> ()
+ | LexicalKind.String -> lastToken <- ValueSome literalPlaceholder
+ | _ when token.IsWithin line ->
+ let value = line.Substring(token.Start, token.Length)
+
+ if not (String.IsNullOrWhiteSpace value) then
+ match value with
+ | Opening -> openBrackets <- openBrackets + 1
+ | Closing -> openBrackets <- openBrackets - 1
+ | Ordinary -> ()
+
+ lastToken <- ValueSome value
+ | _ -> ()
+
+ {
+ OpenBrackets = openBrackets
+ InsideMultiLineConstruct = scanner.EndsInsideMultiLineConstruct
+ LastToken = lastToken
+ }
+
+ let isComplete (scanners: ILexicalScannerFactory) (text: string) =
+ if String.IsNullOrWhiteSpace text then
+ true
+ else
+ match scan scanners text with
+ | scanned when scanned.EndsWithTerminator -> true
+ | { InsideMultiLineConstruct = true } -> false
+ | scanned when scanned.OpenBrackets > 0 -> false
+ | { LastToken = ValueSome token } -> not (continuationTokens.Contains token)
+ | _ -> true
+
+ let withTerminator (scanners: ILexicalScannerFactory) (text: string) =
+ if (scan scanners text).EndsWithTerminator then
+ text
+ else
+ $"{text}\n;;"
diff --git a/vsintegration/src/FSharp.Interactive.Window/SubmissionOrigins.fs b/vsintegration/src/FSharp.Interactive.Window/SubmissionOrigins.fs
new file mode 100644
index 00000000000..ba9f0998a7a
--- /dev/null
+++ b/vsintegration/src/FSharp.Interactive.Window/SubmissionOrigins.fs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Interactive
+
+open System
+
+[]
+type internal SubmissionOrigins() =
+
+ let waiting = ResizeArray()
+
+ static let normalized (text: string) = text.Replace("\r\n", "\n").Trim()
+
+ member _.Add(text: string, sourcePath: string, startLine: int) =
+ lock waiting (fun () -> waiting.Add(struct (normalized text, sourcePath, startLine)))
+
+ /// Drops the origins of submissions sent before this one that never ran.
+ member _.Take(text: string) : struct (string * int) voption =
+ let key = normalized text
+
+ lock waiting (fun () ->
+ match waiting.FindIndex(fun (struct (queued, _, _)) -> String.Equals(queued, key, StringComparison.Ordinal)) with
+ | -1 -> ValueNone
+ | index ->
+ let struct (_, sourcePath, startLine) = waiting[index]
+ waiting.RemoveRange(0, index + 1)
+ ValueSome(struct (sourcePath, startLine)))
diff --git a/vsintegration/src/FSharp.VS.FSI/FSharp.VS.FSI.fsproj b/vsintegration/src/FSharp.VS.FSI/FSharp.VS.FSI.fsproj
index 5827d12b71a..768b647869f 100644
--- a/vsintegration/src/FSharp.VS.FSI/FSharp.VS.FSI.fsproj
+++ b/vsintegration/src/FSharp.VS.FSI/FSharp.VS.FSI.fsproj
@@ -13,6 +13,7 @@
+
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ShellInProcess_Debugging.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ShellInProcess_Debugging.cs
index 2dac220006f..1286541583a 100644
--- a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ShellInProcess_Debugging.cs
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ShellInProcess_Debugging.cs
@@ -8,6 +8,7 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
namespace Microsoft.VisualStudio.Extensibility.Testing;
@@ -117,6 +118,7 @@ public async Task GetConfiguredBreakpointLinesAsync(CancellationToken can
private static string BreakpointSummary(EnvDTE.Debugger dbg)
{
+ ThreadHelper.ThrowIfNotOnUIThread();
var items = new List();
foreach (EnvDTE.Breakpoint bp in dbg.Breakpoints)
{
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs
index 7b330eddc6f..957e4295d1b 100644
--- a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs
@@ -212,16 +212,14 @@ private async Task> WaitForBuildToFinishAsync(IVsOutputWindo
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var buildManager = await GetRequiredGlobalServiceAsync(cancellationToken);
- using var semaphore = new SemaphoreSlim(1);
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var solutionEvents = new UpdateSolutionEvents(buildManager);
- await semaphore.WaitAsync();
-
- void HandleUpdateSolutionDone(bool succeeded, bool modified, bool canceled) => semaphore.Release();
+ void HandleUpdateSolutionDone(bool succeeded, bool modified, bool canceled) => completion.TrySetResult(succeeded);
solutionEvents.OnUpdateSolutionDone += HandleUpdateSolutionDone;
try
{
- await semaphore.WaitAsync();
+ await completion.Task.WithCancellation(cancellationToken);
}
finally
{
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess_Files.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess_Files.cs
index f639decdb82..e64f373c394 100644
--- a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess_Files.cs
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess_Files.cs
@@ -66,7 +66,11 @@ private async Task GetAbsolutePathForProjectRelativeFilePathAsync(string
var dte = await GetRequiredGlobalServiceAsync(cancellationToken);
var solution = dte.Solution;
- var project = solution.Projects.Cast().First(x => x.Name == projectName);
+ var project = solution.Projects.Cast().First(x =>
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ return x.Name == projectName;
+ });
var projectPath = Path.GetDirectoryName(project.FullName);
return Path.Combine(projectPath, relativeFilePath);
}
@@ -77,11 +81,19 @@ private async Task GetAbsolutePathForProjectRelativeFilePathAsync(string
var solution = (await GetRequiredGlobalServiceAsync(cancellationToken)).Solution;
var projects = solution.Projects.Cast();
- var project = projects.FirstOrDefault(x => x.Name == projectName);
+ var project = projects.FirstOrDefault(x =>
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ return x.Name == projectName;
+ });
var projectPath = Path.GetDirectoryName(project.FullName);
var fullFilePath = Path.Combine(projectPath, relativeFilePath);
var projectItems = project.ProjectItems.Cast();
- var document = projectItems.FirstOrDefault(d => d.get_FileNames(1).Equals(fullFilePath));
+ var document = projectItems.FirstOrDefault(d =>
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ return d.get_FileNames(1).Equals(fullFilePath);
+ });
return document;
}