Skip to content

Commit

Permalink
initial support for F# (#237)
Browse files Browse the repository at this point in the history
* initial support for F#

* plumb through language name

* allow code blocks of type `fsharp`

* consolidate region and buffer extraction

* add buffer/region extraction tests

* expand F# math demo

* format F# code with proper indentation levels

* use language switch

* remove unnecessary interface property
  • Loading branch information
brettfo committed Jun 6, 2019
1 parent ae7f206 commit 80e77b6
Show file tree
Hide file tree
Showing 49 changed files with 862 additions and 223 deletions.
7 changes: 7 additions & 0 deletions DotNetTry.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
23 changes: 23 additions & 0 deletions FSharpWorkspaceShim/FSharpWorkspaceShim.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);2003</NoWarn><!-- AssemblyInformationalVersionAttribute contains a non-standard value -->
</PropertyGroup>

<PropertyGroup Condition="$(TargetFramework.StartsWith('netstandard')) AND '$(OS)' == 'Windows_NT'">
<!-- the 2.1.503 F# compiler can produce PDBs that can't properly be converted, see https://github.com/Microsoft/visualfsharp/issues/5976 -->
<PublishWindowsPdb>false</PublishWindowsPdb>
</PropertyGroup>

<ItemGroup>
<Compile Include="Library.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.6.2" />
<PackageReference Include="FSharp.Compiler.Service" Version="28.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.1.0-beta3-final" />
</ItemGroup>

</Project>
130 changes: 130 additions & 0 deletions FSharpWorkspaceShim/Library.fs
Original file line number Diff line number Diff line change
@@ -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
16 changes: 8 additions & 8 deletions MLS.Agent.Tests/ApiContracts/ApiInputContractChangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> 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
}}";

Expand All @@ -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<int> 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);
Expand Down Expand Up @@ -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<int> 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
}}";

Expand All @@ -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<int> 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);
Expand Down
6 changes: 5 additions & 1 deletion MLS.Agent.Tests/ApiViaHttpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ public static IEnumerable<int> 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();
Expand Down Expand Up @@ -434,6 +435,7 @@ public static IEnumerable<int> Fibonacci()
requestId: "TestRun",
workspace: Workspace.FromSources(
"console",
language: "csharp",
("Program.cs", program, 0),
("generators/FibonacciGenerator.cs", processed, position)
)).ToJson();
Expand Down Expand Up @@ -511,6 +513,7 @@ public static IEnumerable<int> Fibonacci()
requestId: "TestRun",
workspace: Workspace.FromSources(
package.Name,
language: "csharp",
("Program.cs", program, 0),
("generators/FibonacciGenerator.cs", processed, position)
)).ToJson();
Expand Down Expand Up @@ -588,6 +591,7 @@ public static IEnumerable<int> Fibonacci()
requestId: "TestRun",
workspace: Workspace.FromSources(
"console",
language: "csharp",
("Program.cs", program, 0),
("generators/FibonacciGenerator.cs", processed, position)
)).ToJson();
Expand Down
4 changes: 2 additions & 2 deletions MLS.Agent/CommandLine/VerifyCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -33,7 +33,7 @@ public static async Task<int> Do(
packageRegistry,
startupOptions);
var errorCount = 0;
var workspaceServer = new Lazy<RoslynWorkspaceServer>(() => new RoslynWorkspaceServer(packageRegistry));
var workspaceServer = new Lazy<IWorkspaceServer>(() => new WorkspaceServerMultiplexer(packageRegistry));

var markdownFiles = markdownProject.GetAllMarkdownFiles().ToArray();

Expand Down
6 changes: 3 additions & 3 deletions MLS.Agent/Controllers/CompileController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MLS.Agent.Controllers.CompileController>;

namespace MLS.Agent.Controllers
Expand All @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions MLS.Agent/Controllers/LanguageServicesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using MLS.Agent.Middleware;
using Pocket;
using WorkspaceServer;
using WorkspaceServer.Servers.Roslyn;
using WorkspaceServer.Servers;
using static Pocket.Logger<MLS.Agent.Controllers.LanguageServicesController>;

namespace MLS.Agent.Controllers
Expand All @@ -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));
}
Expand Down
3 changes: 2 additions & 1 deletion MLS.Agent/Controllers/ProjectController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public IActionResult GenerateRegionsFromFiles([FromBody] CreateRegionsFromFilesR
private static IEnumerable<SourceFileRegion> 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;
}

Expand Down
7 changes: 3 additions & 4 deletions MLS.Agent/Controllers/RunController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MLS.Agent.Controllers.RunController>;
using MLS.Agent.CommandLine;
using WorkspaceServer.Servers;

namespace MLS.Agent.Controllers
{
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 80e77b6

Please sign in to comment.