@@ -106,19 +106,26 @@ function startsWithPrefix(input: InputStream): boolean {
106106 * Checks if a '(' at the given offset starts a balanced ParenExpr.
107107 * Uses peek() to avoid modifying stream position.
108108 * Returns true if we find a matching ')' that closes the initial '('.
109+ * Handles escaped characters (backslash followed by any character).
109110 */
110111function hasBalancedParensAt ( input : InputStream , startOffset : number ) : boolean {
111112 if ( input . peek ( startOffset ) !== OPEN_PAREN ) {
112113 return false ;
113114 }
114-
115+
115116 let offset = startOffset + 1 ;
116117 let depth = 1 ;
117-
118+
118119 while ( true ) {
119120 const ch = input . peek ( offset ) ;
120121 if ( ch === EOF ) break ;
121-
122+
123+ // Handle escaped characters - skip the next character after a backslash
124+ if ( ch === 92 /* backslash */ ) {
125+ offset += 2 ; // Skip backslash and the escaped character
126+ continue ;
127+ }
128+
122129 if ( ch === OPEN_PAREN ) {
123130 depth ++ ;
124131 } else if ( ch === CLOSE_PAREN ) {
@@ -129,7 +136,7 @@ function hasBalancedParensAt(input: InputStream, startOffset: number): boolean {
129136 }
130137 offset ++ ;
131138 }
132-
139+
133140 return false ;
134141}
135142
@@ -139,6 +146,7 @@ function hasBalancedParensAt(input: InputStream, startOffset: number): boolean {
139146 *
140147 * We only consider a '(' as a ParenExpr start if it's preceded by whitespace or
141148 * start-of-input, since "test()" has a '(' that's part of a word, not a ParenExpr.
149+ * Handles escaped characters (backslash followed by any character).
142150 */
143151function hasUnmatchedOpenParen ( input : InputStream ) : boolean {
144152 // Count parens backwards from current position
@@ -153,27 +161,42 @@ function hasUnmatchedOpenParen(input: InputStream): boolean {
153161 return depth < 0 ;
154162 }
155163
164+ // Check if this character is escaped (preceded by backslash)
165+ // Note: we need to be careful about escaped backslashes (\\)
166+ // For simplicity, if we see a backslash immediately before, skip this char
167+ const prevCh = input . peek ( offset - 1 ) ;
168+ if ( prevCh === 92 /* backslash */ ) {
169+ // Check if the backslash itself is escaped
170+ const prevPrevCh = input . peek ( offset - 2 ) ;
171+ if ( prevPrevCh !== 92 ) {
172+ // Single backslash - this char is escaped, skip it
173+ offset -- ;
174+ continue ;
175+ }
176+ // Double backslash - the backslash is escaped, so current char is not
177+ }
178+
156179 if ( ch === CLOSE_PAREN ) {
157180 depth ++ ;
158181 } else if ( ch === OPEN_PAREN ) {
159182 // Check what's before this '('
160- const prevCh = input . peek ( offset - 1 ) ;
183+ const beforeParen = input . peek ( offset - 1 ) ;
161184
162185 // A '(' starts a ParenExpr if it's preceded by:
163186 // - EOF or whitespace (e.g., "(hello)" or "test (hello)")
164187 // - '-' for negation (e.g., "-(hello)")
165188 // - ':' for prefix values (e.g., "repo:(foo or bar)")
166189 const isDefinitelyParenExprStart =
167- prevCh === EOF ||
168- isWhitespace ( prevCh ) ||
169- prevCh === DASH ||
170- prevCh === COLON ;
190+ beforeParen === EOF ||
191+ isWhitespace ( beforeParen ) ||
192+ beforeParen === DASH ||
193+ beforeParen === COLON ;
171194
172195 // Special case: '(' preceded by '(' could be nested ParenExprs like "((hello))"
173196 // BUT it could also be part of a word like "test((nested))"
174197 // To distinguish: if prev is '(', check what's before THAT '('
175198 let isParenExprStart = isDefinitelyParenExprStart ;
176- if ( ! isParenExprStart && prevCh === OPEN_PAREN ) {
199+ if ( ! isParenExprStart && beforeParen === OPEN_PAREN ) {
177200 // Check what's before the previous '('
178201 const prevPrevCh = input . peek ( offset - 2 ) ;
179202 // Only count as ParenExpr if the preceding '(' is also at a token boundary
@@ -192,7 +215,7 @@ function hasUnmatchedOpenParen(input: InputStream): boolean {
192215 return true ;
193216 }
194217 }
195- // If prevCh is something else, this '(' is part of a word like "test()"
218+ // If beforeParen is something else, this '(' is part of a word like "test()"
196219 // Don't count it in our depth tracking
197220 }
198221 offset -- ;
0 commit comments