From e132a787d93346a4ad5c31bb7caded76a535a616 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:07:16 -0500 Subject: [PATCH 01/16] test: add test for annotation on same line as method (issue #44) - Add test case for annotation on same line as method declaration - Test verifies correct highlighting in this scenario - Test currently passes, indicating issue may already be resolved Closes #44 --- test/annotation.tests.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/annotation.tests.ts b/test/annotation.tests.ts index 4e81674..3670bee 100644 --- a/test/annotation.tests.ts +++ b/test/annotation.tests.ts @@ -131,5 +131,27 @@ public class MyTestClass { }`); Token.Punctuation.Semicolon, ]); }); + + it('annotation on same line as method declaration (issue #44)', async () => { + const input = Input.InClass(`@Future(callout=true) public static void method() {}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Modifiers.AnnotationName('@Future'), + Token.Punctuation.OpenParen, + Token.Variables.ReadWrite('callout'), + Token.Operators.Assignment, + Token.Literals.Boolean.True, + Token.Punctuation.CloseParen, + Token.Keywords.Modifiers.Public, + Token.Keywords.Modifiers.Static, + Token.PrimitiveType.Void, + Token.Identifiers.MethodName('method'), + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); From 6a3a423dc0e39d79479fbce2b08e6f77232185c3 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:08:43 -0500 Subject: [PATCH 02/16] test: add tests for ternary expressions (issue #43) - Add test cases for nested ternary expressions - Add test case for ternary with method calls - Tests verify correct highlighting in these scenarios - Tests currently pass, indicating issue may already be resolved Closes #43 --- test/expressions.tests.ts | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/expressions.tests.ts b/test/expressions.tests.ts index 7bae3a9..482996e 100644 --- a/test/expressions.tests.ts +++ b/test/expressions.tests.ts @@ -191,6 +191,48 @@ Object newPoint = new Vector(point.x * z, 0);`); Token.Punctuation.Semicolon, ]); }); + + it('nested ternary expression (issue #43)', async () => { + const input = Input.InMethod(`Integer result = x ? y ? 1 : 2 : 3;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.Integer, + Token.Identifiers.LocalName('result'), + Token.Operators.Assignment, + Token.Variables.ReadWrite('x'), + Token.Operators.Conditional.QuestionMark, + Token.Variables.ReadWrite('y'), + Token.Operators.Conditional.QuestionMark, + Token.Literals.Numeric.Decimal('1'), + Token.Operators.Conditional.Colon, + Token.Literals.Numeric.Decimal('2'), + Token.Operators.Conditional.Colon, + Token.Literals.Numeric.Decimal('3'), + Token.Punctuation.Semicolon, + ]); + }); + + it('ternary with method call (issue #43)', async () => { + const input = Input.InMethod(`String s = x ? getValue() : getDefault();`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.String, + Token.Identifiers.LocalName('s'), + Token.Operators.Assignment, + Token.Variables.ReadWrite('x'), + Token.Operators.Conditional.QuestionMark, + Token.Identifiers.MethodName('getValue'), + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Operators.Conditional.Colon, + Token.Identifiers.MethodName('getDefault'), + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + ]); + }); }); describe('Element Access', () => { From c9e5baa57fe2b4decace540d9a0db6e942c035bf Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:15:40 -0500 Subject: [PATCH 03/16] fix: DML operations on method call results (issue #26) - Add dml-expression pattern to handle DML operations on expressions - Pattern matches insert/update/delete/upsert/undelete followed by expressions - Ensures DML operations receive same scope (support.function.apex) whether applied to new objects or method call results like Map.values() - Add test case verifying insert accounts.values() gets same scope as insert new List Closes #26 --- src/apex.tmLanguage.yml | 10 +++++++++ test/system.tests.ts | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index 3cfc1bd..b852e5b 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -92,6 +92,7 @@ repository: expression: patterns: - include: '#comment' + - include: '#dml-expression' - include: '#merge-expression' - include: '#support-expression' - include: '#throw-expression' @@ -210,6 +211,15 @@ repository: - include: '#support-type' - include: '#punctuation-comma' + dml-expression: + begin: \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b) + beginCaptures: + '1': { name: support.function.apex } + end: (?<=\;) + patterns: + - include: '#expression' + - include: '#punctuation-semicolon' + merge-expression: begin: (merge)\b\s+ beginCaptures: diff --git a/test/system.tests.ts b/test/system.tests.ts index 918b412..d1d5715 100644 --- a/test/system.tests.ts +++ b/test/system.tests.ts @@ -506,5 +506,51 @@ describe('Grammar', () => { Token.Punctuation.CloseBrace, ]); }); + + it('DML on Map.values() receives same scope as direct insert (issue #26)', async () => { + const input = Input.InMethod(` +Map accounts = new Map(); +insert accounts.values(); +insert new List(); +`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Type('Map'), + Token.Punctuation.TypeParameters.Begin, + Token.Type('Id'), + Token.Punctuation.Comma, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Identifiers.LocalName('accounts'), + Token.Operators.Assignment, + Token.Keywords.Control.New, + Token.Type('Map'), + Token.Punctuation.TypeParameters.Begin, + Token.Type('Id'), + Token.Punctuation.Comma, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Support.Class.FunctionText('insert'), + Token.Variables.Object('accounts'), + Token.Punctuation.Accessor, + Token.Identifiers.MethodName('values'), + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Support.Class.FunctionText('insert'), + Token.Keywords.Control.New, + Token.Type('List'), + Token.Punctuation.TypeParameters.Begin, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + ]); + }); }); }); From 4c7eca214990899acf5533635aae88c0f0d717c3 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:29:24 -0500 Subject: [PATCH 04/16] fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to use 'Id' instead of 'ID' to match Apex convention - Add test cases for namespace-qualified extends and implements Closes #50 --- src/apex.tmLanguage.yml | 20 +++++++++++++++++--- test/annotation.tests.ts | 6 ++++-- test/class.tests.ts | 37 +++++++++++++++++++++++++++++++++++++ test/interface.tests.ts | 18 +++++++++++++++++- test/utils/tokenize.ts | 2 +- 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index b852e5b..43c9677 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -420,21 +420,35 @@ repository: - include: '#comment' extends-class: - begin: (extends)\b\s+([_[:alpha:]][_[:alnum:]]*) + begin: (extends)\b\s+ beginCaptures: '1': { name: keyword.other.extends.apex } - '2': { name: entity.name.type.extends.apex } end: '(?={|implements)' + patterns: + - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end: '(?={|implements)' + patterns: + - include: '#support-type' + - include: '#type' + - match: ([_[:alpha:]][_[:alnum:]]*) + captures: + '1': { name: entity.name.type.extends.apex } implements-class: begin: (implements)\b beginCaptures: '1': { name: keyword.other.implements.apex } patterns: + - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end: '(?={|extends|,)' + patterns: + - include: '#support-type' + - include: '#type' - match: ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)? captures: '1': { name: entity.name.type.implements.apex } '2': { name: punctuation.separator.comma.apex } + - include: '#punctuation-comma' end: '(?={|extends)' soql-query-expression: @@ -1654,7 +1668,7 @@ repository: - include: '#type-nullable-suffix' type-builtin: - match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b + match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|Integer|Long|Object|String|Time|void)\b captures: '1': { name: keyword.type.apex } diff --git a/test/annotation.tests.ts b/test/annotation.tests.ts index 3670bee..7173309 100644 --- a/test/annotation.tests.ts +++ b/test/annotation.tests.ts @@ -104,7 +104,7 @@ public class MyTestClass { }`); it('annotation with multiple parameters on field', async () => { const input = Input.InClass(`@InvocableMethod(label='Insert Accounts' description='Inserts new accounts.' required=false) - global ID leadId; + global Id leadId; `); const tokens = await tokenize(input); @@ -133,7 +133,9 @@ public class MyTestClass { }`); }); it('annotation on same line as method declaration (issue #44)', async () => { - const input = Input.InClass(`@Future(callout=true) public static void method() {}`); + const input = Input.InClass( + `@Future(callout=true) public static void method() {}` + ); const tokens = await tokenize(input); tokens.should.deep.equal([ diff --git a/test/class.tests.ts b/test/class.tests.ts index 69ceb08..7bc4217 100644 --- a/test/class.tests.ts +++ b/test/class.tests.ts @@ -216,5 +216,42 @@ public abstract class PublicAbstractClass { } Token.Punctuation.CloseBrace, ]); }); + + it('class extends namespace-qualified type (issue #50)', async () => { + const input = Input.FromText(`class MyClass extends System.Exception {}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Class, + Token.Identifiers.ClassName('MyClass'), + Token.Keywords.Extends, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('Exception'), + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); + + it('class implements namespace-qualified type (issue #50)', async () => { + const input = Input.FromText( + `class MyClass implements Database.Batchable {}` + ); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Class, + Token.Identifiers.ClassName('MyClass'), + Token.Keywords.Implements, + Token.Support.Class.Database, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('Batchable'), + Token.Punctuation.TypeParameters.Begin, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); diff --git a/test/interface.tests.ts b/test/interface.tests.ts index aa2cd97..23ba80c 100644 --- a/test/interface.tests.ts +++ b/test/interface.tests.ts @@ -5,7 +5,7 @@ *--------------------------------------------------------------------------------------------*/ import { should } from 'chai'; -import { tokenize, Token } from './utils/tokenize'; +import { tokenize, Input, Token } from './utils/tokenize'; describe('Grammar', () => { before(() => { @@ -78,5 +78,21 @@ interface IBar extends IFoo { } Token.Punctuation.CloseBrace, ]); }); + + it('interface extends namespace-qualified type (issue #50)', async () => { + const input = Input.FromText(`interface MyInterface extends System.IComparable {}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Interface, + Token.Identifiers.InterfaceName('MyInterface'), + Token.Keywords.Extends, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('IComparable'), + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); diff --git a/test/utils/tokenize.ts b/test/utils/tokenize.ts index 8edd438..81c96fe 100644 --- a/test/utils/tokenize.ts +++ b/test/utils/tokenize.ts @@ -646,7 +646,7 @@ export namespace Token { export const Datetime = createToken('Datetime', 'keyword.type.apex'); export const Decimal = createToken('Decimal', 'keyword.type.apex'); export const Double = createToken('Double', 'keyword.type.apex'); - export const ID = createToken('ID', 'keyword.type.apex'); + export const ID = createToken('Id', 'keyword.type.apex'); export const Integer = createToken('Integer', 'keyword.type.apex'); export const Long = createToken('Long', 'keyword.type.apex'); export const Object = createToken('Object', 'keyword.type.apex'); From 74824a3792bff356a298e581ade6a02a76a7a5d3 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:30:15 -0500 Subject: [PATCH 05/16] fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 --- src/apex.tmLanguage.yml | 20 +++++++++++++++++--- test/annotation.tests.ts | 6 ++++-- test/class.tests.ts | 37 +++++++++++++++++++++++++++++++++++++ test/interface.tests.ts | 18 +++++++++++++++++- test/utils/tokenize.ts | 2 +- 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index b852e5b..15201a7 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -420,21 +420,35 @@ repository: - include: '#comment' extends-class: - begin: (extends)\b\s+([_[:alpha:]][_[:alnum:]]*) + begin: (extends)\b\s+ beginCaptures: '1': { name: keyword.other.extends.apex } - '2': { name: entity.name.type.extends.apex } end: '(?={|implements)' + patterns: + - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end: '(?={|implements)' + patterns: + - include: '#support-type' + - include: '#type' + - match: ([_[:alpha:]][_[:alnum:]]*) + captures: + '1': { name: entity.name.type.extends.apex } implements-class: begin: (implements)\b beginCaptures: '1': { name: keyword.other.implements.apex } patterns: + - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end: '(?={|extends|,)' + patterns: + - include: '#support-type' + - include: '#type' - match: ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)? captures: '1': { name: entity.name.type.implements.apex } '2': { name: punctuation.separator.comma.apex } + - include: '#punctuation-comma' end: '(?={|extends)' soql-query-expression: @@ -1654,7 +1668,7 @@ repository: - include: '#type-nullable-suffix' type-builtin: - match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b + match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b captures: '1': { name: keyword.type.apex } diff --git a/test/annotation.tests.ts b/test/annotation.tests.ts index 3670bee..7173309 100644 --- a/test/annotation.tests.ts +++ b/test/annotation.tests.ts @@ -104,7 +104,7 @@ public class MyTestClass { }`); it('annotation with multiple parameters on field', async () => { const input = Input.InClass(`@InvocableMethod(label='Insert Accounts' description='Inserts new accounts.' required=false) - global ID leadId; + global Id leadId; `); const tokens = await tokenize(input); @@ -133,7 +133,9 @@ public class MyTestClass { }`); }); it('annotation on same line as method declaration (issue #44)', async () => { - const input = Input.InClass(`@Future(callout=true) public static void method() {}`); + const input = Input.InClass( + `@Future(callout=true) public static void method() {}` + ); const tokens = await tokenize(input); tokens.should.deep.equal([ diff --git a/test/class.tests.ts b/test/class.tests.ts index 69ceb08..7bc4217 100644 --- a/test/class.tests.ts +++ b/test/class.tests.ts @@ -216,5 +216,42 @@ public abstract class PublicAbstractClass { } Token.Punctuation.CloseBrace, ]); }); + + it('class extends namespace-qualified type (issue #50)', async () => { + const input = Input.FromText(`class MyClass extends System.Exception {}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Class, + Token.Identifiers.ClassName('MyClass'), + Token.Keywords.Extends, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('Exception'), + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); + + it('class implements namespace-qualified type (issue #50)', async () => { + const input = Input.FromText( + `class MyClass implements Database.Batchable {}` + ); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Class, + Token.Identifiers.ClassName('MyClass'), + Token.Keywords.Implements, + Token.Support.Class.Database, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('Batchable'), + Token.Punctuation.TypeParameters.Begin, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); diff --git a/test/interface.tests.ts b/test/interface.tests.ts index aa2cd97..23ba80c 100644 --- a/test/interface.tests.ts +++ b/test/interface.tests.ts @@ -5,7 +5,7 @@ *--------------------------------------------------------------------------------------------*/ import { should } from 'chai'; -import { tokenize, Token } from './utils/tokenize'; +import { tokenize, Input, Token } from './utils/tokenize'; describe('Grammar', () => { before(() => { @@ -78,5 +78,21 @@ interface IBar extends IFoo { } Token.Punctuation.CloseBrace, ]); }); + + it('interface extends namespace-qualified type (issue #50)', async () => { + const input = Input.FromText(`interface MyInterface extends System.IComparable {}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Interface, + Token.Identifiers.InterfaceName('MyInterface'), + Token.Keywords.Extends, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('IComparable'), + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); diff --git a/test/utils/tokenize.ts b/test/utils/tokenize.ts index 8edd438..81c96fe 100644 --- a/test/utils/tokenize.ts +++ b/test/utils/tokenize.ts @@ -646,7 +646,7 @@ export namespace Token { export const Datetime = createToken('Datetime', 'keyword.type.apex'); export const Decimal = createToken('Decimal', 'keyword.type.apex'); export const Double = createToken('Double', 'keyword.type.apex'); - export const ID = createToken('ID', 'keyword.type.apex'); + export const ID = createToken('Id', 'keyword.type.apex'); export const Integer = createToken('Integer', 'keyword.type.apex'); export const Long = createToken('Long', 'keyword.type.apex'); export const Object = createToken('Object', 'keyword.type.apex'); From c07015e015291079fa938a3ae7513c71a57be68d Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:31:37 -0500 Subject: [PATCH 06/16] test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type --- test/type-name.tests.ts | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/type-name.tests.ts b/test/type-name.tests.ts index c7ab5c9..8d94e7f 100644 --- a/test/type-name.tests.ts +++ b/test/type-name.tests.ts @@ -86,5 +86,59 @@ describe('Grammar', () => { Token.Punctuation.Semicolon, ]); }); + + it('Id type (lowercase d) - Apex is case-insensitive', async () => { + const input = Input.InClass(`Id recordId;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.ID, + Token.Identifiers.FieldName('recordId'), + Token.Punctuation.Semicolon, + ]); + }); + + it('ID type (uppercase D) - Apex is case-insensitive', async () => { + const input = Input.InClass(`ID recordId;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + { text: 'ID', type: 'keyword.type.apex' }, + Token.Identifiers.FieldName('recordId'), + Token.Punctuation.Semicolon, + ]); + }); + + it('Id in generic type parameter', async () => { + const input = Input.InClass(`Map accounts;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Type('Map'), + Token.Punctuation.TypeParameters.Begin, + Token.PrimitiveType.ID, + Token.Punctuation.Comma, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Identifiers.FieldName('accounts'), + Token.Punctuation.Semicolon, + ]); + }); + + it('ID in generic type parameter', async () => { + const input = Input.InClass(`Map accounts;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Type('Map'), + Token.Punctuation.TypeParameters.Begin, + { text: 'ID', type: 'keyword.type.apex' }, + Token.Punctuation.Comma, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Identifiers.FieldName('accounts'), + Token.Punctuation.Semicolon, + ]); + }); }); }); From ff83f99a024c9416c76fb116f243e69938661f94 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:33:15 -0500 Subject: [PATCH 07/16] feat: support for varied casing on ID/Id --- grammars/apex.tmLanguage | 94 ++++++++++++++++++++++++++++++++--- grammars/apex.tmLanguage.cson | 60 ++++++++++++++++++++-- grammars/soql.tmLanguage | 94 ++++++++++++++++++++++++++++++++--- test/expressions.tests.ts | 4 +- test/interface.tests.ts | 4 +- test/system.tests.ts | 4 +- 6 files changed, 235 insertions(+), 25 deletions(-) diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index c82768f..d439b59 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -309,6 +309,10 @@ include #comment + + include + #dml-expression + include #merge-expression @@ -693,6 +697,32 @@ + dml-expression + + begin + \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b) + beginCaptures + + 1 + + name + support.function.apex + + + end + (?<=\;) + patterns + + + include + #expression + + + include + #punctuation-semicolon + + + merge-expression begin @@ -1328,7 +1358,7 @@ extends-class begin - (extends)\b\s+([_[:alpha:]][_[:alnum:]]*) + (extends)\b\s+ beginCaptures 1 @@ -1336,14 +1366,41 @@ name keyword.other.extends.apex - 2 - - name - entity.name.type.extends.apex - end (?={|implements) + patterns + + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|implements) + patterns + + + include + #support-type + + + include + #type + + + + + match + ([_[:alpha:]][_[:alnum:]]*) + captures + + 1 + + name + entity.name.type.extends.apex + + + + implements-class @@ -1359,6 +1416,23 @@ patterns + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|extends|,) + patterns + + + include + #support-type + + + include + #type + + + match ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)? @@ -1376,6 +1450,10 @@ + + include + #punctuation-comma + end (?={|extends) @@ -4251,7 +4329,7 @@ match (?x) -(?:(?:\b(this)\b)\s+)? +(?:(?:\b(this|final)\b)\s+)? (?<type_name> (?: (?:ref\s+)? # ref return @@ -4438,7 +4516,7 @@ type-builtin match - \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b + \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b captures 1 diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index d7a91db..9ebca1b 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -215,6 +215,9 @@ repository: { include: '#comment' } + { + include: '#dml-expression' + } { include: '#merge-expression' } @@ -433,6 +436,20 @@ repository: include: '#punctuation-comma' } ] + 'dml-expression': + begin: '\\b(delete|insert|undelete|update|upsert)\\b\\s+(?!new\\b)' + beginCaptures: + '1': + name: 'support.function.apex' + end: '(?<=\\;)' + patterns: [ + { + include: '#expression' + } + { + include: '#punctuation-semicolon' + } + ] 'merge-expression': begin: '(merge)\\b\\s+' beginCaptures: @@ -797,19 +814,49 @@ repository: } ] 'extends-class': - begin: '(extends)\\b\\s+([_[:alpha:]][_[:alnum:]]*)' + begin: '(extends)\\b\\s+' beginCaptures: '1': name: 'keyword.other.extends.apex' - '2': - name: 'entity.name.type.extends.apex' end: '(?={|implements)' + patterns: [ + { + begin: '(?=[_[:alpha:]][_[:alnum:]]*\\s*\\.)' + end: '(?={|implements)' + patterns: [ + { + include: '#support-type' + } + { + include: '#type' + } + ] + } + { + match: '([_[:alpha:]][_[:alnum:]]*)' + captures: + '1': + name: 'entity.name.type.extends.apex' + } + ] 'implements-class': begin: '(implements)\\b' beginCaptures: '1': name: 'keyword.other.implements.apex' patterns: [ + { + begin: '(?=[_[:alpha:]][_[:alnum:]]*\\s*\\.)' + end: '(?={|extends|,)' + patterns: [ + { + include: '#support-type' + } + { + include: '#type' + } + ] + } { match: '([_[:alpha:]][_[:alnum:]]*)\\b\\s*(,)?' captures: @@ -818,6 +865,9 @@ repository: '2': name: 'punctuation.separator.comma.apex' } + { + include: '#punctuation-comma' + } ] end: '(?={|extends)' 'soql-query-expression': @@ -2533,7 +2583,7 @@ repository: parameter: match: ''' (?x) - (?:(?:\\b(this)\\b)\\s+)? + (?:(?:\\b(this|final)\\b)\\s+)? (? (?: (?:ref\\s+)? # ref return @@ -2644,7 +2694,7 @@ repository: } ] 'type-builtin': - match: '\\b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\\b' + match: '\\b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\\b' captures: '1': name: 'keyword.type.apex' diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index e768771..fb7d752 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -295,6 +295,10 @@ include #comment + + include + #dml-expression + include #merge-expression @@ -679,6 +683,32 @@ + dml-expression + + begin + \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b) + beginCaptures + + 1 + + name + support.function.apex + + + end + (?<=\;) + patterns + + + include + #expression + + + include + #punctuation-semicolon + + + merge-expression begin @@ -1314,7 +1344,7 @@ extends-class begin - (extends)\b\s+([_[:alpha:]][_[:alnum:]]*) + (extends)\b\s+ beginCaptures 1 @@ -1322,14 +1352,41 @@ name keyword.other.extends.apex - 2 - - name - entity.name.type.extends.apex - end (?={|implements) + patterns + + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|implements) + patterns + + + include + #support-type + + + include + #type + + + + + match + ([_[:alpha:]][_[:alnum:]]*) + captures + + 1 + + name + entity.name.type.extends.apex + + + + implements-class @@ -1345,6 +1402,23 @@ patterns + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|extends|,) + patterns + + + include + #support-type + + + include + #type + + + match ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)? @@ -1362,6 +1436,10 @@ + + include + #punctuation-comma + end (?={|extends) @@ -4233,7 +4311,7 @@ match (?x) -(?:(?:\b(this)\b)\s+)? +(?:(?:\b(this|final)\b)\s+)? (?<type_name> (?: (?:ref\s+)? # ref return @@ -4420,7 +4498,7 @@ type-builtin match - \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b + \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b captures 1 diff --git a/test/expressions.tests.ts b/test/expressions.tests.ts index 482996e..a63775e 100644 --- a/test/expressions.tests.ts +++ b/test/expressions.tests.ts @@ -214,7 +214,9 @@ Object newPoint = new Vector(point.x * z, 0);`); }); it('ternary with method call (issue #43)', async () => { - const input = Input.InMethod(`String s = x ? getValue() : getDefault();`); + const input = Input.InMethod( + `String s = x ? getValue() : getDefault();` + ); const tokens = await tokenize(input); tokens.should.deep.equal([ diff --git a/test/interface.tests.ts b/test/interface.tests.ts index 23ba80c..feade0f 100644 --- a/test/interface.tests.ts +++ b/test/interface.tests.ts @@ -80,7 +80,9 @@ interface IBar extends IFoo { } }); it('interface extends namespace-qualified type (issue #50)', async () => { - const input = Input.FromText(`interface MyInterface extends System.IComparable {}`); + const input = Input.FromText( + `interface MyInterface extends System.IComparable {}` + ); const tokens = await tokenize(input); tokens.should.deep.equal([ diff --git a/test/system.tests.ts b/test/system.tests.ts index d1d5715..ac52840 100644 --- a/test/system.tests.ts +++ b/test/system.tests.ts @@ -518,7 +518,7 @@ insert new List(); tokens.should.deep.equal([ Token.Type('Map'), Token.Punctuation.TypeParameters.Begin, - Token.Type('Id'), + Token.PrimitiveType.ID, Token.Punctuation.Comma, Token.Type('Account'), Token.Punctuation.TypeParameters.End, @@ -527,7 +527,7 @@ insert new List(); Token.Keywords.Control.New, Token.Type('Map'), Token.Punctuation.TypeParameters.Begin, - Token.Type('Id'), + Token.PrimitiveType.ID, Token.Punctuation.Comma, Token.Type('Account'), Token.Punctuation.TypeParameters.End, From f3f3d5ba6ddc369934804606d68f6b5b7e109621 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:57:57 -0500 Subject: [PATCH 08/16] fix: Add syntax highlighting for initialization blocks Add initializer-block pattern to grammar to properly highlight code inside initialization blocks (standalone { } blocks at class member level). The pattern matches standalone curly brace blocks and includes statement patterns for proper syntax highlighting, matching method body behavior. Fixes: https://github.com/forcedotcom/salesforcedx-vscode/issues/4920 --- grammars/apex.tmLanguage | 34 ++++++++++++++++++++++++++++++++++ grammars/apex.tmLanguage.cson | 17 +++++++++++++++++ grammars/soql.tmLanguage | 34 ++++++++++++++++++++++++++++++++++ src/apex.tmLanguage.yml | 11 +++++++++++ 4 files changed, 96 insertions(+) diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index d439b59..253b978 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -175,6 +175,10 @@ include #method-declaration + + include + #initializer-block + include #punctuation-semicolon @@ -2514,6 +2518,36 @@ + initializer-block + + begin + \{ + beginCaptures + + 0 + + name + punctuation.curlybrace.open.apex + + + end + \} + endCaptures + + 0 + + name + punctuation.curlybrace.close.apex + + + patterns + + + include + #statement + + + variable-initializer begin diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index 9ebca1b..f48d95f 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -119,6 +119,9 @@ repository: { include: '#method-declaration' } + { + include: '#initializer-block' + } { include: '#punctuation-semicolon' } @@ -1504,6 +1507,20 @@ repository: include: '#statement' } ] + 'initializer-block': + begin: '\\{' + beginCaptures: + '0': + name: 'punctuation.curlybrace.open.apex' + end: '\\}' + endCaptures: + '0': + name: 'punctuation.curlybrace.close.apex' + patterns: [ + { + include: '#statement' + } + ] 'variable-initializer': begin: '(?)' beginCaptures: diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index fb7d752..798131c 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -161,6 +161,10 @@ include #method-declaration + + include + #initializer-block + include #punctuation-semicolon @@ -2496,6 +2500,36 @@ + initializer-block + + begin + \{ + beginCaptures + + 0 + + name + punctuation.curlybrace.open.apex + + + end + \} + endCaptures + + 0 + + name + punctuation.curlybrace.close.apex + + + patterns + + + include + #statement + + + variable-initializer begin diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index 15201a7..b5477fb 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -54,6 +54,7 @@ repository: - include: '#variable-initializer' - include: '#constructor-declaration' - include: '#method-declaration' + - include: '#initializer-block' - include: '#punctuation-semicolon' interface-members: @@ -859,6 +860,16 @@ repository: patterns: - include: '#statement' + initializer-block: + begin: \{ + beginCaptures: + '0': { name: punctuation.curlybrace.open.apex } + end: \} + endCaptures: + '0': { name: punctuation.curlybrace.close.apex } + patterns: + - include: '#statement' + variable-initializer: begin: (?) beginCaptures: From 0ddf7b60f7441edb8f009bba431c8df8baf4151c Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 13:02:25 -0500 Subject: [PATCH 09/16] test: Add tests for initialization block syntax highlighting Add comprehensive tests to verify initialization blocks are properly highlighted, including: - Empty initialization blocks - Method calls with string literals (the main issue #4920) - Multiple statements - Nested class scenario - Comparison with method body highlighting --- test/initializer-block.tests.ts | 175 ++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 test/initializer-block.tests.ts diff --git a/test/initializer-block.tests.ts b/test/initializer-block.tests.ts new file mode 100644 index 0000000..9cd1907 --- /dev/null +++ b/test/initializer-block.tests.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Modifications Copyright (c) 2018 Salesforce. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { should } from 'chai'; +import { tokenize, Input, Token } from './utils/tokenize'; + +describe('Grammar', () => { + before(() => { + should(); + }); + + describe('Initializer Blocks', () => { + it('empty initialization block', async () => { + const input = Input.InClass(`{ }`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block with method call and string literal', async () => { + const input = Input.InClass(` +{ + this.setMessage('Object graph should be a Directed Acyclic Graph.'); +}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Punctuation.OpenBrace, + Token.Keywords.This, + Token.Punctuation.Accessor, + Token.Identifiers.MethodName('setMessage'), + Token.Punctuation.OpenParen, + Token.Punctuation.String.Begin, + Token.Literals.String('Object graph should be a Directed Acyclic Graph.'), + Token.Punctuation.String.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block with multiple statements', async () => { + const input = Input.InClass(` +{ + Integer x = 5; + String message = 'test'; + this.setMessage(message); +}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Punctuation.OpenBrace, + Token.PrimitiveType.Integer, + Token.Identifiers.LocalName('x'), + Token.Operators.Assignment, + Token.Literals.Numeric.Decimal('5'), + Token.Punctuation.Semicolon, + Token.PrimitiveType.String, + Token.Identifiers.LocalName('message'), + Token.Operators.Assignment, + Token.Punctuation.String.Begin, + Token.Literals.String('test'), + Token.Punctuation.String.End, + Token.Punctuation.Semicolon, + Token.Keywords.This, + Token.Punctuation.Accessor, + Token.Identifiers.MethodName('setMessage'), + Token.Punctuation.OpenParen, + Token.Variables.ReadWrite('message'), + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block in nested class (issue #4920)', async () => { + const input = Input.FromText(` +public class TestDataBuilder { + public class NoneDAGException extends Exception { + // Initializer + { + this.setMessage('Object graph should be a Directed Acyclic Graph.'); + } + + // Sample method for comparison + public void anotherMethod() { + this.setMessage('Object graph should be a Directed Acyclic Graph.'); + } + } +}`); + const tokens = await tokenize(input); + + // Find the initialization block tokens (should start after the comment) + const initBlockStart = tokens.findIndex( + (t, i) => + i > 0 && + tokens[i - 1].text === '//' && + tokens[i].text === 'Initializer' + ); + const initBlockEnd = tokens.findIndex( + (t, i) => + i > initBlockStart && + t.text === '}' && + tokens[i - 1]?.text === ';' + ); + + // Extract tokens for the initialization block + const initBlockTokens = tokens.slice( + initBlockStart + 3, // Skip comment tokens + initBlockEnd + 1 + ); + + // Verify initialization block has proper string highlighting + initBlockTokens.should.include.deep.members([ + Token.Punctuation.OpenBrace, + Token.Keywords.This, + Token.Punctuation.Accessor, + Token.Identifiers.MethodName('setMessage'), + Token.Punctuation.OpenParen, + Token.Punctuation.String.Begin, + Token.Literals.String('Object graph should be a Directed Acyclic Graph.'), + Token.Punctuation.String.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block syntax highlighting matches method body', async () => { + const input = Input.InClass(` +{ + this.setMessage('test'); +} + +public void testMethod() { + this.setMessage('test'); +}`); + const tokens = await tokenize(input); + + // Find initialization block tokens + const initStart = tokens.findIndex((t) => t.text === '{'); + const initEnd = tokens.findIndex( + (t, i) => i > initStart && t.text === '}' && tokens[i - 1]?.text === ';' + ); + const initTokens = tokens.slice(initStart, initEnd + 1); + + // Find method body tokens + const methodStart = tokens.findIndex( + (t, i) => i > initEnd && tokens[i - 1]?.text === ')' && t.text === '{' + ); + const methodEnd = tokens.findIndex( + (t, i) => i > methodStart && t.text === '}' && tokens[i - 1]?.text === ';' + ); + const methodTokens = tokens.slice(methodStart, methodEnd + 1); + + // Both should have the same highlighting for the string literal + const initStringTokens = initTokens.filter( + (t) => t.type === 'string.quoted.single.apex' + ); + const methodStringTokens = methodTokens.filter( + (t) => t.type === 'string.quoted.single.apex' + ); + + initStringTokens.length.should.be.greaterThan(0); + methodStringTokens.length.should.be.greaterThan(0); + initStringTokens[0].type.should.equal(methodStringTokens[0].type); + }); + }); +}); From 4868efaf0cb900c236a7b995cc123e998314ca27 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 13:05:31 -0500 Subject: [PATCH 10/16] test: Add test for static keyword before block Even though Apex doesn't support static initialization blocks, verify the grammar handles the syntax for highlighting purposes. --- test/initializer-block.tests.ts | 38 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/test/initializer-block.tests.ts b/test/initializer-block.tests.ts index 9cd1907..44a56cb 100644 --- a/test/initializer-block.tests.ts +++ b/test/initializer-block.tests.ts @@ -37,7 +37,9 @@ describe('Grammar', () => { Token.Identifiers.MethodName('setMessage'), Token.Punctuation.OpenParen, Token.Punctuation.String.Begin, - Token.Literals.String('Object graph should be a Directed Acyclic Graph.'), + Token.Literals.String( + 'Object graph should be a Directed Acyclic Graph.' + ), Token.Punctuation.String.End, Token.Punctuation.CloseParen, Token.Punctuation.Semicolon, @@ -105,9 +107,7 @@ public class TestDataBuilder { ); const initBlockEnd = tokens.findIndex( (t, i) => - i > initBlockStart && - t.text === '}' && - tokens[i - 1]?.text === ';' + i > initBlockStart && t.text === '}' && tokens[i - 1]?.text === ';' ); // Extract tokens for the initialization block @@ -124,7 +124,9 @@ public class TestDataBuilder { Token.Identifiers.MethodName('setMessage'), Token.Punctuation.OpenParen, Token.Punctuation.String.Begin, - Token.Literals.String('Object graph should be a Directed Acyclic Graph.'), + Token.Literals.String( + 'Object graph should be a Directed Acyclic Graph.' + ), Token.Punctuation.String.End, Token.Punctuation.CloseParen, Token.Punctuation.Semicolon, @@ -155,7 +157,8 @@ public void testMethod() { (t, i) => i > initEnd && tokens[i - 1]?.text === ')' && t.text === '{' ); const methodEnd = tokens.findIndex( - (t, i) => i > methodStart && t.text === '}' && tokens[i - 1]?.text === ';' + (t, i) => + i > methodStart && t.text === '}' && tokens[i - 1]?.text === ';' ); const methodTokens = tokens.slice(methodStart, methodEnd + 1); @@ -171,5 +174,28 @@ public void testMethod() { methodStringTokens.length.should.be.greaterThan(0); initStringTokens[0].type.should.equal(methodStringTokens[0].type); }); + + it('static keyword before block is handled (even though static blocks are not valid Apex)', async () => { + // Note: Apex does NOT support static initialization blocks like Java + // This test verifies the grammar handles the syntax correctly for highlighting + // even though it's not valid Apex code + const input = Input.InClass(` +static { + Integer x = 5; +}`); + const tokens = await tokenize(input); + + // The static keyword should be matched, then the block should be matched + tokens.should.include.deep.members([ + Token.Keywords.Modifiers.Static, + Token.Punctuation.OpenBrace, + Token.PrimitiveType.Integer, + Token.Identifiers.LocalName('x'), + Token.Operators.Assignment, + Token.Literals.Numeric.Decimal('5'), + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); }); }); From 033f08aa54f6a16a9a741093c1b56e0132c4b47f Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 13:32:54 -0500 Subject: [PATCH 11/16] fix: Fix switch/when statement syntax highlighting and brace matching - Remove lookbehind end patterns from all when-* statements that prevented proper brace matching - Update end patterns to use lookahead that ends on closing brace or next when clause - Improve when-string pattern to support multiple comma-separated strings - Fix issue #2134: syntax highlighting and brace matching for switch/when statements All switch statement tests passing. --- grammars/apex.tmLanguage | 28 +++++++++++++++++++++------- grammars/apex.tmLanguage.cson | 23 ++++++++++++++++------- grammars/soql.tmLanguage | 28 +++++++++++++++++++++------- src/apex.tmLanguage.yml | 18 +++++++++++------- 4 files changed, 69 insertions(+), 28 deletions(-) diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index 253b978..bc67749 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -2893,7 +2893,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2909,7 +2909,7 @@ when-string begin - (when)\b\s*('[^'\n]*')(,)? + (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s* beginCaptures 1 @@ -2927,7 +2927,7 @@ - 3 + 4 patterns @@ -2937,9 +2937,19 @@ + 5 + + patterns + + + include + #string-literal + + + end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2970,7 +2980,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2996,7 +3006,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -3007,6 +3017,10 @@ include #expression + + include + #punctuation-comma + when-sobject-statement @@ -3032,7 +3046,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index f48d95f..33878f1 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -1704,7 +1704,7 @@ repository: include: '#expression' } ] - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1714,7 +1714,7 @@ repository: } ] 'when-string': - begin: "(when)\\b\\s*('[^'\\n]*')(,)?" + begin: "(when)\\b\\s*('[^'\\n]*')(\\s*(,)\\s*('[^'\\n]*'))*\\s*" beginCaptures: '1': name: 'keyword.control.switch.when.apex' @@ -1724,13 +1724,19 @@ repository: include: '#string-literal' } ] - '3': + '4': patterns: [ { include: '#punctuation-comma' } ] - end: '(?<=\\})' + '5': + patterns: [ + { + include: '#string-literal' + } + ] + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1746,7 +1752,7 @@ repository: name: 'keyword.control.switch.when.apex' '2': name: 'keyword.control.switch.else.apex' - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1760,7 +1766,7 @@ repository: beginCaptures: '1': name: 'keyword.control.switch.when.apex' - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1768,6 +1774,9 @@ repository: { include: '#expression' } + { + include: '#punctuation-comma' + } ] 'when-sobject-statement': begin: '(when)\\b\\s+([_[:alnum:]]+)\\s+([_[:alnum:]]+)\\s*' @@ -1778,7 +1787,7 @@ repository: name: 'storage.type.apex' '3': name: 'entity.name.variable.local.apex' - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index 798131c..0ed6ace 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -2875,7 +2875,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2891,7 +2891,7 @@ when-string begin - (when)\b\s*('[^'\n]*')(,)? + (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s* beginCaptures 1 @@ -2909,7 +2909,7 @@ - 3 + 4 patterns @@ -2919,9 +2919,19 @@ + 5 + + patterns + + + include + #string-literal + + + end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2952,7 +2962,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2978,7 +2988,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2989,6 +2999,10 @@ include #expression + + include + #punctuation-comma + when-sobject-statement @@ -3014,7 +3028,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index b5477fb..4b1ef47 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -985,22 +985,25 @@ repository: '2': patterns: - include: '#expression' - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' when-string: - begin: (when)\b\s*('[^'\n]*')(,)? + begin: (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s* beginCaptures: '1': { name: keyword.control.switch.when.apex } '2': patterns: - include: '#string-literal' - '3': + '4': patterns: - include: '#punctuation-comma' - end: (?<=\}) + '5': + patterns: + - include: '#string-literal' + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' @@ -1010,7 +1013,7 @@ repository: beginCaptures: '1': { name: keyword.control.switch.when.apex } '2': { name: keyword.control.switch.else.apex } - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' @@ -1019,10 +1022,11 @@ repository: begin: (when)\b\s* beginCaptures: '1': { name: keyword.control.switch.when.apex } - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' + - include: '#punctuation-comma' when-sobject-statement: begin: (when)\b\s+([_[:alnum:]]+)\s+([_[:alnum:]]+)\s* @@ -1030,7 +1034,7 @@ repository: '1': { name: keyword.control.switch.when.apex } '2': { name: storage.type.apex } '3': { name: entity.name.variable.local.apex } - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' From bea0c18047b2ccaed16dfe1cb803a78cd7e4a14d Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 13:35:36 -0500 Subject: [PATCH 12/16] fix: Fix dash highlighting in switch statement string literals - Remove quote character from when-statement pattern character class - Change from ['_\-[:alnum:]]+ to [_\-[:alnum:]]+ - Ensures string literals with dashes like 'de-CH' and 'fr-CH' are matched by when-string pattern - Add test case for string literals containing dashes in switch statements This fixes an issue where the when-statement pattern would incorrectly match string literals containing dashes, preventing proper syntax highlighting. --- grammars/apex.tmLanguage | 2 +- grammars/apex.tmLanguage.cson | 2 +- grammars/soql.tmLanguage | 2 +- src/apex.tmLanguage.yml | 2 +- test/switch.tests.ts | 67 +++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 4 deletions(-) diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index bc67749..a9d52cc 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -2873,7 +2873,7 @@ when-statement begin - (when)\b\s+([\'_\-[:alnum:]]+)\s* + (when)\b\s+([_\-[:alnum:]]+)\s* beginCaptures 1 diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index 33878f1..4accb9c 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -1694,7 +1694,7 @@ repository: } ] 'when-statement': - begin: "(when)\\b\\s+([\\'_\\-[:alnum:]]+)\\s*" + begin: '(when)\\b\\s+([_\\-[:alnum:]]+)\\s*' beginCaptures: '1': name: 'keyword.control.switch.when.apex' diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index 0ed6ace..f679de1 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -2855,7 +2855,7 @@ when-statement begin - (when)\b\s+([\'_\-[:alnum:]]+)\s* + (when)\b\s+([_\-[:alnum:]]+)\s* beginCaptures 1 diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index 4b1ef47..5915856 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -979,7 +979,7 @@ repository: - include: '#punctuation-semicolon' when-statement: - begin: (when)\b\s+([\'_\-[:alnum:]]+)\s* + begin: (when)\b\s+([_\-[:alnum:]]+)\s* beginCaptures: '1': { name: keyword.control.switch.when.apex } '2': diff --git a/test/switch.tests.ts b/test/switch.tests.ts index 70afbae..cb2a0a1 100644 --- a/test/switch.tests.ts +++ b/test/switch.tests.ts @@ -736,6 +736,73 @@ when 'label' {} ]); }); + it('switch with string literals containing dashes', async () => { + const input = Input.InMethod(` +switch on locale { +when 'de-CH' { + System.debug('German Switzerland'); +} +when 'fr-CH' { + System.debug('French Switzerland'); +} +when else { + System.debug('Other locale'); +} +}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Switch.Switch, + Token.Keywords.Switch.On, + Token.Variables.ReadWrite('locale'), + Token.Punctuation.OpenBrace, + Token.Keywords.Switch.When, + Token.Punctuation.String.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('de-CH'), + Token.Punctuation.String.End, + Token.Punctuation.OpenBrace, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.FunctionText('debug'), + Token.Punctuation.OpenParen, + Token.XmlDocComments.String.SingleQuoted.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('German Switzerland'), + Token.XmlDocComments.String.SingleQuoted.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + Token.Keywords.Switch.When, + Token.Punctuation.String.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('fr-CH'), + Token.Punctuation.String.End, + Token.Punctuation.OpenBrace, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.FunctionText('debug'), + Token.Punctuation.OpenParen, + Token.XmlDocComments.String.SingleQuoted.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('French Switzerland'), + Token.XmlDocComments.String.SingleQuoted.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + Token.Keywords.Switch.When, + Token.Keywords.Switch.Else, + Token.Punctuation.OpenBrace, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.FunctionText('debug'), + Token.Punctuation.OpenParen, + Token.XmlDocComments.String.SingleQuoted.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('Other locale'), + Token.XmlDocComments.String.SingleQuoted.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + Token.Punctuation.CloseBrace, + ]); + }); + /* it('switch usage in triggers', () => { const input = Input.InTrigger(` switch on sobject { From f2f3ff2e6d60f8357e39b50f10ad892947db1898 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Thu, 30 Oct 2025 14:57:19 -0500 Subject: [PATCH 13/16] fix: support namespace-qualified types in extends/implements (#50) (#72) * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 * test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type * feat: support for varied casing on ID/Id --- grammars/apex.tmLanguage | 94 ++++++++++++++++++++++++++++++++--- grammars/apex.tmLanguage.cson | 60 ++++++++++++++++++++-- grammars/soql.tmLanguage | 94 ++++++++++++++++++++++++++++++++--- src/apex.tmLanguage.yml | 20 ++++++-- test/annotation.tests.ts | 6 ++- test/class.tests.ts | 37 ++++++++++++++ test/expressions.tests.ts | 4 +- test/interface.tests.ts | 20 +++++++- test/system.tests.ts | 4 +- test/type-name.tests.ts | 54 ++++++++++++++++++++ test/utils/tokenize.ts | 2 +- 11 files changed, 364 insertions(+), 31 deletions(-) diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index c82768f..d439b59 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -309,6 +309,10 @@ include #comment + + include + #dml-expression + include #merge-expression @@ -693,6 +697,32 @@ + dml-expression + + begin + \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b) + beginCaptures + + 1 + + name + support.function.apex + + + end + (?<=\;) + patterns + + + include + #expression + + + include + #punctuation-semicolon + + + merge-expression begin @@ -1328,7 +1358,7 @@ extends-class begin - (extends)\b\s+([_[:alpha:]][_[:alnum:]]*) + (extends)\b\s+ beginCaptures 1 @@ -1336,14 +1366,41 @@ name keyword.other.extends.apex - 2 - - name - entity.name.type.extends.apex - end (?={|implements) + patterns + + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|implements) + patterns + + + include + #support-type + + + include + #type + + + + + match + ([_[:alpha:]][_[:alnum:]]*) + captures + + 1 + + name + entity.name.type.extends.apex + + + + implements-class @@ -1359,6 +1416,23 @@ patterns + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|extends|,) + patterns + + + include + #support-type + + + include + #type + + + match ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)? @@ -1376,6 +1450,10 @@ + + include + #punctuation-comma + end (?={|extends) @@ -4251,7 +4329,7 @@ match (?x) -(?:(?:\b(this)\b)\s+)? +(?:(?:\b(this|final)\b)\s+)? (?<type_name> (?: (?:ref\s+)? # ref return @@ -4438,7 +4516,7 @@ type-builtin match - \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b + \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b captures 1 diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index d7a91db..9ebca1b 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -215,6 +215,9 @@ repository: { include: '#comment' } + { + include: '#dml-expression' + } { include: '#merge-expression' } @@ -433,6 +436,20 @@ repository: include: '#punctuation-comma' } ] + 'dml-expression': + begin: '\\b(delete|insert|undelete|update|upsert)\\b\\s+(?!new\\b)' + beginCaptures: + '1': + name: 'support.function.apex' + end: '(?<=\\;)' + patterns: [ + { + include: '#expression' + } + { + include: '#punctuation-semicolon' + } + ] 'merge-expression': begin: '(merge)\\b\\s+' beginCaptures: @@ -797,19 +814,49 @@ repository: } ] 'extends-class': - begin: '(extends)\\b\\s+([_[:alpha:]][_[:alnum:]]*)' + begin: '(extends)\\b\\s+' beginCaptures: '1': name: 'keyword.other.extends.apex' - '2': - name: 'entity.name.type.extends.apex' end: '(?={|implements)' + patterns: [ + { + begin: '(?=[_[:alpha:]][_[:alnum:]]*\\s*\\.)' + end: '(?={|implements)' + patterns: [ + { + include: '#support-type' + } + { + include: '#type' + } + ] + } + { + match: '([_[:alpha:]][_[:alnum:]]*)' + captures: + '1': + name: 'entity.name.type.extends.apex' + } + ] 'implements-class': begin: '(implements)\\b' beginCaptures: '1': name: 'keyword.other.implements.apex' patterns: [ + { + begin: '(?=[_[:alpha:]][_[:alnum:]]*\\s*\\.)' + end: '(?={|extends|,)' + patterns: [ + { + include: '#support-type' + } + { + include: '#type' + } + ] + } { match: '([_[:alpha:]][_[:alnum:]]*)\\b\\s*(,)?' captures: @@ -818,6 +865,9 @@ repository: '2': name: 'punctuation.separator.comma.apex' } + { + include: '#punctuation-comma' + } ] end: '(?={|extends)' 'soql-query-expression': @@ -2533,7 +2583,7 @@ repository: parameter: match: ''' (?x) - (?:(?:\\b(this)\\b)\\s+)? + (?:(?:\\b(this|final)\\b)\\s+)? (? (?: (?:ref\\s+)? # ref return @@ -2644,7 +2694,7 @@ repository: } ] 'type-builtin': - match: '\\b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\\b' + match: '\\b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\\b' captures: '1': name: 'keyword.type.apex' diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index e768771..fb7d752 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -295,6 +295,10 @@ include #comment + + include + #dml-expression + include #merge-expression @@ -679,6 +683,32 @@ + dml-expression + + begin + \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b) + beginCaptures + + 1 + + name + support.function.apex + + + end + (?<=\;) + patterns + + + include + #expression + + + include + #punctuation-semicolon + + + merge-expression begin @@ -1314,7 +1344,7 @@ extends-class begin - (extends)\b\s+([_[:alpha:]][_[:alnum:]]*) + (extends)\b\s+ beginCaptures 1 @@ -1322,14 +1352,41 @@ name keyword.other.extends.apex - 2 - - name - entity.name.type.extends.apex - end (?={|implements) + patterns + + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|implements) + patterns + + + include + #support-type + + + include + #type + + + + + match + ([_[:alpha:]][_[:alnum:]]*) + captures + + 1 + + name + entity.name.type.extends.apex + + + + implements-class @@ -1345,6 +1402,23 @@ patterns + + begin + (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end + (?={|extends|,) + patterns + + + include + #support-type + + + include + #type + + + match ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)? @@ -1362,6 +1436,10 @@ + + include + #punctuation-comma + end (?={|extends) @@ -4233,7 +4311,7 @@ match (?x) -(?:(?:\b(this)\b)\s+)? +(?:(?:\b(this|final)\b)\s+)? (?<type_name> (?: (?:ref\s+)? # ref return @@ -4420,7 +4498,7 @@ type-builtin match - \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b + \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b captures 1 diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index b852e5b..15201a7 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -420,21 +420,35 @@ repository: - include: '#comment' extends-class: - begin: (extends)\b\s+([_[:alpha:]][_[:alnum:]]*) + begin: (extends)\b\s+ beginCaptures: '1': { name: keyword.other.extends.apex } - '2': { name: entity.name.type.extends.apex } end: '(?={|implements)' + patterns: + - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end: '(?={|implements)' + patterns: + - include: '#support-type' + - include: '#type' + - match: ([_[:alpha:]][_[:alnum:]]*) + captures: + '1': { name: entity.name.type.extends.apex } implements-class: begin: (implements)\b beginCaptures: '1': { name: keyword.other.implements.apex } patterns: + - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.) + end: '(?={|extends|,)' + patterns: + - include: '#support-type' + - include: '#type' - match: ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)? captures: '1': { name: entity.name.type.implements.apex } '2': { name: punctuation.separator.comma.apex } + - include: '#punctuation-comma' end: '(?={|extends)' soql-query-expression: @@ -1654,7 +1668,7 @@ repository: - include: '#type-nullable-suffix' type-builtin: - match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b + match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b captures: '1': { name: keyword.type.apex } diff --git a/test/annotation.tests.ts b/test/annotation.tests.ts index 3670bee..7173309 100644 --- a/test/annotation.tests.ts +++ b/test/annotation.tests.ts @@ -104,7 +104,7 @@ public class MyTestClass { }`); it('annotation with multiple parameters on field', async () => { const input = Input.InClass(`@InvocableMethod(label='Insert Accounts' description='Inserts new accounts.' required=false) - global ID leadId; + global Id leadId; `); const tokens = await tokenize(input); @@ -133,7 +133,9 @@ public class MyTestClass { }`); }); it('annotation on same line as method declaration (issue #44)', async () => { - const input = Input.InClass(`@Future(callout=true) public static void method() {}`); + const input = Input.InClass( + `@Future(callout=true) public static void method() {}` + ); const tokens = await tokenize(input); tokens.should.deep.equal([ diff --git a/test/class.tests.ts b/test/class.tests.ts index 69ceb08..7bc4217 100644 --- a/test/class.tests.ts +++ b/test/class.tests.ts @@ -216,5 +216,42 @@ public abstract class PublicAbstractClass { } Token.Punctuation.CloseBrace, ]); }); + + it('class extends namespace-qualified type (issue #50)', async () => { + const input = Input.FromText(`class MyClass extends System.Exception {}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Class, + Token.Identifiers.ClassName('MyClass'), + Token.Keywords.Extends, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('Exception'), + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); + + it('class implements namespace-qualified type (issue #50)', async () => { + const input = Input.FromText( + `class MyClass implements Database.Batchable {}` + ); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Class, + Token.Identifiers.ClassName('MyClass'), + Token.Keywords.Implements, + Token.Support.Class.Database, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('Batchable'), + Token.Punctuation.TypeParameters.Begin, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); diff --git a/test/expressions.tests.ts b/test/expressions.tests.ts index 482996e..a63775e 100644 --- a/test/expressions.tests.ts +++ b/test/expressions.tests.ts @@ -214,7 +214,9 @@ Object newPoint = new Vector(point.x * z, 0);`); }); it('ternary with method call (issue #43)', async () => { - const input = Input.InMethod(`String s = x ? getValue() : getDefault();`); + const input = Input.InMethod( + `String s = x ? getValue() : getDefault();` + ); const tokens = await tokenize(input); tokens.should.deep.equal([ diff --git a/test/interface.tests.ts b/test/interface.tests.ts index aa2cd97..feade0f 100644 --- a/test/interface.tests.ts +++ b/test/interface.tests.ts @@ -5,7 +5,7 @@ *--------------------------------------------------------------------------------------------*/ import { should } from 'chai'; -import { tokenize, Token } from './utils/tokenize'; +import { tokenize, Input, Token } from './utils/tokenize'; describe('Grammar', () => { before(() => { @@ -78,5 +78,23 @@ interface IBar extends IFoo { } Token.Punctuation.CloseBrace, ]); }); + + it('interface extends namespace-qualified type (issue #50)', async () => { + const input = Input.FromText( + `interface MyInterface extends System.IComparable {}` + ); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Interface, + Token.Identifiers.InterfaceName('MyInterface'), + Token.Keywords.Extends, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.TypeText('IComparable'), + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); diff --git a/test/system.tests.ts b/test/system.tests.ts index d1d5715..ac52840 100644 --- a/test/system.tests.ts +++ b/test/system.tests.ts @@ -518,7 +518,7 @@ insert new List(); tokens.should.deep.equal([ Token.Type('Map'), Token.Punctuation.TypeParameters.Begin, - Token.Type('Id'), + Token.PrimitiveType.ID, Token.Punctuation.Comma, Token.Type('Account'), Token.Punctuation.TypeParameters.End, @@ -527,7 +527,7 @@ insert new List(); Token.Keywords.Control.New, Token.Type('Map'), Token.Punctuation.TypeParameters.Begin, - Token.Type('Id'), + Token.PrimitiveType.ID, Token.Punctuation.Comma, Token.Type('Account'), Token.Punctuation.TypeParameters.End, diff --git a/test/type-name.tests.ts b/test/type-name.tests.ts index c7ab5c9..8d94e7f 100644 --- a/test/type-name.tests.ts +++ b/test/type-name.tests.ts @@ -86,5 +86,59 @@ describe('Grammar', () => { Token.Punctuation.Semicolon, ]); }); + + it('Id type (lowercase d) - Apex is case-insensitive', async () => { + const input = Input.InClass(`Id recordId;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.ID, + Token.Identifiers.FieldName('recordId'), + Token.Punctuation.Semicolon, + ]); + }); + + it('ID type (uppercase D) - Apex is case-insensitive', async () => { + const input = Input.InClass(`ID recordId;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + { text: 'ID', type: 'keyword.type.apex' }, + Token.Identifiers.FieldName('recordId'), + Token.Punctuation.Semicolon, + ]); + }); + + it('Id in generic type parameter', async () => { + const input = Input.InClass(`Map accounts;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Type('Map'), + Token.Punctuation.TypeParameters.Begin, + Token.PrimitiveType.ID, + Token.Punctuation.Comma, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Identifiers.FieldName('accounts'), + Token.Punctuation.Semicolon, + ]); + }); + + it('ID in generic type parameter', async () => { + const input = Input.InClass(`Map accounts;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Type('Map'), + Token.Punctuation.TypeParameters.Begin, + { text: 'ID', type: 'keyword.type.apex' }, + Token.Punctuation.Comma, + Token.Type('Account'), + Token.Punctuation.TypeParameters.End, + Token.Identifiers.FieldName('accounts'), + Token.Punctuation.Semicolon, + ]); + }); }); }); diff --git a/test/utils/tokenize.ts b/test/utils/tokenize.ts index 8edd438..81c96fe 100644 --- a/test/utils/tokenize.ts +++ b/test/utils/tokenize.ts @@ -646,7 +646,7 @@ export namespace Token { export const Datetime = createToken('Datetime', 'keyword.type.apex'); export const Decimal = createToken('Decimal', 'keyword.type.apex'); export const Double = createToken('Double', 'keyword.type.apex'); - export const ID = createToken('ID', 'keyword.type.apex'); + export const ID = createToken('Id', 'keyword.type.apex'); export const Integer = createToken('Integer', 'keyword.type.apex'); export const Long = createToken('Long', 'keyword.type.apex'); export const Object = createToken('Object', 'keyword.type.apex'); From 6a40600b4b1a4446e0f1f92d8f7c37fbe57bea12 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 15:04:16 -0500 Subject: [PATCH 14/16] chore: pr reproducers in sfdx project for visual inspection --- .gitignore | 3 + grammars/apex.tmLanguage | 28 +++- grammars/apex.tmLanguage.cson | 22 ++- grammars/soql.tmLanguage | 28 +++- src/apex.tmLanguage.yml | 10 +- test/method.tests.ts | 29 ++++ test/repros/.forceignore | 6 + test/repros/.vscode/settings.json | 3 + .../AnnotationOnSameLine_PR68_PR69.cls | 25 ++++ ...nnotationOnSameLine_PR68_PR69.cls-meta.xml | 5 + .../classes/DMLOnMethodCallResults_PR71.cls | 41 ++++++ .../DMLOnMethodCallResults_PR71.cls-meta.xml | 5 + .../FinalKeywordInMethodParams_PR67.cls | 50 +++++++ ...nalKeywordInMethodParams_PR67.cls-meta.xml | 5 + .../classes/InitializerBlockSyntax_PR73.cls | 82 +++++++++++ .../InitializerBlockSyntax_PR73.cls-meta.xml | 5 + .../classes/NamespaceQualifiedTypes_PR72.cls | 42 ++++++ .../NamespaceQualifiedTypes_PR72.cls-meta.xml | 5 + .../SwitchWhenBraceMatching_PR74_PR75.cls | 138 ++++++++++++++++++ ...chWhenBraceMatching_PR74_PR75.cls-meta.xml | 5 + .../classes/TernaryExpressions_PR70.cls | 42 ++++++ .../TernaryExpressions_PR70.cls-meta.xml | 5 + test/repros/sfdx-project.json | 12 ++ 23 files changed, 585 insertions(+), 11 deletions(-) create mode 100644 test/repros/.forceignore create mode 100644 test/repros/.vscode/settings.json create mode 100644 test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls create mode 100644 test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls create mode 100644 test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls create mode 100644 test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls create mode 100644 test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls create mode 100644 test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls create mode 100644 test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls create mode 100644 test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml create mode 100644 test/repros/sfdx-project.json diff --git a/.gitignore b/.gitignore index 36e7754..ffc033d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ jspm_packages/ # MacOS folder atttribute tracking **/.DS_Store + +**/.sfdx +**/.sf \ No newline at end of file diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index a9d52cc..421a040 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -580,7 +580,7 @@ support-functions match - \b(delete|execute|finish|insert|start|undelete|update|upsert)\b + \b(execute|finish|start)\b captures 1 @@ -4403,15 +4403,37 @@ 2 + name + meta.type.apex patterns include - #support-type + #comment include - #type + #type-builtin + + + include + #type-name + + + include + #type-arguments + + + include + #type-array-suffix + + + include + #type-nullable-suffix + + + include + #support-type diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index 4accb9c..a83b9d3 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -378,7 +378,7 @@ repository: '1': name: 'support.class.apex' 'support-functions': - match: '\\b(delete|execute|finish|insert|start|undelete|update|upsert)\\b' + match: '\\b(execute|finish|start)\\b' captures: '1': name: 'support.function.apex' @@ -2631,12 +2631,28 @@ repository: '1': name: 'storage.modifier.apex' '2': + name: 'meta.type.apex' patterns: [ { - include: '#support-type' + include: '#comment' } { - include: '#type' + include: '#type-builtin' + } + { + include: '#type-name' + } + { + include: '#type-arguments' + } + { + include: '#type-array-suffix' + } + { + include: '#type-nullable-suffix' + } + { + include: '#support-type' } ] '6': diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index f679de1..12d435f 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -566,7 +566,7 @@ support-functions match - \b(delete|execute|finish|insert|start|undelete|update|upsert)\b + \b(execute|finish|start)\b captures 1 @@ -4385,15 +4385,37 @@ 2 + name + meta.type.apex patterns include - #support-type + #comment include - #type + #type-builtin + + + include + #type-name + + + include + #type-arguments + + + include + #type-array-suffix + + + include + #type-nullable-suffix + + + include + #support-type diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index 5915856..76f3b2c 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -176,7 +176,7 @@ repository: '1': { name: support.class.apex } support-functions: - match: \b(delete|execute|finish|insert|start|undelete|update|upsert)\b + match: \b(execute|finish|start)\b captures: '1': { name: support.function.apex } @@ -1630,9 +1630,15 @@ repository: captures: '1': { name: storage.modifier.apex } '2': + name: meta.type.apex patterns: + - include: '#comment' + - include: '#type-builtin' + - include: '#type-name' + - include: '#type-arguments' + - include: '#type-array-suffix' + - include: '#type-nullable-suffix' - include: '#support-type' - - include: '#type' # '3': ? is a sub-expression. It's final value is not considered. # '4': ? is a sub-expression. It's final value is not considered. # '5': ? is a sub-expression. It's final value is not considered. diff --git a/test/method.tests.ts b/test/method.tests.ts index 568a785..0a842c7 100644 --- a/test/method.tests.ts +++ b/test/method.tests.ts @@ -173,6 +173,35 @@ Integer Add(Integer x, Integer y) ]); }); + it('final keyword does not unhighlight types (PR #67)', async () => { + const input = Input.InClass(` +void method(final String str1, String str2, final Integer num, final Boolean flag) { }`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.Void, + Token.Identifiers.MethodName('method'), + Token.Punctuation.OpenParen, + Token.Keywords.Modifiers.Final, + Token.PrimitiveType.String, + Token.Identifiers.ParameterName('str1'), + Token.Punctuation.Comma, + Token.PrimitiveType.String, + Token.Identifiers.ParameterName('str2'), + Token.Punctuation.Comma, + Token.Keywords.Modifiers.Final, + Token.PrimitiveType.Integer, + Token.Identifiers.ParameterName('num'), + Token.Punctuation.Comma, + Token.Keywords.Modifiers.Final, + Token.PrimitiveType.Boolean, + Token.Identifiers.ParameterName('flag'), + Token.Punctuation.CloseParen, + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); + it('commented parameters are highlighted properly (issue omnisharp-vscode#802)', async () => { const input = Input.InClass( `public void methodWithParametersCommented(Integer p1, /*Integer p2*/, Integer p3) {}` diff --git a/test/repros/.forceignore b/test/repros/.forceignore new file mode 100644 index 0000000..3e7553c --- /dev/null +++ b/test/repros/.forceignore @@ -0,0 +1,6 @@ +# Standard SFDX ignore patterns +**/__tests__/** +**/.sfdx/** +**/.localdevserver/** +**/node_modules/** + diff --git a/test/repros/.vscode/settings.json b/test/repros/.vscode/settings.json new file mode 100644 index 0000000..0a40bef --- /dev/null +++ b/test/repros/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "xml.preferences.showSchemaDocumentationType": "none" +} diff --git a/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls new file mode 100644 index 0000000..5049a3e --- /dev/null +++ b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls @@ -0,0 +1,25 @@ +/** + * PR #68/#69: Annotation on same line as method declaration + * + * This class demonstrates annotations on the same line as method declarations. + * The syntax distinguishes annotations from method modifiers. + */ +public class AnnotationOnSameLine_PR68_PR69 { + + @Future(callout=true) public static void futureMethod() { + System.debug('Future method'); + } + + @InvocableMethod(label='Test') public static void invocableMethod() { + System.debug('Invocable method'); + } + + @TestVisible private void testVisibleMethod() { + System.debug('Test visible method'); + } + + @deprecated public void deprecatedMethod() { + System.debug('Deprecated method'); + } +} + diff --git a/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls new file mode 100644 index 0000000..077e5fc --- /dev/null +++ b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls @@ -0,0 +1,41 @@ +/** + * PR #71: DML operations on method call results + * + * This class demonstrates DML operations performed on method call results. + * The syntax highlighting should recognize DML keywords when followed by method calls. + */ +public class DMLOnMethodCallResults_PR71 { + + public void dmlOnMapValues() { + // Issue #26: DML on Map.values() receives same scope as direct insert + Map accounts = new Map(); + insert accounts.values(); + insert new List(); + } + + public void dmlOnQueryResults() { + List accounts = [SELECT Id FROM Account]; + update accounts; + delete [SELECT Id FROM Contact]; + } + + public void dmlOnMethodReturn() { + List accounts = getAccounts(); + upsert accounts; + undelete getDeletedAccounts(); + } + + public void dmlOnChainedMethods() { + insert getAccounts().clone(); + update new Map(getAccounts()); + } + + private List getAccounts() { + return new List(); + } + + private List getDeletedAccounts() { + return new List(); + } +} + diff --git a/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls new file mode 100644 index 0000000..4cc8ce4 --- /dev/null +++ b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls @@ -0,0 +1,50 @@ +/** + * PR #67: Support final keyword in method parameters + * + * This class demonstrates methods with final keyword in parameters. + * The syntax highlighting should properly recognize the final modifier. + */ +public class FinalKeywordInMethodParams_PR67 { + + public void methodWithFinalParameter(final String param) { + System.debug(param); + } + + public void methodWithMultipleFinalParams(final String str, final Integer num) { + System.debug(str + num); + } + + public void methodWithFinalAndRegular(final String finalParam, String regularParam) { + System.debug(finalParam + regularParam); + } + + public static void staticMethodWithFinal(final Boolean flag) { + System.debug(flag); + } + + /** + * Test case: final keyword should not unhighlight the type that follows it. + * Both types (with and without final) should have consistent syntax highlighting. + * Issue: When final precedes a type, the type loses its keyword highlighting. + */ + public void testFinalKeywordDoesNotUnhighlightType(final String str1, String str2) { + // str1 parameter: final String - String should be highlighted as keyword + // str2 parameter: String - String IS highlighted as keyword (correct) + // Both String types should have identical highlighting + System.debug(str1 + str2); + } + + public void testMultipleTypesWithFinal(final Integer num, final Boolean flag, final Decimal dec) { + // All three types (Integer, Boolean, Decimal) should be highlighted as keywords + // even though they follow the final keyword + System.debug(num + ' ' + flag + ' ' + dec); + } + + public void contrastTest(String regularType, final String finalType) { + // regularType: String is highlighted (correct) + // finalType: String should also be highlighted (but currently isn't) + // This demonstrates the highlighting inconsistency + System.debug(regularType.equals(finalType)); + } +} + diff --git a/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls new file mode 100644 index 0000000..a6699e4 --- /dev/null +++ b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls @@ -0,0 +1,82 @@ +/** + * PR #73: Initializer block syntax highlighting + * + * This class demonstrates initialization blocks in nested classes with method calls. + * Issue #4920: Initializer block syntax highlighting should match method body highlighting. + */ +public class InitializerBlockSyntax_PR73 { + + public class NoneDAGException extends Exception { + // Initializer - string literal should be consistently highlighted + // Issue: String highlighting is broken in initializer blocks + { + this.setMessage('Object graph should be a Directed Acyclic Graph.'); + this.setMessage('Simple string'); + this.setMessage('String with "quotes" inside'); + System.debug('Debug message in initializer'); + } + + // Sample method for comparison - same strings highlighted correctly here + public void anotherMethod() { + this.setMessage('Object graph should be a Directed Acyclic Graph.'); + this.setMessage('Simple string'); + this.setMessage('String with "quotes" inside'); + System.debug('Debug message in method'); + } + } + + public class ExampleWithInitializer { + private String message; + + // Empty initializer + { + } + + // Initializer with multiple statements + { + Integer x = 5; + String msg = 'test'; + message = msg; + System.debug(message); + } + } + + public class StaticInitializerExample { + private static Integer counter; + + // Note: Apex does NOT support static initialization blocks like Java + // This tests grammar handling + static { + counter = 0; + } + } + + /** + * Test case: String literals in initializer blocks should have consistent highlighting. + * Issue: Strings in initializer blocks show inconsistent/incorrect highlighting + * compared to the same strings in method bodies. + */ + public class StringHighlightingInInitializer { + private String message; + + // Initializer block with various string scenarios + { + // All these strings should be highlighted consistently + message = 'Initializer string'; + message = 'String with keywords like Object and Graph'; + message = 'String with numbers 123 and symbols !@#'; + this.setMessage('Method call with string parameter'); + System.debug('Debug: initializer block string'); + } + + // Method body for comparison - same strings work correctly here + public void compareMethod() { + message = 'Initializer string'; + message = 'String with keywords like Object and Graph'; + message = 'String with numbers 123 and symbols !@#'; + this.setMessage('Method call with string parameter'); + System.debug('Debug: method body string'); + } + } +} + diff --git a/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls new file mode 100644 index 0000000..e20035e --- /dev/null +++ b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls @@ -0,0 +1,42 @@ +/** + * PR #72: Support namespace-qualified types in extends/implements + * + * This class demonstrates classes extending and implementing namespace-qualified types. + * The syntax highlighting should properly parse System.Exception and Database.Batchable. + */ +public class NamespaceQualifiedTypes_PR72 extends System.Exception { + // Class extends namespace-qualified type - issue #50 +} + +// Another example extending namespace-qualified type +public class MyCustomException_PR72 extends System.Exception { + public MyCustomException_PR72(String message) { + super(message); + } +} + +// Class implementing namespace-qualified type - issue #50 +public class BatchableProcessor_PR72 implements Database.Batchable { + public Database.QueryLocator start(Database.BatchableContext bc) { + return Database.getQueryLocator('SELECT Id FROM Account'); + } + + public void execute(Database.BatchableContext bc, List scope) { + // Process accounts + Integer foo = 0; + } + + public void finish(Database.BatchableContext bc) { + // Finish processing + } +} + +// Class extending and implementing namespace-qualified types +public class ComplexExample_PR72 extends System.OctopusException implements Database.Stateful { + private Integer state; + + public ComplexExample_PR72(String message) { + super(message); + } +} + diff --git a/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls new file mode 100644 index 0000000..f2e81aa --- /dev/null +++ b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls @@ -0,0 +1,138 @@ +/** + * PR #74/#75: Switch/when statement syntax highlighting and brace matching + * + * This class demonstrates switch statements with when clauses. + * Issue #2134: Switch/when statement syntax highlighting and brace matching. + */ +public class SwitchWhenBraceMatching_PR74_PR75 { + + /** + * Test case matching issue #2134: + * - Strings in when clause should be consistently syntax highlighted + * - Bracket matching should match up pairs + * - Brace matching issues when brace follows when clause + */ + public void issue2134_switchWhenBraceMatching() { + String param = 'test'; + switch on param { + when 'A' { + System.debug('when A'); + } + when 'B' { + System.debug('when B'); + } + when 'C' + { + // Brace on new line after when clause + System.debug('when C'); + } + when 'D'{ + // Brace immediately after when clause without space + System.debug('when D'); + } + when else { + System.debug('else'); + } + } + } + + public void simpleSwitchOnString() { + String param = 'A'; + switch on (param) { + when 'A' { + System.debug('test'); + } + when else { + callExternalMethod(); + } + } + } + + public void switchWithMultipleStringValues() { + String locale = 'de-CH'; + switch on locale { + when 'de-CH' { + System.debug('German Switzerland'); + } + when 'fr-CH' { + System.debug('French Switzerland'); + } + when else { + System.debug('Other locale'); + } + } + } + + public void switchOnInteger() { + Integer i = 5; + switch on i { + when 2, 3, 4 { + System.debug('when block 2 and 3 and 4'); + } + when 7 { + System.debug('when block 7'); + } + when else { + // @TODO. + } + } + } + + public void switchOnSObject() { + SObject sobject = new Account(); + switch on sobject { + when Account a { + System.debug('account ' + a); + } + when null { + System.debug('null'); + } + when else { + System.debug('default'); + } + } + } + + public void switchOnMethodResult() { + switch on someInteger(getValue()) { + when 2, 3, 4 { + System.debug('when block 2 and 3 and 4'); + } + when 7 { + System.debug('when block 7'); + } + when else { + // @TODO. + } + } + } + + public void switchWithSafeNavigator() { + MyClass obj = new MyClass(); + switch on (obj?.param) { + when 'A' { + System.debug('test'); + } + when else { + callExternalMethod(); + } + } + } + + private Integer someInteger(Integer val) { + return val; + } + + private Integer getValue() { + return 5; + } + + private void callExternalMethod() { + System.debug('external'); + } + + private class MyClass { + public String param; + } +} + diff --git a/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls new file mode 100644 index 0000000..702f11a --- /dev/null +++ b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls @@ -0,0 +1,42 @@ +/** + * PR #70: Ternary expression syntax highlighting + * + * This class demonstrates nested ternary expressions and ternary with method calls. + * The syntax highlighting should properly distinguish nested conditionals. + */ +public class TernaryExpressions_PR70 { + + public void simpleTernary() { + Integer result = x ? 19 : 23; + } + + public void nestedTernary() { + // Nested ternary expression - issue #43 + Integer result = x ? y ? 1 : 2 : 3; + } + + public void ternaryWithMethodCalls() { + // Ternary with method call - issue #43 + String s = condition ? getValue() : getDefault(); + } + + public void ternaryAsArgument() { + processValue(x ? 19 : 23); + } + + public void complexNestedTernary() { + String output = a ? (b ? 'both' : 'a only') : (c ? 'c only' : 'neither'); + } + + private String getValue() { return 'value'; } + private String getDefault() { return 'default'; } + private void processValue(Integer val) { } + + private Boolean x; + private Boolean y; + private Boolean a; + private Boolean b; + private Boolean c; + private Boolean condition; +} + diff --git a/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/test/repros/sfdx-project.json b/test/repros/sfdx-project.json new file mode 100644 index 0000000..46bbf25 --- /dev/null +++ b/test/repros/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "apex-tmLanguage PR Reproducers", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "60.0" +} \ No newline at end of file From d1695ddb1662c2a70cd9fc9dafd4b4ad75a8f391 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Thu, 30 Oct 2025 15:07:55 -0500 Subject: [PATCH 15/16] fix: 4920 initializer block syntax W-19265631 (#73) * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 * test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type * feat: support for varied casing on ID/Id * fix: Add syntax highlighting for initialization blocks Add initializer-block pattern to grammar to properly highlight code inside initialization blocks (standalone { } blocks at class member level). The pattern matches standalone curly brace blocks and includes statement patterns for proper syntax highlighting, matching method body behavior. Fixes: https://github.com/forcedotcom/salesforcedx-vscode/issues/4920 * test: Add tests for initialization block syntax highlighting Add comprehensive tests to verify initialization blocks are properly highlighted, including: - Empty initialization blocks - Method calls with string literals (the main issue #4920) - Multiple statements - Nested class scenario - Comparison with method body highlighting * test: Add test for static keyword before block Even though Apex doesn't support static initialization blocks, verify the grammar handles the syntax for highlighting purposes. --- grammars/apex.tmLanguage | 34 ++++++ grammars/apex.tmLanguage.cson | 17 +++ grammars/soql.tmLanguage | 34 ++++++ src/apex.tmLanguage.yml | 11 ++ test/initializer-block.tests.ts | 201 ++++++++++++++++++++++++++++++++ 5 files changed, 297 insertions(+) create mode 100644 test/initializer-block.tests.ts diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index d439b59..253b978 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -175,6 +175,10 @@ include #method-declaration + + include + #initializer-block + include #punctuation-semicolon @@ -2514,6 +2518,36 @@ + initializer-block + + begin + \{ + beginCaptures + + 0 + + name + punctuation.curlybrace.open.apex + + + end + \} + endCaptures + + 0 + + name + punctuation.curlybrace.close.apex + + + patterns + + + include + #statement + + + variable-initializer begin diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index 9ebca1b..f48d95f 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -119,6 +119,9 @@ repository: { include: '#method-declaration' } + { + include: '#initializer-block' + } { include: '#punctuation-semicolon' } @@ -1504,6 +1507,20 @@ repository: include: '#statement' } ] + 'initializer-block': + begin: '\\{' + beginCaptures: + '0': + name: 'punctuation.curlybrace.open.apex' + end: '\\}' + endCaptures: + '0': + name: 'punctuation.curlybrace.close.apex' + patterns: [ + { + include: '#statement' + } + ] 'variable-initializer': begin: '(?)' beginCaptures: diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index fb7d752..798131c 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -161,6 +161,10 @@ include #method-declaration + + include + #initializer-block + include #punctuation-semicolon @@ -2496,6 +2500,36 @@ + initializer-block + + begin + \{ + beginCaptures + + 0 + + name + punctuation.curlybrace.open.apex + + + end + \} + endCaptures + + 0 + + name + punctuation.curlybrace.close.apex + + + patterns + + + include + #statement + + + variable-initializer begin diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index 15201a7..b5477fb 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -54,6 +54,7 @@ repository: - include: '#variable-initializer' - include: '#constructor-declaration' - include: '#method-declaration' + - include: '#initializer-block' - include: '#punctuation-semicolon' interface-members: @@ -859,6 +860,16 @@ repository: patterns: - include: '#statement' + initializer-block: + begin: \{ + beginCaptures: + '0': { name: punctuation.curlybrace.open.apex } + end: \} + endCaptures: + '0': { name: punctuation.curlybrace.close.apex } + patterns: + - include: '#statement' + variable-initializer: begin: (?) beginCaptures: diff --git a/test/initializer-block.tests.ts b/test/initializer-block.tests.ts new file mode 100644 index 0000000..44a56cb --- /dev/null +++ b/test/initializer-block.tests.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Modifications Copyright (c) 2018 Salesforce. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { should } from 'chai'; +import { tokenize, Input, Token } from './utils/tokenize'; + +describe('Grammar', () => { + before(() => { + should(); + }); + + describe('Initializer Blocks', () => { + it('empty initialization block', async () => { + const input = Input.InClass(`{ }`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block with method call and string literal', async () => { + const input = Input.InClass(` +{ + this.setMessage('Object graph should be a Directed Acyclic Graph.'); +}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Punctuation.OpenBrace, + Token.Keywords.This, + Token.Punctuation.Accessor, + Token.Identifiers.MethodName('setMessage'), + Token.Punctuation.OpenParen, + Token.Punctuation.String.Begin, + Token.Literals.String( + 'Object graph should be a Directed Acyclic Graph.' + ), + Token.Punctuation.String.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block with multiple statements', async () => { + const input = Input.InClass(` +{ + Integer x = 5; + String message = 'test'; + this.setMessage(message); +}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Punctuation.OpenBrace, + Token.PrimitiveType.Integer, + Token.Identifiers.LocalName('x'), + Token.Operators.Assignment, + Token.Literals.Numeric.Decimal('5'), + Token.Punctuation.Semicolon, + Token.PrimitiveType.String, + Token.Identifiers.LocalName('message'), + Token.Operators.Assignment, + Token.Punctuation.String.Begin, + Token.Literals.String('test'), + Token.Punctuation.String.End, + Token.Punctuation.Semicolon, + Token.Keywords.This, + Token.Punctuation.Accessor, + Token.Identifiers.MethodName('setMessage'), + Token.Punctuation.OpenParen, + Token.Variables.ReadWrite('message'), + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block in nested class (issue #4920)', async () => { + const input = Input.FromText(` +public class TestDataBuilder { + public class NoneDAGException extends Exception { + // Initializer + { + this.setMessage('Object graph should be a Directed Acyclic Graph.'); + } + + // Sample method for comparison + public void anotherMethod() { + this.setMessage('Object graph should be a Directed Acyclic Graph.'); + } + } +}`); + const tokens = await tokenize(input); + + // Find the initialization block tokens (should start after the comment) + const initBlockStart = tokens.findIndex( + (t, i) => + i > 0 && + tokens[i - 1].text === '//' && + tokens[i].text === 'Initializer' + ); + const initBlockEnd = tokens.findIndex( + (t, i) => + i > initBlockStart && t.text === '}' && tokens[i - 1]?.text === ';' + ); + + // Extract tokens for the initialization block + const initBlockTokens = tokens.slice( + initBlockStart + 3, // Skip comment tokens + initBlockEnd + 1 + ); + + // Verify initialization block has proper string highlighting + initBlockTokens.should.include.deep.members([ + Token.Punctuation.OpenBrace, + Token.Keywords.This, + Token.Punctuation.Accessor, + Token.Identifiers.MethodName('setMessage'), + Token.Punctuation.OpenParen, + Token.Punctuation.String.Begin, + Token.Literals.String( + 'Object graph should be a Directed Acyclic Graph.' + ), + Token.Punctuation.String.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); + + it('initialization block syntax highlighting matches method body', async () => { + const input = Input.InClass(` +{ + this.setMessage('test'); +} + +public void testMethod() { + this.setMessage('test'); +}`); + const tokens = await tokenize(input); + + // Find initialization block tokens + const initStart = tokens.findIndex((t) => t.text === '{'); + const initEnd = tokens.findIndex( + (t, i) => i > initStart && t.text === '}' && tokens[i - 1]?.text === ';' + ); + const initTokens = tokens.slice(initStart, initEnd + 1); + + // Find method body tokens + const methodStart = tokens.findIndex( + (t, i) => i > initEnd && tokens[i - 1]?.text === ')' && t.text === '{' + ); + const methodEnd = tokens.findIndex( + (t, i) => + i > methodStart && t.text === '}' && tokens[i - 1]?.text === ';' + ); + const methodTokens = tokens.slice(methodStart, methodEnd + 1); + + // Both should have the same highlighting for the string literal + const initStringTokens = initTokens.filter( + (t) => t.type === 'string.quoted.single.apex' + ); + const methodStringTokens = methodTokens.filter( + (t) => t.type === 'string.quoted.single.apex' + ); + + initStringTokens.length.should.be.greaterThan(0); + methodStringTokens.length.should.be.greaterThan(0); + initStringTokens[0].type.should.equal(methodStringTokens[0].type); + }); + + it('static keyword before block is handled (even though static blocks are not valid Apex)', async () => { + // Note: Apex does NOT support static initialization blocks like Java + // This test verifies the grammar handles the syntax correctly for highlighting + // even though it's not valid Apex code + const input = Input.InClass(` +static { + Integer x = 5; +}`); + const tokens = await tokenize(input); + + // The static keyword should be matched, then the block should be matched + tokens.should.include.deep.members([ + Token.Keywords.Modifiers.Static, + Token.Punctuation.OpenBrace, + Token.PrimitiveType.Integer, + Token.Identifiers.LocalName('x'), + Token.Operators.Assignment, + Token.Literals.Numeric.Decimal('5'), + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + ]); + }); + }); +}); From 429fb490eddebb484f823f9bc83f3cc65f7ddc16 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Thu, 30 Oct 2025 15:10:13 -0500 Subject: [PATCH 16/16] fix: Fix switch/when statement syntax highlighting and brace matching (issue #2134) (#74) * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 * test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type * feat: support for varied casing on ID/Id * fix: Add syntax highlighting for initialization blocks Add initializer-block pattern to grammar to properly highlight code inside initialization blocks (standalone { } blocks at class member level). The pattern matches standalone curly brace blocks and includes statement patterns for proper syntax highlighting, matching method body behavior. Fixes: https://github.com/forcedotcom/salesforcedx-vscode/issues/4920 * test: Add tests for initialization block syntax highlighting Add comprehensive tests to verify initialization blocks are properly highlighted, including: - Empty initialization blocks - Method calls with string literals (the main issue #4920) - Multiple statements - Nested class scenario - Comparison with method body highlighting * test: Add test for static keyword before block Even though Apex doesn't support static initialization blocks, verify the grammar handles the syntax for highlighting purposes. * fix: Fix switch/when statement syntax highlighting and brace matching - Remove lookbehind end patterns from all when-* statements that prevented proper brace matching - Update end patterns to use lookahead that ends on closing brace or next when clause - Improve when-string pattern to support multiple comma-separated strings - Fix issue #2134: syntax highlighting and brace matching for switch/when statements All switch statement tests passing. * fix: Fix dash highlighting in switch statement string literals - Remove quote character from when-statement pattern character class - Change from ['_\-[:alnum:]]+ to [_\-[:alnum:]]+ - Ensures string literals with dashes like 'de-CH' and 'fr-CH' are matched by when-string pattern - Add test case for string literals containing dashes in switch statements This fixes an issue where the when-statement pattern would incorrectly match string literals containing dashes, preventing proper syntax highlighting. --- grammars/apex.tmLanguage | 30 +++++++++++----- grammars/apex.tmLanguage.cson | 25 ++++++++----- grammars/soql.tmLanguage | 30 +++++++++++----- src/apex.tmLanguage.yml | 20 ++++++----- test/switch.tests.ts | 67 +++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 32 deletions(-) diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index 253b978..a9d52cc 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -2873,7 +2873,7 @@ when-statement begin - (when)\b\s+([\'_\-[:alnum:]]+)\s* + (when)\b\s+([_\-[:alnum:]]+)\s* beginCaptures 1 @@ -2893,7 +2893,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2909,7 +2909,7 @@ when-string begin - (when)\b\s*('[^'\n]*')(,)? + (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s* beginCaptures 1 @@ -2927,7 +2927,7 @@ - 3 + 4 patterns @@ -2937,9 +2937,19 @@ + 5 + + patterns + + + include + #string-literal + + + end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2970,7 +2980,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2996,7 +3006,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -3007,6 +3017,10 @@ include #expression + + include + #punctuation-comma + when-sobject-statement @@ -3032,7 +3046,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index f48d95f..4accb9c 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -1694,7 +1694,7 @@ repository: } ] 'when-statement': - begin: "(when)\\b\\s+([\\'_\\-[:alnum:]]+)\\s*" + begin: '(when)\\b\\s+([_\\-[:alnum:]]+)\\s*' beginCaptures: '1': name: 'keyword.control.switch.when.apex' @@ -1704,7 +1704,7 @@ repository: include: '#expression' } ] - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1714,7 +1714,7 @@ repository: } ] 'when-string': - begin: "(when)\\b\\s*('[^'\\n]*')(,)?" + begin: "(when)\\b\\s*('[^'\\n]*')(\\s*(,)\\s*('[^'\\n]*'))*\\s*" beginCaptures: '1': name: 'keyword.control.switch.when.apex' @@ -1724,13 +1724,19 @@ repository: include: '#string-literal' } ] - '3': + '4': patterns: [ { include: '#punctuation-comma' } ] - end: '(?<=\\})' + '5': + patterns: [ + { + include: '#string-literal' + } + ] + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1746,7 +1752,7 @@ repository: name: 'keyword.control.switch.when.apex' '2': name: 'keyword.control.switch.else.apex' - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1760,7 +1766,7 @@ repository: beginCaptures: '1': name: 'keyword.control.switch.when.apex' - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' @@ -1768,6 +1774,9 @@ repository: { include: '#expression' } + { + include: '#punctuation-comma' + } ] 'when-sobject-statement': begin: '(when)\\b\\s+([_[:alnum:]]+)\\s+([_[:alnum:]]+)\\s*' @@ -1778,7 +1787,7 @@ repository: name: 'storage.type.apex' '3': name: 'entity.name.variable.local.apex' - end: '(?<=\\})' + end: '(?=\\})|(?=when\\b)' patterns: [ { include: '#block' diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage index 798131c..f679de1 100644 --- a/grammars/soql.tmLanguage +++ b/grammars/soql.tmLanguage @@ -2855,7 +2855,7 @@ when-statement begin - (when)\b\s+([\'_\-[:alnum:]]+)\s* + (when)\b\s+([_\-[:alnum:]]+)\s* beginCaptures 1 @@ -2875,7 +2875,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2891,7 +2891,7 @@ when-string begin - (when)\b\s*('[^'\n]*')(,)? + (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s* beginCaptures 1 @@ -2909,7 +2909,7 @@ - 3 + 4 patterns @@ -2919,9 +2919,19 @@ + 5 + + patterns + + + include + #string-literal + + + end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2952,7 +2962,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2978,7 +2988,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns @@ -2989,6 +2999,10 @@ include #expression + + include + #punctuation-comma + when-sobject-statement @@ -3014,7 +3028,7 @@ end - (?<=\}) + (?=\})|(?=when\b) patterns diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index b5477fb..5915856 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -979,28 +979,31 @@ repository: - include: '#punctuation-semicolon' when-statement: - begin: (when)\b\s+([\'_\-[:alnum:]]+)\s* + begin: (when)\b\s+([_\-[:alnum:]]+)\s* beginCaptures: '1': { name: keyword.control.switch.when.apex } '2': patterns: - include: '#expression' - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' when-string: - begin: (when)\b\s*('[^'\n]*')(,)? + begin: (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s* beginCaptures: '1': { name: keyword.control.switch.when.apex } '2': patterns: - include: '#string-literal' - '3': + '4': patterns: - include: '#punctuation-comma' - end: (?<=\}) + '5': + patterns: + - include: '#string-literal' + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' @@ -1010,7 +1013,7 @@ repository: beginCaptures: '1': { name: keyword.control.switch.when.apex } '2': { name: keyword.control.switch.else.apex } - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' @@ -1019,10 +1022,11 @@ repository: begin: (when)\b\s* beginCaptures: '1': { name: keyword.control.switch.when.apex } - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' + - include: '#punctuation-comma' when-sobject-statement: begin: (when)\b\s+([_[:alnum:]]+)\s+([_[:alnum:]]+)\s* @@ -1030,7 +1034,7 @@ repository: '1': { name: keyword.control.switch.when.apex } '2': { name: storage.type.apex } '3': { name: entity.name.variable.local.apex } - end: (?<=\}) + end: (?=\})|(?=when\b) patterns: - include: '#block' - include: '#expression' diff --git a/test/switch.tests.ts b/test/switch.tests.ts index 70afbae..cb2a0a1 100644 --- a/test/switch.tests.ts +++ b/test/switch.tests.ts @@ -736,6 +736,73 @@ when 'label' {} ]); }); + it('switch with string literals containing dashes', async () => { + const input = Input.InMethod(` +switch on locale { +when 'de-CH' { + System.debug('German Switzerland'); +} +when 'fr-CH' { + System.debug('French Switzerland'); +} +when else { + System.debug('Other locale'); +} +}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Switch.Switch, + Token.Keywords.Switch.On, + Token.Variables.ReadWrite('locale'), + Token.Punctuation.OpenBrace, + Token.Keywords.Switch.When, + Token.Punctuation.String.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('de-CH'), + Token.Punctuation.String.End, + Token.Punctuation.OpenBrace, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.FunctionText('debug'), + Token.Punctuation.OpenParen, + Token.XmlDocComments.String.SingleQuoted.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('German Switzerland'), + Token.XmlDocComments.String.SingleQuoted.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + Token.Keywords.Switch.When, + Token.Punctuation.String.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('fr-CH'), + Token.Punctuation.String.End, + Token.Punctuation.OpenBrace, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.FunctionText('debug'), + Token.Punctuation.OpenParen, + Token.XmlDocComments.String.SingleQuoted.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('French Switzerland'), + Token.XmlDocComments.String.SingleQuoted.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + Token.Keywords.Switch.When, + Token.Keywords.Switch.Else, + Token.Punctuation.OpenBrace, + Token.Support.Class.System, + Token.Punctuation.Accessor, + Token.Support.Class.FunctionText('debug'), + Token.Punctuation.OpenParen, + Token.XmlDocComments.String.SingleQuoted.Begin, + Token.XmlDocComments.String.SingleQuoted.Text('Other locale'), + Token.XmlDocComments.String.SingleQuoted.End, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + Token.Punctuation.CloseBrace, + Token.Punctuation.CloseBrace, + ]); + }); + /* it('switch usage in triggers', () => { const input = Input.InTrigger(` switch on sobject {