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" },