You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Semantic Syntax is incorrectly highlighting arguments as commands #2860
The problem is that there is no way to distinguish between a function name in a declaration and a parameter from just the token information alone. I think that we'll need to add other checks (i.e. position or looking into the AST)
Rob Holt (@rjmholt) I think what they're getting at is that the current regex based highlighter treats the name in a function definition like a command (which doesn't have the flag).
Personally I'd rather have the name in a function definition be parsed as a bare word than the current situation (that's how both the ISE and PSRL work currently). But a fix for both might be just keeping track of if the previous token kind was Function. Then also use TokenFlags.CommandName.
There's no way to differentiate between the Get-AA token (should be labelled as a command/function) and the That-Thing token (which should be labelled as something else) with only the Token info. So without using additional data, we either have the situation we have now, or Get-AA would not be highlighted
I think what they're getting at is that the current regex based highlighter treats the name in a function definition like a command (which doesn't have the flag).
Yes, I noticed that. But in Joel Bennett (@Jaykul)'s original issue description he seems to be referring to the CommandAst situation rather than the FunctionDefinitionAst situation.
The FunctionDefinitionAst scenario is easy to address: we look back a token and see if it's the function keyword. Given that the grammar defines the full nature of the context, rather than just the tokens, we can't do a simple token search for all tokens, but I believe we can in this case.
A more sophisticated approach is to track our position in both the AST and the token array simulataneously and for each token look into where we are in the AST to determine the token semantics.
I've written an example where we track through tokens as we move through an AST here.
Or the converse, where we find the AST element corresponding to our token:
/// <summary>/// An AST visitor to find the smallest AST containing a given IScriptPosition./// </summary>internalclassFindAstFromPositionVisitor:AstVisitor2{privatereadonlyIScriptPosition_position;privateAst_astAtPosition;/// <summary>/// Create a FindAstFromPositionVisitor around a given position,/// ready to find the smallest AST containing that position./// </summary>/// <param name="position">The position to find the containing AST of.</param>publicFindAstFromPositionVisitor(IScriptPositionposition){_position=position;}/// <summary>/// Retrieve the computed smallest containing AST after a visit has been performed./// </summary>/// <returns>The smallest AST containing the originally given position in the visited AST.</returns>publicAstGetAst(){return_astAtPosition;}publicoverrideAstVisitActionVisitArrayExpression(ArrayExpressionAstarrayExpressionAst)=>VisitAst(arrayExpressionAst);publicoverrideAstVisitActionVisitArrayLiteral(ArrayLiteralAstarrayLiteralAst)=>VisitAst(arrayLiteralAst);publicoverrideAstVisitActionVisitAssignmentStatement(AssignmentStatementAstassignmentStatementAst)=>VisitAst(assignmentStatementAst);publicoverrideAstVisitActionVisitAttribute(AttributeAstattributeAst)=>VisitAst(attributeAst);publicoverrideAstVisitActionVisitAttributedExpression(AttributedExpressionAstattributedExpressionAst)=>VisitAst(attributedExpressionAst);publicoverrideAstVisitActionVisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAstbaseCtorInvokeMemberExpressionAst)=>AstVisitAction.SkipChildren;publicoverrideAstVisitActionVisitBinaryExpression(BinaryExpressionAstbinaryExpressionAst)=>VisitAst(binaryExpressionAst);publicoverrideAstVisitActionVisitBlockStatement(BlockStatementAstblockStatementAst)=>VisitAst(blockStatementAst);publicoverrideAstVisitActionVisitBreakStatement(BreakStatementAstbreakStatementAst)=>VisitAst(breakStatementAst);publicoverrideAstVisitActionVisitCatchClause(CatchClauseAstcatchClauseAst)=>VisitAst(catchClauseAst);publicoverrideAstVisitActionVisitCommand(CommandAstcommandAst)=>VisitAst(commandAst);publicoverrideAstVisitActionVisitCommandExpression(CommandExpressionAstcommandExpressionAst)=>VisitAst(commandExpressionAst);publicoverrideAstVisitActionVisitCommandParameter(CommandParameterAstcommandParameterAst)=>VisitAst(commandParameterAst);publicoverrideAstVisitActionVisitConfigurationDefinition(ConfigurationDefinitionAstconfigurationDefinitionAst)=>VisitAst(configurationDefinitionAst);publicoverrideAstVisitActionVisitConstantExpression(ConstantExpressionAstconstantExpressionAst)=>VisitAst(constantExpressionAst);publicoverrideAstVisitActionVisitContinueStatement(ContinueStatementAstcontinueStatementAst)=>VisitAst(continueStatementAst);publicoverrideAstVisitActionVisitConvertExpression(ConvertExpressionAstconvertExpressionAst)=>VisitAst(convertExpressionAst);publicoverrideAstVisitActionVisitDataStatement(DataStatementAstdataStatementAst)=>VisitAst(dataStatementAst);publicoverrideAstVisitActionVisitDoUntilStatement(DoUntilStatementAstdoUntilStatementAst)=>VisitAst(doUntilStatementAst);publicoverrideAstVisitActionVisitDoWhileStatement(DoWhileStatementAstdoWhileStatementAst)=>VisitAst(doWhileStatementAst);publicoverrideAstVisitActionVisitDynamicKeywordStatement(DynamicKeywordStatementAstdynamicKeywordStatementAst)=>VisitAst(dynamicKeywordStatementAst);publicoverrideAstVisitActionVisitErrorExpression(ErrorExpressionAsterrorExpressionAst)=>VisitAst(errorExpressionAst);publicoverrideAstVisitActionVisitErrorStatement(ErrorStatementAsterrorStatementAst)=>VisitAst(errorStatementAst);publicoverrideAstVisitActionVisitExitStatement(ExitStatementAstexitStatementAst)=>VisitAst(exitStatementAst);publicoverrideAstVisitActionVisitExpandableStringExpression(ExpandableStringExpressionAstexpandableStringExpressionAst)=>VisitAst(expandableStringExpressionAst);publicoverrideAstVisitActionVisitFileRedirection(FileRedirectionAstredirectionAst)=>VisitAst(redirectionAst);publicoverrideAstVisitActionVisitForEachStatement(ForEachStatementAstforEachStatementAst)=>VisitAst(forEachStatementAst);publicoverrideAstVisitActionVisitForStatement(ForStatementAstforStatementAst)=>VisitAst(forStatementAst);publicoverrideAstVisitActionVisitFunctionDefinition(FunctionDefinitionAstfunctionDefinitionAst)=>VisitAst(functionDefinitionAst);publicoverrideAstVisitActionVisitFunctionMember(FunctionMemberAstfunctionMemberAst)=>VisitAst(functionMemberAst);publicoverrideAstVisitActionVisitHashtable(HashtableAsthashtableAst)=>VisitAst(hashtableAst);publicoverrideAstVisitActionVisitIfStatement(IfStatementAstifStmtAst)=>VisitAst(ifStmtAst);publicoverrideAstVisitActionVisitIndexExpression(IndexExpressionAstindexExpressionAst)=>VisitAst(indexExpressionAst);publicoverrideAstVisitActionVisitInvokeMemberExpression(InvokeMemberExpressionAstmethodCallAst)=>VisitAst(methodCallAst);publicoverrideAstVisitActionVisitMemberExpression(MemberExpressionAstmemberExpressionAst)=>VisitAst(memberExpressionAst);publicoverrideAstVisitActionVisitMergingRedirection(MergingRedirectionAstredirectionAst)=>VisitAst(redirectionAst);publicoverrideAstVisitActionVisitNamedAttributeArgument(NamedAttributeArgumentAstnamedAttributeArgumentAst)=>VisitAst(namedAttributeArgumentAst);publicoverrideAstVisitActionVisitNamedBlock(NamedBlockAstnamedBlockAst)=>VisitAst(namedBlockAst);publicoverrideAstVisitActionVisitParamBlock(ParamBlockAstparamBlockAst)=>VisitAst(paramBlockAst);publicoverrideAstVisitActionVisitParameter(ParameterAstparameterAst)=>VisitAst(parameterAst);publicoverrideAstVisitActionVisitParenExpression(ParenExpressionAstparenExpressionAst)=>VisitAst(parenExpressionAst);publicoverrideAstVisitActionVisitPipeline(PipelineAstpipelineAst)=>VisitAst(pipelineAst);publicoverrideAstVisitActionVisitPropertyMember(PropertyMemberAstpropertyMemberAst)=>VisitAst(propertyMemberAst);publicoverrideAstVisitActionVisitReturnStatement(ReturnStatementAstreturnStatementAst)=>VisitAst(returnStatementAst);publicoverrideAstVisitActionVisitScriptBlock(ScriptBlockAstscriptBlockAst)=>VisitAst(scriptBlockAst);publicoverrideAstVisitActionVisitScriptBlockExpression(ScriptBlockExpressionAstscriptBlockExpressionAst)=>VisitAst(scriptBlockExpressionAst);publicoverrideAstVisitActionVisitStatementBlock(StatementBlockAststatementBlockAst)=>VisitAst(statementBlockAst);publicoverrideAstVisitActionVisitStringConstantExpression(StringConstantExpressionAststringConstantExpressionAst)=>VisitAst(stringConstantExpressionAst);publicoverrideAstVisitActionVisitSubExpression(SubExpressionAstsubExpressionAst)=>VisitAst(subExpressionAst);publicoverrideAstVisitActionVisitSwitchStatement(SwitchStatementAstswitchStatementAst)=>VisitAst(switchStatementAst);publicoverrideAstVisitActionVisitThrowStatement(ThrowStatementAstthrowStatementAst)=>VisitAst(throwStatementAst);publicoverrideAstVisitActionVisitTrap(TrapStatementAsttrapStatementAst)=>VisitAst(trapStatementAst);publicoverrideAstVisitActionVisitTryStatement(TryStatementAsttryStatementAst)=>VisitAst(tryStatementAst);publicoverrideAstVisitActionVisitTypeConstraint(TypeConstraintAsttypeConstraintAst)=>VisitAst(typeConstraintAst);publicoverrideAstVisitActionVisitTypeDefinition(TypeDefinitionAsttypeDefinitionAst)=>VisitAst(typeDefinitionAst);publicoverrideAstVisitActionVisitTypeExpression(TypeExpressionAsttypeExpressionAst)=>VisitAst(typeExpressionAst);publicoverrideAstVisitActionVisitUnaryExpression(UnaryExpressionAstunaryExpressionAst)=>VisitAst(unaryExpressionAst);publicoverrideAstVisitActionVisitUsingExpression(UsingExpressionAstusingExpressionAst)=>VisitAst(usingExpressionAst);publicoverrideAstVisitActionVisitUsingStatement(UsingStatementAstusingStatementAst)=>VisitAst(usingStatementAst);publicoverrideAstVisitActionVisitVariableExpression(VariableExpressionAstvariableExpressionAst)=>VisitAst(variableExpressionAst);publicoverrideAstVisitActionVisitWhileStatement(WhileStatementAstwhileStatementAst)=>VisitAst(whileStatementAst);privateAstVisitActionVisitAst(Astast){if(!AstContainsPosition(ast)){returnAstVisitAction.SkipChildren;}_astAtPosition=ast;returnAstVisitAction.Continue;}privateboolAstContainsPosition(Astast){returnIsBefore(ast.Extent.StartScriptPosition,_position)&&IsBefore(_position,ast.Extent.EndScriptPosition);}privatestaticboolIsBefore(IScriptPositionleft,IScriptPositionright){returnleft.LineNumber<right.LineNumber||(left.LineNumber==right.LineNumber&&left.ColumnNumber<=right.ColumnNumber);}}
Bareword arguments without -s are tokenised as identifiers rather than generic tokens, so they fall through to the grammar.
We can recognise a bareword identifier most correctly as an identifier or a generic token that is subordinate to a command token... Harder to do though.
We need to pick a new value based on this list. I would guess Member, Property or EnumMember would work.
Joel Bennett (@Jaykul) true but nothing currently highlights a bare word string literal command arg like every other string literal. member is probably the closest to what existing tools highlight it as.
Are you really arguing that since everyone else is wrong, this should be wrong too? It's a string. The fact that PowerShell allows you to leave off the quotes doesn't change the fact that it's a string. It should be highlighted as a string if it's Kind = "Identifier" or Kind = "Generic" with TokenFlags = "None" (and follows a command?).
It's most definitely not a member. Highlighting it as member is worse than nothing (although not worse than highlighting it as a command) in my opinion.
If you really decide you need to use member, please instead send a PR to the SemanticTokenTypes enum to add an a "unspecified" or "otherliteral" or something.
Yeah it's not about how PowerShell uses it, it's about how it's highlighted. Categorising it as a string for highlighting purposes will make 'input' and input the same colour, whereas in PSReadLine for example, they are not at all (here vs 'there'):
Are you really arguing that since everyone else is wrong, this should be wrong too? It's a string.
Please keep your tone civil. We can be more constructive without rhetoric like this.
I don't think there's that much backward compatibility to worry about. It's already drastically changing most of the highlighting... and not just breaking changes like using statements with paths, escaped characters, #requires, etc.
And PSReadline is wrong about that too. Both your here and 'there' are strings. Why would you want to make them seem different? No offense intended. I mean, I did most of the regex parser, I'm not trying to blame anyone. I'm just pointing out that it's literally a string (and now, we know that) but it's not highlighted as one just because ... we never bothered to get it right.
Joel Bennett (@Jaykul) that's all fair and I don't necessarily disagree. Tbh, I think it would bother people. I don't really have any specific logic to back that up with, but I think it would get a lot of complaints.
Personally I'd also rather stick closer to what PSRL does, even if it isn't correct. PSRL highlighting barewords differently changed how they feel vs quoted strings imo, for better or worse.
Both your here and 'there' are strings. Why would you want to make them seem different?
Yeah, I definitely understand your reasoning here.
We're in a strange set because bareword strings are a rare occurrence among languages, so there's not good precedent or mechanisms built in (in an environment mainly designed to cater to C# and TypeScript) for them.
I don't think there's that much backward compatibility to worry about
Agreed. Certainly as a visual feature, I don't personally see any concerns about "breaking changes" so to speak.
I think of highlighting as a visual representation of tokeniser mode. Even though bareword strings and quoted strings represent the same type of value, when I'm typing them or looking at my program, the thing I care about is what delimits them. A quote is easy to miss, but the specific colour of a quoted string tells me that I'm in a special mode where the rules are different. That's not true of bareword strings, because the tokeniser will find a lot more reasons to end the string token.
Certainly I think that if we made a change to classify quoted strings and bareword strings the same way, the majority of our users would have complaints due to struggling to differentiate the different types of strings in their scripts.
In an ideal world, we would have a BareWordString token type that a VSCode theme could pick up on so that anyone could choose to make them the same or different colours. But in the real world we need a fallback that will cater to common themes out there that are mainly built for programming languages with tame syntaxes. I think that's the essence of my proposal in #2852 (comment).
(The unfortunate side-effect of the success of the C-syntax-family languages is that much tooling forgets just how different some languages are)
While you're convincing me this whole product is not for me, I disagree fundamentally with the base assumption that your users want what they already have.
Here's PSReadLine, Semantic, and EditorSyntax:
PSReadLine is better than the others, and (when it's not coloring strings as commands) sematic is better than editor syntax, but in all three the (lack of) highlighting of bare strings is incorrect and does not help authors understand their code.
I have no problem with the idea of having a language-specific token (or, in this case, a shell-language-specific token) and a fallback token type for themes that don't understand the type of language that PowerShell is, but if you want to do that, you also need to provide a first-class theme that you can point to as an example (and in this day and age, you need a dark version of it too). However, that's not what's happening here. And in any case, the ISE theme doesn't provide better information. If anything, it's worse in this case, because keywords are virtually indistinquishable from bareword strings.
If you wanted to have different colors for bareword, single quoted, and double quoted strings, I would would be down with that. But that falls down if Single and Double quoted strings are one kind of string, and bare strings are the only one that's totally different...
The first stage is parity: being as close to PSReadLine as possible. This will allow both the console and editing experience to be on the same playing field. (this was actually the original goal of doing this work from the beginning)
The second stage is improving: Once both experiences are roughly aligned, then we can go back and make the changes in both to support the language better.
Reacted by Keith Hill, Rob Holt, Sydney Smith, Troy Campbell and Andy Jordan
Activity
justinytchen commentedon Aug 5, 2020
The problem is that there is no way to distinguish between a function name in a declaration and a parameter from just the token information alone. I think that we'll need to add other checks (i.e. position or looking into the AST)
rjmholt commentedon Aug 5, 2020
I think the important part is that the
Testtoken and theThat-Thingtoken have different flags:That's how PSReadLine is able to differentiate them I imagine
SeeminglyScience commentedon Aug 5, 2020
Rob Holt (@rjmholt) I think what they're getting at is that the current regex based highlighter treats the name in a function definition like a command (which doesn't have the flag).
Personally I'd rather have the name in a function definition be parsed as a bare word than the current situation (that's how both the ISE and PSRL work currently). But a fix for both might be just keeping track of if the previous token kind was
Function. Then also useTokenFlags.CommandName.justinytchen commentedon Aug 5, 2020
There's no way to differentiate between the Get-AA token (should be labelled as a command/function) and the That-Thing token (which should be labelled as something else) with only the Token info. So without using additional data, we either have the situation we have now, or Get-AA would not be highlighted
SeeminglyScience commentedon Aug 5, 2020
Justin Chen (@justinytchen) You also could change this up a bit:
https://github.com/PowerShell/PowerShellEditorServices/blob/c40734354631043cf3c390d36c9b1ef6a80b3949/src/PowerShellEditorServices/Services/TextDocument/Handlers/PsesSemanticTokensHandler.cs#L51-L54
to be:
rjmholt commentedon Aug 5, 2020
Yes, I noticed that. But in Joel Bennett (@Jaykul)'s original issue description he seems to be referring to the
CommandAstsituation rather than theFunctionDefinitionAstsituation.The
FunctionDefinitionAstscenario is easy to address: we look back a token and see if it's thefunctionkeyword. Given that the grammar defines the full nature of the context, rather than just the tokens, we can't do a simple token search for all tokens, but I believe we can in this case.A more sophisticated approach is to track our position in both the AST and the token array simulataneously and for each token look into where we are in the AST to determine the token semantics.
I've written an example where we track through tokens as we move through an AST here.
Or the converse, where we find the AST element corresponding to our token:
rjmholt commentedon Aug 6, 2020
Ok so the problem is actually that generic tokens with no flags are currently highlighted as functions.
Bareword arguments without
-s are tokenised as identifiers rather than generic tokens, so they fall through to the grammar.We can recognise a bareword identifier most correctly as an identifier or a generic token that is subordinate to a command token... Harder to do though.
We need to pick a new value based on this list. I would guess
Member,PropertyorEnumMemberwould work.TylerLeonhardt commentedon Aug 6, 2020
Property is probably the least threatening because Member in the Monokai theme was lime green lol
1 remaining item
Jaykul commentedon Aug 7, 2020
A "generic token" that's a parameter ... is actually a
string, not amember.SeeminglyScience commentedon Aug 7, 2020
Joel Bennett (@Jaykul) true but nothing currently highlights a bare word string literal command arg like every other string literal.
memberis probably the closest to what existing tools highlight it as.Jaykul commentedon Aug 7, 2020
Are you really arguing that since everyone else is wrong, this should be wrong too? It's a string. The fact that PowerShell allows you to leave off the quotes doesn't change the fact that it's a string. It should be highlighted as a string if it's
Kind = "Identifier"orKind = "Generic"withTokenFlags = "None"(and follows a command?).It's most definitely not a member. Highlighting it as member is worse than nothing (although not worse than highlighting it as a command) in my opinion.
If you really decide you need to use member, please instead send a PR to the
SemanticTokenTypesenum to add an a "unspecified" or "otherliteral" or something.SeeminglyScience commentedon Aug 7, 2020
Nah, I'm saying people will be annoyed if it's highlighted like every other string literal, because that's what they're used to.
Yeah, no one here thinks it isn't a string. Doesn't change the fact that folks expect it to be highlighted differently 🤷
rjmholt commentedon Aug 7, 2020
Yeah it's not about how PowerShell uses it, it's about how it's highlighted. Categorising it as a string for highlighting purposes will make
'input'andinputthe same colour, whereas in PSReadLine for example, they are not at all (herevs'there'):Please keep your tone civil. We can be more constructive without rhetoric like this.
Jaykul commentedon Aug 8, 2020
I don't think there's that much backward compatibility to worry about. It's already drastically changing most of the highlighting... and not just breaking changes like using statements with paths, escaped characters,
#requires, etc.And PSReadline is wrong about that too. Both your
hereand'there'are strings. Why would you want to make them seem different? No offense intended. I mean, I did most of the regex parser, I'm not trying to blame anyone. I'm just pointing out that it's literally a string (and now, we know that) but it's not highlighted as one just because ... we never bothered to get it right.SeeminglyScience commentedon Aug 8, 2020
Joel Bennett (@Jaykul) that's all fair and I don't necessarily disagree. Tbh, I think it would bother people. I don't really have any specific logic to back that up with, but I think it would get a lot of complaints.
Personally I'd also rather stick closer to what PSRL does, even if it isn't correct. PSRL highlighting barewords differently changed how they feel vs quoted strings imo, for better or worse.
rjmholt commentedon Aug 10, 2020
Yeah, I definitely understand your reasoning here.
We're in a strange set because bareword strings are a rare occurrence among languages, so there's not good precedent or mechanisms built in (in an environment mainly designed to cater to C# and TypeScript) for them.
Agreed. Certainly as a visual feature, I don't personally see any concerns about "breaking changes" so to speak.
I think of highlighting as a visual representation of tokeniser mode. Even though bareword strings and quoted strings represent the same type of value, when I'm typing them or looking at my program, the thing I care about is what delimits them. A quote is easy to miss, but the specific colour of a quoted string tells me that I'm in a special mode where the rules are different. That's not true of bareword strings, because the tokeniser will find a lot more reasons to end the string token.
Certainly I think that if we made a change to classify quoted strings and bareword strings the same way, the majority of our users would have complaints due to struggling to differentiate the different types of strings in their scripts.
In an ideal world, we would have a
BareWordStringtoken type that a VSCode theme could pick up on so that anyone could choose to make them the same or different colours. But in the real world we need a fallback that will cater to common themes out there that are mainly built for programming languages with tame syntaxes. I think that's the essence of my proposal in #2852 (comment).(The unfortunate side-effect of the success of the C-syntax-family languages is that much tooling forgets just how different some languages are)
Jaykul commentedon Aug 10, 2020
While you're convincing me this whole product is not for me, I disagree fundamentally with the base assumption that your users want what they already have.
Here's PSReadLine, Semantic, and EditorSyntax:
PSReadLine is better than the others, and (when it's not coloring strings as commands) sematic is better than editor syntax, but in all three the (lack of) highlighting of bare strings is incorrect and does not help authors understand their code.
I have no problem with the idea of having a language-specific token (or, in this case, a shell-language-specific token) and a fallback token type for themes that don't understand the type of language that PowerShell is, but if you want to do that, you also need to provide a first-class theme that you can point to as an example (and in this day and age, you need a dark version of it too). However, that's not what's happening here. And in any case, the ISE theme doesn't provide better information. If anything, it's worse in this case, because keywords are virtually indistinquishable from bareword strings.
If you wanted to have different colors for bareword, single quoted, and double quoted strings, I would would be down with that. But that falls down if Single and Double quoted strings are one kind of string, and bare strings are the only one that's totally different...
TylerLeonhardt commentedon Aug 11, 2020
So I think we need to take this in two stages.
The first stage is parity: being as close to PSReadLine as possible. This will allow both the console and editing experience to be on the same playing field. (this was actually the original goal of doing this work from the beginning)
The second stage is improving: Once both experiences are roughly aligned, then we can go back and make the changes in both to support the language better.
jessiewestlake commentedon Sep 1, 2021
To make matters worse... You also have the same problem when the parameter argument is not a string, but a negative number (e.g.
-1or-3.14).