Skip to content

Commit

Permalink
Merge pull request #2357 from 333fred/inlay-hints
Browse files Browse the repository at this point in the history
Support inlay hints
  • Loading branch information
333fred authored Mar 17, 2022
2 parents 0dc35e4 + 9ed057d commit b1dd617
Show file tree
Hide file tree
Showing 14 changed files with 1,923 additions and 1 deletion.
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,12 @@
using OmniSharp.Mef;
using OmniSharp.Models.V2;

#nullable enable annotations

namespace OmniSharp.Models.v1.InlayHints;

[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
187 changes: 187 additions & 0 deletions src/OmniSharp.Roslyn.CSharp/Services/InlayHints/InlayHintService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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)
{
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);
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

0 comments on commit b1dd617

Please sign in to comment.