-
Notifications
You must be signed in to change notification settings - Fork 417
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2357 from 333fred/inlay-hints
Support inlay hints
- Loading branch information
Showing
14 changed files
with
1,923 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
src/OmniSharp.Abstractions/LoggingExtensions/LoggingExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
35
src/OmniSharp.Abstractions/Models/v1/InlayHints/InlayHint.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
12 changes: 12 additions & 0 deletions
12
src/OmniSharp.Abstractions/Models/v1/InlayHints/InlayHintRequest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
9 changes: 9 additions & 0 deletions
9
src/OmniSharp.Abstractions/Models/v1/InlayHints/InlayHintResolveRequest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
11 changes: 11 additions & 0 deletions
11
src/OmniSharp.Abstractions/Models/v1/InlayHints/InlayHintResponse.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
src/OmniSharp.Roslyn.CSharp/Services/InlayHints/InlayHintService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.