Skip to content

Commit 516585c

Browse files
authored
Merge pull request #3085 from jramosg/uncomment-with-not-first-column
Uncomment with not first column Fixes #3081
2 parents 19b4258 + a9ab8df commit 516585c

File tree

4 files changed

+100
-20
lines changed

4 files changed

+100
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changes to Calva.
55
## [Unreleased]
66

77
- Fix: [Toggle comments off, indent glitch](https://github.com/BetterThanTomorrow/calva/issues/3078)
8+
- Fix: [Toggle comments off fails when first line doesn't start with ;;](https://github.com/BetterThanTomorrow/calva/issues/3081)
89
- Fix: [Toggle Line Comment can break structure for unformatted partial multiline selections](https://github.com/BetterThanTomorrow/calva/issues/3083)
910
- Fix: [Clojure-lsp not starting when offline](https://github.com/BetterThanTomorrow/calva/issues/1299)
1011

src/comment-prefix.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
/** Matches one or more leading semicolons (`;`, `;;`, `;;;`, etc.) */
2-
export const commentPrefixPattern = /^;+/;
2+
const commentPrefixPattern = /^;+/;
3+
4+
/**
5+
* Returns the first position from `candidatePositions` where a comment prefix
6+
* (one or more semicolons) starts, or `undefined` if none match.
7+
*/
8+
export function findCommentPrefixStart(
9+
lineText: string,
10+
candidatePositions: number[]
11+
): number | undefined {
12+
for (const pos of candidatePositions) {
13+
if (commentPrefixPattern.test(lineText.slice(pos))) {
14+
return pos;
15+
}
16+
}
17+
return undefined;
18+
}
319

420
/**
521
* Calculates the end index for removing a comment prefix (semicolons + trailing space)
@@ -8,20 +24,20 @@ export const commentPrefixPattern = /^;+/;
824
* if the line is otherwise empty after the prefix).
925
*
1026
* @param lineText - The full text of the line
11-
* @param firstNonWhitespace - The index of the first non-whitespace character in the line
27+
* @param removalStart - The index at which the comment prefix starts
1228
* @returns The end index for removal, or `undefined` if no comment prefix is found
1329
*/
1430
export function calculateCommentPrefixRemovalEnd(
1531
lineText: string,
16-
firstNonWhitespace: number
32+
removalStart: number
1733
): number | undefined {
18-
const remainder = lineText.slice(firstNonWhitespace);
34+
const remainder = lineText.slice(removalStart);
1935
const match = remainder.match(commentPrefixPattern);
2036
if (!match) {
2137
return undefined;
2238
}
2339

24-
let removalEnd = firstNonWhitespace + match[0].length;
40+
let removalEnd = removalStart + match[0].length;
2541
if (removalEnd < lineText.length && lineText[removalEnd] === ' ') {
2642
let spaceRunEnd = removalEnd;
2743
// If the line is otherwise empty after the comment prefix, remove all trailing spaces as well

src/edit.ts

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import * as select from './select';
66
import * as printer from './printer';
77
import { _semiColonWouldBreakStructureWhere } from './cursor-doc/paredit';
88
import * as format from './calva-fmt/src/format';
9-
import { calculateCommentPrefixRemovalEnd, commentPrefixPattern } from './comment-prefix';
9+
import { calculateCommentPrefixRemovalEnd, findCommentPrefixStart } from './comment-prefix';
1010

11-
export { commentPrefixPattern } from './comment-prefix';
11+
type CandidatesMap = Map<number, number[]>;
1212

1313
// Relies on that `when` claus guards this from being called
1414
// when the cursor is before the comment marker
@@ -58,19 +58,39 @@ function getAffectedLineNumbers(editor: vscode.TextEditor, lineCount: number): n
5858
.sort((a, b) => a - b);
5959
}
6060

61+
/**
62+
* Builds candidate positions where a comment prefix might start on a line:
63+
* first non-whitespace, then any selection start columns past that point.
64+
*/
65+
function commentCandidatePositions(
66+
firstNonWhitespace: number,
67+
lineNum: number,
68+
selections?: readonly vscode.Selection[]
69+
): number[] {
70+
const positions = [firstNonWhitespace];
71+
if (selections) {
72+
for (const sel of selections) {
73+
if (sel.start.line === lineNum && sel.start.character > firstNonWhitespace) {
74+
positions.push(sel.start.character);
75+
}
76+
}
77+
}
78+
return positions;
79+
}
80+
6181
function areAllNonEmptyTargetLinesCommented(
6282
document: vscode.TextDocument,
63-
lineNumbers: number[]
83+
lineNumbers: number[],
84+
candidatesMap: CandidatesMap
6485
): boolean {
6586
const nonEmptyLines = lineNumbers.filter(
6687
(lineNum) => !document.lineAt(lineNum).isEmptyOrWhitespace
6788
);
6889
return (
6990
nonEmptyLines.length > 0 &&
7091
nonEmptyLines.every((lineNum) => {
71-
const line = document.lineAt(lineNum);
72-
const lineText = line.text.slice(line.firstNonWhitespaceCharacterIndex);
73-
return commentPrefixPattern.test(lineText);
92+
const candidates = candidatesMap.get(lineNum) ?? [];
93+
return findCommentPrefixStart(document.lineAt(lineNum).text, candidates) !== undefined;
7494
})
7595
);
7696
}
@@ -401,31 +421,38 @@ async function reformatEnclosingFormsForLines(
401421
async function updateLineComments(
402422
editor: vscode.TextEditor,
403423
affectedLineNumbers: number[],
404-
shouldUncomment: boolean
424+
shouldUncomment: boolean,
425+
candidatesMap: CandidatesMap
405426
) {
406427
const descendingLineNumbers = affectedLineNumbers.sort((a, b) => b - a);
407428

408429
await editor.edit(
409430
(editBuilder) => {
410431
for (const lineNum of descendingLineNumbers) {
411432
const line = editor.document.lineAt(lineNum);
412-
const firstNonWhitespace = line.firstNonWhitespaceCharacterIndex;
413433
const lineText = line.text;
414434

415435
if (shouldUncomment) {
416-
const removalEnd = calculateCommentPrefixRemovalEnd(lineText, firstNonWhitespace);
436+
const removalStart = findCommentPrefixStart(lineText, candidatesMap.get(lineNum) ?? []);
437+
if (removalStart === undefined) {
438+
continue;
439+
}
440+
const removalEnd = calculateCommentPrefixRemovalEnd(lineText, removalStart);
417441
if (removalEnd === undefined) {
418442
continue;
419443
}
420444

421445
editBuilder.delete(
422446
new vscode.Range(
423-
new vscode.Position(lineNum, firstNonWhitespace),
447+
new vscode.Position(lineNum, removalStart),
424448
new vscode.Position(lineNum, removalEnd)
425449
)
426450
);
427451
} else {
428-
editBuilder.insert(new vscode.Position(lineNum, firstNonWhitespace), ';; ');
452+
editBuilder.insert(
453+
new vscode.Position(lineNum, line.firstNonWhitespaceCharacterIndex),
454+
';; '
455+
);
429456
}
430457
}
431458
},
@@ -444,9 +471,10 @@ async function updateLineComments(
444471
async function toggleCommentsThenReformatEnclosingForms(
445472
editor: vscode.TextEditor,
446473
affectedLineNumbers: number[],
447-
shouldUncomment: boolean
474+
shouldUncomment: boolean,
475+
candidatesMap: CandidatesMap
448476
) {
449-
await updateLineComments(editor, affectedLineNumbers, shouldUncomment);
477+
await updateLineComments(editor, affectedLineNumbers, shouldUncomment, candidatesMap);
450478
await reformatEnclosingFormsForLines(editor, affectedLineNumbers);
451479
}
452480

@@ -475,9 +503,19 @@ export async function toggleLineCommentCommand() {
475503
return;
476504
}
477505

506+
const candidatesMap = new Map<number, number[]>();
507+
for (const lineNum of affectedLineNumbers) {
508+
const line = document.lineAt(lineNum);
509+
candidatesMap.set(
510+
lineNum,
511+
commentCandidatePositions(line.firstNonWhitespaceCharacterIndex, lineNum, editor.selections)
512+
);
513+
}
514+
478515
const allNonEmptyLinesCommented = areAllNonEmptyTargetLinesCommented(
479516
document,
480-
affectedLineNumbers
517+
affectedLineNumbers,
518+
candidatesMap
481519
);
482520

483521
const isSingleSelection = editor.selections.length === 1;
@@ -489,7 +527,8 @@ export async function toggleLineCommentCommand() {
489527
await toggleCommentsThenReformatEnclosingForms(
490528
editor,
491529
affectedLineNumbers,
492-
allNonEmptyLinesCommented
530+
allNonEmptyLinesCommented,
531+
candidatesMap
493532
);
494533
}
495534

src/extension-test/integration/suite/toggle-comment-test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,28 @@ suite(suiteName, () => {
315315
'(x• (j (y |;; (a b c)|• ))• z)'
316316
);
317317
});
318+
319+
it('should uncomment partial-selection comments round-trip (issue #3081)', async () => {
320+
const editor = vscode.window.activeTextEditor;
321+
322+
// Simple partial selection
323+
const simple = '(a |(b c• d)|• e)';
324+
await performToggle(editor, simple);
325+
const simpleCommented = textNotationFromDocAndSelections(editor.document, editor.selections);
326+
assert.equal(simpleCommented, '(a |;; (b c• ;; d)|• e)');
327+
await vscode.commands.executeCommand('calva.toggleLineComment');
328+
await new Promise((resolve) => setTimeout(resolve, pauseMs));
329+
const simpleUncommented = textNotationFromDocAndSelections(editor.document, editor.selections);
330+
assert.equal(simpleUncommented, simple);
331+
332+
// Nested partial selection
333+
const nested = '(a |(b c• (d e• f)• g)|• e)';
334+
await performToggle(editor, nested);
335+
const nestedCommented = textNotationFromDocAndSelections(editor.document, editor.selections);
336+
assert.equal(nestedCommented, '(a |;; (b c• ;; (d e• ;; f)• ;; g)|• e)');
337+
await vscode.commands.executeCommand('calva.toggleLineComment');
338+
await new Promise((resolve) => setTimeout(resolve, pauseMs));
339+
const nestedUncommented = textNotationFromDocAndSelections(editor.document, editor.selections);
340+
assert.equal(nestedUncommented, nested);
341+
});
318342
});

0 commit comments

Comments
 (0)