diff --git a/DotNetTry.sln b/DotNetTry.sln
index 67167e9b9..ce79c7ad8 100644
--- a/DotNetTry.sln
+++ b/DotNetTry.sln
@@ -53,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Try.Projec
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ProjectTemplate", "Microsoft.DotNet.Try.ProjectTemplate\Tutorial\Microsoft.DotNet.ProjectTemplate.csproj", "{E047D81A-7A18-4A1A-98E8-EDBB51EBB7DB}"
EndProject
+Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpWorkspaceShim", "FSharpWorkspaceShim\FSharpWorkspaceShim.fsproj", "{9128FCED-2A19-4502-BCEE-BE1BAB6882EB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -151,6 +153,10 @@ Global
{E047D81A-7A18-4A1A-98E8-EDBB51EBB7DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E047D81A-7A18-4A1A-98E8-EDBB51EBB7DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E047D81A-7A18-4A1A-98E8-EDBB51EBB7DB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9128FCED-2A19-4502-BCEE-BE1BAB6882EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9128FCED-2A19-4502-BCEE-BE1BAB6882EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9128FCED-2A19-4502-BCEE-BE1BAB6882EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9128FCED-2A19-4502-BCEE-BE1BAB6882EB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -179,6 +185,7 @@ Global
{11FDD0E8-D07B-41C9-AF7E-E7F735D91ECF} = {8192FEAD-BCE6-4E62-97E5-2E9EA884BD71}
{1F1A7554-1E88-4514-8602-EC00899E0C49} = {8192FEAD-BCE6-4E62-97E5-2E9EA884BD71}
{E047D81A-7A18-4A1A-98E8-EDBB51EBB7DB} = {6EE8F484-DFA2-4F0F-939F-400CE78DFAC2}
+ {9128FCED-2A19-4502-BCEE-BE1BAB6882EB} = {6EE8F484-DFA2-4F0F-939F-400CE78DFAC2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D6CD99BA-B16B-4570-8910-225CBDFFA3AD}
diff --git a/FSharpWorkspaceShim/FSharpWorkspaceShim.fsproj b/FSharpWorkspaceShim/FSharpWorkspaceShim.fsproj
new file mode 100644
index 000000000..5a97f9240
--- /dev/null
+++ b/FSharpWorkspaceShim/FSharpWorkspaceShim.fsproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ $(NoWarn);2003
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FSharpWorkspaceShim/Library.fs b/FSharpWorkspaceShim/Library.fs
new file mode 100644
index 000000000..3a9808ce3
--- /dev/null
+++ b/FSharpWorkspaceShim/Library.fs
@@ -0,0 +1,130 @@
+namespace FSharpWorkspaceShim
+
+open System
+open System.IO
+open FSharp.Compiler.SourceCodeServices
+open Microsoft.CodeAnalysis
+open Microsoft.CodeAnalysis.Text
+
+module Shim =
+
+ let private checker = FSharpChecker.Create()
+
+ let private getIndex (text: string) (line: int) (column: int) =
+ let mutable index = -1
+ let mutable currentLine = 0
+ let mutable currentColumn = 0
+ text.ToCharArray()
+ |> Array.iteri (fun i c ->
+ if line = currentLine && column = currentColumn then index <- i
+ match c with
+ | '\n' ->
+ currentLine <- currentLine + 1
+ currentColumn <- 0
+ | _ -> currentColumn <- currentColumn + 1)
+ index
+
+ let private newlineProxy = System.String [|char 29|]
+
+ // adapted from https://github.com/dotnet/fsharp/blob/master/src/fsharp/ErrorLogger.fs
+ let private normalizeErrorString (text : string) =
+ if isNull text then nullArg "text"
+ let text = text.Trim()
+
+ let buf = System.Text.StringBuilder()
+ let mutable i = 0
+ while i < text.Length do
+ let delta =
+ match text.[i] with
+ | '\r' when i + 1 < text.Length && text.[i + 1] = '\n' ->
+ // handle \r\n sequence - replace it with one single space
+ buf.Append newlineProxy |> ignore
+ 2
+ | '\n' | '\r' ->
+ buf.Append newlineProxy |> ignore
+ 1
+ | c ->
+ // handle remaining chars: control - replace with space, others - keep unchanged
+ let c = if Char.IsControl c then ' ' else c
+ buf.Append c |> ignore
+ 1
+ i <- i + delta
+ buf.ToString()
+
+ let private newlineifyErrorString (message:string) = message.Replace(newlineProxy, Environment.NewLine)
+
+ // adapted from https://github.com/dotnet/fsharp/blob/master/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs
+ let private convertError (error: FSharpErrorInfo) (location: Location) =
+ // Normalize the error message into the same format that we will receive it from the compiler.
+ // This ensures that IntelliSense and Compiler errors in the 'Error List' are de-duplicated.
+ // (i.e the same error does not appear twice, where the only difference is the line endings.)
+ let normalizedMessage = error.Message |> normalizeErrorString |> newlineifyErrorString
+
+ let id = "FS" + error.ErrorNumber.ToString("0000")
+ let emptyString = LocalizableString.op_Implicit("")
+ let description = LocalizableString.op_Implicit(normalizedMessage)
+ let severity = if error.Severity = FSharpErrorSeverity.Error then DiagnosticSeverity.Error else DiagnosticSeverity.Warning
+ let customTags =
+ match error.ErrorNumber with
+ | 1182 -> WellKnownDiagnosticTags.Unnecessary
+ | _ -> null
+ let descriptor = new DiagnosticDescriptor(id, emptyString, description, error.Subcategory, severity, true, emptyString, String.Empty, customTags)
+ Diagnostic.Create(descriptor, location)
+
+ let GetDiagnostics (projectPath: string) (files: string[]) (pathMapSource: string) (pathMapDest: string) =
+ async {
+ let projectOptions = {
+ ProjectFileName = projectPath
+ ProjectId = None
+ SourceFiles = files
+ OtherOptions = [||]
+ ReferencedProjects = [||]
+ IsIncompleteTypeCheckEnvironment = false
+ UseScriptResolutionRules = false
+ LoadTime = DateTime.Now
+ UnresolvedReferences = None
+ OriginalLoadReferences = []
+ ExtraProjectInfo = None
+ Stamp = None
+ }
+ let ensureDirectorySeparator (path: string) =
+ if path.EndsWith(Path.DirectorySeparatorChar |> string) |> not then path + (string Path.DirectorySeparatorChar)
+ else path
+ let pathMapSource = ensureDirectorySeparator pathMapSource
+ let pathMapDest = ensureDirectorySeparator pathMapDest
+ let! results = checker.ParseAndCheckProject projectOptions
+ // adapted from from https://github.com/dotnet/fsharp/blob/master/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs
+ let diagnostics =
+ results.Errors
+ |> Seq.choose (fun error ->
+ if error.StartLineAlternate = 0 || error.EndLineAlternate = 0 then
+ // F# error line numbers are one-based. Compiler returns 0 for global errors (reported by ProjectDiagnosticAnalyzer)
+ None
+ else
+ // Roslyn line numbers are zero-based
+ let linePositionSpan = LinePositionSpan(LinePosition(error.StartLineAlternate - 1, error.StartColumn), LinePosition(error.EndLineAlternate - 1, error.EndColumn))
+ let text = File.ReadAllText(error.FileName)
+ let textSpan =
+ TextSpan.FromBounds(
+ getIndex text (error.StartLineAlternate - 1) error.StartColumn,
+ getIndex text (error.EndLineAlternate - 1) error.EndColumn)
+
+ // F# compiler report errors at end of file if parsing fails. It should be corrected to match Roslyn boundaries
+ let correctedTextSpan =
+ if textSpan.End <= text.Length then
+ textSpan
+ else
+ let start =
+ min textSpan.Start (text.Length - 1)
+ |> max 0
+
+ TextSpan.FromBounds(start, text.Length)
+
+ let filePath =
+ if error.FileName.StartsWith(pathMapSource) then String.Concat(pathMapDest, error.FileName.Substring(pathMapSource.Length))
+ else error.FileName
+ let location = Location.Create(filePath, correctedTextSpan, linePositionSpan)
+ Some(convertError error location))
+ |> Seq.toArray
+ return diagnostics
+ } |> Async.StartAsTask
diff --git a/MLS.Agent.Tests/ApiContracts/ApiInputContractChangeTests.cs b/MLS.Agent.Tests/ApiContracts/ApiInputContractChangeTests.cs
index 0d650486e..3cf68fabe 100644
--- a/MLS.Agent.Tests/ApiContracts/ApiInputContractChangeTests.cs
+++ b/MLS.Agent.Tests/ApiContracts/ApiInputContractChangeTests.cs
@@ -28,14 +28,14 @@ public async Task Changing_the_completion_request_format_does_not_change_the_res
""files"": [],
""buffers"": [
{{
- ""id"": """",
+ ""id"": ""file.cs"",
""content"": ""using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\npublic class Program\n{{\n public static void Main()\n {{\n foreach (var i in Fibonacci().Take(20))\n {{\n Console.\n }}\n }}\n\n private static IEnumerable Fibonacci()\n {{\n int current = 1, next = 1;\n\n while (true) \n {{\n yield return current;\n next = current + (current = next);\n }}\n }}\n}}\n"",
""position"": 0
}}
],
""usings"": []
}},
- ""activeBufferId"": """",
+ ""activeBufferId"": ""file.cs"",
""position"": 187
}}";
@@ -46,14 +46,14 @@ public async Task Changing_the_completion_request_format_does_not_change_the_res
""files"": [],
""buffers"": [
{{
- ""id"": """",
+ ""id"": ""file.cs"",
""content"": ""using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\npublic class Program\n{{\n public static void Main()\n {{\n foreach (var i in Fibonacci().Take(20))\n {{\n Console.\n }}\n }}\n\n private static IEnumerable Fibonacci()\n {{\n int current = 1, next = 1;\n\n while (true) \n {{\n yield return current;\n next = current + (current = next);\n }}\n }}\n}}\n"",
""position"": 187
}}
],
""usings"": []
}},
- ""activeBufferId"": """"
+ ""activeBufferId"": ""file.cs""
}}";
var responseToOldFormatRequest = await CallCompletion(oldFormatRequest);
@@ -83,14 +83,14 @@ public async Task Changing_the_signature_help_request_format_does_not_change_the
""files"": [],
""buffers"": [
{{
- ""id"": """",
+ ""id"": ""file.cs"",
""content"": ""using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\npublic class Program\n{{\n public static void Main()\n {{\n foreach (var i in Fibonacci().Take(20))\n {{\n Console.WriteLine()\n }}\n }}\n\n private static IEnumerable Fibonacci()\n {{\n int current = 1, next = 1;\n\n while (true) \n {{\n yield return current;\n next = current + (current = next);\n }}\n }}\n}}\n"",
""position"": 0
}}
],
""usings"": []
}},
- ""activeBufferId"": """",
+ ""activeBufferId"": ""file.cs"",
""position"": 197
}}";
@@ -101,14 +101,14 @@ public async Task Changing_the_signature_help_request_format_does_not_change_the
""files"": [],
""buffers"": [
{{
- ""id"": """",
+ ""id"": ""file.cs"",
""content"": ""using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\npublic class Program\n{{\n public static void Main()\n {{\n foreach (var i in Fibonacci().Take(20))\n {{\n Console.WriteLine()\n }}\n }}\n\n private static IEnumerable Fibonacci()\n {{\n int current = 1, next = 1;\n\n while (true) \n {{\n yield return current;\n next = current + (current = next);\n }}\n }}\n}}\n"",
""position"": 197
}}
],
""usings"": []
}},
- ""activeBufferId"": """"
+ ""activeBufferId"": ""file.cs""
}}";
var responseToOldFormatRequest = await CallSignatureHelp(oldFormatRequest);
diff --git a/MLS.Agent.Tests/ApiViaHttpTests.cs b/MLS.Agent.Tests/ApiViaHttpTests.cs
index 4dda58523..77ec580cd 100644
--- a/MLS.Agent.Tests/ApiViaHttpTests.cs
+++ b/MLS.Agent.Tests/ApiViaHttpTests.cs
@@ -357,7 +357,8 @@ public static IEnumerable Fibonacci()
new WorkspaceRequest(activeBufferId: "generators/FibonacciGenerator.cs",
requestId: "TestRun",
workspace: Workspace.FromSources(
- "console",
+ workspaceType:"console",
+ language:"csharp",
("Program.cs", program, 0),
("generators/FibonacciGenerator.cs", processed, position)
)).ToJson();
@@ -434,6 +435,7 @@ public static IEnumerable Fibonacci()
requestId: "TestRun",
workspace: Workspace.FromSources(
"console",
+ language: "csharp",
("Program.cs", program, 0),
("generators/FibonacciGenerator.cs", processed, position)
)).ToJson();
@@ -511,6 +513,7 @@ public static IEnumerable Fibonacci()
requestId: "TestRun",
workspace: Workspace.FromSources(
package.Name,
+ language: "csharp",
("Program.cs", program, 0),
("generators/FibonacciGenerator.cs", processed, position)
)).ToJson();
@@ -588,6 +591,7 @@ public static IEnumerable Fibonacci()
requestId: "TestRun",
workspace: Workspace.FromSources(
"console",
+ language: "csharp",
("Program.cs", program, 0),
("generators/FibonacciGenerator.cs", processed, position)
)).ToJson();
diff --git a/MLS.Agent/CommandLine/VerifyCommand.cs b/MLS.Agent/CommandLine/VerifyCommand.cs
index a421a63f4..693dc66a1 100644
--- a/MLS.Agent/CommandLine/VerifyCommand.cs
+++ b/MLS.Agent/CommandLine/VerifyCommand.cs
@@ -12,7 +12,7 @@
using Microsoft.DotNet.Try.Protocol;
using MLS.Agent.Markdown;
using WorkspaceServer;
-using WorkspaceServer.Servers.Roslyn;
+using WorkspaceServer.Servers;
using Buffer = Microsoft.DotNet.Try.Protocol.Buffer;
using File = Microsoft.DotNet.Try.Protocol.File;
@@ -33,7 +33,7 @@ public static async Task Do(
packageRegistry,
startupOptions);
var errorCount = 0;
- var workspaceServer = new Lazy(() => new RoslynWorkspaceServer(packageRegistry));
+ var workspaceServer = new Lazy(() => new WorkspaceServerMultiplexer(packageRegistry));
var markdownFiles = markdownProject.GetAllMarkdownFiles().ToArray();
diff --git a/MLS.Agent/Controllers/CompileController.cs b/MLS.Agent/Controllers/CompileController.cs
index cc73a0736..e53b2a925 100644
--- a/MLS.Agent/Controllers/CompileController.cs
+++ b/MLS.Agent/Controllers/CompileController.cs
@@ -8,7 +8,7 @@
using Microsoft.DotNet.Try.Protocol;
using MLS.Agent.Middleware;
using Pocket;
-using WorkspaceServer.Servers.Roslyn;
+using WorkspaceServer.Servers;
using static Pocket.Logger;
namespace MLS.Agent.Controllers
@@ -19,11 +19,11 @@ public class CompileController : Controller
public static RequestDescriptor CompileApi => new RequestDescriptor(CompileRoute, timeoutMs: 600000);
- private readonly RoslynWorkspaceServer _workspaceServer;
+ private readonly IWorkspaceServer _workspaceServer;
private readonly CompositeDisposable _disposables = new CompositeDisposable();
public CompileController(
- RoslynWorkspaceServer workspaceServer)
+ IWorkspaceServer workspaceServer)
{
_workspaceServer = workspaceServer;
}
diff --git a/MLS.Agent/Controllers/LanguageServicesController.cs b/MLS.Agent/Controllers/LanguageServicesController.cs
index 74f47a1c5..4280c8b23 100644
--- a/MLS.Agent/Controllers/LanguageServicesController.cs
+++ b/MLS.Agent/Controllers/LanguageServicesController.cs
@@ -10,7 +10,7 @@
using MLS.Agent.Middleware;
using Pocket;
using WorkspaceServer;
-using WorkspaceServer.Servers.Roslyn;
+using WorkspaceServer.Servers;
using static Pocket.Logger;
namespace MLS.Agent.Controllers
@@ -33,9 +33,9 @@ public class LanguageServicesController : Controller
public static RequestDescriptor SignatureHelpApi => new RequestDescriptor(SignatureHelpRoute, timeoutMs: 60000);
private readonly CompositeDisposable _disposables = new CompositeDisposable();
- private readonly RoslynWorkspaceServer _workspaceServer;
+ private readonly IWorkspaceServer _workspaceServer;
- public LanguageServicesController(RoslynWorkspaceServer workspaceServer)
+ public LanguageServicesController(IWorkspaceServer workspaceServer)
{
_workspaceServer = workspaceServer ?? throw new ArgumentNullException(nameof(workspaceServer));
}
diff --git a/MLS.Agent/Controllers/ProjectController.cs b/MLS.Agent/Controllers/ProjectController.cs
index 10723a98b..b0625dc2d 100644
--- a/MLS.Agent/Controllers/ProjectController.cs
+++ b/MLS.Agent/Controllers/ProjectController.cs
@@ -38,7 +38,8 @@ public IActionResult GenerateRegionsFromFiles([FromBody] CreateRegionsFromFilesR
private static IEnumerable ExtractRegions(SourceFile sourceFile)
{
var sc = SourceText.From(sourceFile.Content);
- var regions = sc.ExtractRegions(sourceFile.Name).Select(region => new SourceFileRegion(region.id, region.content)).ToArray();
+ var regions = sc.ExtractRegions(sourceFile.Name).Select(
+ region => new SourceFileRegion(region.bufferId.ToString(), sc.ToString(region.span).FormatSourceCode(sourceFile.Name))).ToArray();
return regions;
}
diff --git a/MLS.Agent/Controllers/RunController.cs b/MLS.Agent/Controllers/RunController.cs
index e9e677fea..b263e6b49 100644
--- a/MLS.Agent/Controllers/RunController.cs
+++ b/MLS.Agent/Controllers/RunController.cs
@@ -8,13 +8,12 @@
using Microsoft.DotNet.Try.Protocol;
using MLS.Agent.Middleware;
using Pocket;
-using WorkspaceServer;
using WorkspaceServer.Models.Execution;
-using WorkspaceServer.Servers.Roslyn;
using WorkspaceServer.Servers.Scripting;
using WorkspaceServer.Features;
using static Pocket.Logger;
using MLS.Agent.CommandLine;
+using WorkspaceServer.Servers;
namespace MLS.Agent.Controllers
{
@@ -24,12 +23,12 @@ public class RunController : Controller
public static RequestDescriptor RunApi => new RequestDescriptor(RunRoute, timeoutMs:600000);
private readonly StartupOptions _options;
- private readonly RoslynWorkspaceServer _workspaceServer;
+ private readonly IWorkspaceServer _workspaceServer;
private readonly CompositeDisposable _disposables = new CompositeDisposable();
public RunController(
StartupOptions options,
- RoslynWorkspaceServer workspaceServer)
+ IWorkspaceServer workspaceServer)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_workspaceServer = workspaceServer;
diff --git a/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs b/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs
index f9a92de56..b1e33ed32 100644
--- a/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs
+++ b/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs
@@ -22,10 +22,10 @@ public LocalCodeFenceAnnotationsParser(
PackageRegistry packageRegistry,
IDefaultCodeBlockAnnotations defaultAnnotations = null) : base(defaultAnnotations,
csharp =>
- {
- AddCsharpProjectOption(csharp, directoryAccessor);
- AddSourceFileOption(csharp);
- },
+ {
+ AddCsharpProjectOption(csharp, directoryAccessor);
+ AddSourceFileOption(csharp);
+ },
fsharp =>
{
AddFsharpProjectOption(fsharp, directoryAccessor);
diff --git a/MLS.Agent/Program.cs b/MLS.Agent/Program.cs
index fc3139d0d..f82a3d98b 100644
--- a/MLS.Agent/Program.cs
+++ b/MLS.Agent/Program.cs
@@ -12,18 +12,16 @@
using Recipes;
using Serilog.Sinks.RollingFileAlternate;
using System;
-using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.DotNet.Try.Jupyter;
-using WorkspaceServer.Servers.Roslyn;
using static Pocket.Logger;
using SerilogLoggerConfiguration = Serilog.LoggerConfiguration;
-using WorkspaceServer;
using MLS.Agent.CommandLine;
+using WorkspaceServer.Servers;
namespace MLS.Agent
{
@@ -45,7 +43,7 @@ public static X509Certificate2 ParseKey(string base64EncodedKey)
private static readonly Assembly[] assembliesEmittingPocketLoggerLogs = {
typeof(Startup).Assembly,
typeof(AsyncLazy<>).Assembly,
- typeof(RoslynWorkspaceServer).Assembly,
+ typeof(IWorkspaceServer).Assembly,
typeof(Shell).Assembly
};
diff --git a/MLS.Agent/Startup.cs b/MLS.Agent/Startup.cs
index c4d24c39e..88ec21cea 100644
--- a/MLS.Agent/Startup.cs
+++ b/MLS.Agent/Startup.cs
@@ -26,7 +26,7 @@
using Pocket;
using Recipes;
using WorkspaceServer;
-using WorkspaceServer.Servers.Roslyn;
+using WorkspaceServer.Servers;
using static Pocket.Logger;
using IApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime;
using IHostingEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment;
@@ -80,7 +80,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton(Configuration);
- services.AddSingleton(c => new RoslynWorkspaceServer(c.GetRequiredService()));
+ services.AddSingleton(c => new WorkspaceServerMultiplexer(c.GetRequiredService()));
services.TryAddSingleton(c => new BrowserLauncher());
diff --git a/Microsoft.DotNet.Try.Jupyter/JupyterRequestContextHandler.cs b/Microsoft.DotNet.Try.Jupyter/JupyterRequestContextHandler.cs
index 664d1393a..56f18bc5d 100644
--- a/Microsoft.DotNet.Try.Jupyter/JupyterRequestContextHandler.cs
+++ b/Microsoft.DotNet.Try.Jupyter/JupyterRequestContextHandler.cs
@@ -9,7 +9,7 @@
using Microsoft.DotNet.Try.Protocol;
using Newtonsoft.Json.Linq;
using WorkspaceServer;
-using WorkspaceServer.Servers.Roslyn;
+using WorkspaceServer.Servers;
using Buffer = Microsoft.DotNet.Try.Protocol.Buffer;
namespace Microsoft.DotNet.Try.Jupyter
@@ -50,7 +50,7 @@ public async Task Handle(
var workspaceRequest = new WorkspaceRequest(workspace);
- var server = new RoslynWorkspaceServer(new PackageRegistry());
+ var server = new WorkspaceServerMultiplexer(new PackageRegistry());
var result = await server.Run(workspaceRequest);
diff --git a/Microsoft.DotNet.Try.Project.Tests/BufferCreationTests.cs b/Microsoft.DotNet.Try.Project.Tests/BufferCreationTests.cs
index cebe0e60b..61f39c758 100644
--- a/Microsoft.DotNet.Try.Project.Tests/BufferCreationTests.cs
+++ b/Microsoft.DotNet.Try.Project.Tests/BufferCreationTests.cs
@@ -65,5 +65,17 @@ public void can_create_buffer_with_markup()
buffer.Content.Should().Be("Console.WriteLine();");
buffer.AbsolutePosition.Should().Be(18);
}
+
+ [Fact]
+ public void Can_create_buffer_from_fsharp_file()
+ {
+ var file = FileGenerator.Create("Program.fs", SourceCodeProvider.FSharpConsoleProgramMultipleRegions);
+ var buffers = BufferGenerator.CreateBuffers(file).ToList();
+
+ buffers.Should().NotBeNullOrEmpty();
+ buffers.Count.Should().Be(2);
+ buffers.Should().Contain(b => b.Id == "Program.fs@alpha");
+ buffers.Should().Contain(b => b.Id == "Program.fs@beta");
+ }
}
}
diff --git a/Microsoft.DotNet.Try.Project.Tests/BufferExtractorTests.cs b/Microsoft.DotNet.Try.Project.Tests/BufferExtractorTests.cs
index e3d950378..396981778 100644
--- a/Microsoft.DotNet.Try.Project.Tests/BufferExtractorTests.cs
+++ b/Microsoft.DotNet.Try.Project.Tests/BufferExtractorTests.cs
@@ -69,5 +69,25 @@ public void it_generates_content_with_correct_indentation()
result.Buffers.First().Content.Replace("\r\n", "\n").Should().Be(expectedCode);
}
+
+ [Fact]
+ public void it_generates_a_file_and_buffers_workspace_from_fsharp()
+ {
+ var files = new[]
+ {
+ new File("Program.fs", SourceCodeProvider.FSharpConsoleProgramMultipleRegions),
+ };
+
+ var transformer = new BufferFromRegionExtractor();
+ var result = transformer.Extract(files, workspaceType: "console");
+ result.Should().NotBeNull();
+
+ result.Buffers.Should().Contain(found => found.Id == "Program.fs@alpha" && found.Content == "let sum = numbers |> Seq.sum");
+
+ // ensure buffer lines were dedented to the level of the `//#region` marker
+ result.Buffers.Should().Contain(found => found.Id == "Program.fs@beta" && found.Content.EnforceLF() == "printfn \"The sum was %d\" sum\nprintfn \"goodbye\"");
+ result.Files.Should().NotBeNullOrEmpty();
+ result.Files.Should().Contain(found => found.Name == "Program.fs" && found.Text == SourceCodeProvider.FSharpConsoleProgramMultipleRegions);
+ }
}
}
diff --git a/Microsoft.DotNet.Try.Project.Tests/BufferInliningTransformerTests.cs b/Microsoft.DotNet.Try.Project.Tests/BufferInliningTransformerTests.cs
index cfeb8142b..da7642818 100644
--- a/Microsoft.DotNet.Try.Project.Tests/BufferInliningTransformerTests.cs
+++ b/Microsoft.DotNet.Try.Project.Tests/BufferInliningTransformerTests.cs
@@ -300,5 +300,46 @@ public static void Main(){{
processed.Files[0].Text.EnforceLF().Should().Be(expectedFileContent);
}
}
+
+ [Fact]
+ public async Task FSharp_buffer_can_be_injected_into_region()
+ {
+ var original = new Workspace(
+ files: new[]
+ {
+ new Protocol.File("Program.fs", SourceCodeProvider.FSharpConsoleProgramMultipleRegions)
+ },
+ buffers: new[]
+ {
+ // original:
+ // | let sum = numbers |> Seq.sum
+ // with newlines:
+ // | let sum =
+ // | numbers
+ // | |> Seq.sum
+ // e.g., the buffer lines are indented 0, 4, 4 spaces while the resultant backing file
+ // should be indented 4, 8, 8.
+ new Buffer("Program.fs@alpha",
+@"let sum =
+ numbers
+ |> Seq.sum".EnforceLF())
+ });
+ var processor = new FSharpBufferInliningTransformer();
+
+ var processed = await processor.TransformAsync(original);
+ processed.Should().NotBeNull();
+ processed.Files.Should().NotBeEmpty();
+ var newCode = processed.Files.ElementAt(0).Text;
+
+ newCode.Should().NotBe(original.Files.ElementAt(0).Text);
+ newCode.EnforceLF().Should().Contain(
+@"
+ //#region alpha
+ let sum =
+ numbers
+ |> Seq.sum
+ //#endregion
+".EnforceLF());
+ }
}
}
\ No newline at end of file
diff --git a/Microsoft.DotNet.Try.Project/BufferInliningTransformer.cs b/Microsoft.DotNet.Try.Project/BufferInliningTransformer.cs
index 24ade3d93..f5712d1a8 100644
--- a/Microsoft.DotNet.Try.Project/BufferInliningTransformer.cs
+++ b/Microsoft.DotNet.Try.Project/BufferInliningTransformer.cs
@@ -33,7 +33,7 @@ public async Task TransformAsync(Workspace source)
includeInstrumentation: source.IncludeInstrumentation);
}
- private static async Task<(Protocol.File[] files, Buffer[] buffers)> InlineBuffersAsync(Workspace source)
+ protected async Task<(Protocol.File[] files, Buffer[] buffers)> InlineBuffersAsync(Workspace source)
{
var files = (source.Files ?? Array.Empty()).ToDictionary(f => f.Name, f =>
{
@@ -81,7 +81,7 @@ public async Task TransformAsync(Workspace source)
return (processedFiles, processedBuffers);
}
- private static Task InjectBuffer(Viewport viewPort, Buffer sourceBuffer, ICollection buffers, IDictionary files,
+ protected Task InjectBuffer(Viewport viewPort, Buffer sourceBuffer, ICollection buffers, IDictionary files,
BufferInjectionPoints bufferIdInjectionPoints)
{
TextSpan targetSpan;
@@ -110,7 +110,7 @@ private static TextSpan CreateTextSpanBefore(TextSpan viewPortRegion)
return new TextSpan(viewPortRegion.Start, 0);
}
- private static async Task InjectBufferAtSpan(Viewport viewPort, Buffer sourceBuffer, ICollection buffers, IDictionary files, TextSpan span)
+ protected virtual async Task InjectBufferAtSpan(Viewport viewPort, Buffer sourceBuffer, ICollection buffers, IDictionary files, TextSpan span)
{
var tree = CSharpSyntaxTree.ParseText(viewPort.Destination.Text.ToString());
var textChange = new TextChange(
diff --git a/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs b/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs
index 83a39547d..e959df92b 100644
--- a/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs
+++ b/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs
@@ -25,9 +25,13 @@ public static SerializableDiagnostic ToSerializableDiagnostic(
var startPosition = diagnostic.Location.GetLineSpan().Span.Start;
+ var diagnosticFilePath = diagnostic?.Location.SourceTree?.FilePath
+ ?? bufferId?.FileName // F# doesn't have a source tree
+ ?? diagnostic?.Location.GetLineSpan().Path;
+
var location =
diagnostic.Location != null
- ? $"{diagnostic.Location.SourceTree?.FilePath}({startPosition.Line + 1},{startPosition.Character + 1}): {GetMessagePrefix()}"
+ ? $"{diagnosticFilePath}({startPosition.Line + 1},{startPosition.Character + 1}): {GetMessagePrefix()}"
: null;
return new SerializableDiagnostic(diagnostic.Location?.SourceSpan.Start ?? throw new ArgumentException(nameof(diagnostic.Location)),
diff --git a/Microsoft.DotNet.Try.Project/FSharpBufferInliningTransformer.cs b/Microsoft.DotNet.Try.Project/FSharpBufferInliningTransformer.cs
new file mode 100644
index 000000000..f17ecb392
--- /dev/null
+++ b/Microsoft.DotNet.Try.Project/FSharpBufferInliningTransformer.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Text;
+using Buffer = Microsoft.DotNet.Try.Protocol.Buffer;
+
+namespace Microsoft.DotNet.Try.Project
+{
+ public class FSharpBufferInliningTransformer : BufferInliningTransformer
+ {
+ protected override Task InjectBufferAtSpan(Viewport viewPort, Buffer sourceBuffer, ICollection buffers, IDictionary files, TextSpan span)
+ {
+ var replacementPosition = viewPort.Destination.Text.Lines.GetLinePosition(viewPort.OuterRegion.Start);
+ var indentLevel = replacementPosition.Character;
+ var indentText = new string(' ', indentLevel);
+ var indentedLines = sourceBuffer.Content.Split('\n').Select(l => indentText + l).ToList();
+ var indentedText = string.Join("\n", indentedLines);
+ var textChange = new TextChange(span, indentedText);
+ var newText = viewPort.Destination.Text.WithChanges(textChange);
+ buffers.Add(new Buffer(
+ sourceBuffer.Id,
+ sourceBuffer.Content,
+ sourceBuffer.Position,
+ span.Start));
+ files[viewPort.Destination.Name] = SourceFile.Create(newText.ToString(), viewPort.Destination.Name);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Microsoft.DotNet.Try.Project/FileExtensions.cs b/Microsoft.DotNet.Try.Project/FileExtensions.cs
index cd3c5bbab..eb6be6ae4 100644
--- a/Microsoft.DotNet.Try.Project/FileExtensions.cs
+++ b/Microsoft.DotNet.Try.Project/FileExtensions.cs
@@ -4,11 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Text;
using Microsoft.DotNet.Try.Protocol;
-using Workspace = Microsoft.DotNet.Try.Protocol.Workspace;
namespace Microsoft.DotNet.Try.Project
{
@@ -26,13 +22,18 @@ public static IEnumerable ExtractViewPorts(this File file)
public static IEnumerable ExtractViewPorts(this SourceFile sourceFile)
{
-
var code = sourceFile.Text;
var fileName = sourceFile.Name;
- var regions = ExtractRegions(code, fileName);
+ var regions = code.ExtractRegions(fileName);
+ var seenBuffers = new HashSet();
foreach (var region in regions)
{
+ if (!seenBuffers.Add(region.bufferId.ToString()))
+ {
+ throw new InvalidOperationException("viewport identifiers must be unique");
+ }
+
yield return new Viewport(sourceFile, region.span, region.outerSpan, region.bufferId);
}
}
@@ -46,69 +47,5 @@ public static IEnumerable ExtractViewports(this IEnumerable f.ExtractViewPorts());
}
-
- private static IEnumerable<(BufferId bufferId, TextSpan span, TextSpan outerSpan)> ExtractRegions(SourceText code, string fileName)
- {
- var ids = new HashSet();
- IEnumerable<(SyntaxTrivia startRegion, SyntaxTrivia endRegion, BufferId bufferId)> FindRegions(SyntaxNode syntaxNode)
- {
- var nodesWithRegionDirectives =
- from node in syntaxNode.DescendantNodesAndTokens()
- where node.HasLeadingTrivia
- from leadingTrivia in node.GetLeadingTrivia()
- where leadingTrivia.Kind() == SyntaxKind.RegionDirectiveTrivia ||
- leadingTrivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia
- select node;
-
- var stack = new Stack();
- var processedSpans = new HashSet();
-
- foreach (var nodeWithRegionDirective in nodesWithRegionDirectives)
- {
- var triviaList = nodeWithRegionDirective.GetLeadingTrivia();
-
- foreach (var currentTrivia in triviaList)
- {
- if (processedSpans.Add(currentTrivia.FullSpan))
- {
- if (currentTrivia.Kind() == SyntaxKind.RegionDirectiveTrivia)
- {
- stack.Push(currentTrivia);
- }
- else if (currentTrivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia && stack.Count > 0)
- {
- var start = stack.Pop();
- var regionName = start.ToFullString().Replace("#region", string.Empty).Trim();
- yield return (start, currentTrivia, new BufferId(fileName, regionName));
- }
- }
- }
- }
- }
-
- var sourceCodeText = code.ToString();
- var root = CSharpSyntaxTree.ParseText(sourceCodeText).GetRoot();
-
- foreach (var (startRegion, endRegion, label) in FindRegions(root))
- {
- var innerStart = startRegion.GetLocation().SourceSpan.End;
-
- var innerLength = endRegion.GetLocation().SourceSpan.Start -
- startRegion.GetLocation().SourceSpan.End;
-
- var innerLoc = new TextSpan(innerStart, innerLength);
-
- var outerStart = startRegion.GetLocation().SourceSpan.Start;
- var outerLength = endRegion.GetLocation().SourceSpan.End -
- startRegion.GetLocation().SourceSpan.Start;
- var outerLoc = new TextSpan(outerStart, outerLength);
-
- if (!ids.Add(label.RegionName))
- {
- throw new InvalidOperationException("viewports identifiers must be unique");
- }
- yield return (label, innerLoc, outerLoc);
- }
- }
}
}
\ No newline at end of file
diff --git a/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs b/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs
index 83c99643a..77317f7e5 100644
--- a/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs
+++ b/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
@@ -8,16 +9,37 @@
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using Microsoft.DotNet.Try.Protocol;
-using Workspace = Microsoft.DotNet.Try.Protocol.Workspace;
+using Buffer = Microsoft.DotNet.Try.Protocol.Buffer;
+using InvalidOperationException = System.InvalidOperationException;
+using Path = System.IO.Path;
namespace Microsoft.DotNet.Try.Project
{
public static class SourceTextExtensions
{
- public static IEnumerable<(string id, string content)> ExtractRegions(this SourceText code, string fileName)
+ private const string FSharpRegionStart = "//#region";
+ private const string FSharpRegionEnd = "//#endregion";
+
+ public static IEnumerable<(BufferId bufferId, TextSpan span, TextSpan outerSpan)> ExtractRegions(this SourceText code, string fileName)
+ {
+ var extension = Path.GetExtension(fileName);
+ switch (extension)
+ {
+ case ".cs":
+ case ".csx":
+ return ExtractRegionsCSharp(code, fileName);
+ case ".fs":
+ case ".fsx":
+ return ExtractRegionsFSharp(code, fileName);
+ default:
+ throw new InvalidOperationException($"Unsupported file extension '{extension}'");
+ }
+ }
+
+ private static IEnumerable<(BufferId bufferId, TextSpan span, TextSpan outerSpan)> ExtractRegionsCSharp(SourceText code, string fileName)
{
- List<(SyntaxTrivia startRegion, SyntaxTrivia endRegion, string label)> FindRegions(SyntaxNode syntaxNode)
+ IEnumerable<(SyntaxTrivia startRegion, SyntaxTrivia endRegion, BufferId bufferId)> FindRegions(SyntaxNode syntaxNode)
{
var nodesWithRegionDirectives =
from node in syntaxNode.DescendantNodesAndTokens()
@@ -27,121 +49,135 @@ where leadingTrivia.Kind() == SyntaxKind.RegionDirectiveTrivia ||
leadingTrivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia
select node;
- var regions = new List<(SyntaxTrivia startRegion, SyntaxTrivia endRegion, string label)>();
var stack = new Stack();
var processedSpans = new HashSet();
+
foreach (var nodeWithRegionDirective in nodesWithRegionDirectives)
{
var triviaList = nodeWithRegionDirective.GetLeadingTrivia();
foreach (var currentTrivia in triviaList)
{
- if (!processedSpans.Add(currentTrivia.FullSpan)) continue;
-
- if (currentTrivia.Kind() == SyntaxKind.RegionDirectiveTrivia)
+ if (processedSpans.Add(currentTrivia.FullSpan))
{
- stack.Push(currentTrivia);
- }
- else if (currentTrivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia)
- {
- var start = stack.Pop();
- var regionName = start.ToFullString().Replace("#region", string.Empty).Trim();
- var regionId = $"{fileName}@{regionName}";
- regions.Add(
- (start, currentTrivia, regionId));
+ if (currentTrivia.Kind() == SyntaxKind.RegionDirectiveTrivia)
+ {
+ stack.Push(currentTrivia);
+ }
+ else if (currentTrivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia && stack.Count > 0)
+ {
+ var start = stack.Pop();
+ var regionName = start.ToFullString().Replace("#region", string.Empty).Trim();
+ yield return (start, currentTrivia, new BufferId(fileName, regionName));
+ }
}
}
}
-
- return regions;
}
var sourceCodeText = code.ToString();
var root = CSharpSyntaxTree.ParseText(sourceCodeText).GetRoot();
- var extractedRegions = new List<(string regionId, string content)>();
+
foreach (var (startRegion, endRegion, label) in FindRegions(root))
{
- var start = startRegion.GetLocation().SourceSpan.End;
- var length = endRegion.GetLocation().SourceSpan.Start -
+ var innerStart = startRegion.GetLocation().SourceSpan.End;
+
+ var innerLength = endRegion.GetLocation().SourceSpan.Start -
startRegion.GetLocation().SourceSpan.End;
- var loc = new TextSpan(start, length);
- var content = code.ToString(loc);
+ var innerLoc = new TextSpan(innerStart, innerLength);
- content = FormatSourceCode(content);
- extractedRegions.Add((label, content));
- }
+ var outerStart = startRegion.GetLocation().SourceSpan.Start;
+ var outerLength = endRegion.GetLocation().SourceSpan.End -
+ startRegion.GetLocation().SourceSpan.Start;
+ var outerLoc = new TextSpan(outerStart, outerLength);
- return extractedRegions;
+ yield return (label, innerLoc, outerLoc);
+ }
}
- public static IEnumerable ExtractBuffers(this SourceText code, string fileName)
+ private static IEnumerable<(BufferId bufferId, TextSpan span, TextSpan outerSpan)> ExtractRegionsFSharp(SourceText code, string fileName)
{
- List<(SyntaxTrivia startRegion, SyntaxTrivia endRegion, string label)> FindRegions(SyntaxNode syntaxNode)
+ var extractedRegions = new List<(BufferId, TextSpan, TextSpan)>();
+ var text = code.ToString();
+ int regionTagStartIndex = text.IndexOf(FSharpRegionStart);
+ while (regionTagStartIndex >= 0)
{
- var nodesWithRegionDirectives =
- from node in syntaxNode.DescendantNodesAndTokens()
- where node.HasLeadingTrivia
- from leadingTrivia in node.GetLeadingTrivia()
- where leadingTrivia.Kind() == SyntaxKind.RegionDirectiveTrivia ||
- leadingTrivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia
- select node;
-
- var regions = new List<(SyntaxTrivia startRegion, SyntaxTrivia endRegion, string label)>();
- var stack = new Stack();
- var processedSpans = new HashSet();
- foreach (var nodeWithRegionDirective in nodesWithRegionDirectives)
+ var regionLabelStartIndex = regionTagStartIndex + FSharpRegionStart.Length;
+ var regionLabelEndIndex = text.IndexOf('\n', regionTagStartIndex);
+ var regionLabel = text.Substring(regionLabelStartIndex, regionLabelEndIndex - regionLabelStartIndex).Trim();
+ var regionTagEndIndex = text.IndexOf(FSharpRegionEnd, regionTagStartIndex);
+ if (regionTagEndIndex >= 0)
{
- var triviaList = nodeWithRegionDirective.GetLeadingTrivia();
+ var regionEndTagLastIndex = regionTagEndIndex + FSharpRegionEnd.Length;
- foreach (var currentTrivia in triviaList)
- {
- if (!processedSpans.Add(currentTrivia.FullSpan)) continue;
+ var contentStart = regionLabelEndIndex + 1; // swallow newline
- if (currentTrivia.Kind() == SyntaxKind.RegionDirectiveTrivia)
- {
- stack.Push(currentTrivia);
- }
- else if (currentTrivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia)
- {
- var start = stack.Pop();
- var regionName = start.ToFullString().Replace("#region", string.Empty).Trim();
- var regionId = $"{fileName}@{regionName}";
- regions.Add(
- (start, currentTrivia, regionId));
- }
- }
- }
+ var newlineBeforeEndRegionTag = text.LastIndexOf('\n', regionTagEndIndex);
+ var endRegionIndentOffset = regionTagEndIndex - newlineBeforeEndRegionTag;
+ var contentEnd = regionTagEndIndex - endRegionIndentOffset;
+
+ var contentSpan = new TextSpan(contentStart, contentEnd - contentStart);
+ var regionSpan = new TextSpan(regionTagStartIndex, regionEndTagLastIndex - regionTagStartIndex);
+ extractedRegions.Add((new BufferId(fileName, regionLabel), contentSpan, regionSpan));
- return regions;
+ regionTagStartIndex = text.IndexOf(FSharpRegionStart, regionTagEndIndex);
+ }
+ else
+ {
+ break;
+ }
}
- var sourceCodeText = code.ToString();
- var root = CSharpSyntaxTree.ParseText(sourceCodeText).GetRoot();
- var extractedRegions = new List();
- foreach (var (startRegion, endRegion, label) in FindRegions(root))
+ return extractedRegions;
+ }
+
+ public static IEnumerable ExtractBuffers(this SourceText code, string fileName)
+ {
+ var extractedBuffers = new List();
+ foreach ((var bufferId, var contentSpan, var regionSpan) in ExtractRegions(code, fileName))
{
- var start = startRegion.GetLocation().SourceSpan.End;
- var length = endRegion.GetLocation().SourceSpan.Start -
- startRegion.GetLocation().SourceSpan.End;
- var loc = new TextSpan(start, length);
+ var content = code.ToString(contentSpan);
+ content = content.FormatSourceCode(fileName);
+ extractedBuffers.Add(new Buffer(bufferId, content));
+ }
- var content = code.ToString(loc);
+ return extractedBuffers;
+ }
- content = FormatSourceCode(content);
- extractedRegions.Add(new Buffer(label, content));
+ public static string FormatSourceCode(this string sourceCode, string fileName)
+ {
+ var extension = Path.GetExtension(fileName);
+ switch (extension)
+ {
+ case ".cs":
+ case ".csx":
+ return FormatSourceCodeCSharp(sourceCode);
+ case ".fs":
+ case ".fsx":
+ return FormatSourceCodeFSharp(sourceCode);
+ default:
+ throw new InvalidOperationException($"Unsupported file extension '{extension}'");
}
-
- return extractedRegions;
}
- private static string FormatSourceCode(string sourceCode)
+ private static string FormatSourceCodeCSharp(string sourceCode)
{
var tree = CSharpSyntaxTree.ParseText(sourceCode.Trim(), new CSharpParseOptions(kind: SourceCodeKind.Script));
var cw = new AdhocWorkspace();
var formattedCode = Formatter.Format(tree.GetRoot(), cw);
return formattedCode.ToFullString();
}
+
+ private static string FormatSourceCodeFSharp(string sourceCode)
+ {
+ // dedent lines the number of spaces before the first non-space character
+ var dedentedCode = sourceCode.TrimStart(' ');
+ var dedentLevel = sourceCode.Length - dedentedCode.Length;
+ var lines = sourceCode.Split('\n');
+ var dedentedLines = lines.Select(l => l.Length > dedentLevel ? l.Substring(dedentLevel) : string.Empty);
+ var formattedCode = string.Join("\n", dedentedLines);
+ return formattedCode;
+ }
}
}
-
diff --git a/Microsoft.DotNet.Try.Protocol.Tests/SourceCodeProvider.cs b/Microsoft.DotNet.Try.Protocol.Tests/SourceCodeProvider.cs
index b0bf32ac9..0f0f06b3a 100644
--- a/Microsoft.DotNet.Try.Protocol.Tests/SourceCodeProvider.cs
+++ b/Microsoft.DotNet.Try.Protocol.Tests/SourceCodeProvider.cs
@@ -169,5 +169,22 @@ static void Main(string[] args)
}
}
}";
+
+ public static string FSharpConsoleProgramMultipleRegions =>
+ @"//
+module FSharpConsole
+
+[]
+let main(args: string[]) =
+ let numbers = seq { 1; 2; 3; 4 }
+ //#region alpha
+ let sum = numbers |> Seq.sum
+ //#endregion
+ //#region beta
+ printfn ""The sum was %d"" sum
+ printfn ""goodbye""
+ //#endregion
+ 0
+".EnforceLF();
}
}
diff --git a/Microsoft.DotNet.Try.Protocol/Workspace.cs b/Microsoft.DotNet.Try.Protocol/Workspace.cs
index f963c3f7c..1d26463bb 100644
--- a/Microsoft.DotNet.Try.Protocol/Workspace.cs
+++ b/Microsoft.DotNet.Try.Protocol/Workspace.cs
@@ -66,10 +66,12 @@ public static Workspace FromSource(
string workspaceType,
string id = "Program.cs",
string[] usings = null,
+ string language = DefaultLanguage,
int position = 0)
{
return new Workspace(
workspaceType: workspaceType,
+ language: language,
buffers: new[]
{
new Buffer(BufferId.Parse(id ?? throw new ArgumentNullException(nameof(id))), source, position)
@@ -79,9 +81,11 @@ public static Workspace FromSource(
public static Workspace FromSources(
string workspaceType = null,
+ string language = DefaultLanguage,
params (string id, string content, int position)[] sources) =>
new Workspace(
workspaceType: workspaceType,
+ language: language,
buffers: sources.Select(s => new Buffer(BufferId.Parse(s.id), s.content, s.position)).ToArray());
}
}
diff --git a/WorkspaceServer.Tests/Create.cs b/WorkspaceServer.Tests/Create.cs
index 4e769a5ae..1156f125d 100644
--- a/WorkspaceServer.Tests/Create.cs
+++ b/WorkspaceServer.Tests/Create.cs
@@ -122,7 +122,8 @@ public static async Task InstalledPackageWithBlazorEnabled([CallerMemb
public static string SimpleWorkspaceRequestAsJson(
string consoleOutput = "Hello!",
- string workspaceType = null)
+ string workspaceType = null,
+ string workspaceLanguage = "csharp")
{
var workspace = Workspace.FromSource(
SimpleConsoleAppCodeWithoutNamespaces(consoleOutput),
diff --git a/WorkspaceServer.Tests/PackageTests.cs b/WorkspaceServer.Tests/PackageTests.cs
index cb0f42b11..7107c15bc 100644
--- a/WorkspaceServer.Tests/PackageTests.cs
+++ b/WorkspaceServer.Tests/PackageTests.cs
@@ -52,7 +52,7 @@ public async Task Package_after_create_actions_are_not_run_more_than_once()
var initializer = new PackageInitializer(
"console",
"test",
- async (_, __) =>
+ afterCreate: async (_, __) =>
{
await Task.Yield();
afterCreateCallCount++;
diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs
index 250cdb91a..311acb640 100644
--- a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs
+++ b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs
@@ -26,12 +26,12 @@ public async Task Get_diagnostics()
var code = @"addd";
var (processed, markLocation) = CodeManipulation.ProcessMarkup(code);
- var ws = new Workspace(buffers: new[] { new Buffer("", processed, markLocation) });
- var request = new WorkspaceRequest(ws, activeBufferId: "");
+ var ws = new Workspace(buffers: new[] { new Buffer("file.csx", processed, markLocation) });
+ var request = new WorkspaceRequest(ws, activeBufferId: "file.csx");
var server = GetLanguageService();
var result = await server.GetDiagnostics(request);
result.Diagnostics.Should().NotBeEmpty();
- result.Diagnostics.Should().Contain(diagnostics => diagnostics.Message == "(1,1): error CS0103: The name \'addd\' does not exist in the current context");
+ result.Diagnostics.Should().Contain(diagnostics => diagnostics.Message == "file.csx(1,1): error CS0103: The name \'addd\' does not exist in the current context");
}
protected override ILanguageService GetLanguageService() => new RoslynWorkspaceServer(Default.PackageFinder);
diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs
index c928f7fbf..01ff846a9 100644
--- a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs
+++ b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs
@@ -48,8 +48,8 @@ private static IEnumerable Fibonacci()
}";
var (processed, markLocation) = CodeManipulation.ProcessMarkup(code);
- var ws = new Workspace(buffers: new[] { new Buffer("", processed, markLocation) });
- var request = new WorkspaceRequest(ws, activeBufferId: "");
+ var ws = new Workspace(buffers: new[] { new Buffer("file.csx", processed, markLocation) });
+ var request = new WorkspaceRequest(ws, activeBufferId: "file.csx");
var server = GetLanguageService();
var result = await server.GetSignatureHelp(request);
result.Should().NotBeNull();
@@ -85,8 +85,8 @@ private static IEnumerable Fibonacci()
}
}";
var (processed, markLocation) = CodeManipulation.ProcessMarkup(code);
- var ws = new Workspace( buffers: new[] { new Buffer("", processed, markLocation) });
- var request = new WorkspaceRequest(ws, activeBufferId: "");
+ var ws = new Workspace( buffers: new[] { new Buffer("file.csx", processed, markLocation) });
+ var request = new WorkspaceRequest(ws, activeBufferId: "file.csx");
var server = GetLanguageService();
var result = await server.GetSignatureHelp(request);
result.Signatures.Should().NotBeEmpty();
diff --git a/WorkspaceServer.Tests/TestPackageInitializer.cs b/WorkspaceServer.Tests/TestPackageInitializer.cs
index 12c5de497..549112949 100644
--- a/WorkspaceServer.Tests/TestPackageInitializer.cs
+++ b/WorkspaceServer.Tests/TestPackageInitializer.cs
@@ -14,17 +14,18 @@ public class TestPackageInitializer : PackageInitializer
public int InitializeCount { get; private set; }
public TestPackageInitializer(
- string template,
- string projectName,
- Func afterCreate = null) :
- base(template, projectName, afterCreate)
+ string template,
+ string projectName,
+ string language = null,
+ Func afterCreate = null) :
+ base(template, projectName, language, afterCreate)
{
}
+
public override Task Initialize(DirectoryInfo directory, Budget budget = null)
{
InitializeCount++;
return base.Initialize(directory, budget);
}
-
}
}
diff --git a/WorkspaceServer/PackageRegistry.cs b/WorkspaceServer/PackageRegistry.cs
index 3c846620e..b92c84cfe 100644
--- a/WorkspaceServer/PackageRegistry.cs
+++ b/WorkspaceServer/PackageRegistry.cs
@@ -236,6 +236,12 @@ public static PackageRegistry CreateForHostedMode()
packageBuilder.EnableBlazor(registry);
});
+ registry.Add("fsharp-console",
+ packageBuilder =>
+ {
+ packageBuilder.CreateUsingDotnet("console", language: "F#");
+ });
+
return registry;
}
diff --git a/WorkspaceServer/Packaging/PackageBuilder.cs b/WorkspaceServer/Packaging/PackageBuilder.cs
index 1d7eebc49..c85226e91 100644
--- a/WorkspaceServer/Packaging/PackageBuilder.cs
+++ b/WorkspaceServer/Packaging/PackageBuilder.cs
@@ -37,11 +37,12 @@ public PackageBuilder(string packageName, IPackageInitializer packageInitializer
public bool BlazorSupported { get; private set; }
- public void CreateUsingDotnet(string template, string projectName = null)
+ public void CreateUsingDotnet(string template, string projectName = null, string language = null)
{
PackageInitializer = new PackageInitializer(
template,
projectName ?? PackageName,
+ language,
AfterCreate);
}
diff --git a/WorkspaceServer/Packaging/PackageInitializer.cs b/WorkspaceServer/Packaging/PackageInitializer.cs
index d053a0971..514cc3602 100644
--- a/WorkspaceServer/Packaging/PackageInitializer.cs
+++ b/WorkspaceServer/Packaging/PackageInitializer.cs
@@ -15,11 +15,14 @@ public class PackageInitializer : IPackageInitializer
public string Template { get; }
+ public string Language { get; }
+
public string ProjectName { get; }
public PackageInitializer(
- string template,
+ string template,
string projectName,
+ string language = null,
Func afterCreate = null)
{
if (string.IsNullOrWhiteSpace(template))
@@ -35,8 +38,8 @@ public PackageInitializer(
this.afterCreate = afterCreate;
Template = template;
-
ProjectName = projectName;
+ Language = language ?? GetLanguageFromProjectName(ProjectName);
}
public virtual async Task Initialize(
@@ -49,7 +52,7 @@ public virtual async Task Initialize(
var result = await dotnet
.New(Template,
- args: $"--name \"{ProjectName}\" --output \"{directory.FullName}\"",
+ args: $"--name \"{ProjectName}\" --language \"{Language}\" --output \"{directory.FullName}\"",
budget: budget);
result.ThrowOnFailure($"Error initializing in {directory.FullName}");
@@ -58,6 +61,17 @@ public virtual async Task Initialize(
await afterCreate(directory, budget);
}
}
+
+ private static string GetLanguageFromProjectName(string projectName)
+ {
+ if (projectName.EndsWith(".fsproj", StringComparison.OrdinalIgnoreCase))
+ {
+ return "F#";
+ }
+
+ // default to C#
+ return "C#";
+ }
}
}
diff --git a/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs b/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs
index 4f0421098..6b7185922 100644
--- a/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs
+++ b/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs
@@ -21,8 +21,9 @@ public Task Locate(
Budget budget = null)
{
var projectFile = packageDescriptor.Name;
+ var extension = Path.GetExtension(projectFile);
- if (Path.GetExtension(projectFile) == ".csproj" && File.Exists(projectFile))
+ if ((extension == ".csproj" || extension == ".fsproj") && File.Exists(projectFile))
{
PackageBuilder packageBuilder = new PackageBuilder(packageDescriptor.Name);
packageBuilder.CreateRebuildablePackage = _createRebuildablePackage;
diff --git a/WorkspaceServer/Packaging/RebuildablePackage.cs b/WorkspaceServer/Packaging/RebuildablePackage.cs
index c4970a94c..2bb67bd6d 100644
--- a/WorkspaceServer/Packaging/RebuildablePackage.cs
+++ b/WorkspaceServer/Packaging/RebuildablePackage.cs
@@ -29,12 +29,14 @@ public RebuildablePackage(string name = null, IPackageInitializer initializer =
private static bool IsProjectFile(string fileName)
{
- return fileName.EndsWith(".csproj", StringComparison.InvariantCultureIgnoreCase);
+ return fileName.EndsWith(".csproj", StringComparison.InvariantCultureIgnoreCase)
+ || fileName.EndsWith(".fsproj", StringComparison.InvariantCultureIgnoreCase);
}
private static bool IsCodeFile(string fileName)
{
- return fileName.EndsWith(".cs", StringComparison.InvariantCultureIgnoreCase);
+ return fileName.EndsWith(".cs", StringComparison.InvariantCultureIgnoreCase)
+ || fileName.EndsWith(".fs", StringComparison.InvariantCultureIgnoreCase);
}
private static bool IsBuildLogFile(string fileName)
diff --git a/WorkspaceServer/Servers/FSharp/FSharpWorkspaceServer.cs b/WorkspaceServer/Servers/FSharp/FSharpWorkspaceServer.cs
new file mode 100644
index 000000000..695749bb9
--- /dev/null
+++ b/WorkspaceServer/Servers/FSharp/FSharpWorkspaceServer.cs
@@ -0,0 +1,166 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Clockwise;
+using FSharpWorkspaceShim;
+using Microsoft.DotNet.Try.Project;
+using Microsoft.DotNet.Try.Protocol;
+using WorkspaceServer.Packaging;
+using WorkspaceServer.Servers.Roslyn;
+using WorkspaceServer.Transformations;
+using DiagnosticSeverity = Microsoft.DotNet.Try.Protocol.DiagnosticSeverity;
+using File = System.IO.File;
+using Package = WorkspaceServer.Packaging.Package;
+using Workspace = Microsoft.DotNet.Try.Protocol.Workspace;
+
+namespace WorkspaceServer.Servers.FSharp
+{
+ public partial class FSharpWorkspaceServer : IWorkspaceServer
+ {
+ private readonly IPackageFinder _packageFinder;
+ private readonly IWorkspaceTransformer _transformer = new FSharpBufferInliningTransformer();
+
+ public FSharpWorkspaceServer(IPackageFinder packageRegistry)
+ {
+ _packageFinder = packageRegistry ?? throw new ArgumentNullException(nameof(packageRegistry));
+ }
+
+ public async Task Compile(WorkspaceRequest request, Budget budget = null)
+ {
+ var workspace = request.Workspace;
+ var package = await _packageFinder.Find(workspace.WorkspaceType);
+ var (packageWithChanges, compileResult) = await Compile(package, workspace, request.RequestId);
+ using (packageWithChanges)
+ {
+ return compileResult;
+ }
+ }
+
+ public Task GetCompletionList(WorkspaceRequest request, Budget budget = null)
+ {
+ // TODO:
+ return Task.FromResult(new CompletionResult());
+ }
+
+ public async Task GetDiagnostics(WorkspaceRequest request, Budget budget = null)
+ {
+ var workspace = request.Workspace;
+ var package = await _packageFinder.Find(workspace.WorkspaceType);
+ workspace = await _transformer.TransformAsync(workspace);
+ var packageWithChanges = await CreatePackageWithChanges(package, workspace);
+ var packageFiles = packageWithChanges.GetFiles();
+ var diagnostics = await Shim.GetDiagnostics(packageWithChanges.Name, packageFiles, packageWithChanges.Directory.FullName, package.Directory.FullName);
+ var serializableDiagnostics = workspace.MapDiagnostics(request.ActiveBufferId, diagnostics, budget).DiagnosticsInActiveBuffer;
+ return new DiagnosticResult(serializableDiagnostics, request.RequestId);
+ }
+
+ public Task GetSignatureHelp(WorkspaceRequest request, Budget budget = null)
+ {
+ // TODO:
+ return Task.FromResult(new SignatureHelpResult());
+ }
+
+ public async Task Run(WorkspaceRequest request, Budget budget = null)
+ {
+ var workspace = request.Workspace;
+ var package = await _packageFinder.Find(workspace.WorkspaceType);
+ workspace = await _transformer.TransformAsync(workspace);
+ var (packageWithChanges, _) = await Compile(package, workspace, request.RequestId);
+ using (packageWithChanges)
+ {
+ return await RoslynWorkspaceServer.RunConsoleAsync(
+ packageWithChanges,
+ new SerializableDiagnostic[] { },
+ budget,
+ request.RequestId,
+ workspace.IncludeInstrumentation,
+ request.RunArgs);
+ }
+ }
+
+ private static async Task CreatePackageWithChanges(Package package, Workspace workspace)
+ {
+ // copy project and assets to temporary location
+ var tempDirName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ var packageWithChanges = new RedirectedPackage(workspace, package, Directory.CreateDirectory(tempDirName));
+ try
+ {
+ await CopyDirectory(package.Directory.FullName, packageWithChanges.Directory.FullName);
+
+ // overwrite files
+ foreach (var file in workspace.Files)
+ {
+ File.WriteAllText(Path.Combine(packageWithChanges.Directory.FullName, Path.GetFileName(file.Name)), file.Text);
+ }
+
+ return packageWithChanges;
+ }
+ catch
+ {
+ packageWithChanges.Clean();
+ return null;
+ }
+ }
+
+ private async Task<(RedirectedPackage, CompileResult)> Compile(Package package, Workspace workspace, string requestId)
+ {
+ var packageWithChanges = await CreatePackageWithChanges(package, workspace);
+ try
+ {
+ await package.FullBuild(); // ensure `package.EntryPointAssemblyPath.FullName` has a value
+ await packageWithChanges.FullBuild();
+
+ // copy the entire output directory back
+ await CopyDirectory(
+ Path.GetDirectoryName(packageWithChanges.EntryPointAssemblyPath.FullName),
+ Path.GetDirectoryName(package.EntryPointAssemblyPath.FullName));
+
+ return (packageWithChanges, new CompileResult(
+ true, // succeeded
+ Convert.ToBase64String(File.ReadAllBytes(package.EntryPointAssemblyPath.FullName)),
+ diagnostics: null,
+ requestId: requestId));
+ }
+ catch (Exception e)
+ {
+ packageWithChanges.Clean();
+ return (null, new CompileResult(
+ false, // succeeded
+ string.Empty, // assembly base64
+ new SerializableDiagnostic[]
+ {
+ // TODO: populate with real compiler diagnostics
+ new SerializableDiagnostic(0, 0, e.Message, DiagnosticSeverity.Error, "Compile error")
+ },
+ requestId));
+ }
+ }
+
+ private static async Task CopyDirectory(string source, string destination)
+ {
+ foreach (var dir in Directory.GetDirectories(source, "*", SearchOption.AllDirectories))
+ {
+ Directory.CreateDirectory(dir.Replace(source, destination));
+ }
+
+ foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
+ {
+ var attempt = 0;
+ var totalAttempts = 100;
+ try
+ {
+ File.Copy(file, file.Replace(source, destination), true);
+ }
+ catch (IOException)
+ {
+ if (attempt++ == totalAttempts)
+ {
+ throw;
+ }
+
+ await Task.Delay(10);
+ }
+ }
+ }
+ }
+}
diff --git a/WorkspaceServer/Servers/FSharp/RedirectedPackage.cs b/WorkspaceServer/Servers/FSharp/RedirectedPackage.cs
new file mode 100644
index 000000000..814166a22
--- /dev/null
+++ b/WorkspaceServer/Servers/FSharp/RedirectedPackage.cs
@@ -0,0 +1,47 @@
+using System;
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.Try.Protocol;
+using WorkspaceServer.Servers.Roslyn;
+using Package = WorkspaceServer.Packaging.Package;
+
+namespace WorkspaceServer.Servers.FSharp
+{
+ internal class RedirectedPackage : Package, IDisposable
+ {
+ private Package _parentPackage;
+ private DirectoryInfo _redirectedDirectory;
+ private Workspace _workspace;
+
+ public RedirectedPackage(Workspace workspace, Package parentPackage, DirectoryInfo directory)
+ : base(parentPackage.Name, parentPackage.Initializer, directory)
+ {
+ _parentPackage = parentPackage;
+ _redirectedDirectory = directory;
+ _workspace = workspace;
+ }
+
+ public string[] GetFiles()
+ {
+ var sourcePath = _parentPackage.Directory.FullName.EnsureTrailingSeparator();
+ var destPath = _redirectedDirectory.FullName.EnsureTrailingSeparator();
+ return _workspace.Files.Select(f => f.Name.Replace(sourcePath, destPath)).ToArray();
+ }
+
+ public void Clean()
+ {
+ try
+ {
+ Directory.Delete(true);
+ }
+ catch
+ {
+ }
+ }
+
+ public void Dispose()
+ {
+ Clean();
+ }
+ }
+}
diff --git a/WorkspaceServer/Servers/IWorkspaceServer.cs b/WorkspaceServer/Servers/IWorkspaceServer.cs
new file mode 100644
index 000000000..a3b970470
--- /dev/null
+++ b/WorkspaceServer/Servers/IWorkspaceServer.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace WorkspaceServer.Servers
+{
+ public interface IWorkspaceServer : ILanguageService, ICodeRunner, ICodeCompiler
+ {
+ }
+}
diff --git a/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs b/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs
index 3b25661d2..31dead496 100644
--- a/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs
+++ b/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs
@@ -28,7 +28,7 @@
namespace WorkspaceServer.Servers.Roslyn
{
- public class RoslynWorkspaceServer : ILanguageService, ICodeRunner, ICodeCompiler
+ public class RoslynWorkspaceServer : IWorkspaceServer
{
private readonly IPackageFinder _packageFinder;
private const int defaultBudgetInSeconds = 30;
@@ -304,7 +304,7 @@ private static async Task EmitCompilationAsync(Compilation compilation, Package
}
}
- private static async Task RunConsoleAsync(
+ internal static async Task RunConsoleAsync(
Package package,
IEnumerable diagnostics,
Budget budget,
diff --git a/WorkspaceServer/Servers/WorkspaceServerMultiplexer.cs b/WorkspaceServer/Servers/WorkspaceServerMultiplexer.cs
new file mode 100644
index 000000000..9048d6ce6
--- /dev/null
+++ b/WorkspaceServer/Servers/WorkspaceServerMultiplexer.cs
@@ -0,0 +1,64 @@
+using System.Threading.Tasks;
+using Clockwise;
+using Microsoft.DotNet.Try.Protocol;
+using WorkspaceServer.Packaging;
+using WorkspaceServer.Servers.FSharp;
+using WorkspaceServer.Servers.Roslyn;
+using Package = WorkspaceServer.Packaging.Package;
+
+namespace WorkspaceServer.Servers
+{
+ public class WorkspaceServerMultiplexer : IWorkspaceServer
+ {
+ private IPackageFinder _packageFinder;
+ private readonly IWorkspaceServer _roslynWorkspaceServer;
+ private readonly IWorkspaceServer _fsharpWorkspaceServer;
+
+ public WorkspaceServerMultiplexer(IPackageFinder packageFinder)
+ {
+ _packageFinder = packageFinder;
+ _roslynWorkspaceServer = new RoslynWorkspaceServer(packageFinder);
+ _fsharpWorkspaceServer = new FSharpWorkspaceServer(packageFinder);
+ }
+
+ public async Task Compile(WorkspaceRequest request, Budget budget = null)
+ {
+ return IsFSharpWorkspaceRequest(request.Workspace)
+ ? await _fsharpWorkspaceServer.Compile(request, budget)
+ : await _roslynWorkspaceServer.Compile(request, budget);
+ }
+
+ public async Task GetCompletionList(WorkspaceRequest request, Budget budget = null)
+ {
+ return IsFSharpWorkspaceRequest(request.Workspace)
+ ? await _fsharpWorkspaceServer.GetCompletionList(request, budget)
+ : await _roslynWorkspaceServer.GetCompletionList(request, budget);
+ }
+
+ public async Task GetDiagnostics(WorkspaceRequest request, Budget budget = null)
+ {
+ return IsFSharpWorkspaceRequest(request.Workspace)
+ ? await _fsharpWorkspaceServer.GetDiagnostics(request, budget)
+ : await _roslynWorkspaceServer.GetDiagnostics(request, budget);
+ }
+
+ public async Task GetSignatureHelp(WorkspaceRequest request, Budget budget = null)
+ {
+ return IsFSharpWorkspaceRequest(request.Workspace)
+ ? await _fsharpWorkspaceServer.GetSignatureHelp(request, budget)
+ : await _roslynWorkspaceServer.GetSignatureHelp(request, budget);
+ }
+
+ public async Task Run(WorkspaceRequest request, Budget budget = null)
+ {
+ return IsFSharpWorkspaceRequest(request.Workspace)
+ ? await _fsharpWorkspaceServer.Run(request, budget)
+ : await _roslynWorkspaceServer.Run(request, budget);
+ }
+
+ private bool IsFSharpWorkspaceRequest(Workspace workspace)
+ {
+ return workspace.Language == "fsharp";
+ }
+ }
+}
diff --git a/WorkspaceServer/Transformations/DiagnosticTransformer.cs b/WorkspaceServer/Transformations/DiagnosticTransformer.cs
index dc4a8006d..93f6a7ee8 100644
--- a/WorkspaceServer/Transformations/DiagnosticTransformer.cs
+++ b/WorkspaceServer/Transformations/DiagnosticTransformer.cs
@@ -131,8 +131,8 @@ private static SerializableDiagnostic AlignDiagnosticLocation(
int paddingSize)
{
// this diagnostics does not apply to viewport
- if (diagnostic.Location!= Location.None
- && !string.IsNullOrWhiteSpace(diagnostic.Location.SourceTree.FilePath)
+ if (diagnostic.Location!= Location.None
+ && !string.IsNullOrWhiteSpace(diagnostic.Location.SourceTree?.FilePath)
&& !diagnostic.Location.SourceTree.FilePath.Contains(viewport.Destination.Name))
{
return null;
diff --git a/WorkspaceServer/WorkspaceServer.csproj b/WorkspaceServer/WorkspaceServer.csproj
index fea6e7bf8..ce7efd136 100644
--- a/WorkspaceServer/WorkspaceServer.csproj
+++ b/WorkspaceServer/WorkspaceServer.csproj
@@ -99,6 +99,7 @@
+
diff --git a/docs/Learn math.md b/docs/Learn math.md
deleted file mode 100644
index 8ae9123e0..000000000
--- a/docs/Learn math.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# dotnet try
-
-Learn math with .NET.
-
-Given the following equation
-
-$$
-\sum ^{20}_{i=0}\left(x_{i}+a_{i}y_{i}\right)
-$$
-
-create an implementation using real code
-
-
-```csharp --source-file ./samples/BasicConsole/Program.cs --project ./samples/BasicConsole/BasicConsole.csproj --region wat --session "say meow..."
-```
\ No newline at end of file
diff --git a/docs/Math with FSharp.md b/docs/Math with FSharp.md
new file mode 100644
index 000000000..a9cfcf4ca
--- /dev/null
+++ b/docs/Math with FSharp.md
@@ -0,0 +1,14 @@
+# dotnet try
+
+Learn math with .NET.
+
+Given the following mathematical expression
+
+$$
+\sum ^{20}_{i=0}\left(x_{i}+a_{i}y_{i}\right)
+$$
+
+Create an implementation using `F#`. The sequences `x`, `y` and `a` have been already declared.
+
+```fsharp --source-file ./samples/FSharpMath/Program.fs --project ./samples/FSharpMath/FSharpMath.fsproj --region some_region
+```
diff --git a/docs/fsharp.md b/docs/fsharp.md
new file mode 100644
index 000000000..1bc695f8b
--- /dev/null
+++ b/docs/fsharp.md
@@ -0,0 +1,7 @@
+# dotnet try
+
+This is an interactive Try .NET editor.
+
+``` fsharp --source-file ./samples/FSharpConsole/Program.fs --project ./samples/FSharpConsole/FSharpConsole.fsproj --region some_region
+printfn "hello from F#"
+```
diff --git a/docs/samples/FSharpConsole/FSharpConsole.fsproj b/docs/samples/FSharpConsole/FSharpConsole.fsproj
new file mode 100644
index 000000000..9a7674ad2
--- /dev/null
+++ b/docs/samples/FSharpConsole/FSharpConsole.fsproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ netcoreapp2.0
+
+
+
+
+
+
+
diff --git a/docs/samples/FSharpConsole/Program.fs b/docs/samples/FSharpConsole/Program.fs
new file mode 100644
index 000000000..7d5546edd
--- /dev/null
+++ b/docs/samples/FSharpConsole/Program.fs
@@ -0,0 +1,11 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+module FSharpConsole
+
+[]
+let main(args: string[]) =
+ //#region some_region
+ printfn "hello from F#"
+ //#endregion
+ 0
diff --git a/docs/samples/FSharpMath/FSharpMath.fsproj b/docs/samples/FSharpMath/FSharpMath.fsproj
new file mode 100644
index 000000000..9a7674ad2
--- /dev/null
+++ b/docs/samples/FSharpMath/FSharpMath.fsproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ netcoreapp2.0
+
+
+
+
+
+
+
diff --git a/docs/samples/FSharpMath/Program.fs b/docs/samples/FSharpMath/Program.fs
new file mode 100644
index 000000000..11b68e96c
--- /dev/null
+++ b/docs/samples/FSharpMath/Program.fs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+module FSharpMath
+
+[]
+let main(args: string[]) =
+ let mutable acc = 0
+ let x = {1 .. 35}
+ let a = {1 .. 60}
+ let y = {1 .. 30}
+ //#region some_region
+ let map3 = fun w y z ->
+ Seq.map2 (fun x1 (a1,y1) -> (x1,a1,y1)) w (Seq.map2 (fun a1 y1 -> (a1,y1)) y z)
+ acc <- (Seq.take 20 (map3 x a y)) |> Seq.fold (fun acc (x, a, y) -> acc + (x + a * y)) 0
+ //#endregion
+ printfn "%i" acc
+ 0
diff --git a/global.json b/global.json
index 302ea296a..27a5cbaf4 100644
--- a/global.json
+++ b/global.json
@@ -1,4 +1,7 @@
{
+ "sdk": {
+ "version": "2.1.503"
+ },
"tools": {
"dotnet": "2.1.503"
},