Skip to content

Commit

Permalink
Generics inlay hints (#427)
Browse files Browse the repository at this point in the history
* Script fixes

* Exposed more of the symbols API

* Update package-lock.json

* Added config on VSC side

* Added inlay hints on langserver side
  • Loading branch information
LPeter1997 authored Aug 13, 2024
1 parent b94433c commit a7af6e1
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 28 deletions.
2 changes: 1 addition & 1 deletion scripts/install_langserver.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

$ErrorActionPreference = "Stop"
Push-Location $PSScriptRoot
dotnet pack ../src/Draco.LanguageServer --output .
dotnet pack ../src/Draco.LanguageServer --configuration Debug --output .
if ((dotnet tool list --global) -match "Draco.LanguageServer") {
dotnet tool uninstall --global Draco.LanguageServer
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/install_vscext.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ $ErrorActionPreference = "Stop"
Push-Location $PSScriptRoot
cd ../src/Draco.Extension.VsCode
npm i
vsce package --skip-license
echo 'y' | vsce package
$vsixFile = Get-ChildItem -Filter "*.vsix" | Select-Object -First 1
code --install-extension $vsixFile --force
Pop-Location
6 changes: 6 additions & 0 deletions src/Draco.Compiler/Api/Semantics/Symbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public interface ISymbol : IEquatable<ISymbol>
/// The generic definition of this symbol, in case this is a generic instance.
/// </summary>
public ISymbol? GenericDefinition { get; }

/// <summary>
/// The generic arguments of this symbol, in case this is a generic instance.
/// </summary>
public IEnumerable<ITypeSymbol> GenericArguments { get; }
}

/// <summary>
Expand Down Expand Up @@ -210,6 +215,7 @@ internal abstract class SymbolBase(Symbol symbol) : ISymbol
public bool IsGenericDefinition => this.Symbol.IsGenericDefinition;
public bool IsGenericInstance => this.Symbol.IsGenericInstance;
public ISymbol? GenericDefinition => this.Symbol.GenericDefinition?.ToApiSymbol();
public IEnumerable<ITypeSymbol> GenericArguments => this.Symbol.GenericArguments.Select(a => a.ToApiSymbol());

public bool Equals(ISymbol? other) => other is SymbolBase o
&& ReferenceEquals(this.Symbol, o.Symbol);
Expand Down
6 changes: 6 additions & 0 deletions src/Draco.Compiler/Internal/Symbols/TypeVariable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public override TypeSymbol Substitution
private TypeSymbol? substitution;
private readonly int index = index;

public override Api.Semantics.ITypeSymbol ToApiSymbol()
{
if (this.substitution is null) throw new InvalidOperationException("type variable not substituted");
return this.Substitution.ToApiSymbol();
}

public override string ToString() => this.Substitution switch
{
TypeVariable typeVar => $"{StringUtils.IndexToExcelColumnName(typeVar.index)}'",
Expand Down
28 changes: 14 additions & 14 deletions src/Draco.Extension.VsCode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Draco.Extension.VsCode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@
"type": "boolean",
"default": true,
"description": "Show variable type hints."
},
"draco.inlayHints.genericArguments": {
"type": "boolean",
"default": true,
"description": "Show generic argument type hints."
}
}
},
Expand Down
52 changes: 40 additions & 12 deletions src/Draco.LanguageServer/Capabilities/InlayHint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal sealed partial class DracoLanguageServer : IInlayHint
{
public InlayHintRegistrationOptions InlayHintRegistrationOptions => new()
{
DocumentSelector = DocumentSelector,
DocumentSelector = this.DocumentSelector,
};

public Task<IList<InlayHint>> InlayHintAsync(InlayHintParams param, CancellationToken cancellationToken)
Expand All @@ -33,7 +33,9 @@ public Task<IList<InlayHint>> InlayHintAsync(InlayHintParams param, Cancellation
var inlayHints = new List<InlayHint>();
foreach (var node in syntaxTree.TraverseSubtreesIntersectingRange(range))
{
if (config.VariableTypes && node is VariableDeclarationSyntax varDecl)
switch (node)
{
case VariableDeclarationSyntax varDecl when config.VariableTypes:
{
// Type is already specified by user
if (varDecl.Type is not null) continue;
Expand All @@ -50,8 +52,9 @@ public Task<IList<InlayHint>> InlayHintAsync(InlayHintParams param, Cancellation
Kind = InlayHintKind.Type,
Label = $": {varType}",
});
break;
}
else if (config.VariableTypes && node is ForExpressionSyntax @for)
case ForExpressionSyntax @for when config.ParameterNames:
{
if (@for.ElementType is not null) continue;

Expand All @@ -67,25 +70,50 @@ public Task<IList<InlayHint>> InlayHintAsync(InlayHintParams param, Cancellation
Kind = InlayHintKind.Type,
Label = $": {varType}",
});
break;
}
else if (config.ParameterNames && node is CallExpressionSyntax call)
case CallExpressionSyntax call:
{
var symbol = semanticModel.GetReferencedSymbol(call.Function);
if (symbol is not IFunctionSymbol funcSymbol) continue;
if (config.ParameterNames)
{
var symbol = semanticModel.GetReferencedSymbol(call.Function);
if (symbol is not IFunctionSymbol funcSymbol) continue;

foreach (var (argSyntax, paramSymbol) in call.ArgumentList.Values.Zip(funcSymbol.Parameters))
foreach (var (argSyntax, paramSymbol) in call.ArgumentList.Values.Zip(funcSymbol.Parameters))
{
var position = argSyntax.Range.Start;
var name = paramSymbol.Name;
if (string.IsNullOrWhiteSpace(name)) continue;

inlayHints.Add(new InlayHint()
{
Position = Translator.ToLsp(position),
Kind = InlayHintKind.Parameter,
Label = $"{name} = ",
});
}
}
if (config.GenericArguments)
{
var position = argSyntax.Range.Start;
var name = paramSymbol.Name;
if (string.IsNullOrWhiteSpace(name)) continue;
var symbol = semanticModel.GetReferencedSymbol(call.Function);
if (symbol is not IFunctionSymbol funcSymbol) continue;
// Not even a generic function
if (!symbol.IsGenericInstance) continue;
// See, if the syntax provided generic arguments
if (call.Function is GenericExpressionSyntax) continue;
// It did not, we can provide hints
var position = call.Function.Range.End;
var genericArgs = string.Join(", ", funcSymbol.GenericArguments.Select(arg => arg.Name));

inlayHints.Add(new InlayHint()
{
Position = Translator.ToLsp(position),
Kind = InlayHintKind.Parameter,
Label = $"{name} = ",
Kind = InlayHintKind.Type,
Label = $"<{genericArgs}>",
});
}
break;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ internal sealed class InlayHintsConfiguration
{
public bool ParameterNames { get; set; }
public bool VariableTypes { get; set; }
public bool GenericArguments { get; set; }
}
1 change: 1 addition & 0 deletions src/Draco.LanguageServer/DracoConfigurationRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ public async Task UpdateConfigurationAsync()

this.InlayHints.ParameterNames = cfg[0].GetProperty("parameterNames"u8).GetBoolean();
this.InlayHints.VariableTypes = cfg[0].GetProperty("variableTypes"u8).GetBoolean();
this.InlayHints.GenericArguments = cfg[0].GetProperty("genericArguments"u8).GetBoolean();
}
}

0 comments on commit a7af6e1

Please sign in to comment.