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);