Skip to content

Commit 3299599

Browse files
authored
fix: toggle line comment in PROC PYTHON (#1571)
1 parent 2402cf7 commit 3299599

File tree

5 files changed

+83
-133
lines changed

5 files changed

+83
-133
lines changed
Lines changed: 41 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,52 @@
11
// Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import { TextEditor, commands } from "vscode";
3+
import { Selection, TextEditor, commands } from "vscode";
4+
import type { BaseLanguageClient } from "vscode-languageclient";
45

56
/**
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
7+
* Register a command to toggle SAS block comment by line
98
*/
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(
9+
export async function toggleLineComment(
10010
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-
}
11+
client: BaseLanguageClient,
12+
): Promise<void> {
13+
const { selections, document } = editor;
14+
if (selections.length === 1) {
15+
// We have to depend on VS Code native command for embedded (e.g. Python, Lua) code
16+
// VS Code native command works on multiple selections together
17+
// so we're not able to only change some of selections
18+
// We only do for single selection for now
11519

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;
20+
const selection = selections[0];
21+
const endLine =
22+
// should not include the last line if it just selected the last return character
23+
selection.end.line > selection.start.line && selection.end.character === 0
24+
? selection.end.line - 1
25+
: selection.end.line;
26+
const fullSelection = new Selection(
27+
selection.start.line,
28+
0,
29+
endLine,
30+
document.lineAt(endLine).range.end.character,
31+
);
32+
33+
if (!fullSelection.isSingleLine) {
34+
const result = await client.sendRequest<string | null>(
35+
"sas/toggleLineComment",
36+
{
37+
textDocument:
38+
client.code2ProtocolConverter.asTextDocumentIdentifier(document),
39+
range: client.code2ProtocolConverter.asRange(fullSelection),
40+
},
41+
);
42+
if (result) {
43+
editor.selection = fullSelection;
44+
await editor.edit((editBuilder) => {
45+
editBuilder.replace(fullSelection, result);
46+
});
47+
return;
13248
}
13349
}
13450
}
135-
if (minIndentColumn === Number.MAX_VALUE) {
136-
minIndentColumn = 0;
137-
}
138-
return { shouldUncomment, minIndentColumn };
51+
commands.executeCommand("editor.action.commentLine");
13952
}

client/src/node/extension.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
updateProfile,
3333
} from "../commands/profile";
3434
import { run, runRegion, runSelected } from "../commands/run";
35-
import { registerToggleLineCommentCommand } from "../commands/toggleLineComment";
35+
import { toggleLineComment } from "../commands/toggleLineComment";
3636
import { getRestAPIs } from "../components/APIProvider";
3737
import { SASAuthProvider } from "../components/AuthProvider";
3838
import { installCAs } from "../components/CAHelper";
@@ -204,7 +204,9 @@ export function activate(context: ExtensionContext) {
204204
),
205205
tasks.registerTaskProvider(SAS_TASK_TYPE, new SasTaskProvider()),
206206
...sasDiagnostic.getSubscriptions(),
207-
registerToggleLineCommentCommand(),
207+
commands.registerTextEditorCommand("SAS.toggleLineComment", (editor) => {
208+
toggleLineComment(editor, client);
209+
}),
208210
);
209211

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

server/src/sas/LanguageServiceProvider.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
FoldingRange,
66
SymbolKind,
77
} from "vscode-languageserver";
8-
import { Range, TextDocument } from "vscode-languageserver-textdocument";
8+
import type { Range, TextDocument } from "vscode-languageserver-textdocument";
99

1010
import { CodeZoneManager } from "./CodeZoneManager";
1111
import { CompletionProvider } from "./CompletionProvider";
@@ -244,6 +244,36 @@ export class LanguageServiceProvider {
244244
);
245245
}
246246

247+
toggleLineComment(range: Range) {
248+
const token = this.syntaxProvider.getSyntax(range.start.line)[0];
249+
if (token.style === "embedded-code") {
250+
return null;
251+
}
252+
const lines = this.model
253+
.getText({
254+
start: {
255+
line: range.start.line,
256+
column: 0,
257+
},
258+
end: {
259+
line: range.end.line,
260+
column: range.end.character,
261+
},
262+
})
263+
.split(/\n|\r\n/);
264+
const shouldAdd = lines.some(
265+
(line) => line.trim() !== "" && !/^\s*\/\*.*\*\/\s*$/.test(line),
266+
);
267+
if (shouldAdd) {
268+
return lines
269+
.map((line) => (line.trim() !== "" ? `/* ${line} */` : line))
270+
.join("\n");
271+
}
272+
return lines
273+
.map((line) => line.replace(/^(\s*)\/\* ?| ?\*\/(\s*)$/g, "$1"))
274+
.join("\n");
275+
}
276+
247277
setLibService(fn: LibService): void {
248278
return this.syntaxProvider.lexer.syntaxDb.setLibService(fn);
249279
}

server/src/server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@ export const runServer = (
294294
}
295295
});
296296

297+
connection.onRequest("sas/toggleLineComment", (params) => {
298+
const languageService = getLanguageService(params.textDocument.uri);
299+
return languageService.toggleLineComment(params.range);
300+
});
301+
297302
connection.onDocumentOnTypeFormatting(async (params) => {
298303
return await dispatch(params, {
299304
async sas(languageService) {

syntaxes/sas.tmLanguage.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"include": "#strings-or-comments"
3636
},
3737
{
38-
"begin": "(?i)(?<=submit|interactive|i)(\\s|/\\*.*?\\*/)*;",
38+
"begin": "(?i)(?<=\\bsubmit|\\binteractive|\\bi)(\\s|/\\*.*?\\*/)*;",
3939
"end": "(?i)(endsubmit|endinteractive)(\\s|/\\*.*?\\*/)*;",
4040
"name": "source.lua",
4141
"beginCaptures": {
@@ -60,7 +60,7 @@
6060
"include": "#strings-or-comments"
6161
},
6262
{
63-
"begin": "(?i)(?<=submit|interactive|i)(\\s|/\\*.*?\\*/)*;",
63+
"begin": "(?i)(?<=\\bsubmit|\\binteractive|\\bi)(\\s|/\\*.*?\\*/)*;",
6464
"end": "(?i)(endsubmit|endinteractive)(\\s|/\\*.*?\\*/)*;",
6565
"name": "source.python",
6666
"beginCaptures": {

0 commit comments

Comments
 (0)