Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
Expand Down Expand Up @@ -31,42 +36,61 @@ internal class ExpandAliasHandler : IExpandAliasHandler

public async Task<ExpandAliasResult> Handle(ExpandAliasParams request, CancellationToken cancellationToken)
{
const string script = @"
function __Expand-Alias {
[System.Diagnostics.DebuggerHidden()]
param($targetScript)
string targetScript = request.Text;

[ref]$errors=$null
// Use the modern language parser to tokenize the script, then collect
// the command-name tokens (the first token of each command invocation).
Parser.ParseInput(targetScript, out Token[] tokens, out _);
List<Token> commandNameTokens = tokens
.Where(static token => (token.TokenFlags & TokenFlags.CommandName) == TokenFlags.CommandName)
.ToList();

$tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) |
Sort-Object Start -Descending
if (commandNameTokens.Count == 0)
{
return new ExpandAliasResult { Text = targetScript };
}

foreach ($token in $tokens) {
$definition=(Get-Command ('`'+$token.Content) -CommandType Alias -ErrorAction SilentlyContinue).Definition
// Resolve all the distinct command names to their alias definitions in a
// single round-trip. Wildcard metacharacters are escaped so that aliases
// like `?` (Where-Object) and `%` (ForEach-Object) resolve to themselves
// rather than being treated as patterns.
string[] names = commandNameTokens
.Select(static token => WildcardPattern.Escape(token.Text))
.Distinct()
.ToArray();

if($definition) {
$lhs=$targetScript.Substring(0, $token.Start)
$rhs=$targetScript.Substring($token.Start + $token.Length)
PSCommand psCommand = new PSCommand()
.AddCommand("Get-Command")
.AddParameter("Name", names)
.AddParameter("CommandType", CommandTypes.Alias)
.AddParameter("ErrorAction", ActionPreference.SilentlyContinue);

$targetScript=$lhs + $definition + $rhs
}
}
IReadOnlyList<AliasInfo> aliases = await _executionService
.ExecutePSCommandAsync<AliasInfo>(psCommand, cancellationToken)
.ConfigureAwait(false);

$targetScript
}";
Dictionary<string, string> definitions = new(StringComparer.OrdinalIgnoreCase);
foreach (AliasInfo alias in aliases)
{
definitions[alias.Name] = alias.Definition;
}

// TODO: Refactor to not rerun the function definition every time.
PSCommand psCommand = new();
psCommand
.AddScript(script)
.AddStatement()
.AddCommand("__Expand-Alias")
.AddArgument(request.Text);
System.Collections.Generic.IReadOnlyList<string> result = await _executionService.ExecutePSCommandAsync<string>(psCommand, cancellationToken).ConfigureAwait(false);
// Substitute from the end of the script backwards so that earlier offsets
// remain valid as the text length changes.
StringBuilder expanded = new(targetScript);
foreach (Token token in commandNameTokens.OrderByDescending(static token => token.Extent.StartOffset))
{
if (definitions.TryGetValue(token.Text, out string definition))
{
int start = token.Extent.StartOffset;
int length = token.Extent.EndOffset - start;
expanded.Remove(start, length).Insert(start, definition);
}
}

return new ExpandAliasResult
{
Text = result[0]
Text = expanded.ToString()
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,29 @@ await PsesLanguageClient
Assert.Equal("Get-ChildItem", expandAliasResult.Text);
}

[SkippableFact]
public async Task CanSendExpandAliasRequestWithMultipleAliasesAsync()
{
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
"The expand alias request doesn't work in Constrained Language Mode.");

// Exercises multiple aliases on one line, including `?` whose name is a
// wildcard metacharacter that the legacy tokenizer-based implementation
// would have to escape by hand. Substitution happens from the end so the
// earlier offsets stay valid as the text grows.
ExpandAliasResult expandAliasResult =
await PsesLanguageClient
.SendRequest(
"powerShell/expandAlias",
new ExpandAliasParams
{
Text = "gci | ? Name | % Name"
})
.Returning<ExpandAliasResult>(CancellationToken.None);

Assert.Equal("Get-ChildItem | Where-Object Name | ForEach-Object Name", expandAliasResult.Text);
}

[Fact]
public async Task CanSendSemanticTokenRequestAsync()
{
Expand Down
Loading