Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support inlay hints #2357

Merged
merged 7 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/Settings.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
Expand Down
77 changes: 77 additions & 0 deletions src/OmniSharp.Abstractions/LoggingExtensions/LoggingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;

#nullable enable

namespace Microsoft.Extensions.Logging
{
public static class LoggingExtensions
{
public static void Log(this ILogger logger, LogLevel logLevel, [InterpolatedStringHandlerArgument("logger", "logLevel")] LoggerInterpolatedStringHandler handler)
{
logger.Log(logLevel, handler.ToString());
}
}

[InterpolatedStringHandler]
public struct LoggerInterpolatedStringHandler
{
private readonly StringBuilder? _builder;
public LoggerInterpolatedStringHandler(int literalLength, int formattedCount, ILogger logger, LogLevel level, out bool shouldAppend)
{
if (logger.IsEnabled(level))
{
shouldAppend = true;
_builder = new(literalLength);
}
else
{
shouldAppend = false;
_builder = null;
}
}

public void AppendLiteral(string literal)
{
Debug.Assert(_builder != null);
_builder!.Append(literal);
}

public void AppendFormatted<T>(T t)
{
Debug.Assert(_builder != null);
_builder!.Append(t?.ToString());
}

public void AppendFormatted<T>(T t, int alignment, string format)
{
Debug.Assert(_builder != null);
_builder!.Append(string.Format($"{{0,{alignment}:{format}}}", t));
}

public override string ToString()
{
return _builder?.ToString() ?? string.Empty;
}
}
}

#if !NET6_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
internal sealed class InterpolatedStringHandlerAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
{
public InterpolatedStringHandlerArgumentAttribute(string argument) => Arguments = new string[] { argument };

public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) => Arguments = arguments;

public string[] Arguments { get; }
}
}
#endif
35 changes: 35 additions & 0 deletions src/OmniSharp.Abstractions/Models/v1/InlayHints/InlayHint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using OmniSharp.Models.V2;

#nullable enable annotations

namespace OmniSharp.Models.v1.InlayHints;

public sealed record InlayHint
{
public Point Position { get; set; }
public string Label { get; set; }
public string? Tooltip { get; set; }
public (string SolutionVersion, int Position) Data { get; set; }

#nullable enable
public override string ToString()
{
return $"InlineHint {{ {nameof(Position)} = {Position}, {nameof(Label)} = {Label}, {nameof(Tooltip)} = {Tooltip} }}";
}

public bool Equals(InlayHint? other)
{
if (ReferenceEquals(this, other)) return true;
if (other is null) return false;

return Position == other.Position && Label == other.Label && Tooltip == other.Tooltip;
}

public override int GetHashCode() => (Position, Label, Tooltip).GetHashCode();
}

public enum InlayHintKind
{
Type = 1,
Parameter = 2,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using OmniSharp.Mef;
using OmniSharp.Models.V2;

#nullable enable annotations

namespace OmniSharp.Models.v1.InlayHints;


filipw marked this conversation as resolved.
Show resolved Hide resolved
[OmniSharpEndpoint(OmniSharpEndpoints.InlayHint, typeof(InlayHintRequest), typeof(InlayHintResponse))]
public record InlayHintRequest : IRequest
{
public Location Location { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using OmniSharp.Mef;

namespace OmniSharp.Models.v1.InlayHints;

[OmniSharpEndpoint(OmniSharpEndpoints.InlayHintResolve, typeof(InlayHintResolveRequest), typeof(InlayHint))]
public record InlayHintResolveRequest : IRequest
{
public InlayHint Hint { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable annotations

using System.Collections.Generic;

namespace OmniSharp.Models.v1.InlayHints;

public record InlayHintResponse
{
public static readonly InlayHintResponse None = new() { InlayHints = new() };
public List<InlayHint> InlayHints { get; set; }
}
3 changes: 3 additions & 0 deletions src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public static class OmniSharpEndpoints
public const string UpdateSourceGeneratedFile = "/updatesourcegeneratedfile";
public const string SourceGeneratedFileClosed = "/sourcegeneratedfileclosed";

public const string InlayHint = "/inlayHint";
public const string InlayHintResolve = "/inlayHint/resolve";

public static class V2
{
public const string GetCodeActions = "/v2/getcodeactions";
Expand Down
186 changes: 186 additions & 0 deletions src/OmniSharp.Roslyn.CSharp/Services/InlayHints/InlayHintService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.InlineHints;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OmniSharp.Extensions;
using OmniSharp.Mef;
using OmniSharp.Models.v1.InlayHints;
using OmniSharp.Options;
using OmniSharp.Roslyn.CSharp.Helpers;

#nullable enable

namespace OmniSharp.Roslyn.CSharp.Services.InlayHints;

[Shared]
[OmniSharpHandler(OmniSharpEndpoints.InlayHint, LanguageNames.CSharp)]
[OmniSharpHandler(OmniSharpEndpoints.InlayHintResolve, LanguageNames.CSharp)]
internal class InlayHintService :
IRequestHandler<InlayHintRequest, InlayHintResponse>,
IRequestHandler<InlayHintResolveRequest, InlayHint>
{
private readonly OmniSharpWorkspace _workspace;
private readonly IOptionsMonitor<OmniSharpOptions> _omniSharpOptions;
private readonly ILogger _logger;
private readonly InlineHintCache _cache;
private readonly FormattingOptions _formattingOptions;

[ImportingConstructor]
public InlayHintService(OmniSharpWorkspace workspace, FormattingOptions formattingOptions, ILoggerFactory loggerFactory, IOptionsMonitor<OmniSharpOptions> omniSharpOptions)
{
_workspace = workspace;
_formattingOptions = formattingOptions;
_logger = loggerFactory.CreateLogger<InlayHintService>();
_omniSharpOptions = omniSharpOptions;
_cache = new(_logger);
}
public async Task<InlayHintResponse> Handle(InlayHintRequest request)
filipw marked this conversation as resolved.
Show resolved Hide resolved
{
var document = _workspace.GetDocument(request.Location.FileName);
if (document == null)
{
_logger.Log(LogLevel.Warning, $"Inlay hints requested for document not in workspace {request.Location}");
return InlayHintResponse.None;
}

var sourceText = await document.GetTextAsync();
var mappedSpan = sourceText.GetSpanFromRange(request.Location.Range);

var inlayHintsOptions = _omniSharpOptions.CurrentValue.RoslynExtensionsOptions.InlayHintsOptions;
var options = new OmniSharpInlineHintsOptions
{
ParameterOptions = new()
{
EnabledForParameters = inlayHintsOptions.EnableForParameters,
ForIndexerParameters = inlayHintsOptions.ForIndexerParameters,
ForLiteralParameters = inlayHintsOptions.ForLiteralParameters,
ForObjectCreationParameters = inlayHintsOptions.ForObjectCreationParameters,
ForOtherParameters = inlayHintsOptions.ForOtherParameters,
SuppressForParametersThatDifferOnlyBySuffix = inlayHintsOptions.SuppressForParametersThatDifferOnlyBySuffix,
SuppressForParametersThatMatchArgumentName = inlayHintsOptions.SuppressForParametersThatMatchArgumentName,
SuppressForParametersThatMatchMethodIntent = inlayHintsOptions.SuppressForParametersThatMatchMethodIntent,
},
TypeOptions = new()
{
EnabledForTypes = inlayHintsOptions.EnableForTypes,
ForImplicitObjectCreation = inlayHintsOptions.ForImplicitObjectCreation,
ForImplicitVariableTypes = inlayHintsOptions.ForImplicitVariableTypes,
ForLambdaParameterTypes = inlayHintsOptions.ForLambdaParameterTypes,
}
};

var hints = await OmniSharpInlineHintsService.GetInlineHintsAsync(document, mappedSpan, options, CancellationToken.None);

var solutionVersion = _workspace.CurrentSolution.Version;

return new()
{
InlayHints = _cache.MapAndCacheHints(hints, document, solutionVersion, sourceText)
};
}

public async Task<InlayHint> Handle(InlayHintResolveRequest request)
{
if (!_cache.TryGetFromCache(request.Hint, out var roslynHint, out var document))
{
return request.Hint;
}

var descriptionTags = await roslynHint.GetDescrptionAsync(document, CancellationToken.None);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened dotnet/roslyn#60217 for this typo

StringBuilder stringBuilder = new StringBuilder();
MarkdownHelpers.TaggedTextToMarkdown(
descriptionTags,
stringBuilder,
_formattingOptions,
MarkdownFormat.FirstLineAsCSharp,
out _);

return request.Hint with
{
Tooltip = stringBuilder.ToString(),
};
}

private class InlineHintCache
{
private readonly object _lock = new();
private string? _currentVersionString;
private List<(OmniSharpInlineHint Hint, Document Document)>? _hints;
private readonly ILogger _logger;

public InlineHintCache(ILogger logger)
{
_logger = logger;
}

public List<InlayHint> MapAndCacheHints(ImmutableArray<OmniSharpInlineHint> roslynHints, Document document, VersionStamp solutionVersion, SourceText text)
{
var resultList = new List<InlayHint>();
var solutionVersionString = solutionVersion.ToString();
lock (_lock)
{
var hintsList = _currentVersionString == solutionVersionString
? _hints
: new();

foreach (var hint in roslynHints)
{
var position = hintsList!.Count;
resultList.Add(new InlayHint()
{
Label = string.Concat(hint.DisplayParts),
Position = text.GetPointFromPosition(hint.Span.End),
Data = (solutionVersionString, position)
});

hintsList.Add((hint, document));
}

_currentVersionString = solutionVersionString;
_hints = hintsList;
}

return resultList;
}

public bool TryGetFromCache(InlayHint hint, out OmniSharpInlineHint roslynHint, [NotNullWhen(true)] out Document? document)
{
(roslynHint, document) = (default, null);
lock (_lock)
{
if (_hints is null)
{
_logger.LogWarning("Attempted to resolve hint before hints were requested");
return false;
}

if (_currentVersionString == hint.Data.SolutionVersion)
{
if (hint.Data.Position >= _hints.Count)
{
_logger.LogWarning("Hint position is not found in the list");
roslynHint = default;
return false;
}

(roslynHint, document) = _hints[hint.Data.Position];
return true;
}
else
{
_logger.LogInformation("Requested hint for outdated solution version");
roslynHint = default;
return false;
}
}
}
}
}
35 changes: 35 additions & 0 deletions src/OmniSharp.Shared/Options/InlayHintsOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace OmniSharp.Options
{
public struct InlayHintsOptions
{
public static readonly InlayHintsOptions AllOn = new()
{
EnableForParameters = true,
ForLiteralParameters = true,
ForIndexerParameters = true,
ForObjectCreationParameters = true,
ForOtherParameters = true,
SuppressForParametersThatDifferOnlyBySuffix = true,
SuppressForParametersThatMatchMethodIntent = true,
SuppressForParametersThatMatchArgumentName = true,
EnableForTypes = true,
ForImplicitVariableTypes = true,
ForLambdaParameterTypes = true,
ForImplicitObjectCreation = true
};

public bool EnableForParameters { get; set; }
public bool ForLiteralParameters { get; set; }
public bool ForIndexerParameters { get; set; }
public bool ForObjectCreationParameters { get; set; }
public bool ForOtherParameters { get; set; }
public bool SuppressForParametersThatDifferOnlyBySuffix { get; set; }
public bool SuppressForParametersThatMatchMethodIntent { get; set; }
public bool SuppressForParametersThatMatchArgumentName { get; set; }

public bool EnableForTypes { get; set; }
public bool ForImplicitVariableTypes { get; set; }
public bool ForLambdaParameterTypes { get; set; }
public bool ForImplicitObjectCreation { get; set; }
}
}
1 change: 1 addition & 0 deletions src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class RoslynExtensionsOptions : OmniSharpExtensionsOptions
public int DocumentAnalysisTimeoutMs { get; set; } = 30 * 1000;
public int DiagnosticWorkersThreadCount { get; set; } = Math.Max(1, (int)(Environment.ProcessorCount * 0.75)); // Use 75% of available processors by default (but at least one)
public bool AnalyzeOpenDocumentsOnly { get; set; }
public InlayHintsOptions InlayHintsOptions { get; set; } = new();
}

public class OmniSharpExtensionsOptions
Expand Down
Loading