Navigation Menu

Skip to content

Commit

Permalink
F# completion (#666)
Browse files Browse the repository at this point in the history
* update F# script package

* compute F# kernel completions
  • Loading branch information
brettfo committed Nov 25, 2019
1 parent ab4c299 commit b30860e
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 57 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.targets
Expand Up @@ -15,8 +15,8 @@

<!-- Consolidate FSharp package versions -->
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="5.0.0-beta.19551.5" />
<PackageReference Update="FSharp.Compiler.Private.Scripting" Version="11.0.0-beta.19551.5" />
<PackageReference Update="FSharp.Core" Version="5.0.0-beta.19572.3" />
<PackageReference Update="FSharp.Compiler.Private.Scripting" Version="11.0.0-beta.19572.3" />
<PackageReference Update="FSharp.Compiler.Service" Version="31.0.0" />
</ItemGroup>

Expand Down
83 changes: 73 additions & 10 deletions Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs
Expand Up @@ -5,12 +5,13 @@ namespace Microsoft.DotNet.Interactive.FSharp

open System
open System.Collections.Generic
open System.IO
open System.Threading
open System.Threading.Tasks
open FSharp.Compiler.Interactive.Shell
open FSharp.Compiler.Scripting
open FSharp.Compiler.SourceCodeServices
open FSharp.DependencyManager
open Microsoft.CodeAnalysis.Tags
open Microsoft.DotNet.Interactive
open Microsoft.DotNet.Interactive.Commands
open Microsoft.DotNet.Interactive.Events
Expand All @@ -29,11 +30,11 @@ type FSharpKernel() =
let messageMap = Dictionary<string, string>()

let parseReference text =
let reference, binLogging = FSharpDependencyManager.parsePackageReference [text]
(reference |> List.tryHead), binLogging
let reference, binLogPath = FSharpDependencyManager.parsePackageReference [text]
(reference |> List.tryHead), binLogPath

let packageInstallingMessages (refSpec:PackageReference option * bool) =
let ref, binLogging = refSpec
let packageInstallingMessages (refSpec: PackageReference option * string option option) =
let ref, binLogPath = refSpec
let versionText =
match ref with
| Some ref when ref.Version <> "*" -> ", version " + ref.Version
Expand All @@ -42,18 +43,68 @@ type FSharpKernel() =
let installingMessage ref = "Installing package " + ref.Include + versionText + "."
let loggingMessage = "Binary Logging enabled"
[|
match ref, binLogging with
| Some reference, true ->
match ref, binLogPath with
| Some reference, Some _ ->
yield installingMessage reference
yield loggingMessage
| Some reference, false ->
| Some reference, None ->
yield installingMessage reference
| None, true ->
| None, Some _ ->
yield loggingMessage
| None, false ->
| None, None ->
()
|]

let getLineAndColumn (text: string) offset =
let rec getLineAndColumn' i l c =
if i >= offset then l, c
else
match text.[i] with
| '\n' -> getLineAndColumn' (i + 1) (l + 1) 0
| _ -> getLineAndColumn' (i + 1) l (c + 1)
getLineAndColumn' 0 1 0

let kindString (glyph: FSharpGlyph) =
match glyph with
| FSharpGlyph.Class -> WellKnownTags.Class
| FSharpGlyph.Constant -> WellKnownTags.Constant
| FSharpGlyph.Delegate -> WellKnownTags.Delegate
| FSharpGlyph.Enum -> WellKnownTags.Enum
| FSharpGlyph.EnumMember -> WellKnownTags.EnumMember
| FSharpGlyph.Event -> WellKnownTags.Event
| FSharpGlyph.Exception -> WellKnownTags.Class
| FSharpGlyph.Field -> WellKnownTags.Field
| FSharpGlyph.Interface -> WellKnownTags.Interface
| FSharpGlyph.Method -> WellKnownTags.Method
| FSharpGlyph.OverridenMethod -> WellKnownTags.Method
| FSharpGlyph.Module -> WellKnownTags.Module
| FSharpGlyph.NameSpace -> WellKnownTags.Namespace
| FSharpGlyph.Property -> WellKnownTags.Property
| FSharpGlyph.Struct -> WellKnownTags.Structure
| FSharpGlyph.Typedef -> WellKnownTags.Class
| FSharpGlyph.Type -> WellKnownTags.Class
| FSharpGlyph.Union -> WellKnownTags.Enum
| FSharpGlyph.Variable -> WellKnownTags.Local
| FSharpGlyph.ExtensionMethod -> WellKnownTags.ExtensionMethod
| FSharpGlyph.Error -> WellKnownTags.Error

let filterText (declarationItem: FSharpDeclarationListItem) =
match declarationItem.NamespaceToOpen, declarationItem.Name.Split '.' with
// There is no namespace to open and the item name does not contain dots, so we don't need to pass special FilterText to Roslyn.
| None, [|_|] -> null
// Either we have a namespace to open ("DateTime (open System)") or item name contains dots ("Array.map"), or both.
// We are passing last part of long ident as FilterText.
| _, idents -> Array.last idents

let documentation (declarationItem: FSharpDeclarationListItem) =
declarationItem.DescriptionText.ToString()

let completionItem (declarationItem: FSharpDeclarationListItem) =
let kind = kindString declarationItem.Glyph
let filterText = filterText declarationItem
let documentation = documentation declarationItem
CompletionItem(declarationItem.Name, kind, filterText=filterText, documentation=documentation)

let handleSubmitCode (codeSubmission: SubmitCode) (context: KernelInvocationContext) =
async {

Expand Down Expand Up @@ -129,6 +180,17 @@ type FSharpKernel() =
context.Publish(new CommandFailed(null, codeSubmission, "Command cancelled"))
}

let handleRequestCompletion (requestCompletion: RequestCompletion) (context: KernelInvocationContext) =
async {
context.Publish(CompletionRequestReceived(requestCompletion))
let l, c = getLineAndColumn requestCompletion.Code requestCompletion.CursorPosition
let! declarationItems = script.GetCompletionItems(requestCompletion.Code, l, c)
let completionItems =
declarationItems
|> Array.map completionItem
context.Publish(CompletionRequestCompleted(completionItems, requestCompletion))
}

let handleCancelCurrentCommand (cancelCurrentCommand: CancelCurrentCommand) (context: KernelInvocationContext) =
async {
cancellationTokenSource.Cancel()
Expand All @@ -141,6 +203,7 @@ type FSharpKernel() =
async {
match command with
| :? SubmitCode as submitCode -> submitCode.Handler <- fun invocationContext -> (handleSubmitCode submitCode invocationContext) |> Async.StartAsTask :> Task
| :? RequestCompletion as requestCompletion -> requestCompletion.Handler <- fun invocationContext -> (handleRequestCompletion requestCompletion invocationContext) |> Async.StartAsTask :> Task
| :? CancelCurrentCommand as cancelCurrentCommand -> cancelCurrentCommand.Handler <- fun invocationContext -> (handleCancelCurrentCommand cancelCurrentCommand invocationContext) |> Async.StartAsTask :> Task
| _ -> ()
} |> Async.StartAsTask :> Task
Expand Up @@ -30,6 +30,7 @@

<ItemGroup>
<PackageReference Include="FSharp.Compiler.Private.Scripting" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(MicrosoftCodeAnalysisWorkspacesCommonVersion)" />
</ItemGroup>

</Project>
40 changes: 3 additions & 37 deletions Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs
@@ -1,22 +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.

using System;
using System.Linq;
using System.Reactive.Concurrency;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.DotNet.Interactive.Commands;
using Microsoft.DotNet.Interactive.Events;
using Microsoft.DotNet.Interactive.Jupyter.Protocol;
using Microsoft.DotNet.Interactive.Utility;

namespace Microsoft.DotNet.Interactive.Jupyter
{
public class CompleteRequestHandler : RequestHandlerBase<CompleteRequest>
{
private static readonly Regex _lastToken = new Regex(@"(?<lastToken>\S+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline);


public CompleteRequestHandler(IKernel kernel, IScheduler scheduler = null)
: base(kernel, scheduler ?? CurrentThreadScheduler.Instance)
{
Expand Down Expand Up @@ -49,40 +45,10 @@ private static void OnCompletionRequestCompleted(CompletionRequestCompleted comp
{
var command = completionRequestCompleted.Command as RequestCompletion;

var pos = ComputeReplacementStartPosition(command.Code, command.CursorPosition);
var reply = new CompleteReply(pos, command.CursorPosition, matches: completionRequestCompleted.CompletionList.Select(e => e.InsertText).ToList());
var pos = SourceUtilities.ComputeReplacementStartPosition(command.Code, command.CursorPosition);
var reply = new CompleteReply(pos, command.CursorPosition, matches: completionRequestCompleted.CompletionList.Select(e => e.InsertText ?? e.DisplayText).ToList());

jupyterMessageSender.Send(reply);
}

private static int ComputeReplacementStartPosition(string code, int cursorPosition)
{
var pos = cursorPosition;

if (pos > 0)
{
var codeToCursor = code.Substring(0, pos);
var match = _lastToken.Match(codeToCursor);
if (match.Success)
{
var token = match.Groups["lastToken"];
if (token.Success)
{
var lastDotPosition = token.Value.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase);
if (lastDotPosition >= 0)
{
pos = token.Index + lastDotPosition + 1;
}
else
{
pos = token.Index;
}
}
}

}

return pos;
}
}
}
11 changes: 3 additions & 8 deletions Microsoft.DotNet.Interactive.Tests/LanguageKernelTests.cs
Expand Up @@ -845,17 +845,12 @@ public async Task it_formats_func_instances(Language language)

[Theory]
[InlineData(Language.CSharp)]
// [InlineData(Language.FSharp)] // Todo: need to generate CompletionRequestReceived event ... perhaps
[InlineData(Language.FSharp)]
public async Task it_returns_completion_list_for_types(Language language)
{
var kernel = CreateKernel(language);

var source = language switch
{
Language.FSharp => @"System.Console.",

Language.CSharp => @"System.Console."
};
var source = "System.Console."; // same code is valid regarless of the language

await kernel.SendAsync(new RequestCompletion(source, 15));

Expand All @@ -873,7 +868,7 @@ public async Task it_returns_completion_list_for_types(Language language)

[Theory]
[InlineData(Language.CSharp)]
//[InlineData(Language.FSharp)] //Todo: completion for F#
[InlineData(Language.FSharp)]
public async Task it_returns_completion_list_for_previously_declared_variables(Language language)
{
var kernel = CreateKernel(language);
Expand Down
54 changes: 54 additions & 0 deletions Microsoft.DotNet.Interactive.Tests/UtilityTests.cs
@@ -0,0 +1,54 @@
// 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 Microsoft.DotNet.Interactive.Utility;
using Xunit;

namespace Microsoft.DotNet.Interactive.Tests
{
public class UtilityTests
{
[Fact]
public void replacement_position_can_be_found_at_the_end_of_a_string()
{
var code = "System.Linq.Enumerable.";
// finding this ^
var pos = SourceUtilities.ComputeReplacementStartPosition(code, code.Length);
Assert.Equal(code.Length, pos);
}

[Fact]
public void replacement_position_can_be_found_not_at_the_end_of_a_string()
{
var code = "System.Linq.Enumerable.Ran";
// finding this ^
var lastDotPos = code.LastIndexOf('.') + 1;
var pos = SourceUtilities.ComputeReplacementStartPosition(code, lastDotPos);
Assert.Equal(lastDotPos, pos);
}

[Fact]
public void replacement_position_can_be_found_at_the_end_of_a_multiline_string()
{
var code = @"
using System.Linq;
Enumerable.
// ^ finding this";
var lastDotPos = code.LastIndexOf('.') + 1;
var pos = SourceUtilities.ComputeReplacementStartPosition(code, lastDotPos);
Assert.Equal(lastDotPos, pos);
}

[Fact]
public void replacement_position_can_be_found_not_at_the_end_of_a_multiline_string()
{
var code = @"
using System.Linq;
Enumerable.Ran
// ^ finding this";
var lastDotPos = code.LastIndexOf('.') + 1;
var pos = SourceUtilities.ComputeReplacementStartPosition(code, lastDotPos);
Assert.Equal(lastDotPos, pos);
}
}
}
46 changes: 46 additions & 0 deletions Microsoft.DotNet.Interactive/Utility/SourceUtilities.cs
@@ -0,0 +1,46 @@
// 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.Text.RegularExpressions;

namespace Microsoft.DotNet.Interactive.Utility
{
public static class SourceUtilities
{
private static readonly Regex _lastToken = new Regex(@"(?<lastToken>\S+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

/// <summary>
/// Given the specified code and cursor position, finds the location where replacement text will begin
/// insertion, usually the location of the last dot ('.').
/// </summary>
public static int ComputeReplacementStartPosition(string code, int cursorPosition)
{
var pos = cursorPosition;

if (pos > 0)
{
var codeToCursor = code.Substring(0, pos);
var match = _lastToken.Match(codeToCursor);
if (match.Success)
{
var token = match.Groups["lastToken"];
if (token.Success)
{
var lastDotPosition = token.Value.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase);
if (lastDotPosition >= 0)
{
pos = token.Index + lastDotPosition + 1;
}
else
{
pos = token.Index;
}
}
}
}

return pos;
}
}
}

0 comments on commit b30860e

Please sign in to comment.