-
Notifications
You must be signed in to change notification settings - Fork 237
Introduce Get-EditorServicesParserAst to expand on ExpandAlias #1199
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// | ||
// Copyright (c) Microsoft. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
// | ||
|
||
using System; | ||
using System.Collections.ObjectModel; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Management.Automation; | ||
using Microsoft.PowerShell.Commands; | ||
|
||
namespace Microsoft.PowerShell.EditorServices.Commands | ||
{ | ||
|
||
/// <summary> | ||
/// The Get-EditorServicesParserAst command will parse and expand out data parsed by ast. | ||
/// </summary> | ||
[Cmdlet(VerbsCommon.Get, "EditorServicesParserAst")] | ||
public sealed class GetEditorServicesParserAst : PSCmdlet | ||
{ | ||
|
||
/// <summary> | ||
/// The Scriptblock or block of code that gets parsed by ast. | ||
/// </summary> | ||
[Parameter(Mandatory = true)] | ||
public string ScriptBlock { get; set; } | ||
|
||
/// <summary> | ||
/// Specify a specific command type | ||
/// [System.Management.Automation.CommandTypes] | ||
/// </summary> | ||
[Parameter(Mandatory = true)] | ||
public CommandTypes CommandType { get; set; } | ||
|
||
/// <summary> | ||
/// Specify a specific token type | ||
/// [System.Management.Automation.PSTokenType] | ||
/// </summary> | ||
[Parameter(Mandatory = true)] | ||
public PSTokenType PSTokenType { get; set; } | ||
|
||
protected override void EndProcessing() | ||
{ | ||
var errors = new Collection<PSParseError>(); | ||
|
||
var tokens = | ||
System.Management.Automation.PSParser.Tokenize(ScriptBlock, out errors) | ||
.Where(token => token.Type == this.PSTokenType) | ||
.OrderBy(token => token.Content); | ||
|
||
foreach (PSToken token in tokens) | ||
{ | ||
if (PSTokenType == PSTokenType.Command) | ||
{ | ||
var result = SessionState.InvokeCommand.GetCommand(token.Content, CommandType); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You've done a great job of converting the code from PowerShell to C# that I'm wondering if you could put this code in the Handler below instead of in its own cmdlet... then the only thing you'd need to do in script is the call to Also, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically rather than implementing the cmdlet, you can do something like this: ScriptBlockAst scriptAst = Parser.ParseInput(request.Text, out Token[] _, out ParseError[] _);
var commandsToExpand = new List<string>();
foreach (Ast foundAst in scriptAst.FindAll(ast => ast is CommandAst, true))
{
if (!(foundAst is CommandAst commandAst))
{
continue;
}
string commandName = commandAst.GetCommandName();
if (commandName != null)
{
commandsToExpand.Add(commandName);
}
}
var psCommand = new PSCommand()
.AddCommand("Get-Command")
.AddParameter("Name", commandsToExpand)
.AddParameter("ErrorAction", "Ignore");
IEnumerable<CommandInfo> expandedCommands = _powerShellContextService.InvokeCommandAsync<CommandInfo>(psCommand);
var expandedCommands = new List<string>(commandsToExpand.Count);
foreach (CommandInfo command in expandedCommands)
{
if (command is AliasInfo alias)
{
expandedCommands.Add(alias.ReferencedCommand.Name);
continue;
}
expandedCommands.Add(command.Name);
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Posted this to document the thought, but it's non-trivial at this point. You now need to be able to insert the expanded command names back into the original AST while not modifying anything else. That will require some amount of custom logic to put the script back together from the AST, and there's not currently a nice API for that beyond writing a new AST visitor... We could try this on tokens instead, but I'm not sure that's the best idea. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Originally I had thought we can do much more with the tokenizer in order to remove repetitious code that will come up. Like Expand Alias, Expand Command.. etc.. However I don't really see much more of a use case other than those two. So moving forward..
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with using the pipeline is that the more you use the pipeline the longer you exclude other services from using it. So basically you only want to use the pipeline for things you absolutely need its context for, which in this case is alias resolution. Beyond that, we want to do as much as we can in off-thread C# code. The problem is that the old function rebuilt the whole script one command at a time by rebuilding the string for each command. The new cmdlet doesn't do that and my code above doesn't have an implementation for that either. We need to be able to reconstruct the whole script with the expanded commands given in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ideally the ScriptBlockAst ast = Parser.ParseInput(request.Text, out _, out _);
CommandAst[] commands = ast
.FindAll(a => a is CommandAst, searchNestedScriptBlocks: true)
.Cast<CommandAst>()
.ToArray();
var commandNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (CommandAst command in commands)
{
string commandName = command.GetCommandName();
if (string.IsNullOrEmpty(commandName))
{
continue;
}
commandNames.Add(commandName);
}
var psCommand = new PSCommand()
.AddCommand("Microsoft.PowerShell.Core\\Get-Command")
.AddParameter("CommandTypes", CommandTypes.Alias)
.AddParameter("ErrorAction", ActionPreference.Ignore)
.AddParameter("Name", commandNames.ToArray());
AliasInfo[] aliases = (await _powerShellContextService
.ExecuteCommandAsync<AliasInfo>(psCommand, sendErrorToHost: false).ConfigureAwait(false))
.ToArray();
var aliasMap = new Dictionary<string, string>(
capacity: aliases.Length,
StringComparer.OrdinalIgnoreCase);
foreach (AliasInfo alias in aliases)
{
aliasMap.Add(alias.Name, alias.Definition);
}
var edits = new List<TextEdit>(capacity: commands.Length);
foreach (CommandAst command in commands)
{
string commandName = command.GetCommandName();
if (string.IsNullOrEmpty(commandName))
{
continue;
}
if (!aliasMap.TryGetValue(commandName, out string definition))
{
continue;
}
IScriptExtent extent = command.CommandElements[0].Extent;
var edit = new TextEdit()
{
NewText = definition,
Range = new Range(
new Position(extent.StartLineNumber - 1, extent.StartColumnNumber - 1),
new Position(extent.EndLineNumber - 1, extent.EndColumnNumber - 1))
};
edits.Add(edit);
}
// make and return WorkspaceEdit |
||
WriteObject(result); | ||
} | ||
else { | ||
WriteObject(token); | ||
} | ||
|
||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.