When a line ends with as or satisfies, the type on the next line gets expression scopes instead of type scopes.
JavaScript and TypeScript Nightly version: not tested in the extension. Reproduced with TypeScript.tmLanguage at eeeb0dc (current master), vscode-textmate 9.3.2, vscode-oniguruma 2.0.1, Node 26.7.0, macOS arm64.
Code
const a = b as A | undefined
const a = b as
A | undefined
const a = b satisfies
A | undefined
Prettier 3.9.8 emits this layout when an as expression exceeds the print width, e.g.:
const inlineSourceMap = convertSourceMap.fromSource(content)?.toObject() as
SourceMap | undefined
Actual
| Token |
Same line |
Next line |
A |
entity.name.type.ts |
variable.other.constant.ts |
| |
keyword.operator.type.ts |
keyword.operator.bitwise.ts |
undefined |
support.type.builtin.ts |
constant.language.undefined.ts |
A type on the line after satisfies gets the same scopes as a type on the line after as.
Expected
The same scopes on both layouts, because TypeScript parses both as a type assertion with the type A | undefined.
Repro script
npm install vscode-textmate@9.3.2 vscode-oniguruma@2.0.1
curl -sLO https://raw.githubusercontent.com/microsoft/TypeScript-TmLanguage/eeeb0dc4daa8793b8227bb3b34ddfc267c85fd81/TypeScript.tmLanguage
node repro.mjs
// repro.mjs
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import vsctm from "vscode-textmate";
import oniguruma from "vscode-oniguruma";
const require = createRequire(import.meta.url);
await oniguruma.loadWASM(
readFileSync(require.resolve("vscode-oniguruma/release/onig.wasm")).buffer,
);
const registry = new vsctm.Registry({
onigLib: Promise.resolve({
createOnigScanner: (patterns) => new oniguruma.OnigScanner(patterns),
createOnigString: (s) => new oniguruma.OnigString(s),
}),
loadGrammar: async () =>
vsctm.parseRawGrammar(
readFileSync("TypeScript.tmLanguage", "utf8"),
"TypeScript.tmLanguage",
),
});
const grammar = await registry.loadGrammar("source.ts");
const tokenize = (code) => {
let state = vsctm.INITIAL;
for (const line of code.split("\n")) {
const { tokens, ruleStack } = grammar.tokenizeLine(line, state);
state = ruleStack;
for (const { startIndex, endIndex, scopes } of tokens) {
const text = line.slice(startIndex, endIndex);
if (text.trim())
console.log(JSON.stringify(text).padEnd(10), scopes.at(-1));
}
}
console.log();
};
tokenize("const a = b as A | undefined");
tokenize("const a = b as\n A | undefined");
tokenize("const a = b satisfies\n A | undefined");
Possible cause (unverified)
The as/satisfies rule's end includes $ through lookAheadEndOfType, so the type ends at the line break right after the keyword:
|
- begin: '{{startOfIdentifier}}(?:(as)|(satisfies))\s+' |
|
beginCaptures: |
|
'1': { name: keyword.control.as.ts } |
|
'2': { name: keyword.control.satisfies.ts } |
|
end: (?=^|{{lookAheadEndOfType}}|({{startOfIdentifier}}(as|satisfies)\s+)|(\s+\<)) |
|
patterns: |
|
- include: '#type' |
|
lookAheadEndOfType: '[;),}\]:?\-\+\>]|\|\||\&\&|\!\=\=|$' |
The $ is presumably what ends the type at the end of a line without a semicolon (x as T followed by a new statement), so dropping it could break that case.
When a line ends with
asorsatisfies, the type on the next line gets expression scopes instead of type scopes.JavaScript and TypeScript Nightly version: not tested in the extension. Reproduced with
TypeScript.tmLanguageat eeeb0dc (currentmaster), vscode-textmate 9.3.2, vscode-oniguruma 2.0.1, Node 26.7.0, macOS arm64.Code
Prettier 3.9.8 emits this layout when an
asexpression exceeds the print width, e.g.:Actual
Aentity.name.type.tsvariable.other.constant.ts|keyword.operator.type.tskeyword.operator.bitwise.tsundefinedsupport.type.builtin.tsconstant.language.undefined.tsA type on the line after
satisfiesgets the same scopes as a type on the line afteras.Expected
The same scopes on both layouts, because TypeScript parses both as a type assertion with the type
A | undefined.Repro script
Possible cause (unverified)
The
as/satisfiesrule'sendincludes$throughlookAheadEndOfType, so the type ends at the line break right after the keyword:TypeScript-TmLanguage/TypeScript.YAML-tmLanguage
Lines 1755 to 1761 in eeeb0dc
TypeScript-TmLanguage/TypeScript.YAML-tmLanguage
Line 62 in eeeb0dc
The
$is presumably what ends the type at the end of a line without a semicolon (x as Tfollowed by a new statement), so dropping it could break that case.