From 7ca97676de3b40ed45895af8eb56261ecbd658fa Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Sat, 28 Mar 2026 17:25:35 +0000 Subject: [PATCH] Fix Python multi-line f-strings. --- .../definitions/python/python.test.ts | 125 ++++++++++++++++++ src/languages/definitions/python/python.ts | 26 +++- 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/languages/definitions/python/python.test.ts b/src/languages/definitions/python/python.test.ts index 019758a413..82cc4af08a 100644 --- a/src/languages/definitions/python/python.test.ts +++ b/src/languages/definitions/python/python.test.ts @@ -253,5 +253,130 @@ testTokenization('python', [ { startIndex: 6, type: 'string.escape.python' } ] } + ], + + // https://github.com/microsoft/monaco-editor/issues/4601 + // Multiline triple-quoted f-strings. + [ + { + line: 'x = f"""first line', + tokens: [ + { startIndex: 0, type: 'identifier.python' }, + { startIndex: 1, type: 'white.python' }, + { startIndex: 2, type: '' }, + { startIndex: 3, type: 'white.python' }, + { startIndex: 4, type: 'string.escape.python' }, + { startIndex: 8, type: 'string.python' } + ] + }, + { + line: 'still {var} string', + tokens: [ + { startIndex: 0, type: 'string.python' }, + { startIndex: 6, type: 'identifier.python' }, + { startIndex: 11, type: 'string.python' } + ] + }, + { + line: 'last line"""', + tokens: [ + { startIndex: 0, type: 'string.python' }, + { startIndex: 9, type: 'string.escape.python' } + ] + }, + { + line: 'y = 1', + tokens: [ + { startIndex: 0, type: 'identifier.python' }, + { startIndex: 1, type: 'white.python' }, + { startIndex: 2, type: '' }, + { startIndex: 3, type: 'white.python' }, + { startIndex: 4, type: 'number.python' } + ] + } + ], + [ + { + line: "x = f'''first line", + tokens: [ + { startIndex: 0, type: 'identifier.python' }, + { startIndex: 1, type: 'white.python' }, + { startIndex: 2, type: '' }, + { startIndex: 3, type: 'white.python' }, + { startIndex: 4, type: 'string.escape.python' }, + { startIndex: 8, type: 'string.python' } + ] + }, + { + line: 'still {var} string', + tokens: [ + { startIndex: 0, type: 'string.python' }, + { startIndex: 6, type: 'identifier.python' }, + { startIndex: 11, type: 'string.python' } + ] + }, + { + line: "last line'''", + tokens: [ + { startIndex: 0, type: 'string.python' }, + { startIndex: 9, type: 'string.escape.python' } + ] + }, + { + line: 'y = 1', + tokens: [ + { startIndex: 0, type: 'identifier.python' }, + { startIndex: 1, type: 'white.python' }, + { startIndex: 2, type: '' }, + { startIndex: 3, type: 'white.python' }, + { startIndex: 4, type: 'number.python' } + ] + } + ], + [ + { + line: `f'''it's fine'''`, + tokens: [ + { startIndex: 0, type: 'string.escape.python' }, + { startIndex: 4, type: 'string.python' }, + { startIndex: 13, type: 'string.escape.python' } + ] + } + ], + [ + { + line: 'F"str {var} str"', + tokens: [ + { startIndex: 0, type: 'string.escape.python' }, + { startIndex: 2, type: 'string.python' }, + { startIndex: 6, type: 'identifier.python' }, + { startIndex: 11, type: 'string.python' }, + { startIndex: 15, type: 'string.escape.python' } + ] + } + ], + [ + { + line: 'rf"str {var} str"', + tokens: [ + { startIndex: 0, type: 'string.escape.python' }, + { startIndex: 3, type: 'string.python' }, + { startIndex: 7, type: 'identifier.python' }, + { startIndex: 12, type: 'string.python' }, + { startIndex: 16, type: 'string.escape.python' } + ] + } + ], + [ + { + line: 'fr"str {var} str"', + tokens: [ + { startIndex: 0, type: 'string.escape.python' }, + { startIndex: 3, type: 'string.python' }, + { startIndex: 7, type: 'identifier.python' }, + { startIndex: 12, type: 'string.python' }, + { startIndex: 16, type: 'string.escape.python' } + ] + } ] ]); diff --git a/src/languages/definitions/python/python.ts b/src/languages/definitions/python/python.ts index e16954899b..c932f7b6ac 100644 --- a/src/languages/definitions/python/python.ts +++ b/src/languages/definitions/python/python.ts @@ -250,10 +250,16 @@ export const language = { // Recognize strings, including those broken across lines with \ (but not without) strings: [ [/'$/, 'string.escape', '@popall'], - [/f'{1,3}/, 'string.escape', '@fStringBody'], + [/[fF][rR]?'''/, 'string.escape', '@fTripleStringBody'], + [/[rR][fF]'''/, 'string.escape', '@fTripleStringBody'], + [/[fF][rR]?'/, 'string.escape', '@fStringBody'], + [/[rR][fF]'/, 'string.escape', '@fStringBody'], [/'/, 'string.escape', '@stringBody'], [/"$/, 'string.escape', '@popall'], - [/f"{1,3}/, 'string.escape', '@fDblStringBody'], + [/[fF][rR]?"""/, 'string.escape', '@fDblTripleStringBody'], + [/[rR][fF]"""/, 'string.escape', '@fDblTripleStringBody'], + [/[fF][rR]?"/, 'string.escape', '@fDblStringBody'], + [/[rR][fF]"/, 'string.escape', '@fDblStringBody'], [/"/, 'string.escape', '@dblStringBody'] ], fStringBody: [ @@ -264,6 +270,14 @@ export const language = { [/'/, 'string.escape', '@popall'], [/\\$/, 'string'] ], + fTripleStringBody: [ + [/[^\\'\{\}]+/, 'string'], + [/\{[^\}':!=]+/, 'identifier', '@fStringDetail'], + [/\\./, 'string'], + [/'''/, 'string.escape', '@popall'], + [/'/, 'string'], + [/\\$/, 'string'] + ], stringBody: [ [/[^\\']+$/, 'string', '@popall'], [/[^\\']+/, 'string'], @@ -279,6 +293,14 @@ export const language = { [/"/, 'string.escape', '@popall'], [/\\$/, 'string'] ], + fDblTripleStringBody: [ + [/[^\\"\{\}]+/, 'string'], + [/\{[^\}':!=]+/, 'identifier', '@fStringDetail'], + [/\\./, 'string'], + [/"""/, 'string.escape', '@popall'], + [/"/, 'string'], + [/\\$/, 'string'] + ], dblStringBody: [ [/[^\\"]+$/, 'string', '@popall'], [/[^\\"]+/, 'string'],