From 234fd58d4754b3a0c96ab7b19fde20dbd01a67ac Mon Sep 17 00:00:00 2001 From: peternhale Date: Thu, 9 Apr 2026 13:51:38 -0600 Subject: [PATCH] fix: adjust scope created within multiline string - W-21968889 Modify multiline string so it uses the same highlight scopes found in simple string --- grammars/apex.tmLanguage | 20 +++++--- grammars/apex.tmLanguage.cson | 17 ++++--- grammars/soql.tmLanguage | 20 +++++--- src/apex.tmLanguage.yml | 11 +++-- test/literals.test.ts | 93 ++++++++++++++++++++++------------- 5 files changed, 98 insertions(+), 63 deletions(-) diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage index 8f6263b..e8e7734 100644 --- a/grammars/apex.tmLanguage +++ b/grammars/apex.tmLanguage @@ -2921,6 +2921,10 @@ patterns + + include + #multiline-string-literal + include #string-literal @@ -2941,6 +2945,10 @@ patterns + + include + #multiline-string-literal + include #string-literal @@ -3686,9 +3694,9 @@ multiline-string-literal name - string.quoted.single.multiline.apex + string.quoted.single.apex string.quoted.single.multiline.apex begin - ''' + '''(?=$) beginCaptures 0 @@ -3713,10 +3721,6 @@ include #string-character-escape - - include - #string-template-expression - string-literal @@ -3772,7 +3776,7 @@ name meta.template-expression.apex begin - \$\{ + (?<!\\)\$\{ beginCaptures 0 @@ -3797,7 +3801,7 @@ name variable.other.readwrite.apex match - @?[_[:alpha:]][_[:alnum:]]* + \G@?[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)* diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson index 821b324..8acbbaa 100644 --- a/grammars/apex.tmLanguage.cson +++ b/grammars/apex.tmLanguage.cson @@ -1720,6 +1720,9 @@ repository: name: 'keyword.control.switch.when.apex' '2': patterns: [ + { + include: '#multiline-string-literal' + } { include: '#string-literal' } @@ -1732,6 +1735,9 @@ repository: ] '5': patterns: [ + { + include: '#multiline-string-literal' + } { include: '#string-literal' } @@ -2178,8 +2184,8 @@ repository: } ] 'multiline-string-literal': - name: 'string.quoted.single.multiline.apex' - begin: "'''" + name: 'string.quoted.single.apex string.quoted.single.multiline.apex' + begin: "'''(?=$)" beginCaptures: '0': name: 'punctuation.definition.string.begin.apex' @@ -2191,9 +2197,6 @@ repository: { include: '#string-character-escape' } - { - include: '#string-template-expression' - } ] 'string-literal': name: 'string.quoted.single.apex' @@ -2220,7 +2223,7 @@ repository: match: '\\\\.' 'string-template-expression': name: 'meta.template-expression.apex' - begin: '\\$\\{' + begin: '(? patterns + + include + #multiline-string-literal + include #string-literal @@ -2923,6 +2927,10 @@ patterns + + include + #multiline-string-literal + include #string-literal @@ -3668,9 +3676,9 @@ multiline-string-literal name - string.quoted.single.multiline.apex + string.quoted.single.apex string.quoted.single.multiline.apex begin - ''' + '''(?=$) beginCaptures 0 @@ -3695,10 +3703,6 @@ include #string-character-escape - - include - #string-template-expression - string-literal @@ -3754,7 +3758,7 @@ name meta.template-expression.apex begin - \$\{ + (?<!\\)\$\{ beginCaptures 0 @@ -3779,7 +3783,7 @@ name variable.other.readwrite.apex match - @?[_[:alpha:]][_[:alnum:]]* + \G@?[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)* diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index cc4c6de..080b69e 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -996,12 +996,14 @@ repository: '1': { name: keyword.control.switch.when.apex } '2': patterns: + - include: '#multiline-string-literal' - include: '#string-literal' '4': patterns: - include: '#punctuation-comma' '5': patterns: + - include: '#multiline-string-literal' - include: '#string-literal' end: (?=\})|(?=when\b) patterns: @@ -1299,8 +1301,8 @@ repository: match: \b[0-9_]+(U|u|L|l|UL|Ul|uL|ul|LU|Lu|lU|lu)?\b multiline-string-literal: - name: string.quoted.single.multiline.apex - begin: "'''" + name: string.quoted.single.apex string.quoted.single.multiline.apex + begin: "'''(?=$)" beginCaptures: '0': { name: punctuation.definition.string.begin.apex } end: "'''" @@ -1308,7 +1310,6 @@ repository: '0': { name: punctuation.definition.string.end.apex } patterns: - include: '#string-character-escape' - - include: '#string-template-expression' string-literal: name: string.quoted.single.apex @@ -1329,7 +1330,7 @@ repository: string-template-expression: name: meta.template-expression.apex - begin: \$\{ + begin: "(? { Token.Punctuation.Semicolon, ]); }); + + it('string with escaped dollar is not a template expression', async () => { + const input = Input.InClass("String s = 'hello \\${name}';"); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.String, + Token.Identifiers.FieldName('s'), + Token.Operators.Assignment, + Token.Punctuation.String.Begin, + Token.Literals.String('hello '), + Token.Literals.CharacterEscape('\\$'), + Token.Literals.String('{name}'), + Token.Punctuation.String.End, + Token.Punctuation.Semicolon, + ]); + }); + + it('string with dotted template expression', async () => { + const input = Input.InClass("String s = 'x=${a.b}';"); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.String, + Token.Identifiers.FieldName('s'), + Token.Operators.Assignment, + Token.Punctuation.String.Begin, + Token.Literals.String('x='), + Token.TemplateExpression.Begin, + Token.Variables.ReadWrite('a.b'), + Token.TemplateExpression.End, + Token.Punctuation.String.End, + Token.Punctuation.Semicolon, + ]); + }); }); describe('Multiline Strings', () => { @@ -211,7 +246,7 @@ describe('Grammar', () => { ]); }); - it('multiline string with template expression', async () => { + it('multiline string: ${…} is not separately highlighted (same scopes as body)', async () => { const input = Input.InClass( `String s = '''\nhello \${name}\n''';` ); @@ -222,16 +257,13 @@ describe('Grammar', () => { Token.Identifiers.FieldName('s'), Token.Operators.Assignment, Token.Punctuation.MultilineString.Begin, - Token.Literals.MultilineString('hello '), - Token.TemplateExpression.Begin, - Token.Variables.ReadWrite('name'), - Token.TemplateExpression.End, + Token.Literals.MultilineString('hello ${name}'), Token.Punctuation.MultilineString.End, Token.Punctuation.Semicolon, ]); }); - it('multiline string with multiple template expressions', async () => { + it('multiline string: multiple ${…} spans are plain multiline string text per line', async () => { const input = Input.InClass( `String s = '''\nI am a multi \${var1}\nline \${var2}\nstring \${var3}\n''';` ); @@ -242,18 +274,9 @@ describe('Grammar', () => { Token.Identifiers.FieldName('s'), Token.Operators.Assignment, Token.Punctuation.MultilineString.Begin, - Token.Literals.MultilineString('I am a multi '), - Token.TemplateExpression.Begin, - Token.Variables.ReadWrite('var1'), - Token.TemplateExpression.End, - Token.Literals.MultilineString('line '), - Token.TemplateExpression.Begin, - Token.Variables.ReadWrite('var2'), - Token.TemplateExpression.End, - Token.Literals.MultilineString('string '), - Token.TemplateExpression.Begin, - Token.Variables.ReadWrite('var3'), - Token.TemplateExpression.End, + Token.Literals.MultilineString('I am a multi ${var1}'), + Token.Literals.MultilineString('line ${var2}'), + Token.Literals.MultilineString('string ${var3}'), Token.Punctuation.MultilineString.End, Token.Punctuation.Semicolon, ]); @@ -279,7 +302,7 @@ describe('Grammar', () => { it('multiline string as method argument', async () => { const input = Input.InMethod( - `System.debug('''multi\nline''');` + `System.debug('''\nmulti\nline''');` ); const tokens = await tokenize(input); @@ -298,7 +321,7 @@ describe('Grammar', () => { }); it('multiline string in return statement', async () => { - const input = Input.InMethod(`return '''hello''';`); + const input = Input.InMethod(`return '''\nhello''';`); const tokens = await tokenize(input); tokens.should.deep.equal([ @@ -312,7 +335,7 @@ describe('Grammar', () => { it('multiline string in SOQL WHERE clause', async () => { const input = Input.InMethod( - `List a = [SELECT Id FROM Account WHERE Name = '''value'''];` + `List a = [SELECT Id FROM Account WHERE Name = '''\nvalue'''];` ); const tokens = await tokenize(input); @@ -341,7 +364,7 @@ describe('Grammar', () => { it('multiline string in Map literal', async () => { const input = Input.InMethod( - `Map m = new Map{'''key''' => '''value'''};` + `Map m = new Map{'''\nkey''' => '''\nvalue'''};` ); const tokens = await tokenize(input); @@ -377,7 +400,7 @@ describe('Grammar', () => { it('multiline string in throw statement', async () => { const input = Input.InMethod( - `throw new TestException('''msg''');` + `throw new TestException('''\nmsg''');` ); const tokens = await tokenize(input); @@ -396,7 +419,7 @@ describe('Grammar', () => { it('multiline string in annotation', async () => { const input = Input.FromText( - `@MyAnnotation(value = '''text''') + `@MyAnnotation(value = '''\ntext''') public class Foo { }` ); const tokens = await tokenize(input); @@ -421,7 +444,7 @@ public class Foo { }` it('multiline string in initializer block', async () => { const input = Input.InClass(` { - this.setMessage('''multiline message'''); + this.setMessage('''\nmultiline message'''); }`); const tokens = await tokenize(input); @@ -492,7 +515,7 @@ public class Foo { }` it('multiline string in throw statement', async () => { const input = Input.InMethod( - `throw new IllegalArgumentException('''msg''');` + `throw new IllegalArgumentException('''\nmsg''');` ); const tokens = await tokenize(input); @@ -511,7 +534,7 @@ public class Foo { }` it('multiline string in annotation', async () => { const input = Input.FromText( - `@MyAnnotation(value = '''text''') + `@MyAnnotation(value = '''\ntext''') public class C { }` ); const tokens = await tokenize(input); @@ -536,7 +559,7 @@ public class C { }` it('multiline string in initializer block', async () => { const input = Input.InClass(` { - String s = '''hello'''; + String s = '''\nhello'''; }`); const tokens = await tokenize(input); @@ -555,7 +578,7 @@ public class C { }` it('multiline string in method argument', async () => { const input = Input.InMethod( - `System.debug('''multi\nline''');` + `System.debug('''\nmulti\nline''');` ); const tokens = await tokenize(input); @@ -574,7 +597,7 @@ public class C { }` }); it('multiline string in return statement', async () => { - const input = Input.InMethod(`return '''hello''';`); + const input = Input.InMethod(`return '''\nhello''';`); const tokens = await tokenize(input); tokens.should.deep.equal([ @@ -588,7 +611,7 @@ public class C { }` it('multiline string in SOQL WHERE clause', async () => { const input = Input.InMethod( - `List a = [SELECT Id FROM Account WHERE Name = '''value'''];` + `List a = [SELECT Id FROM Account WHERE Name = '''\nvalue'''];` ); const tokens = await tokenize(input); @@ -617,7 +640,7 @@ public class C { }` it('multiline string in map literal', async () => { const input = Input.InMethod( - `Map m = new Map{'''key''' => '''value'''};` + `Map m = new Map{'''\nkey''' => '''\nvalue'''};` ); const tokens = await tokenize(input); @@ -653,7 +676,7 @@ public class C { }` it('multiline string in throw statement', async () => { const input = Input.InMethod( - `throw new Exception('''msg''');` + `throw new Exception('''\nmsg''');` ); const tokens = await tokenize(input); @@ -672,7 +695,7 @@ public class C { }` it('multiline string in annotation', async () => { const input = Input.InClass( - `@MyAnnotation(value = '''text''') + `@MyAnnotation(value = '''\ntext''') void m() {}` ); const tokens = await tokenize(input); @@ -698,7 +721,7 @@ public class C { }` it('multiline string in initializer block', async () => { const input = Input.InClass(` { - String s = '''hello'''; + String s = '''\nhello'''; }`); const tokens = await tokenize(input);