From 810ce369c105796310f3270e9c36a1c26c6589d2 Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Fri, 24 May 2019 12:29:46 -0700 Subject: [PATCH] initial support for F# --- DotNetTry.sln | 7 + .../FSharpWorkspaceShim.fsproj | 18 ++ FSharpWorkspaceShim/Library.fs | 130 ++++++++++++++ MLS.Agent/CommandLine/VerifyCommand.cs | 4 +- MLS.Agent/Controllers/CompileController.cs | 6 +- .../Controllers/LanguageServicesController.cs | 6 +- MLS.Agent/Controllers/RunController.cs | 7 +- MLS.Agent/Program.cs | 6 +- MLS.Agent/Startup.cs | 4 +- .../JupyterRequestContextHandler.cs | 4 +- .../BufferInliningTransformer.cs | 7 + .../DiagnosticExtensions.cs | 6 +- Microsoft.DotNet.Try.Project/FSharpMethods.cs | 80 +++++++++ .../FileExtensions.cs | 10 ++ .../SourceTextExtensions.cs | 7 +- .../ProjectFilePackageDiscoveryStrategy.cs | 3 +- .../Servers/FSharp/FSharpWorkspaceServer.cs | 166 ++++++++++++++++++ .../Servers/FSharp/RedirectedPackage.cs | 47 +++++ WorkspaceServer/Servers/IWorkspaceServer.cs | 10 ++ .../Servers/Roslyn/RoslynWorkspaceServer.cs | 4 +- .../Servers/WorkspaceServerMultiplexer.cs | 61 +++++++ .../Transformations/DiagnosticTransformer.cs | 4 +- WorkspaceServer/WorkspaceServer.csproj | 1 + docs/fsharp.md | 7 + .../FSharpConsole/FSharpConsole.fsproj | 12 ++ docs/samples/FSharpConsole/Program.fs | 11 ++ global.json | 3 + 27 files changed, 604 insertions(+), 27 deletions(-) create mode 100644 FSharpWorkspaceShim/FSharpWorkspaceShim.fsproj create mode 100644 FSharpWorkspaceShim/Library.fs create mode 100644 Microsoft.DotNet.Try.Project/FSharpMethods.cs create mode 100644 WorkspaceServer/Servers/FSharp/FSharpWorkspaceServer.cs create mode 100644 WorkspaceServer/Servers/FSharp/RedirectedPackage.cs create mode 100644 WorkspaceServer/Servers/IWorkspaceServer.cs create mode 100644 WorkspaceServer/Servers/WorkspaceServerMultiplexer.cs create mode 100644 docs/fsharp.md create mode 100644 docs/samples/FSharpConsole/FSharpConsole.fsproj create mode 100644 docs/samples/FSharpConsole/Program.fs diff --git a/DotNetTry.sln b/DotNetTry.sln index 67167e9b98..ce79c7ad84 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 0000000000..ed61c6ffc6 --- /dev/null +++ b/FSharpWorkspaceShim/FSharpWorkspaceShim.fsproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + $(NoWarn);2003 + + + + + + + + + + + + + diff --git a/FSharpWorkspaceShim/Library.fs b/FSharpWorkspaceShim/Library.fs new file mode 100644 index 0000000000..3a9808ce33 --- /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/CommandLine/VerifyCommand.cs b/MLS.Agent/CommandLine/VerifyCommand.cs index fc67c19b36..96a1ca41ef 100644 --- a/MLS.Agent/CommandLine/VerifyCommand.cs +++ b/MLS.Agent/CommandLine/VerifyCommand.cs @@ -11,7 +11,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; namespace MLS.Agent.CommandLine @@ -31,7 +31,7 @@ public static class VerifyCommand 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 cc73a0736f..e53b2a925f 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 74f47a1c52..4280c8b234 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/RunController.cs b/MLS.Agent/Controllers/RunController.cs index e9e677fea1..b263e6b49f 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/Program.cs b/MLS.Agent/Program.cs index fc3139d0df..f82a3d98b2 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 ece32cbd27..8d4399b124 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 664d1393a7..56f18bc5d9 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 JupyterRequestContextHandler(PackageRegistry packageRegistry) 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/BufferInliningTransformer.cs b/Microsoft.DotNet.Try.Project/BufferInliningTransformer.cs index 24ade3d93c..1123b1a4c2 100644 --- a/Microsoft.DotNet.Try.Project/BufferInliningTransformer.cs +++ b/Microsoft.DotNet.Try.Project/BufferInliningTransformer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; @@ -112,6 +113,12 @@ private static TextSpan CreateTextSpanBefore(TextSpan viewPortRegion) private static async Task InjectBufferAtSpan(Viewport viewPort, Buffer sourceBuffer, ICollection buffers, IDictionary files, TextSpan span) { + if (Path.GetExtension(sourceBuffer.Id.FileName) == ".fs") + { + await FSharpMethods.InjectBufferAtSpan(viewPort, sourceBuffer, buffers, files, span); + return; + } + var tree = CSharpSyntaxTree.ParseText(viewPort.Destination.Text.ToString()); var textChange = new TextChange( span, diff --git a/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs b/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs index 83a39547de..e959df92b2 100644 --- a/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs +++ b/Microsoft.DotNet.Try.Project/DiagnosticExtensions.cs @@ -25,9 +25,13 @@ public static TdnDiagnosticSeverity ConvertSeverity(this Diagnostic diagnostic) 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/FSharpMethods.cs b/Microsoft.DotNet.Try.Project/FSharpMethods.cs new file mode 100644 index 0000000000..5133d39518 --- /dev/null +++ b/Microsoft.DotNet.Try.Project/FSharpMethods.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.DotNet.Try.Protocol; +using Buffer = Microsoft.DotNet.Try.Protocol.Buffer; + +namespace Microsoft.DotNet.Try.Project +{ + internal class FSharpMethods + { + private const string FSharpRegionStart = "//#region"; + private const string FSharpRegionEnd = "//#endregion"; + + public static IEnumerable ExtractBuffers(SourceText code, string fileName) + { + var extractedBuffers = new List(); + foreach ((var bufferId, var contentSpan, var regionSpan) in ExtractRegions(code, fileName)) + { + var content = code.ToString(contentSpan); + extractedBuffers.Add(new Buffer(bufferId, content)); + } + + return extractedBuffers; + } + + public static IEnumerable<(BufferId bufferId, TextSpan span, TextSpan outerSpan)> ExtractRegions(SourceText code, string fileName) + { + var extractedRegions = new List<(BufferId, TextSpan, TextSpan)>(); + var text = code.ToString(); + int regionTagStartIndex = text.IndexOf(FSharpRegionStart); + while (regionTagStartIndex > 0) + { + var regionLabelEndIndex = text.IndexOf('\n', regionTagStartIndex); + var regionLabelStartIndex = regionTagStartIndex + FSharpRegionStart.Length; + var regionLabel = text.Substring(regionLabelStartIndex, regionLabelEndIndex - regionLabelStartIndex).Trim(); + var regionTagEndIndex = text.IndexOf(FSharpRegionEnd, regionTagStartIndex); + if (regionTagEndIndex > 0) + { + var regionEndTagLastIndex = regionTagEndIndex + FSharpRegionEnd.Length; + var contentSpan = new TextSpan(regionLabelEndIndex, regionTagEndIndex - regionLabelEndIndex); + var regionSpan = new TextSpan(regionTagStartIndex, regionEndTagLastIndex - regionTagStartIndex); + extractedRegions.Add((new BufferId(fileName, regionLabel), contentSpan, regionSpan)); + + regionTagStartIndex = text.IndexOf(FSharpRegionStart, regionTagEndIndex); + } + else + { + break; + } + } + + return extractedRegions; + } + + private const string NewLinePadding = "\n"; + + internal static 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 = + NewLinePadding + // leading `//#region [label]` ends with '\r'; '\n' is necessary + string.Join("\n", indentedLines) + + Environment.NewLine + indentText; // ensure that the trailing `//#endregion` retains its indention level + 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 + NewLinePadding.Length)); + 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 cd3c5bbab7..4bede2d80c 100644 --- a/Microsoft.DotNet.Try.Project/FileExtensions.cs +++ b/Microsoft.DotNet.Try.Project/FileExtensions.cs @@ -86,6 +86,16 @@ where node.HasLeadingTrivia } } + if (System.IO.Path.GetExtension(fileName) == ".fs") + { + foreach (var r in FSharpMethods.ExtractRegions(code, fileName)) + { + yield return r; + } + + yield break; + } + var sourceCodeText = code.ToString(); var root = CSharpSyntaxTree.ParseText(sourceCodeText).GetRoot(); diff --git a/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs b/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs index 83c99643ad..57f97054a7 100644 --- a/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs +++ b/Microsoft.DotNet.Try.Project/SourceTextExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -116,6 +117,11 @@ where node.HasLeadingTrivia return regions; } + if (Path.GetExtension(fileName) == ".fs") + { + return FSharpMethods.ExtractBuffers(code, fileName); + } + var sourceCodeText = code.ToString(); var root = CSharpSyntaxTree.ParseText(sourceCodeText).GetRoot(); var extractedRegions = new List(); @@ -144,4 +150,3 @@ private static string FormatSourceCode(string sourceCode) } } } - diff --git a/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs b/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs index 4f04210981..6b7185922d 100644 --- a/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs +++ b/WorkspaceServer/Packaging/ProjectFilePackageDiscoveryStrategy.cs @@ -21,8 +21,9 @@ public ProjectFilePackageDiscoveryStrategy(bool createRebuildablePackage) 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/Servers/FSharp/FSharpWorkspaceServer.cs b/WorkspaceServer/Servers/FSharp/FSharpWorkspaceServer.cs new file mode 100644 index 0000000000..f09123cf36 --- /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 BufferInliningTransformer(); + + 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 0000000000..814166a225 --- /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 0000000000..a3b9704703 --- /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 f0dc3c2922..fde28b68a8 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 0000000000..afda0ba2a4 --- /dev/null +++ b/WorkspaceServer/Servers/WorkspaceServerMultiplexer.cs @@ -0,0 +1,61 @@ +using System.Threading.Tasks; +using Clockwise; +using Microsoft.DotNet.Try.Protocol; +using WorkspaceServer.Packaging; +using WorkspaceServer.Servers.FSharp; +using WorkspaceServer.Servers.Roslyn; + +namespace WorkspaceServer.Servers +{ + public class WorkspaceServerMultiplexer : IWorkspaceServer + { + private IWorkspaceServer _roslynWorkspaceServer; + private IWorkspaceServer _fsharpWorksapceServer; + + public WorkspaceServerMultiplexer(IPackageFinder packageRegistry) + { + _roslynWorkspaceServer = new RoslynWorkspaceServer(packageRegistry); + _fsharpWorksapceServer = new FSharpWorkspaceServer(packageRegistry); + } + + public Task Compile(WorkspaceRequest request, Budget budget = null) + { + return IsFSharpWorkspaceRequest(request) + ? _fsharpWorksapceServer.Compile(request, budget) + : _roslynWorkspaceServer.Compile(request, budget); + } + + public Task GetCompletionList(WorkspaceRequest request, Budget budget = null) + { + return IsFSharpWorkspaceRequest(request) + ? _fsharpWorksapceServer.GetCompletionList(request, budget) + : _roslynWorkspaceServer.GetCompletionList(request, budget); + } + + public Task GetDiagnostics(WorkspaceRequest request, Budget budget = null) + { + return IsFSharpWorkspaceRequest(request) + ? _fsharpWorksapceServer.GetDiagnostics(request, budget) + : _roslynWorkspaceServer.GetDiagnostics(request, budget); + } + + public Task GetSignatureHelp(WorkspaceRequest request, Budget budget = null) + { + return IsFSharpWorkspaceRequest(request) + ? _fsharpWorksapceServer.GetSignatureHelp(request, budget) + : _roslynWorkspaceServer.GetSignatureHelp(request, budget); + } + + public Task Run(WorkspaceRequest request, Budget budget = null) + { + return IsFSharpWorkspaceRequest(request) + ? _fsharpWorksapceServer.Run(request, budget) + : _roslynWorkspaceServer.Run(request, budget); + } + + private bool IsFSharpWorkspaceRequest(WorkspaceRequest request) + { + return request.Workspace.WorkspaceType.EndsWith(".fsproj"); + } + } +} diff --git a/WorkspaceServer/Transformations/DiagnosticTransformer.cs b/WorkspaceServer/Transformations/DiagnosticTransformer.cs index dc4a8006db..93f6a7ee8e 100644 --- a/WorkspaceServer/Transformations/DiagnosticTransformer.cs +++ b/WorkspaceServer/Transformations/DiagnosticTransformer.cs @@ -131,8 +131,8 @@ string RelativizeDiagnosticMessage() 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 fea6e7bf85..ce7efd1369 100644 --- a/WorkspaceServer/WorkspaceServer.csproj +++ b/WorkspaceServer/WorkspaceServer.csproj @@ -99,6 +99,7 @@ + diff --git a/docs/fsharp.md b/docs/fsharp.md new file mode 100644 index 0000000000..415217bf17 --- /dev/null +++ b/docs/fsharp.md @@ -0,0 +1,7 @@ +# dotnet try + +This is an interactive Try .NET editor. + +``` csharp --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 0000000000..9a7674ad24 --- /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 0000000000..7d5546edd6 --- /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/global.json b/global.json index 302ea296aa..27a5cbaf45 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,7 @@ { + "sdk": { + "version": "2.1.503" + }, "tools": { "dotnet": "2.1.503" },