Skip to content

Commit 1c34200

Browse files
authored
Merge pull request #3076 from jramosg/fix-selection
Toggle Comment: Full commented form including every ;; gets selected * Fixes #3066
2 parents 231db43 + 7bf57ef commit 1c34200

File tree

2 files changed

+111
-27
lines changed

2 files changed

+111
-27
lines changed

src/edit.ts

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,21 +85,40 @@ async function applyStructuralCommentsToSingleSelectionLines(
8585
affectedLineNumbers: number[]
8686
) {
8787
const originalSelections = [...editor.selections];
88+
const singleSelection = editor.selections[0];
8889
const descendingLineNumbers = [...new Set(affectedLineNumbers)].sort((a, b) => b - a);
8990
const affectedLineSet = new Set(affectedLineNumbers);
9091
const originalFirstNonWSMap = new Map<number, number>();
92+
const originalInsertionColumnMap = new Map<number, number>();
93+
let partialSelectionStartOffset: number | undefined;
9194
let alignedCommentColumn: number | undefined;
9295

9396
// Calculate aligned comment column and store original indentation.
9497
for (const lineNum of affectedLineNumbers) {
9598
const line = editor.document.lineAt(lineNum);
9699
const firstNonWhitespace = line.firstNonWhitespaceCharacterIndex;
97100
originalFirstNonWSMap.set(lineNum, firstNonWhitespace);
101+
102+
let insertionColumnCandidate = firstNonWhitespace;
103+
if (
104+
lineNum === singleSelection.start.line &&
105+
!singleSelection.isEmpty &&
106+
singleSelection.start.character > firstNonWhitespace
107+
) {
108+
insertionColumnCandidate = singleSelection.start.character;
109+
if (affectedLineNumbers.length > 1) {
110+
partialSelectionStartOffset = editor.document.offsetAt(
111+
new vscode.Position(lineNum, singleSelection.start.character)
112+
);
113+
}
114+
}
115+
originalInsertionColumnMap.set(lineNum, insertionColumnCandidate);
116+
98117
if (!line.isEmptyOrWhitespace) {
99118
alignedCommentColumn =
100119
alignedCommentColumn === undefined
101-
? firstNonWhitespace
102-
: Math.min(alignedCommentColumn, firstNonWhitespace);
120+
? insertionColumnCandidate
121+
: Math.min(alignedCommentColumn, insertionColumnCandidate);
103122
}
104123
}
105124

@@ -114,10 +133,14 @@ async function applyStructuralCommentsToSingleSelectionLines(
114133
for (const lineNum of descendingLineNumbers) {
115134
const currentLine = editor.document.lineAt(lineNum);
116135
const firstNonWhitespace = originalFirstNonWSMap.get(lineNum) ?? 0;
117-
const insertionColumn =
118-
affectedLineNumbers.length > 1 ? resolvedAlignedCommentColumn : firstNonWhitespace;
136+
const rawInsertionColumn =
137+
affectedLineNumbers.length > 1
138+
? resolvedAlignedCommentColumn
139+
: originalInsertionColumnMap.get(lineNum) ?? firstNonWhitespace;
140+
const insertionColumn = Math.min(rawInsertionColumn, currentLine.text.length);
141+
originalInsertionColumnMap.set(lineNum, insertionColumn);
119142
const insertionOffset = editor.document.offsetAt(
120-
new vscode.Position(lineNum, firstNonWhitespace)
143+
new vscode.Position(lineNum, insertionColumn)
121144
);
122145

123146
const wouldBreakWhere = _semiColonWouldBreakStructureWhere(mirrorDoc, insertionOffset);
@@ -132,7 +155,8 @@ async function applyStructuralCommentsToSingleSelectionLines(
132155
const resolvedBreakOffset = resolveStructuralBreakOffset(
133156
mirrorDoc,
134157
wouldBreakWhere,
135-
affectedLineSet
158+
affectedLineSet,
159+
partialSelectionStartOffset
136160
);
137161
if (resolvedBreakOffset === false) {
138162
skipBreak = true;
@@ -182,7 +206,23 @@ async function applyStructuralCommentsToSingleSelectionLines(
182206
return inserted;
183207
}
184208

185-
function adjustPosition(pos: vscode.Position): vscode.Position {
209+
function adjustPosition(
210+
pos: vscode.Position,
211+
boundary: 'start' | 'end',
212+
selectionIsEmpty: boolean
213+
): vscode.Position {
214+
const isSelectionStartAtInsertionColumn = (
215+
insertionColumn: number | undefined,
216+
position: vscode.Position
217+
) => {
218+
return (
219+
!selectionIsEmpty &&
220+
boundary === 'start' &&
221+
insertionColumn !== undefined &&
222+
position.character === insertionColumn
223+
);
224+
};
225+
186226
const shiftedLine = pos.line + countInsertedLinesBefore(pos.line);
187227

188228
if (shiftedLine >= editor.document.lineCount) {
@@ -192,21 +232,34 @@ async function applyStructuralCommentsToSingleSelectionLines(
192232
const line = editor.document.lineAt(shiftedLine);
193233
const newFirstNonWS = line.firstNonWhitespaceCharacterIndex;
194234
const lineContent = line.text.slice(newFirstNonWS);
235+
const insertionColumn = originalInsertionColumnMap.get(pos.line);
195236

196-
if (!lineContent.startsWith(';; ')) {
197-
return new vscode.Position(shiftedLine, Math.min(pos.character, line.text.length));
237+
if (isSelectionStartAtInsertionColumn(insertionColumn, pos)) {
238+
return new vscode.Position(shiftedLine, insertionColumn);
198239
}
199240

200-
const origFirstNonWS = originalFirstNonWSMap.get(pos.line) ?? pos.character;
201-
const contentOffset = Math.max(0, pos.character - origFirstNonWS);
202-
const newCol = Math.min(newFirstNonWS + 3 + contentOffset, line.text.length);
203-
return new vscode.Position(shiftedLine, newCol);
241+
if (lineContent.startsWith(';; ')) {
242+
const origFirstNonWS = originalFirstNonWSMap.get(pos.line) ?? pos.character;
243+
const baseColumnForOffset = insertionColumn ?? origFirstNonWS;
244+
const contentOffset = Math.max(0, pos.character - baseColumnForOffset);
245+
const newCol = Math.min(newFirstNonWS + 3 + contentOffset, line.text.length);
246+
return new vscode.Position(shiftedLine, newCol);
247+
}
248+
249+
if (insertionColumn !== undefined && pos.character >= insertionColumn) {
250+
return new vscode.Position(shiftedLine, Math.min(pos.character + 3, line.text.length));
251+
}
252+
253+
return new vscode.Position(shiftedLine, Math.min(pos.character, line.text.length));
204254
}
205255

206256
editor.selections = originalSelections.map((selection) => {
207-
const newAnchor = adjustPosition(selection.anchor);
208-
const newActive = adjustPosition(selection.active);
209-
return new vscode.Selection(newAnchor, newActive);
257+
const newStart = adjustPosition(selection.start, 'start', selection.isEmpty);
258+
const newEnd = adjustPosition(selection.end, 'end', selection.isEmpty);
259+
const isReversed = selection.anchor.isAfter(selection.active);
260+
return isReversed
261+
? new vscode.Selection(newEnd, newStart)
262+
: new vscode.Selection(newStart, newEnd);
210263
});
211264
}
212265

@@ -221,7 +274,8 @@ async function applyStructuralCommentsToSingleSelectionLines(
221274
function resolveStructuralBreakOffset(
222275
mirrorDoc: EditableDocument,
223276
wouldBreakWhere: number,
224-
affectedLineSet: Set<number>
277+
affectedLineSet: Set<number>,
278+
partialSelectionStartOffset?: number
225279
): number | false {
226280
const cursor = mirrorDoc.getTokenCursor(wouldBreakWhere);
227281
const token = cursor.getToken();
@@ -233,10 +287,12 @@ function resolveStructuralBreakOffset(
233287
const tok = probe.getToken();
234288
if (tok.type === 'close') {
235289
const finder = probe.clone();
236-
if (!finder.backwardList()) {
237-
return probe.offsetStart;
238-
}
239-
if (!affectedLineSet.has(finder.line)) {
290+
if (
291+
!finder.backwardList() ||
292+
(partialSelectionStartOffset !== undefined &&
293+
finder.offsetStart < partialSelectionStartOffset) ||
294+
!affectedLineSet.has(finder.line)
295+
) {
240296
return probe.offsetStart;
241297
}
242298
}

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,35 +215,35 @@ suite(suiteName, () => {
215215
it('should comment a complete multi-line top-level form without structural breaks and preserve selection', async () => {
216216
assert.equal(
217217
await toggleCommentUsingActiveEditor('|(defn foo []• (prn "hi"))|'),
218-
';; |(defn foo []•;; (prn "hi"|))'
218+
'|;; (defn foo []•;; (prn "hi"))|'
219219
);
220220
});
221221

222222
it('should comment selected lines inside a containing form, preserving outside closers and selection', async () => {
223223
assert.equal(
224224
await toggleCommentUsingActiveEditor('(do• |(prn "a")• (prn "b")|)'),
225-
'(do• ;; |(prn "a")• ;; (prn "b")|• )'
225+
'(do• |;; (prn "a")• ;; (prn "b")|• )'
226226
);
227227
});
228228

229229
it('should comment multi-line nested forms when all lines selected and preserve selection', async () => {
230230
assert.equal(
231231
await toggleCommentUsingActiveEditor('|(do• (prn "a")• (prn "b"))|'),
232-
';; |(do•;; (prn "a")•;; (prn "b"|))'
232+
'|;; (do•;; (prn "a")•;; (prn "b"))|'
233233
);
234234
});
235235

236236
it('should comment complete multi-line let binding without displacing bracket and preserve selection', async () => {
237237
assert.equal(
238238
await toggleCommentUsingActiveEditor('|(let [a 1• b 2])|'),
239-
';; |(let [a 1•;; | b 2])'
239+
'|;; (let [a 1•;; b 2])|'
240240
);
241241
});
242242

243243
it('should structurally comment two selected lines and preserve closing delimiter and selection', async () => {
244244
assert.equal(
245245
await toggleCommentUsingActiveEditor('(assoc {}• |:a• :b|)'),
246-
'(assoc {}• ;; |:a• ;; :b|• )'
246+
'(assoc {}• |;; :a• ;; :b|• )'
247247
);
248248
});
249249

@@ -252,7 +252,35 @@ suite(suiteName, () => {
252252
await toggleCommentUsingActiveEditor(
253253
'(ns main.server• #_(:require [babashka.fs :as fs])• (:gen-class))••(defn -main• "I don\'t do a whole lot ... yet."• [& _args]• (println "Hello, World!"))••(comment• (-main)• (System/getProperty "user.dir")• (rand-int 100)• (with-open [r (java.io.FileInputStream. "/dev/urandom")]• |(mod (->> #(.read r)• repeatedly• (filter #(not (>= % 200)))• (take 1)• doall• first)• 100)|)• :rcf)'
254254
),
255-
'(ns main.server• #_(:require [babashka.fs :as fs])• (:gen-class))••(defn -main• "I don\'t do a whole lot ... yet."• [& _args]• (println "Hello, World!"))••(comment• (-main)• (System/getProperty "user.dir")• (rand-int 100)• (with-open [r (java.io.FileInputStream. "/dev/urandom")]• ;; |(mod (->> #(.read r)• ;; repeatedly• ;; (filter #(not (>= % 200)))• ;; (take 1)• ;; doall• ;; first)• ;; | 100)• )• :rcf)'
255+
'(ns main.server• #_(:require [babashka.fs :as fs])• (:gen-class))••(defn -main• "I don\'t do a whole lot ... yet."• [& _args]• (println "Hello, World!"))••(comment• (-main)• (System/getProperty "user.dir")• (rand-int 100)• (with-open [r (java.io.FileInputStream. "/dev/urandom")]• |;; (mod (->> #(.read r)• ;; repeatedly• ;; (filter #(not (>= % 200)))• ;; (take 1)• ;; doall• ;; first)• ;; 100)|• )• :rcf)'
256+
);
257+
});
258+
259+
it('should structurally comment multiline partial selection and keep full selection over commented text', async () => {
260+
assert.equal(
261+
await toggleCommentUsingActiveEditor('(a |(b c• d)|• e)'),
262+
'(a |;; (b c• ;; d)|• e)'
263+
);
264+
});
265+
266+
it('should structurally comment multiline selection nested in parent form and preserve full selected range', async () => {
267+
assert.equal(
268+
await toggleCommentUsingActiveEditor('(x• (y |(a b• c)|)• z)'),
269+
'(x• (y |;; (a b• ;; c)|• )• z)'
270+
);
271+
});
272+
273+
it('should structurally comment multiline selection nested in j/y forms and keep full selected range', async () => {
274+
assert.equal(
275+
await toggleCommentUsingActiveEditor('(x• (j |(y • (a b c))|)• z)'),
276+
'(x• (j |;; (y • ;; (a b c))|• )• z)'
277+
);
278+
});
279+
280+
it('should insert structural comment at selection start for single-line nested selection', async () => {
281+
assert.equal(
282+
await toggleCommentUsingActiveEditor('(x• (j (y |(a b c)|))• z)'),
283+
'(x• (j (y |;; (a b c)|• ))• z)'
256284
);
257285
});
258286
});

0 commit comments

Comments
 (0)