Skip to content

Commit d5f1a9b

Browse files
committed
Merge branch 'worktree-agent-a029aae3e18630a9d'
2 parents 8bbac02 + b2012cb commit d5f1a9b

4 files changed

Lines changed: 47 additions & 2 deletions

File tree

examples/typescript.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ const Regex_ = token(/\/(?:[^\/\\\[\n]|\\.|\[(?:[^\]\\\n]|\\.)*\])+\/[gims
2727
divisionAfterTypes: ['Ident', 'Number', 'String', 'Template', 'BigInt'],
2828
divisionAfterTexts: [')', ']', '++', '--', 'this', 'super', 'true', 'false', 'null', 'undefined'],
2929
regexAfterTexts: ['in', 'of', 'instanceof', 'typeof', 'delete', 'void', 'await', 'yield', 'throw', 'return', 'case', 'do', 'else', 'new'],
30+
// `kw ( … )` heads (control-flow): the closing `)` is a statement head, not a
31+
// value, so `if (a) /re/` parses `/re/` as a regex rather than division.
32+
regexAfterParenKeywords: ['if', 'while', 'for', 'with'],
33+
// member accessors: after one, those keywords are property NAMES, so
34+
// `obj.for(x) / y` stays a method call + division.
35+
memberAccessTexts: ['.', '?.'],
3036
},
3137
});
3238
const Decorator = token(/@(?:[a-zA-Z_$][a-zA-Z0-9_$.]*)?/, { scope: 'entity.name.function.decorator' });
3339
const PrivateField = token(/#[a-zA-Z_$][a-zA-Z0-9_$]*/, { scope: 'variable.other.property' });
3440
const Shebang = token(/^#![^\n]*/, { skip: true, scope: 'comment.line.shebang' });
35-
const JSDoc = token(/\/\*\*[\s\S]*?\*\//, { skip: true, scope: 'comment.block.documentation', embed: 'jsdoc' });
41+
const JSDoc = token(/\/\*\*(?!\/)[\s\S]*?\*\//, { skip: true, scope: 'comment.block.documentation', embed: 'jsdoc' });
3642
const TripleSlash = token(/\/\/\/\s*<[^\n]*/, { skip: true, scope: 'comment.line.triple-slash' });
3743
const LineComment = token(/\/\/[^\n]*/, { skip: true });
3844
const BlockComment = token(/\/\*[\s\S]*?\*\//, { skip: true });

src/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ interface TokenOptions {
1515
divisionAfterTypes?: string[];
1616
divisionAfterTexts?: string[];
1717
regexAfterTexts?: string[];
18+
regexAfterParenKeywords?: string[];
19+
memberAccessTexts?: string[];
1820
};
1921
string?: boolean;
2022
}
@@ -310,6 +312,8 @@ export function defineGrammar(config: GrammarConfig): CstGrammar & { name: strin
310312
divisionAfterTypes: tok.opts.regexContext.divisionAfterTypes ?? [],
311313
divisionAfterTexts: tok.opts.regexContext.divisionAfterTexts ?? [],
312314
regexAfterTexts: tok.opts.regexContext.regexAfterTexts ?? [],
315+
regexAfterParenKeywords: tok.opts.regexContext.regexAfterParenKeywords ?? [],
316+
memberAccessTexts: tok.opts.regexContext.memberAccessTexts ?? [],
313317
},
314318
string: tok.opts.string,
315319
};

src/gen-lexer.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ export function createLexer(grammar: CstGrammar) {
5050
const divisionPrevTypes = new Set([...(regexCtx?.divisionAfterTypes ?? []), '$templateTail']);
5151
const divisionPrevTexts = new Set(regexCtx?.divisionAfterTexts ?? []);
5252
const expressionStartKeywords = new Set(regexCtx?.regexAfterTexts ?? []);
53+
// Keywords that head a `kw ( … )` control group; the matching `)` is a statement
54+
// head (not a value), so a following `/` is a regex, not division.
55+
const parenHeadKeywords = new Set(regexCtx?.regexAfterParenKeywords ?? []);
56+
// Member-access texts (`.`/`?.`): a keyword right after one is a property name, so
57+
// the control-head rule above does not apply (`obj.for(x) / y` is a call/division).
58+
const memberAccessTexts = new Set(regexCtx?.memberAccessTexts ?? []);
5359

5460
// Scan from inside a template span to its next boundary: an interpolation hole
5561
// (`interpOpen`) or the closing delimiter (`open`). Delimiters come from the
@@ -73,6 +79,11 @@ export function createLexer(grammar: CstGrammar) {
7379
const tokens: Token[] = [];
7480
let pos = 0;
7581
const templateStack: number[] = [];
82+
// For each open `(`, whether it heads a control group (`if`/`while`/…) so the
83+
// matching `)` is a statement head, not a value. `lastCloseWasParenHead` carries
84+
// that to the regex-vs-division check (consulted only when prev is `)`).
85+
const parenHeadStack: boolean[] = [];
86+
let lastCloseWasParenHead = false;
7687

7788
while (pos < source.length) {
7889
// Skip whitespace
@@ -131,7 +142,9 @@ export function createLexer(grammar: CstGrammar) {
131142
if (prev) {
132143
// Expression-start keywords (in, throw, return, etc.) flip back to regex context
133144
const isExprKeyword = prev.type === identTokenName && expressionStartKeywords.has(prev.text);
134-
if (!isExprKeyword && (divisionPrevTypes.has(prev.type) || divisionPrevTexts.has(prev.text))) {
145+
// A `)` that closed a control head (`if (…) /re/`) is not a value → regex.
146+
const isParenHead = prev.text === ')' && lastCloseWasParenHead;
147+
if (!isExprKeyword && !isParenHead && (divisionPrevTypes.has(prev.type) || divisionPrevTexts.has(prev.text))) {
135148
continue;
136149
}
137150
}
@@ -151,6 +164,20 @@ export function createLexer(grammar: CstGrammar) {
151164
// Try punctuation literals (longest first)
152165
for (const lit of punctLiterals) {
153166
if (remaining.startsWith(lit)) {
167+
// Track control-head parens so a `/` after `if (…)`/`while (…)` is a regex.
168+
// The keyword must be a real keyword head, not a member name: `obj.for(x) / y`
169+
// is a method call + division, so skip when the keyword is itself preceded by
170+
// a member accessor (e.g. `.`/`?.`, from divisionAfterTexts) → property access.
171+
if (lit === '(') {
172+
const prev = tokens[tokens.length - 1];
173+
const beforePrev = tokens[tokens.length - 2];
174+
const isMemberName = !!beforePrev && memberAccessTexts.has(beforePrev.text);
175+
parenHeadStack.push(
176+
!isMemberName && !!prev && prev.type === identTokenName && parenHeadKeywords.has(prev.text),
177+
);
178+
} else if (lit === ')') {
179+
lastCloseWasParenHead = parenHeadStack.pop() ?? false;
180+
}
154181
tokens.push({ type: '', text: lit, offset: pos });
155182
pos += lit.length;
156183
matched = true;

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ export interface RegexContext {
2525
divisionAfterTypes: string[]; // token TYPE names after which `/` is division (value-producing tokens)
2626
divisionAfterTexts: string[]; // token TEXTs after which `/` is division (e.g. ')', 'this', 'true')
2727
regexAfterTexts: string[]; // keyword TEXTs that (re)enter expression position → `/` is a regex
28+
// keyword TEXTs that, when they head a parenthesized group `kw ( … )`, make the
29+
// matching `)` a control-head (not a value) so a following `/` is a regex, not
30+
// division (e.g. `if (a) /re/.test(x)`). Overrides `)` being in divisionAfterTexts.
31+
regexAfterParenKeywords?: string[];
32+
// member-access TEXTs (e.g. '.', '?.'): after one of these, an identifier is a
33+
// property NAME, not a keyword, so `obj.for(x) / y` is a call + division (the
34+
// `regexAfterParenKeywords` control-head rule does not apply).
35+
memberAccessTexts?: string[];
2836
}
2937

3038
export interface PrecOperator {

0 commit comments

Comments
 (0)