|
| 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 | +} |
0 commit comments