Skip to content

Commit 0b53ef2

Browse files
Fix escaped quote selection positioning for proper content selection
Co-authored-by: carlocardella <[email protected]>
1 parent 9a30954 commit 0b53ef2

File tree

2 files changed

+80
-14
lines changed

2 files changed

+80
-14
lines changed

src/modules/delimiters.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,13 @@ export function selectTextBetweenDelimiters(delimiterType: delimiterTypes) {
407407
let newSelectionOffsetStart = openingDelimiter.position;
408408
let newSelectionOffsetEnd = closingDelimiter.position;
409409

410+
// For escaped quotes, adjust the start position to exclude the backslash
411+
const openingIsEscaped = (openingDelimiter as any).isEscaped;
412+
if (openingIsEscaped) {
413+
// Start after the escaped quote (skip the backslash and quote)
414+
newSelectionOffsetStart = openingDelimiter.position + 1;
415+
}
416+
410417
let currentSelection = getTextFromSelection(editor, editor.selection);
411418
if (selectionIncludesDelimiters(currentSelection!, delimiterType) || !currentSelection) {
412419
// the current selection already includes the delimiters, so the new selection should not, unless:
@@ -527,8 +534,9 @@ function findOpeningQuote(text: string, delimiterType: delimiterTypes, startOffs
527534
let openingDelimiter = Object.values(openingDelimiters).find((delimiter) => delimiter.char === text.at(position)) ?? undefined;
528535

529536
if (openingDelimiter) {
530-
// For escaped quotes, we still consider them as valid delimiters
531-
// The key is to find the closest matching pair, whether escaped or not
537+
// Check if this opening quote is escaped
538+
const isEscaped = isEscapedQuote(text, position);
539+
532540
return {
533541
name: openingDelimiter.name,
534542
char: text.at(position)!,
@@ -538,7 +546,9 @@ function findOpeningQuote(text: string, delimiterType: delimiterTypes, startOffs
538546
type: openingDelimiter.type,
539547
direction: openingDelimiter.direction,
540548
offset: startOffset,
541-
} as delimiter;
549+
// Store whether this opening delimiter is escaped for use in closing quote search
550+
isEscaped: isEscaped,
551+
} as delimiter & { isEscaped: boolean };
542552
}
543553

544554
position--;
@@ -583,13 +593,13 @@ function isEscapedQuote(text: string, position: number): boolean {
583593
* @param {number} [position=0] The position to start the search from
584594
* @returns {(delimiter | undefined)}
585595
*/
586-
function findClosingQuote(text: string, openingDelimiter: delimiter, startOffset: number, position: number = 0): delimiter | undefined {
596+
function findClosingQuote(text: string, openingDelimiter: delimiter & { isEscaped?: boolean }, startOffset: number, position: number = 0): delimiter | undefined {
587597
if (!text) {
588598
return undefined;
589599
}
590600

591-
// Check if the opening delimiter was escaped
592-
const openingWasEscaped = isEscapedQuote(text, openingDelimiter.position - startOffset);
601+
// Get the escape status from the opening delimiter
602+
const openingWasEscaped = openingDelimiter.isEscaped || false;
593603

594604
while (position < text.length) {
595605
if (text.at(position) === openingDelimiter.pairedChar) {
@@ -602,19 +612,21 @@ function findClosingQuote(text: string, openingDelimiter: delimiter, startOffset
602612
let adjustedPosition = position;
603613
if (currentIsEscaped && position > 0) {
604614
// For escaped quotes, adjust position to exclude the backslash
615+
// We want to end at the position just before the backslash
605616
adjustedPosition = position - 1;
606617
}
607618

608619
return {
609620
name: delimiters.filter((delimiter) => delimiter.char === text.at(position))[0].name,
610621
char: text.at(position)!,
611622
pairedChar: openingDelimiter.char,
612-
position: startOffset + adjustedPosition + 1,
623+
position: startOffset + adjustedPosition + (currentIsEscaped ? 0 : 1),
613624
pairedOffset: undefined, // update
614625
type: openingDelimiter.type,
615626
direction: delimiterTypeDirection.close,
616627
offset: startOffset,
617-
} as delimiter;
628+
isEscaped: currentIsEscaped,
629+
} as delimiter & { isEscaped: boolean };
618630
}
619631
}
620632

src/test/suite/delimiters.test.ts

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ suite("Delimiters Test Suite", () => {
6464

6565
test("Should handle quote selection scenarios from issue", () => {
6666
// This test documents the expected behavior for the issue scenarios
67-
// Scenario 1: At position 1, should select content between outer quotes (1:35)
68-
// Scenario 2: At position 5, should select "prop1" (5:10)
67+
// Scenario 1: At position 3, should select content between outer quotes (1:35)
68+
// Scenario 2: At position 6, should select "prop1" (5:9)
6969

7070
const testString = '"[{\\"prop1\\":0,\\"prop2\\":\\"value2\\"]"';
7171

@@ -78,10 +78,19 @@ suite("Delimiters Test Suite", () => {
7878
assert.strictEqual(isEscapedQuote(testString, 4), true, "Quote at position 4 is escaped");
7979
assert.strictEqual(isEscapedQuote(testString, 11), true, "Quote at position 11 is escaped");
8080

81-
// Verify the fix handles both scenarios:
82-
// 1. Unescaped quotes (0,36) should pair together for outer selection
83-
// 2. Escaped quotes (4,11) should pair together for inner selection
84-
// 3. Selection should exclude escape characters from the content
81+
// Test the fix: cursor at position 6 should find escaped quote pair (4, 11)
82+
// and adjust selection to exclude escape characters
83+
// Expected selection: position 5 to 9 (content: "prop1")
84+
85+
// Simulate text split at cursor position 6
86+
const textBeforeCursor = testString.substring(0, 6); // "[{\"p
87+
const textAfterCursor = testString.substring(6); // rop1\":0,\"prop2\":\"value2\"}]"
88+
89+
// Opening quote should be at position 4 (escaped)
90+
assert.strictEqual(isEscapedQuote(textBeforeCursor, 4), true, "Opening quote at position 4 is escaped");
91+
92+
// Closing quote should be at position 5 in textAfterCursor (position 11 in full string, escaped)
93+
assert.strictEqual(isEscapedQuote(textAfterCursor, 5), true, "Closing quote at relative position 5 is escaped");
8594

8695
assert.ok(true, "Enhanced escape detection and pairing implemented for delimiter matching");
8796
});
@@ -108,4 +117,49 @@ suite("Delimiters Test Suite", () => {
108117

109118
assert.ok(true, "Escaped quote pairing and selection positioning implemented");
110119
});
120+
121+
test("Should fix specific issue: cursor at position 6 selects 'prop1'", () => {
122+
// Test the exact issue reported by the user
123+
// String: "[{\"prop1\":0,\"prop2\":\"value2\"}]"
124+
// Cursor at position 6 should select "prop1" from position 5 to position 9
125+
126+
const testString = '"[{\\"prop1\\":0,\\"prop2\\":\\"value2\\"]"';
127+
128+
// Simulate cursor at position 6 (on 'r' in "prop1")
129+
const cursorPosition = 6;
130+
const textBefore = testString.substring(0, cursorPosition); // "[{\"p
131+
const textAfter = testString.substring(cursorPosition); // rop1\":0,\"prop2\":\"value2\"}]"
132+
133+
// The opening quote should be the escaped quote at position 4
134+
let openingPosition = -1;
135+
for (let i = textBefore.length - 1; i >= 0; i--) {
136+
if (textBefore[i] === '"') {
137+
openingPosition = i;
138+
break;
139+
}
140+
}
141+
assert.strictEqual(openingPosition, 4, "Should find opening quote at position 4");
142+
assert.strictEqual(isEscapedQuote(textBefore, 4), true, "Opening quote should be escaped");
143+
144+
// The closing quote should be the escaped quote at relative position 5 in textAfter
145+
let closingPosition = -1;
146+
for (let i = 0; i < textAfter.length; i++) {
147+
if (textAfter[i] === '"') {
148+
closingPosition = i;
149+
break;
150+
}
151+
}
152+
assert.strictEqual(closingPosition, 5, "Should find closing quote at relative position 5");
153+
assert.strictEqual(isEscapedQuote(textAfter, 5), true, "Closing quote should be escaped");
154+
155+
// With the fix, selection should be:
156+
// Start: after the opening escaped quote (position 5)
157+
// End: before the closing escaped quote's backslash (position 9)
158+
const expectedSelectionStart = 5;
159+
const expectedSelectionEnd = 9;
160+
const expectedContent = "prop1";
161+
const actualContent = testString.substring(expectedSelectionStart, expectedSelectionEnd);
162+
163+
assert.strictEqual(actualContent, expectedContent, `Should select "${expectedContent}" but got "${actualContent}"`);
164+
});
111165
});

0 commit comments

Comments
 (0)