Key binding to toggle Comment/Uncomment

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? :stuck_out_tongue: You could let us swap Comment for a Toggle Comment command in Key Bindings, or give us a “Comment toggle mode” setting.

1 Like

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.

1 Like

+1. I agree that both methods (separate comment/uncomment vs. single toggle) are useful.

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. :rofl:

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 a script that I’ve been using in one version or another for 6 years (other versions here). Functionally it’s similar (but not identical) to fuzzywan’s script in post 6. The comment characters I actually use are "# " which seems to avoid a few issues and works better for me. I run the script by way of FastScripts.

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "--" --set to desired value
	tell application id "com.latenightsw.ScriptDebugger8" 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
	end tell
	set editedCode to getEditedCode(selectedCode, commentCharacters)
	tell application id "com.latenightsw.ScriptDebugger8" to tell document 1
		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 NSString'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
		set theString to (theString's stringByReplacingOccurrencesOfString:thePattern withString:"$1" options:1024 range:{0, theString's |length|()})
	else --add comment characters
		set thePattern to "(?m)^(\\h*\\S)"
		set theString to (theString's stringByReplacingOccurrencesOfString:thePattern withString:(theCharacters & "$1") options:1024 range:{0, theString's |length|()})
	end if
	return theString as text
end getEditedCode

main()

You are welcome. I appreciate your letting me know your results; it is nice to know someone will be getting some use of my script.