Skip to content

Commit

Permalink
load doc comments from xml files when available (#1047)
Browse files Browse the repository at this point in the history
* load doc comments from xml files when available

* Update src/Microsoft.DotNet.Interactive.Tests/LanguageServices/SignatureHelpTests.cs

Co-authored-by: Jon Sequeira <jonsequeira@gmail.com>

* Update src/Microsoft.DotNet.Interactive.Tests/LanguageServices/SignatureHelpTests.cs

Co-authored-by: Jon Sequeira <jonsequeira@gmail.com>

Co-authored-by: Jon Sequeira <jonsequeira@gmail.com>
  • Loading branch information
brettfo and jonsequitur authored Feb 16, 2021
1 parent 44ce8eb commit c3b9a2a
Show file tree
Hide file tree
Showing 8 changed files with 452 additions and 16 deletions.
14 changes: 11 additions & 3 deletions src/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public override async Task SetVariableAsync(string name, object value, Type decl
public async Task HandleAsync(RequestHoverText command, KernelInvocationContext context)
{
using var _ = new GCPressure(1024 * 1024);

var document = _workspace.UpdateWorkingDocument(command.Code);
var text = await document.GetTextAsync();
var cursorPosition = text.Lines.GetPosition(command.LinePosition.ToCodeAnalysisLinePosition());
Expand Down Expand Up @@ -352,7 +352,14 @@ private async Task<IEnumerable<CompletionItem>> GetCompletionList(
return Enumerable.Empty<CompletionItem>();
}

var items = completionList.Items.Select(item => item.ToModel()).ToArray();
var items = new List<CompletionItem>();
foreach (var item in completionList.Items)
{
var description = await service.GetDescriptionAsync(document, item);
var completionItem = item.ToModel(description);
items.Add(completionItem);
}

return items;
}

Expand Down Expand Up @@ -407,7 +414,8 @@ void ISupportNuget.RegisterResolvedPackageReferences(IReadOnlyList<ResolvedPacka
{
var references = resolvedReferences
.SelectMany(r => r.AssemblyPaths)
.Select(r => MetadataReference.CreateFromFile(r));
.Select(r => CachingMetadataResolver.ResolveReferenceWithXmlDocumentationProvider(r))
.ToArray();

ScriptOptions = ScriptOptions.AddReferences(references);
}
Expand Down
20 changes: 13 additions & 7 deletions src/Microsoft.DotNet.Interactive.CSharp/CachingMetadataResolver.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// 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.Concurrent;
using System.Collections.Immutable;

using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Scripting;


namespace Microsoft.DotNet.Interactive.CSharp
{
internal sealed class CachingMetadataResolver : MetadataReferenceResolver, IEquatable<ScriptMetadataResolver>
Expand All @@ -26,7 +25,9 @@ private CachingMetadataResolver(ImmutableArray<string> searchPaths, string baseD

public override ImmutableArray<PortableExecutableReference> ResolveReference(string reference, string baseFilePath, MetadataReferenceProperties properties)
{
return _resolver.ResolveReference(reference, baseFilePath, properties);
var resolvedReferences = _resolver.ResolveReference(reference, baseFilePath, properties);
var xmlResolvedReferences = resolvedReferences.Select(r => ResolveReferenceWithXmlDocumentationProvider(r.FilePath, properties)).ToImmutableArray();
return xmlResolvedReferences;
}

public override bool ResolveMissingAssemblies => _resolver.ResolveMissingAssemblies;
Expand All @@ -39,23 +40,28 @@ public override PortableExecutableReference ResolveMissingAssembly(MetadataRefer

public CachingMetadataResolver WithBaseDirectory(string baseDirectory)
{

if (BaseDirectory == baseDirectory)
{
return this;
}

return new CachingMetadataResolver(SearchPaths, baseDirectory);
}

public ImmutableArray<string> SearchPaths => _resolver.SearchPaths;

public string BaseDirectory => _resolver.BaseDirectory;

internal static PortableExecutableReference ResolveReferenceWithXmlDocumentationProvider(string path, MetadataReferenceProperties properties = default(MetadataReferenceProperties))
{
var peReference = MetadataReference.CreateFromFile(path, properties, XmlDocumentationProvider.CreateFromFile(Path.ChangeExtension(path, ".xml")));
return peReference;
}

public bool Equals(ScriptMetadataResolver other) => _resolver.Equals(other);

public override bool Equals(object other) => Equals(other as ScriptMetadataResolver);

public override int GetHashCode() => _resolver.GetHashCode();

}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// 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.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.DotNet.Interactive.Events;
using RoslynCompletionDescription = Microsoft.CodeAnalysis.Completion.CompletionDescription;
using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem;

namespace Microsoft.DotNet.Interactive.CSharp
Expand Down Expand Up @@ -50,14 +49,15 @@ public static string GetKind(this RoslynCompletionItem completionItem)
return null;
}

public static CompletionItem ToModel(this RoslynCompletionItem item)
public static CompletionItem ToModel(this RoslynCompletionItem item, RoslynCompletionDescription description)
{
return new CompletionItem(
displayText: item.DisplayText,
kind: item.GetKind(),
filterText: item.FilterText,
sortText: item.SortText,
insertText: item.FilterText);
insertText: item.FilterText,
documentation: description.Text);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,5 +316,130 @@ public async Task magic_command_completion_commands_and_events_have_offsets_norm
.Should()
.Be(new LinePositionSpan(new LinePosition(line, 0), new LinePosition(line, 3)));
}

[Theory]
[InlineData(Language.CSharp, "/// <summary>Adds two numbers.</summary>\nint Add(int a, int b) => a + b;", "Ad$$", "Adds two numbers.")]
[InlineData(Language.FSharp, "/// Adds two numbers.\nlet add a b = a + b", "ad$$", "Adds two numbers.")]
public async Task completion_doc_comments_can_be_loaded_from_source_in_a_previous_submission(Language language, string previousSubmission, string markupCode, string expectedCompletionSubString)
{
using var kernel = CreateKernel(language);

await SubmitCode(kernel, previousSubmission);

MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character);
await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character)));

KernelEvents
.Should()
.ContainSingle<CompletionsProduced>()
.Which
.Completions
.Should()
.ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains(expectedCompletionSubString));
}

[Theory]
[InlineData(Language.CSharp)]
[InlineData(Language.FSharp)]
public async Task completion_contains_doc_comments_from_individually_referenced_assemblies_with_xml_files(Language language)
{
using var assembly = new TestAssemblyReference("Project", "netstandard2.0", "Program.cs", @"
public class C
{
/// <summary>This is the answer.</summary>
public static int TheAnswer => 42;
}
");
var assemblyPath = await assembly.BuildAndGetPathToAssembly();

var assemblyReferencePath = language switch
{
Language.CSharp => assemblyPath,
Language.FSharp => assemblyPath.Replace("\\", "\\\\")
};

using var kernel = CreateKernel(language);

await SubmitCode(kernel, $"#r \"{assemblyReferencePath}\"");

var markupCode = "C.TheAns$$";

MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character);
await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character)));

KernelEvents
.Should()
.ContainSingle<CompletionsProduced>()
.Which
.Completions
.Should()
.ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("This is the answer."));
}

[Fact]
public async Task csharp_completions_can_read_doc_comments_from_nuget_packages_after_forcing_the_assembly_to_load()
{
using var kernel = CreateKernel(Language.CSharp);

await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\"");

// The following line forces the assembly and the doc comments to be loaded
await SubmitCode(kernel, "var _unused = Newtonsoft.Json.JsonConvert.Null;");

var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$";

MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character);
await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character)));

KernelEvents
.Should()
.ContainSingle<CompletionsProduced>()
.Which
.Completions
.Should()
.ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("Represents JavaScript's null as a string. This field is read-only."));
}

[Fact(Skip = "https://github.com/dotnet/interactive/issues/1071 N.b., the preceeding test can be deleted when this one is fixed.")]
public async Task csharp_completions_can_read_doc_comments_from_nuget_packages()
{
using var kernel = CreateKernel(Language.CSharp);

await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\"");

var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$";

MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character);
await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character)));

KernelEvents
.Should()
.ContainSingle<CompletionsProduced>()
.Which
.Completions
.Should()
.ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("Represents JavaScript's null as a string. This field is read-only."));
}

[Fact]
public async Task fsharp_completions_can_read_doc_comments_from_nuget_packages()
{
using var kernel = CreateKernel(Language.FSharp);

await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\"");

var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$";

MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character);
await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character)));

KernelEvents
.Should()
.ContainSingle<CompletionsProduced>()
.Which
.Completions
.Should()
.ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("Represents JavaScript's null as a string. This field is read-only."));
}
}
}
}
Loading

0 comments on commit c3b9a2a

Please sign in to comment.