Skip to content

Commit abd180f

Browse files
committed
Refactor selection logic and improve candidate retrieval methods
1 parent c7796d2 commit abd180f

File tree

10 files changed

+191
-134
lines changed

10 files changed

+191
-134
lines changed

src/expandSelection.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,37 @@ export class SelectionProvider {
5050
endIndex: number,
5151
document: vscode.TextDocument,
5252
): { start: number; end: number } | null {
53-
// Get all valid candidates from finders
54-
const candidates = [
55-
findToken(text, startIndex, endIndex, document),
56-
findNearestQuotePair(text, startIndex, endIndex),
57-
findNearestScope(text, startIndex, endIndex),
58-
findLineExpansion(text, startIndex, endIndex, document),
59-
].filter((c) => !!c);
53+
// Get all valid candidates from finders as a map
54+
const candidateMap: Record<string, { start: number; end: number } | null> =
55+
{
56+
token: findToken(text, startIndex, endIndex, document),
57+
quote: findNearestQuotePair(text, startIndex, endIndex),
58+
scope: findNearestScope(text, startIndex, endIndex),
59+
line: findLineExpansion(text, startIndex, endIndex, document),
60+
};
6061

61-
// Return the smallest valid expansion
62+
// Return the smallest valid expansion, with priority logic
6263
let best: { start: number; end: number } | null = null;
6364
let smallest = Infinity;
64-
for (const candidate of candidates) {
65+
66+
// Priority: if scope or quote candidate exists and line candidate is not fully contained, ignore line candidate
67+
const line = candidateMap.line;
68+
const scope = candidateMap.scope;
69+
const quote = candidateMap.quote;
70+
let ignoreLine = false;
71+
if (line) {
72+
if (
73+
(scope && !(line.start >= scope.start && line.end <= scope.end)) ||
74+
(quote && !(line.start >= quote.start && line.end <= quote.end))
75+
) {
76+
candidateMap.line = null;
77+
}
78+
}
79+
80+
for (const [_, candidate] of Object.entries(candidateMap)) {
81+
if (!candidate) {
82+
continue;
83+
}
6584
const range = this.getTrimmedRange(text, candidate.start, candidate.end);
6685
const size = range.end - range.start;
6786
if (

src/finders/line.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { SelectionCandidate } from '../types';
3-
import { isValidExpansion } from './util';
3+
import { getCandidate } from './util';
44

55
/**
66
* Finds line expansion candidate that spans from start of line containing startIndex
@@ -25,30 +25,39 @@ export function findLineExpansion(
2525
new vscode.Position(endPos.line, document.lineAt(endPos.line).text.length),
2626
);
2727

28-
// Extract the content from start of first line to end of last line
29-
const fullContent = text.substring(startLineStart, endLineEnd);
30-
31-
// Trim the content to remove leading and trailing whitespace
32-
const trimmedContent = fullContent.trim();
33-
34-
if (trimmedContent.length === 0) {
28+
// Get the full candidate first
29+
const fullCandidate = getCandidate(
30+
startIndex,
31+
endIndex,
32+
startLineStart,
33+
endLineEnd,
34+
text,
35+
);
36+
if (!fullCandidate) {
3537
return null;
3638
}
3739

38-
// Calculate the actual start and end positions of the trimmed content
39-
const leadingWhitespace = fullContent.length - fullContent.trimStart().length;
40-
const trailingWhitespace = fullContent.length - fullContent.trimEnd().length;
41-
42-
const trimmedStart = startLineStart + leadingWhitespace;
43-
const trimmedEnd = endLineEnd - trailingWhitespace;
44-
45-
// Only return if it would expand the selection (not if selection already matches exactly)
46-
if (!isValidExpansion(startIndex, endIndex, trimmedStart, trimmedEnd)) {
47-
return null;
40+
// Check for trailing semicolon or comma in the trimmed candidate
41+
const lastChar = text[fullCandidate.end - 1];
42+
const startLine = document.positionAt(fullCandidate.start).line;
43+
const endLine = document.positionAt(fullCandidate.end).line;
44+
45+
if (
46+
(lastChar === ';' || lastChar === ',') &&
47+
fullCandidate.end > endIndex &&
48+
startLine === endLine
49+
) {
50+
const candidate = getCandidate(
51+
startIndex,
52+
endIndex,
53+
fullCandidate.start,
54+
fullCandidate.end - 1,
55+
text,
56+
);
57+
if (candidate) {
58+
return candidate;
59+
}
4860
}
4961

50-
return {
51-
start: trimmedStart,
52-
end: trimmedEnd,
53-
};
62+
return fullCandidate;
5463
}

src/finders/quote.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SelectionCandidate } from '../types';
2-
import { isValidExpansion } from './util';
2+
import { getCandidate } from './util';
33

44
/**
55
* Finds all quote pairs (", ', `) and returns the nearest containing candidate
@@ -24,26 +24,27 @@ export function findNearestQuotePair(
2424
if (text[j] === '\\' && !escaped) {
2525
escaped = true;
2626
} else if (text[j] === quote && !escaped) {
27-
const candidate: SelectionCandidate = {
28-
start: i,
29-
end: j + 1,
30-
};
31-
const size = candidate.end - candidate.start;
32-
// Check if this candidate contains the current selection
33-
if (
34-
isValidExpansion(
35-
startIndex,
36-
endIndex,
37-
candidate.start,
38-
candidate.end,
39-
)
40-
) {
41-
// Keep track of the smallest containing candidate
27+
// Try content inside quotes first
28+
const innerCandidate = getCandidate(
29+
startIndex,
30+
endIndex,
31+
i + 1,
32+
j,
33+
text,
34+
);
35+
let candidate = innerCandidate;
36+
if (!candidate) {
37+
// Fallback to full quote including delimiters
38+
candidate = getCandidate(startIndex, endIndex, i, j + 1, text);
39+
}
40+
if (candidate) {
41+
const size = candidate.end - candidate.start;
4242
if (size < smallestSize) {
4343
smallestSize = size;
4444
nearestCandidate = candidate;
4545
}
4646
}
47+
4748
i = j; // Skip to after this quote pair
4849
break;
4950
} else {

src/finders/scope.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import { SelectionCandidate } from '../types';
2-
import { isValidExpansion } from './util';
2+
import { getCandidate } from './util';
3+
4+
function getScopeCandidate(
5+
start: number,
6+
end: number,
7+
startIndex: number,
8+
endIndex: number,
9+
text: string,
10+
): SelectionCandidate | null {
11+
// Try content inside scope
12+
const inner = getCandidate(startIndex, endIndex, start + 1, end, text);
13+
if (inner) {
14+
return inner;
15+
}
16+
// Try scope including delimiters
17+
return getCandidate(startIndex, endIndex, start, end + 1, text);
18+
}
319

420
/**
521
* Finds all balanced scopes ([], {}, (), <>) and returns the nearest containing candidate
@@ -13,7 +29,6 @@ export function findNearestScope(
1329
{ open: '[', close: ']' },
1430
{ open: '{', close: '}' },
1531
{ open: '(', close: ')' },
16-
{ open: '<', close: '>' },
1732
];
1833

1934
let nearestCandidate: SelectionCandidate | null = null;
@@ -27,16 +42,14 @@ export function findNearestScope(
2742
stack.push(i);
2843
} else if (text[i] === close && stack.length > 0) {
2944
const start = stack.pop()!;
30-
const candidate: SelectionCandidate = {
45+
const candidate = getScopeCandidate(
3146
start,
32-
end: i + 1,
33-
};
34-
35-
// Check if this candidate contains the current selection
36-
if (
37-
isValidExpansion(startIndex, endIndex, candidate.start, candidate.end)
38-
) {
39-
// Keep track of the smallest containing candidate
47+
i,
48+
startIndex,
49+
endIndex,
50+
text,
51+
);
52+
if (candidate) {
4053
const size = candidate.end - candidate.start;
4154
if (size < smallestSize) {
4255
smallestSize = size;

src/finders/token.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { SelectionCandidate } from '../types';
3-
import { isValidExpansion } from './util';
3+
import { getCandidate } from './util';
44

55
/**
66
* Finds word boundaries using different regex patterns for extended token detection
@@ -35,12 +35,15 @@ export function findToken(
3535
const wordStart = document.offsetAt(wordRange.start);
3636
const wordEnd = document.offsetAt(wordRange.end);
3737

38-
// Check if this would be a valid expansion
39-
if (isValidExpansion(startIndex, endIndex, wordStart, wordEnd)) {
40-
return {
41-
start: wordStart,
42-
end: wordEnd,
43-
};
38+
const candidate = getCandidate(
39+
startIndex,
40+
endIndex,
41+
wordStart,
42+
wordEnd,
43+
text,
44+
);
45+
if (candidate) {
46+
return candidate;
4447
}
4548
}
4649
}

src/finders/util.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
export function isValidExpansion(
1+
export function getCandidate(
22
startIndex: number,
33
endIndex: number,
44
rangeStart: number,
55
rangeEnd: number,
6-
): boolean {
7-
return (
8-
startIndex >= rangeStart &&
9-
endIndex <= rangeEnd &&
10-
!(startIndex === rangeStart && endIndex === rangeEnd)
11-
);
6+
text: string,
7+
): { start: number; end: number } | null {
8+
const content = text.substring(rangeStart, rangeEnd);
9+
const trimmedContent = content.trim();
10+
if (trimmedContent.length === 0) {
11+
// If only whitespace, use the original range
12+
if (
13+
startIndex >= rangeStart &&
14+
endIndex <= rangeEnd &&
15+
!(startIndex === rangeStart && endIndex === rangeEnd)
16+
) {
17+
return { start: rangeStart, end: rangeEnd };
18+
}
19+
return null;
20+
}
21+
const leadingWhitespace = content.length - content.trimStart().length;
22+
const trailingWhitespace = content.length - content.trimEnd().length;
23+
const trimmedStart = rangeStart + leadingWhitespace;
24+
const trimmedEnd = rangeEnd - trailingWhitespace;
25+
if (
26+
startIndex >= trimmedStart &&
27+
endIndex <= trimmedEnd &&
28+
!(startIndex === trimmedStart && endIndex === trimmedEnd)
29+
) {
30+
return { start: trimmedStart, end: trimmedEnd };
31+
}
32+
return null;
1233
}

src/test/suite/expandSelection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,6 @@ suite('ExpandSelection Test Suite', () => {
306306

307307
assert.ok(result);
308308
const selectedText = text.substring(result.start, result.end);
309-
assert.strictEqual(selectedText, '" hello world "');
309+
assert.strictEqual(selectedText, 'hello world'); // Should prefer content inside quotes
310310
});
311311
});

src/test/suite/finders/line.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ suite('Line Finder Tests', () => {
101101

102102
assert.ok(result);
103103
const selectedText = text.substring(result.start, result.end);
104-
assert.strictEqual(selectedText, 'const value = 123;'); // Trimmed
104+
assert.strictEqual(selectedText, 'const value = 123'); // Without trailing semicolon
105105
});
106106

107107
test('handles multi-line with complex indentation', () => {
@@ -133,7 +133,10 @@ suite('Line Finder Tests', () => {
133133

134134
const result = findLineExpansion(text, 1, 5, mockDoc);
135135

136-
assert.strictEqual(result, null);
136+
// Should return an empty candidate with start/end positions
137+
assert.ok(result);
138+
assert.strictEqual(result.start, 0);
139+
assert.strictEqual(result.end, 7);
137140
});
138141

139142
test('content start and end equal start and end', () => {

0 commit comments

Comments
 (0)