As useful as the existing keyboard shortcuts are, a single Comment/Uncomment toggle (as seen in Nova and Visual Studio Code) would be more efficient. Sometimes I find myself accidentally double-commenting code because I forgot the shift key.
The only time there’s any ambiguity is when you apply the command to code blocks which include comments, but If SD operates on the entire block and not just one line at a time, you could decide between Comment and Uncomment based on whether all of the lines are comments or only some of them are — and bonus points if you don’t count white space as uncommented.
But I wouldn’t want to force people to retrain their muscle memory, and maybe someone out there is used to commenting out their comments one line at a time. Who am I to judge? You could let us swap Comment for a Toggle Comment command in Key Bindings, or give us a “Comment toggle mode” setting.
I also wanted a keyboard shortcut to toggle the “comment-ness” of selected lines. I wrote an AppleScript to do so. I assigned the keyboard shortcut command / to the script using FastScripts, but that could also be done in Script Debugger. The script is:
use framework "Foundation"
property NSRegularExpressionSearch : a reference to current application's NSRegularExpressionSearch
property NSString : a reference to current application's NSString
property NSThread : a reference to current application's NSThread
tell application "Script Debugger"
tell application "System Events" to keystroke "s" using {option down, command down}
NSThread's sleepForTimeInterval:0.1
set selectedLine to NSString's stringWithString:((document 1's selection) as text)
end tell
tell application "System Events" to if (selectedLine's rangeOfString:"^\t*?--" options:(NSRegularExpressionSearch))'s |length| is 0 then
keystroke "c" using {control down, shift down, command down}
else
keystroke "u" using {control down, shift down, command down}
end if
The logic of the script is simple, and assumes that all selected lines are either commented or not. It determines if the selection begins with any number of tabs followed by two dashes. If so it uses Script Debugger’s Uncomment command. If not, it uses the Comment command.
Notice that the first step is to use Script Debugger’s Select Lines command to ensure the entirety of the first and last lines are in the selection.
The keyboard shortcuts for Command and Uncomment are not standard and need to be added. Also, it assumes you are using two dashes for comments.
I used AppleScriptObjC, but it can also be done in “plain” AppleScript.
The scirpt has a bug in that comments can also begin any number of tabs (including 0) followed by spaces followed by dashes. Fixing it would be straightforward, but it has not be a problem for me. The spaces will be removed when the script is compiled.
I thought I created my own shortcuts for them, but when I hit the Factory Defaults button last night as a test they were still there, so I assumed they were built in. But now I see that Factory Defaults doesn’t actually reset anything on my system, which is curious.
What is Opt+Cmd+S doing in your script? It doesn’t appear to be a standard shortcut either.
Using UI scripting to script SD makes me chuckle, but if it gets the job done…
option-command-s invokes Script Debugger’s Select Lines command. It is not a standard key binding so you’ll need to set it yourself. It is what I was referring to when I wrote " the first step is to use Script Debugger’s Select Lines command to ensure the entirety of the first and last lines are in the selection."
I eventually figured it out from the script’s behavior, which I guess was easier than rereading your description.
And against impossible odds, I’ve found a regex (lord save me) that appears to do everything I want.
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
property NSRegularExpressionSearch : a reference to current application's NSRegularExpressionSearch
property NSString : a reference to current application's NSString
property NSThread : a reference to current application's NSThread
tell application "Script Debugger"
tell application "System Events" to tell application process "Script Debugger" to click menu item "Select Lines" of menu "Edit" of menu bar item "Edit" of menu bar 1
NSThread's sleepForTimeInterval:0.01
set selectedLine to NSString's stringWithString:((document 1's selection) as text)
end tell
tell application "System Events" to if (selectedLine's rangeOfString:"(?m)^[\t ]*?[^\t -]" options:(NSRegularExpressionSearch))'s |length| is 0 then
tell application process "Script Debugger" to click menu item "Uncomment" of menu "Lines" of menu item "Lines" of menu "Edit" of menu bar item "Edit" of menu bar 1
else
tell application process "Script Debugger" to click menu item "Comment" of menu "Lines" of menu item "Lines" of menu "Edit" of menu bar item "Edit" of menu bar 1
end if
If the selection contains at least one uncommented line — anywhere, not just the first line — it applies the Comment command to all of them.
Otherwise, it applies the Uncomment command.
That way, a block of code like
-- First variable.
set x to 1
-- Second variable.
set y to 2
becomes
---- First variable.
--set x to 1
--
---- Second variable.
--set y to 2
and the original comments are preserved when I uncomment the block later.
Since I’m matching uncommented lines, I’ve reversed the order of the Comment and Uncomment commands.
Other modifications I’ve made:
Replaced the keystroke commands with menu scripting. The verbosity is ridiculous, but worth its weight in pixels to make the script self-contained — no need for custom key bindings.
Tightened up sleepForTimeInterval to make up for the more complicated regex.
Thanks for getting me 99% of the way there. It never occurred to me that I could do this myself by scripting SD. (That said, a native toggle capability still might be more efficient.)
FWIW, I’ve included below an ASObjC suggestion. It differs from fuzzywan’s script in that one or more commented lines in the selection will cause my script to remove comment characters from the beginning of all selected lines. Otherwise, comments are added to the beginning of all selected lines.
use framework "Foundation"
use scripting additions
on main()
set commentCharacters to "# "
tell application "Script Debugger" to tell document 1
set allCode to source text
set {cr1, cr2} to character range of selection
set {pr1, pr2} to getParagraphRange(allCode, cr1, cr2) of me
set selection to {pr1, pr2}
set selectedCode to selection
set editedCode to getEditedCode(selectedCode, commentCharacters) of me
set contents of selection to editedCode
set selection to {pr1, 0}
end tell
end main
on getParagraphRange(theString, cr1, cr2)
set theString to current application's NSString's stringWithString:theString
set paragraphRange to theString's paragraphRangeForRange:{(cr1 - 1), cr2}
return {((paragraphRange's location) + 1), paragraphRange's |length|}
end getParagraphRange
on getEditedCode(theString, theCharacters)
set theString to current application's NSMutableString's stringWithString:theString
set thePattern to "(?m)^(\\h*)" & theCharacters
set theRange to theString's rangeOfString:thePattern options:1024 --option 1024 is regex
if theRange's |length|() > 0 then --remove comment characters
theString's replaceOccurrencesOfString:thePattern withString:"$1" options:1024 range:{0, theString's |length|()}
else --add comment characters
theString's replaceOccurrencesOfString:"(?m)^(\\h*\\S)" withString:(theCharacters & "$1") options:1024 range:{0, theString's |length|()}
end if
return theString as text
end getEditedCode
main()
Just for learning purposes, I rewrote my earlier script to work in the same fashion as fuzzywan’s script. There remains a difference in the handling of whitespace with indented code. I suspect this could be addressed by adjusting the regex patterns, but I’ll leave that for another day.
use framework "Foundation"
use scripting additions
on main()
set commentCharacters to "--"
tell application "Script Debugger" to tell document 1
set allCode to source text
set {cr1, cr2} to character range of selection
set {pr1, pr2} to getParagraphRange(allCode, cr1, cr2) of me
set selection to {pr1, pr2}
set selectedCode to selection
set {editedCode, editCount, editAction} to getEditedCode(selectedCode, commentCharacters) of me
set contents of selection to editedCode
set pr2 to (pr2 + ((count commentCharacters) * editCount * editAction))
set selection to {pr1, pr2}
end tell
end main
on getParagraphRange(theString, cr1, cr2)
set theString to current application's NSString's stringWithString:theString
set paragraphRange to theString's paragraphRangeForRange:{(cr1 - 1), cr2}
return {((paragraphRange's location) + 1), paragraphRange's |length|}
end getParagraphRange
on getEditedCode(theString, theCharacters)
set removeString to current application's NSMutableString's stringWithString:theString
set addString to current application's NSMutableString's stringWithString:theString
set removeCount to removeString's replaceOccurrencesOfString:("(?m)^(\\h*)" & theCharacters) withString:"$1" options:1024 range:{0, removeString's |length|()}
set addCount to addString's replaceOccurrencesOfString:"(?m)^(\\h*)" withString:(theCharacters & "$1") options:1024 range:{0, addString's |length|()}
if removeCount is equal to addCount then return {removeString as text, removeCount, "-1"}
return {addString as text, addCount, "1"}
end getEditedCode
main()
I’ve been staring at your code for a while, and I think I’m starting to get it now. Later today I’ll try it out.
Do I understand from your regex that backslashes have to be escaped? That would explain why \h was giving me compilation errors but \t did not — and why the editor insists on rendering the latter as an actual tab.
For my regex to work at all (let alone as intended), it surely has to be a violation of multiple laws of physics.
So, with removeString you’re replacing existing comment markers (and any leading whitespace) with the whitespace alone, referenced with $1?
And by comparing the number of characters removed with the characters added to addString, you can tell if there were any uncommented lines without needing a separate regex search, and that determines whether you return addString or removeString…
I like that it shaves 0.1sec off the execution time on my system. And as @dmbrown mentioned, the compiler regularizes whitespace, so it doesn’t really matter that the comment markers are inserted at column 1 instead of the block’s greatest common indent (as SD’s Comment command does). Looks the same in the end.
Does main() do anything different than a run() handler would on its own, or is that just a borrowed C-ish convention?
Other than a bunch of comments for documentation and some tweaks to the handler variables, I have nothing to add.
That’s entirely correct. Technically $1 references the portion of the regex pattern that is contained in parentheses, which in this case is (\\h*).
This is also correct, except that the number or regex replacements (as returned by removeCount and addCount) are compared instead of the number of characters added or removed.
No–there’s no difference. There used to be a technical benefit to placing AppleScript code in a handler other than the run handler, but that’s no longer the case. I continue to use a main handler just as a matter of personal preference.
So in this situation, you’re effectively counting the number of lines modified, since the removeString regex only looks for a single comment marker at the beginning of a line. And addCount will be equal to the total number of lines in the block. Dunno if it’s useful to know that, but now I do.
@peavine, I’ve noticed that 16-bit Unicode characters (which includes emoji) make the script behave oddly. I’m guessing that
set {cr1, cr2} to character range of selection
isn’t aligning with the character boundaries.
As an example:
-- A plain ASCII comment.
set x to "Yes"
-- A comment with a 2-byte Unicode character: ⌘
set y to "No"
-- A comment with a 3-byte Unicode character: 🤨
set z to "Who wants to know?"
-- And one more for luck.
set a to "...Y'know, never mind."
Prior to the line with the emoji, the script works as expected. But it usually captures incomplete lines (or pieces of the previous one) after that point, and it gets worse as I add more problematic characters.
Not sure if this is something we can solve with a code tweak, or a fundamental limitation of how SD and/or AppleScript count characters.
(For what it’s worth, SD’s native comment/uncomment commands — and by extension, @dmbrown’s version — don’t run into this problem.)
Thanks Shane for the suggestion. I wasn’t aware of that property, and I think it can be used to simplify my script.
fuzzywan. As best I can determine, the issue arises when a character anywhere in the script cannot be expressed in 16 bits, and, when any such character is encountered, the paragraphRangeForRange method returns a result that breaks the script. Most characters found in a script including most accented Latin characters can be expressed in 16 bits, but emoticon codes cannot. There are some alternatives to the paragraphRangeForRange method, and I’ll investigate those, but your script in post 6 completely avoids this issue and should probably be preferred.