Skip to content

Commit 5681c78

Browse files
authored
feat: toggle SAS code comments by line (#1521)
* feat: ctrl+/ changes single-line comments to apply /*【code】*/ to each line in the selection, ctrl+shift+/ can cancel it. The ori ginal block comment mode alt+shift+a is retained.(#917) Signed-off-by: zby <[email protected]> * fix: Add the corresponding ctrl+/ key binding for Mac, remove unnecessary translations, and optimize. Signed-off-by: zby <[email protected]> * fix: Change the name of the file "toggleLineComment" and modify the description text of the toggleLineComment command. Signed-off-by: zby <[email protected]> --------- Signed-off-by: zby <[email protected]>
1 parent e7ecac8 commit 5681c78

File tree

5 files changed

+155
-1
lines changed

5 files changed

+155
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ node_modules
55
*.vsix
66
*.node
77
*.qps-ploc.json
8-
l10n/bundle.l10n.json
8+
l10n/bundle.l10n.json
9+
.history
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { TextEditor, commands } from "vscode";
4+
5+
/**
6+
* Register a command to toggle SAS block comment (/* ... *\/)
7+
* If the line is already commented, remove the comment
8+
* If the line is not commented, add comment
9+
*/
10+
export function registerToggleLineCommentCommand() {
11+
return commands.registerTextEditorCommand(
12+
"SAS.toggleLineComment",
13+
async (editor) => {
14+
const { selections, document } = editor;
15+
16+
// Get the line range of all selected areas
17+
const linesRange = selections.map((selection) => {
18+
const startLine = selection.start.line;
19+
const endLine = selection.end.line;
20+
// Ensure endLine doesn't exceed actual line count when selecting the whole line by clicking the line number
21+
if (selection.end.character === 0 && endLine > startLine) {
22+
return { startLine, endLine: endLine - 1 };
23+
}
24+
return { startLine, endLine };
25+
});
26+
27+
await editor.edit((editBuilder) => {
28+
for (const range of linesRange) {
29+
// Analyze comment status and minimum indentation for each selected area
30+
const { shouldUncomment, minIndentColumn } = analyzeLines(
31+
editor,
32+
range,
33+
);
34+
for (
35+
let lineIndex = range.startLine;
36+
lineIndex <= range.endLine;
37+
lineIndex++
38+
) {
39+
const line = document.lineAt(lineIndex);
40+
const lineText = line.text;
41+
const lineRange = line.range;
42+
if (shouldUncomment) {
43+
// Only process commented lines
44+
const matches = lineText.match(
45+
/^([ \t]*)\/\*\s?(.*?)\s?\*\/\s*$/,
46+
);
47+
if (matches) {
48+
const indent = matches[1] || "";
49+
const content = matches[2] || "";
50+
editBuilder.replace(lineRange, `${indent}${content}`);
51+
}
52+
} else {
53+
// Skip blank lines in multi-line selection
54+
if (
55+
isEmptyLine(lineText) &&
56+
!(range.startLine === range.endLine)
57+
) {
58+
continue;
59+
}
60+
// Add comment
61+
const beforeComment = lineText.substring(0, minIndentColumn);
62+
const afterComment = lineText.substring(minIndentColumn);
63+
editBuilder.replace(
64+
lineRange,
65+
`${beforeComment}/* ${afterComment} */`,
66+
);
67+
}
68+
}
69+
}
70+
});
71+
},
72+
);
73+
}
74+
75+
// Utility functions
76+
function isEmptyLine(lineText: string): boolean {
77+
return /^\s*$/.test(lineText);
78+
}
79+
function isCommentedLine(lineText: string): boolean {
80+
return /^\s*\/\*.*\*\/\s*$/.test(lineText);
81+
}
82+
83+
/**
84+
* Independently analyze the comment status and minimum indentation of each selected area,
85+
* used to determine whether to add or remove comments
86+
*
87+
* @param editor Current TextEditor instance, used to access document content
88+
* @param range Line range of selected area, including startLine and endLine
89+
* @returns {Object}
90+
* @returns {boolean} shouldUncomment - Whether to perform uncomment operation
91+
* @returns {number} minIndentColumn - Minimum indentation column in selected lines (for comment alignment)
92+
*
93+
* Function behavior:
94+
* 1. For single line selection, if it's a blank line (including pure indentation), add comment with indentation equal to line length
95+
* 2. For multi-line selection, iterate through all selected lines, check comment status and indentation of each line:
96+
* If any uncommented non-empty line is found, consider adding comments
97+
* Calculate the minimum indentation (default 0) of all non-empty lines for comment alignment
98+
*/
99+
function analyzeLines(
100+
editor: TextEditor,
101+
range: { startLine: number; endLine: number },
102+
) {
103+
const { document } = editor;
104+
// Special handling for single blank line
105+
if (range.startLine === range.endLine) {
106+
const lineIndex = range.startLine;
107+
const lineText = document.lineAt(lineIndex).text;
108+
if (isEmptyLine(lineText)) {
109+
return {
110+
shouldUncomment: false,
111+
minIndentColumn: lineText.length,
112+
};
113+
}
114+
}
115+
116+
let shouldUncomment = true;
117+
let minIndentColumn = Number.MAX_VALUE;
118+
for (
119+
let lineIndex = range.startLine;
120+
lineIndex <= range.endLine;
121+
lineIndex++
122+
) {
123+
const lineText = document.lineAt(lineIndex).text;
124+
if (!isCommentedLine(lineText) && !isEmptyLine(lineText)) {
125+
shouldUncomment = false;
126+
}
127+
if (!isEmptyLine(lineText)) {
128+
const indent =
129+
document.lineAt(lineIndex).firstNonWhitespaceCharacterIndex;
130+
if (indent < minIndentColumn) {
131+
minIndentColumn = indent;
132+
}
133+
}
134+
}
135+
if (minIndentColumn === Number.MAX_VALUE) {
136+
minIndentColumn = 0;
137+
}
138+
return { shouldUncomment, minIndentColumn };
139+
}

client/src/node/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
updateProfile,
3333
} from "../commands/profile";
3434
import { run, runRegion, runSelected } from "../commands/run";
35+
import { registerToggleLineCommentCommand } from "../commands/toggleLineComment";
3536
import { getRestAPIs } from "../components/APIProvider";
3637
import { SASAuthProvider } from "../components/AuthProvider";
3738
import { installCAs } from "../components/CAHelper";
@@ -203,6 +204,7 @@ export function activate(context: ExtensionContext) {
203204
),
204205
tasks.registerTaskProvider(SAS_TASK_TYPE, new SasTaskProvider()),
205206
...sasDiagnostic.getSubscriptions(),
207+
registerToggleLineCommentCommand(),
206208
);
207209

208210
// Reset first to set "No Active Profiles"

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,11 @@
780780
"command": "SAS.saveHTML",
781781
"title": "%commands.SAS.download%",
782782
"category": "SAS"
783+
},
784+
{
785+
"command": "SAS.toggleLineComment",
786+
"title": "%commands.SAS.toggleLineComment%",
787+
"category": "SAS"
783788
}
784789
],
785790
"keybindings": [
@@ -792,6 +797,12 @@
792797
"when": "editorLangId == sas && !SAS.hideRunMenuItem",
793798
"command": "SAS.runSelected",
794799
"key": "f3"
800+
},
801+
{
802+
"key": "ctrl+/",
803+
"mac": "cmd+/",
804+
"command": "SAS.toggleLineComment",
805+
"when": "editorTextFocus && editorLangId == 'sas'"
795806
}
796807
],
797808
"menus": {

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"commands.SAS.upload": "Upload",
3030
"commands.SAS.uploadFiles": "Upload Files",
3131
"commands.SAS.uploadFolders": "Upload Folders",
32+
"commands.SAS.toggleLineComment": "Toggle SAS Code Comments by Line",
3233
"configuration.SAS.connectionProfiles": "Define the connection profiles to connect to SAS servers. If you define more than one profile, you can switch between them.",
3334
"configuration.SAS.connectionProfiles.activeProfile": "Active SAS Connection Profile",
3435
"configuration.SAS.connectionProfiles.profiles": "SAS Connection Profiles",

0 commit comments

Comments
 (0)