Toggle comments in script editor

When editing a script with Script Editor, I often need to disable code sections and, in the past, I have used block comments for this purpose. I found this a bit cumbersome at times and wrote the following script for use in temporarily disabling sections of a script.

This script works as a toggle. If any line in the selection begins with the specified comment characters, then the comment characters are removed from the beginning of all lines in the selection. Otherwise, comment characters are added to the beginning of every line in the selection.

-- Revised 2022.02.23

on main()
	set commentCharacters to "# " -- must begin with "--" or "#"
	
	tell application "Script Editor" to tell document 1
		set allCode to contents
		set characterRange to character range of selection
		set characterRange to getCharacterRange(allCode, characterRange) of me
		set selection to characterRange
		set selectedCode to contents of selection
	end tell
	
	set commentCharactersExist to false
	set selectedCode to paragraphs of selectedCode
	repeat with aParagraph in selectedCode
		set theParagraph to contents of aParagraph
		try
			repeat while text 1 of theParagraph is in {tab, space}
				set theParagraph to text 2 thru -1 of theParagraph
			end repeat
		on error
			set theParagraph to tab
		end try
		set contents of aParagraph to theParagraph
		if theParagraph begins with commentCharacters then set commentCharactersExist to true
	end repeat
	
	if commentCharactersExist then
		set selectedCode to removeComment(commentCharacters, selectedCode)
	else
		set selectedCode to addComment(commentCharacters, selectedCode)
	end if
	
	set text item delimiters to {linefeed}
	set selectedCode to selectedCode as text
	set text item delimiters to {""}
	
	tell application "Script Editor" to tell document 1
		set contents of selection to selectedCode
		set {c1, c2} to character range of selection
		set selection to insertion point c1
	end tell
end main

on addComment(commentCharacters, selectedCode)
	if selectedCode = {} then set selectedCode to {tab}
	repeat with aParagraph in selectedCode
		set contents of aParagraph to (commentCharacters & aParagraph)
	end repeat
	return selectedCode
end addComment

on removeComment(commentCharacters, selectedCode)
	if selectedCode = {} then set selectedCode to {tab}
	repeat with aParagraph in selectedCode
		if aParagraph begins with commentCharacters then
			try
				set contents of aParagraph to (text ((count commentCharacters) + 1) thru -1 of aParagraph)
			on error
				set contents of aParagraph to tab
			end try
		end if
	end repeat
	return selectedCode
end removeComment

on getCharacterRange(allCode, characterRange)
	set {c1, c2} to characterRange
	if c2 = 0 then set c2 to 1
	if item c2 of allCode is in {return, linefeed} then set c2 to (c2 - 1) -- remove linefeed at end of selection
	
	set c1Set to false
	set runningTotal to 0
	set allCode to paragraphs of allCode
	repeat with aParagraph in allCode
		set aParagraph to contents of aParagraph
		set runningTotal to runningTotal + (count aParagraph) + 1
		if c1Set = false and runningTotal > c1 then
			set c1 to runningTotal - (count aParagraph)
			set c1Set to true
		end if
		if c1Set = true and runningTotal > c2 then
			set c2 to runningTotal
			exit repeat
		end if
	end repeat
	
	return {c1 - 1, c2 - 1}
end getCharacterRange

main()

I’ve been using the above script without issue until a recent update of Catalina. Now, when run by way of FastScripts and on a somewhat intermittent basis, a colon is added to the beginning or end of the comment characters. For example:

set testOne to “aa”

:## set testTwo to “bb”
:## set testThree to 1
:## set testFour to 2

I changed the comment characters to “–” but the issue persisted. I do not have this issue when the script is 1) run from within Script Editor, 2) run as an Automator service, 3) run as an app on the desktop. I emailed Daniel of FastScripts but he was unable to reproduce the issue.

Anyone have any idea why those colons are being inserted? Thanks.

FastScripts uses the same component instance of AppleScript for all scripts it runs, so things like text item delimiters are common to all scripts. Your code contains:

set selectedText to selectedList as text

That’s coercing a list to text, which means it will use whatever your TIDs are at that time.

Thanks Shane. Upon further investigation, I found a script, which I run by way of FastScripts, in which I had set the text item delimiter to a colon but had not reset the text item delimiter at the end of the script. As you surmise, that was the root cause of this issue. I fixed that and all is working well.

I greatly appreciate your help

FWIW, there are Comment and Uncomment commands built in to Script Debugger, with toolbar buttons.

I’ve been attempting to get this script to work with Script Debugger but without success. It appears that ‘content’ has to be changed to ‘source text’ and that ‘character range’ and ‘selection’ are supported but apparently ‘paragraph’ is not. Anyways, I thought someone who is familiar with scripting Script Debugger might be able to suggest a fix or perhaps advise that my script won’t work with Script Debugger.

Thanks for the help.

Your script would need to be modified to work with Script Debugger. But I’m not sure why you’d bother, given it has toolbar buttons and menu commands (which you can assign keyboard shortcuts to) that already do the job faster (and in some cases more cleanly).

Shane. I was aware there are toolbar buttons to add and remove comment characters but I didn’t know there are menu items for this. I tested these and they work great.

I do like my script’s ability to set the format of the comment characters, which I find to be a small convenience when editing a script. However, as things go, this is a minor matter and not worth the time it would appear to take to modify my script. I thought it might be an easy fix.

You can change that in Script Debugger by going to Preferences → Editing → Comment Inserts:.

Thanks Shane. I am totally embarrassed :slight_smile:

The native ability of Script Debugger to add and remove comments is great and that’s what I use. However, I wanted to learn a little about scripting Script Debugger and decided to rewrite the above script for use with Script Debugger just for learning purposes.

I’ve gotten everything fixed but cannot overcome one issue. I need to have the script select a particular paragraph (or range of paragraphs) of the script but can’t seem to manage that. The equivalent code in Script Editor would be:

tell application "Script Editor" to tell document 1
	set selection to paragraph 2 -- reposition the selection
end tell

The above doesn’t work in Script Debugger and so I looked at the Script Debugger dictionary, which contains:

So, there does not appear to be a Script-Debugger equivalent to the Script Editor code and I thought perhaps I could get the character range of the numbered paragraph and then use that with the selection commmand but that didn’t work either.

Anyone have an ideas how to get the following to work:

tell application "Script Debugger" to tell document 1
    set selection to paragraph 2 -- reposition the selection
end tell

Thanks for the help.

You need to get the source text as a string, and do your selection calculations based on that. It’s a bit tedious (but relatively fast because there are minimal Apple events).

Thanks Shane. That will be a fun project for me to work on.

The following version of my script works with Script Debugger.

-- revised 2022.07.26

on main()
	set commentCharacters to "# " -- must begin with "--" or "#"
	
	tell application "Script Debugger" to tell document 1
		set allCode to source text
		set characterRange to character range of selection
		set characterRange to getCharacterRange(allCode, characterRange) of me
		set selection to characterRange
		set selectedCode to selection
	end tell
	
	set commentCharactersExist to false
	set selectedCode to paragraphs of selectedCode
	repeat with aParagraph in selectedCode
		set theParagraph to contents of aParagraph
		try
			repeat while text 1 of theParagraph is in {tab, space}
				set theParagraph to text 2 thru -1 of theParagraph
			end repeat
		on error
			set theParagraph to ""
		end try
		set contents of aParagraph to theParagraph
		if theParagraph begins with commentCharacters then set commentCharactersExist to true
	end repeat
	
	if commentCharactersExist then
		set selectedCode to removeComment(commentCharacters, selectedCode)
	else
		set selectedCode to addComment(commentCharacters, selectedCode)
	end if
	
	set text item delimiters to {linefeed}
	set selectedCode to selectedCode as text
	set text item delimiters to {""}
	
	tell application "Script Debugger" to tell document 1
		set contents of selection to selectedCode
		set selection to {item 1 of characterRange, 0} -- remove selection
		-- set selection to {item 1 of characterRange, ((count selectedCode) + 1)} -- keep selection
	end tell
end main

on addComment(commentCharacters, selectedCode)
	if selectedCode = {} then return {commentCharacters}
	repeat with aParagraph in selectedCode
		set contents of aParagraph to (commentCharacters & aParagraph)
	end repeat
	return selectedCode
end addComment

on removeComment(commentCharacters, selectedCode)
	set commentCharactersCount to ((count commentCharacters) + 1)
	repeat with aParagraph in selectedCode
		if aParagraph begins with commentCharacters then
			try
				set contents of aParagraph to text commentCharactersCount thru -1 of aParagraph
			on error
				set contents of aParagraph to ""
			end try
		end if
	end repeat
	return selectedCode
end removeComment

on getCharacterRange(allCode, characterRange)
	set {c1, c2} to characterRange
	set {c1, c2} to {c1, c1 + c2 - 1}
	
	set c1Set to false
	set runningTotal to 0
	set allCode to paragraphs of allCode
	repeat with aParagraph in allCode
		set runningTotal to runningTotal + (count aParagraph) + 1
		if c1Set = false and runningTotal ≥ c1 then
			set c1 to runningTotal - (count aParagraph)
			set c1Set to true
		end if
		if c1Set = true and runningTotal ≥ c2 then
			set c2 to runningTotal
			exit repeat
		end if
	end repeat
	
	return {c1, (c2 - c1)}
end getCharacterRange

main()

The script contained below works with Script Debugger and is faster than the earlier version. The script does not add comment characters to lines that are blank or contain whitespace only, although this behavior is changed by editing the line that contains the following comment:

-- revised 2022.09.23

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "# " -- must begin with "--" or "#"
	
	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 selectedCode to current application's NSString's stringWithString:selectedCode
	set trimmedCode to getTrimmedCode(selectedCode)
	set editedCode to getEditedCode(trimmedCode, 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}
	set theParagraphs to {((paragraphRange's location) + 1), paragraphRange's |length|}
	return theParagraphs
end getParagraphRange

on getTrimmedCode(theString)
	set thePattern to "(?m)^\\h+"
	set theString to (theString's stringByReplacingOccurrencesOfString:thePattern withString:"" options:1024 range:{0, theString's |length|()})
	return theString
end getTrimmedCode

on getEditedCode(theString, theCharacters)
	set thePattern to "(?m)^" & theCharacters
	set theRange to theString's rangeOfString:thePattern options:1024
	if theRange's |length|() > 0 then -- comment characters exist`
		set theString to (theString's stringByReplacingOccurrencesOfString:thePattern withString:"" options:1024 range:{0, theString's |length|()})
	else
		set thePattern to "(?m)^(.+)$" -- change "+" to "*" to comment blank lines
		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()

The script contained below works with Script Editor and is faster than the earlier version. The script does not add comment characters to lines that are blank or contain whitespace only, although this behavior is changed by editing the line that contains the following comment:

This script contains a bug which occurs when a blank line is selected with the line ending but without any preceding or subsequent text. The impact of this bug is relatively small.

-- revised 2022.09.23

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "# " -- must begin with "--" or "#"
	
	tell application "Script Editor" to tell document 1
		set allCode to contents
		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 contents of selection
	end tell
	
	set selectedCode to current application's NSString's stringWithString:selectedCode
	set trimmedCode to getTrimmedCode(selectedCode)
	set editedCode to getEditedCode(trimmedCode, commentCharacters)
	
	tell application "Script Editor" to tell document 1
		set contents of selection to editedCode
		set selection to insertion point (pr1 + 1)
	end tell
end main

on getParagraphRange(theString, cr1, cr2)
	set cr2 to cr2 - cr1
	set theString to current application's NSString's stringWithString:theString
	set paragraphRange to theString's paragraphRangeForRange:{cr1, cr2}
	set {pr1, pr2} to {(paragraphRange's location), paragraphRange's |length|}
	return {pr1, pr2 + pr1}
end getParagraphRange

on getTrimmedCode(theString)
	set thePattern to "(?m)^\\h+"
	set theString to (theString's stringByReplacingOccurrencesOfString:thePattern withString:"" options:1024 range:{0, theString's |length|()})
	return theString
end getTrimmedCode

on getEditedCode(theString, theCharacters)
	set thePattern to "(?m)^" & theCharacters
	set theRange to theString's rangeOfString:thePattern options:1024
	if theRange's |length|() > 0 then -- comment characters exist
		set theString to (theString's stringByReplacingOccurrencesOfString:thePattern withString:"" options:1024 range:{0, theString's |length|()})
	else
		set thePattern to "(?m)^(.+)$" -- change "+" to "*" to comment blank lines
		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()

This script toggles comment characters at the beginning of each paragraph of code selected in Script Debugger. If any paragraphs of selected code begin with comment characters, then comment characters are removed from the beginning of every paragraph of selected code. Otherwise, comment characters are added to the beginning of every paragraph of selected code. The script is similar to that contained in post 15 above except that:

  • White space at the beginning of paragraphs is retained until the script is compiled.

  • The amount of work done by the script is slightly reduced.

--revised 2024.06.07

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "--"
	
	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:("$1" & theCharacters & "$2") options:1024 range:{0, theString's |length|()})
	end if
	return theString as text
end getEditedCode

main()

The above script adds comment characters in the order of existing white space, comment characters, and code. The following replacement handler changes this order to comment characters, existing white space, and code.

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

BTW, the above scripts do not add comment characters to the beginning of paragraphs that are empty or contain whitespace only. This is easily changed by editing the getEditedCode handler.

The following script is identical to that above except that it works with Script Editor:

--revised 2024.06.07

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "--"
	
	tell application "Script Editor" to tell document 1
		set allCode to contents
		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 contents of selection
	end tell
	
	set editedCode to getEditedCode(selectedCode, commentCharacters)
	
	tell application "Script Editor" to tell document 1
		set contents of selection to editedCode
		set selection to insertion point (pr1 + 1)
	end tell
end main

on getParagraphRange(theString, cr1, cr2)
	set cr2 to cr2 - cr1
	set theString to current application's NSString's stringWithString:theString
	set paragraphRange to theString's paragraphRangeForRange:{cr1, cr2}
	set {pr1, pr2} to {(paragraphRange's location), paragraphRange's |length|}
	return {pr1, pr2 + pr1}
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:("$1" & theCharacters & "$2") options:1024 range:{0, theString's |length|()})
	end if
	return theString as text
end getEditedCode

main()

The following is similar to the Script Debugger script in post 17, differing primarily in that the code selection is retained after the comment characters are added or removed.

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "# " --edit as desired
	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
		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} --replace pr2 with 0 to remove code selection
	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
		set editCount to (theString's replaceOccurrencesOfString:thePattern withString:"$1" options:1024 range:{0, theString's |length|()})
		set editAction to "-1"
	else --add comment characters
		set thePattern to "(?m)^(\\h*\\S)"
		set editCount to (theString's replaceOccurrencesOfString:thePattern withString:(theCharacters & "$1") options:1024 range:{0, theString's |length|()})
		set editAction to "1"
	end if
	return {theString as text, editCount, editAction}
end getEditedCode

main()

The Script Debugger scripts included above do not work correctly when the script being edited contains individual characters with more than one 16-bit code unit (typically emojis). The following script fixes this.

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "# " --edit as desired
	tell application "Script Debugger" to tell document 1
		set allCode to source text
		set {cr1, cr2} to selection ASObjC range --uses 16-bit code units
		set {pr1, pr2} to getParagraphRange(allCode, cr1, cr2) of me
		set selection ASObjC range to {pr1, pr2}
		set selectedCode to selection
		set editedCode to getEditedCode(selectedCode, commentCharacters) of me
		set contents of selection to editedCode
		set selection ASObjC range 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, cr2} --uses 16-bit code units
	return {paragraphRange's location, paragraphRange's |length|}
end getParagraphRange

on getEditedCode(theString, theCharacters)
	set theString to current application's NSMutableString's stringWithString:theString
	set removeCount to theString's replaceOccurrencesOfString:("(?m)^(\\h*)" & theCharacters) withString:"$1" options:1024 range:{0, theString's |length|()} --option 1024 is regex
	if removeCount is greater than 0 then return theString as text
	theString's replaceOccurrencesOfString:"(?m)^(\\h*\\S)" withString:(theCharacters & "$1") options:1024 range:{0, theString's |length|()}
	return theString as text
end getEditedCode

main()

This script is the same as the above except that the code selection is not removed when the script ends.

use framework "Foundation"
use scripting additions

on main()
	set commentCharacters to "# " --edit as desired
	tell application "Script Debugger" to tell document 1
		set allCode to source text
		set {cr1, cr2} to selection ASObjC range --character range of current selection
		set {pr1, pr2} to getParagraphRange(allCode, cr1, cr2) of me --paragraph range of current selection
		set selection ASObjC range 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 ASObjC range 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, cr2}
	return {paragraphRange's location, paragraphRange's |length|}
end getParagraphRange

on getEditedCode(theString, theCharacters)
	set theString to current application's NSMutableString's stringWithString:theString
	set removeCount to theString's replaceOccurrencesOfString:("(?m)^(\\h*)" & theCharacters) withString:"$1" options:1024 range:{0, theString's |length|()}
	if removeCount is greater than 0 then return {theString as text, removeCount, "-1"}
	set addCount to theString's replaceOccurrencesOfString:"(?m)^(\\h*\\S)" withString:(theCharacters & "$1") options:1024 range:{0, theString's |length|()}
	return {theString as text, addCount, "1"}
end getEditedCode

main()