A few years ago, I submitted a feature request for Script Debugger 9 that would allow for easy lookup of library handler information (e.g. description, parameters, return type) at the call site. This would serve to make up for some of AppleScript’s limitations around default parameters & enumerations that limit the flexibility of script library handlers.
Unfortunately, with the sad news that Script Debugger has reached its end-of-life, this feature request will not see the light of day.
I want to gauge whether there’s any community interest in making a documentation comment standard for AppleScript handlers, as well as a robust script that can pull up these documentation comments when coding in Script Debugger.
I’ve made a proof-of-concept script to illustrate how this would be used. In it’s current form, it’s neither robust nor performance-optimized, but it does show that this approach will work.
To use the lookup script, you just place the cursor in (or select) the name of the library handler & run the lookup script. A window pops up with the documentation comment for the handler.
The script looks for the documentation comment in the library by checking if there is a multi-line comment starting on the line immediately after the handler definition line.
If you want to test this for yourself, you can download the Docstring Test Library (which contains the example handler replaceText
, the lookup script functionality, and some support handlers).
- Install the script library at
~/Library/Script Libraries/Docstring Test Library.scpt
(note the extension). - Call the lookup script with e.g. FastScripts, or by placing an alias to the library in Script Debugger’s script menu folder & clicking it in the menu.
Example Calling Script
use DTL : script "Docstring Test Library"
set s to "Now is the winter of our discontent."
set stronger_s to DTL's replaceText(s, ".", "!")
Docstring Test Library
### EXAMPLE HANDLER (being used by the script consuming the library - the lookup handler will pull this docstring)
to replaceText(s, search_string, replacement_string)
(* (string OR list of strings, string OR list of strings, string OR list of strings) → string OR list of strings
Return a copy of s with all instances of search_string replaced with replacement_string. s may be either a string or a list of strings. Search_string may be a list containing multiple substrings to be replaced by replacement_string; if replacement_string is also a list of substrings, then each substring in search_string will be sequentially replaced with the corresponding substring in replacement_string.
Parameters:
s [string OR list of strings] : The string(s).
search_string [string OR list of strings] : The substring, or list of substrings, to be replaced.
replacement_string [string OR list of strings] : The substring, or list of substrings, to replace the search string(s).
Result:
[string OR list of strings] : A copy of s with the appropriate substitutions made. *)
if class of replacement_string is string then
set search_string to {search_string}
set replacement_string to {replacement_string}
else if class of search_string is string then
set search_string to {search_string}
end if
if class of s is string then
set previous_TIDs to AppleScript's text item delimiters
repeat with i from 1 to length of replacement_string
set AppleScript's text item delimiters to item i of search_string
set s_text_items to text items of s
set AppleScript's text item delimiters to item i of replacement_string
set s to s_text_items as string
end repeat
set AppleScript's text item delimiters to previous_TIDs
return s
else -- s is a list of strings.
set s_length to length of s
set new_string_list to {}
repeat with i from 1 to s_length
set the end of new_string_list to item i of s
end repeat
set previous_TIDs to AppleScript's text item delimiters
repeat with i from 1 to length of replacement_string
set AppleScript's text item delimiters to item i of search_string
repeat with j from 1 to s_length
set item j of new_string_list to text items of item j of new_string_list
end repeat
set AppleScript's text item delimiters to item i of replacement_string
repeat with j from 1 to s_length
set item j of new_string_list to item j of new_string_list as string
end repeat
end repeat
set AppleScript's text item delimiters to previous_TIDs
return new_string_list
end if
end replaceText
### LOOKUP HANDLERS (Also handlers running the lookup functions in this condensed example).
return lookUpSelectedHandler()
to getHandlerInfoFromScriptDebugger()
tell application "Script Debugger"
set current_document_id to id of document 1
set current_document to a reference to document id current_document_id
set {{selection_offset, selection_length}, selection_contents} to {character range, contents} of selection of current_document
set script_text to source text of current_document
set handler_end to (my getOffsetWithEndpoints("(", script_text, selection_offset, -1)) - 1
set handler_start to (my getLastOffsetWithEndpoints(space, script_text, 1, selection_offset)) + 1
set library_end to handler_start - 2
set library_start to (my getLastOffsetWithEndpoints(space, script_text, 1, library_end)) + 1
set handler_name to text handler_start thru handler_end of script_text
set library_token to text library_start thru library_end of script_text
set library_token to my stripString(library_token, "(-")
if library_token is not in {"my", "its"} then
set library_token to text 1 thru -3 of library_token
set library_specifier to (script property library_token of current_document)'s source text
set library_name to text 9 thru -2 of library_specifier
end if
end tell
return {library_name, handler_name}
end getHandlerInfoFromScriptDebugger
to lookUpSelectedHandler()
set {library_name, handler_name} to getHandlerInfoFromScriptDebugger()
set handler_search to "\nto " & handler_name & "("
set decompiled_library to linefeed & my decompileScriptFile("/Users/David/Library/Mobile Documents/com~apple~CloudDocs/Library/Script Libraries/" & library_name & ".scpt")
set handler_offset to my getOffset(handler_search, decompiled_library)
if handler_offset = 0 then return "HANDLER NOT FOUND"
set handler_offset to handler_offset + 1
set handler_start to handler_offset + 3
set linefeed_found to false
set docstring_start to 0
repeat with i from handler_offset to length of decompiled_library
set char to item i of decompiled_library
set char_is_linefeed to char is linefeed
if char_is_linefeed then
set linefeed_found to true
set handler_end to i - 1
else
if linefeed_found then
if text i thru (i + 2) of decompiled_library is "\t(*" then
set docstring_start to i
else
exit repeat -- No docstring.
end if
else
-- Continue searching.
end if
end if
end repeat
set handler_signature to text handler_start thru handler_end of decompiled_library
if docstring_start is 0 then return "DOCSTRING NOT FOUND"
set docstring_end to (my getOffsetWithEndpoints("*)", decompiled_library, docstring_start, -1)) + 1
set docstring to text (docstring_start + 7) thru (docstring_end - 6) of decompiled_library
set dialog_displayer to current application -- Don't block the UI if run by Script Debugger.
if id of dialog_displayer starts with "com.latenightsw.ScriptDebugger" then
set dialog_displayer to application "System Events" -- FastScripts is a better alternative, if installed.
end if
tell dialog_displayer to display dialog tab & handler_signature & "\n\n\t" & docstring with title (handler_name & " — " & library_name) buttons {"Dismiss"} default button 1
end lookUpSelectedHandler
### SUPPORT HANDLERS (Not relevant to example, but used in the current implementation of the lookup script)
to sliceString(s, slice_start, slice_end)
(* (string, integer, integer) → string
Return a string containing characters slice_start to slice_end of s. Unlike native AppleScript text slicing, slice indices can be out of range, and slicing from a greater index to a lesser index returns an empty string.
Parameters:
s [string] : The string.
slice_start [integer] : The starting slice index.
slice_end [integer] : The ending slice index.
Result:
[string] : A slice of s from slice_start to slice_end. *)
set s_length to length of s
if s_length is 0 then return s -- String is empty.
if slice_start is greater than 0 then
if slice_start is greater than s_length then return "" -- Start is too large.
if slice_end is less than 0 then
-- Positive slice_start & negative slice_end.
if slice_end is less than -s_length then return "" -- End is too small.
if slice_start is greater than slice_end + s_length + 1 then return "" -- End is before start.
else
-- Positive slice_start & positive slice_end.
if slice_start is greater than slice_end then return "" -- End is before start.
if slice_end is greater than s_length then set slice_end to s_length -- End is too large.
end if
else
if slice_start is 0 then set slice_start to -s_length -- Start is 0.
if slice_end is less than 0 then
-- Negative slice_start & negative slice_end.
if slice_end is less than -s_length then return "" -- End is too small.
if slice_end is less than slice_start then return "" -- End is before start.
else
-- Negative slice_start & positive slice_end.
if slice_start is greater than slice_end - s_length - 1 then return "" -- End is before start.
if slice_end is 0 then set slice_end to s_length -- End is 0.
if slice_end is greater than s_length then set slice_end to s_length -- End is too large.
end if
if slice_start is less than -s_length then set slice_start to 1 -- Start is too small.
end if
return text slice_start thru slice_end of s
end sliceString
to getOffset(search_string, s)
(* (string, string) → integer
Return the index of the first occurrence of search_string in s, or 0 if search_string cannot be found.
Parameters:
search_string [string] : The substring to search for.
s [string] : The string in which to search.
Result:
[integer] : The first index of search_string in s, or 0. *)
set s_length to length of s
if s_length is 0 or length of search_string is 0 then return 0
set previous_TIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to search_string
set search_string_offset to length of first text item of s
set AppleScript's text item delimiters to previous_TIDs
if search_string_offset is s_length then
return 0
else
return search_string_offset + 1
end if
end getOffset
to getOffsetWithEndpoints(search_string, s, search_start, search_end)
(* (string, string, integer, integer) → integer
Return the index of the first occurrence of search_string in the slice of s from character search_start to character search_end, or 0 if search_string is not in the slice. Character indices are interpreted as in the sliceString function.
Parameters:
search_string [string] : The substring to search for.
s [string] : The string in which to search.
search_start [integer] : The index at which to start the search.
search_end [integer] : The index at which to end the search.
Result:
[integer] : The offset of the first occurrence of search_string in s sliced from search_start to search_end, or 0 if search_string cannot be found. *)
set sliced_s to sliceString(s, search_start, search_end)
set sliced_s_length to length of sliced_s
if sliced_s_length is 0 or length of search_string is 0 then return 0
set previous_TIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to search_string
set search_string_offset to length of first text item of sliced_s
set AppleScript's text item delimiters to previous_TIDs
if search_string_offset is sliced_s_length then
return 0
else
if search_start is less than 1 then
set s_length to length of s
if search_start is 0 or search_start is less than -s_length then
return search_string_offset + 1
else
return search_string_offset + search_start + s_length + 1
end if
else
return search_string_offset + search_start
end if
end if
end getOffsetWithEndpoints
to getLastOffsetWithEndpoints(search_string, s, search_start, search_end)
(* (string, string, integer, integer) → integer
Return the index of the last occurrence of search_string in the slice of s from character search_start to character search_end, or 0 if search_string is not in the slice. Character indices are interpreted as in the sliceString function.
Parameters:
search_string [string] : The substring to search for.
s [string] : The string in which to search.
search_start [integer] : The index at which to start the search.
search_end [integer] : The index at which to end the search.
Result:
[integer] : The offset of the last occurrence of search_string in s sliced from search_start to search_end, or 0 if search_string cannot be found. *)
set sliced_s to sliceString(s, search_start, search_end)
set sliced_s_length to length of sliced_s
set search_string_length to length of search_string
if sliced_s_length is 0 or search_string_length is 0 then return 0
set previous_TIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to search_string
set search_string_offset to length of last text item of sliced_s
set AppleScript's text item delimiters to previous_TIDs
if search_string_offset is sliced_s_length then
return 0
else
if search_start is less than 1 then
set s_length to length of s
if search_start is 0 or search_start is less than -s_length then
return sliced_s_length - search_string_offset - search_string_length + 1
else
return sliced_s_length - search_string_offset - search_string_length + 1 + search_start + s_length
end if
else
return sliced_s_length - search_string_offset - search_string_length + search_start
end if
end if
end getLastOffsetWithEndpoints
to stripString(s, strip_characters)
(* (string, string OR {string, string}) → string
Return s with all leading and/or trailing strip_characters removed. If strip_characters is a string, then remove the specified characters from both ends of s. If strip_characters is a two-string list, then strip the characters in the first string from the start of s and the characters in the second string from the end of s. If strip_characters is empty, then strip all whitespace from both ends of s.
Parameters:
s [string] : The string to strip.
strip_characters [string OR {string, string}] : The characters to remove from the ends of s. If a string or single-item list, remove those characters from both ends of s. If a multiple-string list, remove the characters in the first string from the start of s and the characters in the last string from the end of s. If empty, remove all whitespace characters from both ends.
Result:
[string] : A copy of s with strip_characters removed from one or both ends. *)
set s_length to length of s
if s_length is 0 then return ""
if length of strip_characters is 0 then set strip_characters to WHITESPACE
if class of strip_characters is list then
set left_strip_characters to item 1 of strip_characters
set right_strip_characters to item -1 of strip_characters
else
set left_strip_characters to strip_characters
set right_strip_characters to strip_characters
end if
repeat with i from 1 to s_length
if character i of s is not in left_strip_characters then exit repeat
end repeat
repeat with j from 1 to (s_length + 1 - i)
if character -j of s is not in right_strip_characters then exit repeat
end repeat
set s to text i thru -j of s
if s is in left_strip_characters or s is in right_strip_characters then set s to ""
return s
end stripString
to decompileScriptFile(file_path)
(* (string) → string
Decompile the script file at file_path and return it as plain text.
Parameters:
file_path [string] : The POSIX path of the script file to decompile.
Result:
[string] : The plain-text representation of the script file at file_path. *)
set script_result to do shell script "osadecompile " & quoted form of file_path without altering line endings
if script_result ends with linefeed then
if script_result is linefeed then
return ""
else
return text 1 thru -2 of script_result
end if
else
return script_result
end if
end decompileScriptFile
Right now, the proof-of-concept lacks error checking & requires the exact syntax used in the example, but the parsing can easily be expanded to cover most valid AppleScript syntax.
My questions for the community are as follows:
- Is there sufficient interest in this feature to standardize documentation comments & develop a robust lookup script?
- Does anyone have any fundamental design changes they think would be useful (e.g. using some kind of persistent floating window instead of using
display dialog
)?