Code Sharing: Library Function Finder

A handy script made with AI help.

(At least, handy in my use case.)

I’ve got a lot of libraries… 18 libraries, totaling thousands of handlers. I forget which handler is in which library and exactly what it’s named and what its arguments are, and waste a lot of time looking for handlers.

So this gets saved as an app, and put on a key command. I use Quicksilver to put it in on a key command so I can restrict the app it’s active in to just Script Debugger.

What the app does:

  • Find all Applescript libraries at default locations
  • Read them as text with OSA Decompile
  • Parse out all the functions
  • Open a UI window with live search at the top and a function list below, also presenting the library the handler is from and its arguments in the list
  • A bottom window shows the any comment immediately preceding the selected handler in the library, assuming that would be the relevant note on what the handler does.
  • The list can be navigated with command + up/down arrow keys. Or if you tab to make the list active instead of search, it can be navigated with arrow keys without command.
  • Double-clicking a selection, or hitting command+return, will activate Script Debugger, and insert a call to the selected function at the current cursor point of the front Script Debugger window/tab. Or if there’s a text selection, it replaces it with the function. (Obviously, you can change that to Script Editor with a couple little changes if you like.)

The script just stays open rather than quitting after insertion, because the startup/indexing time gets annoying. When you need to reindex, just relaunch it. It does quit on command + q.

Working great for me. Hope it’s useful to others.

Note that in my environment, my code lives on git as .applescript files, and my linked libraries are all deployed as read-only, so this isn’t my real version. Mine reads the files from git and doesn’t need to use OSA Decompile, so it’s faster on startup. I tested and think I got everything right here for a more standard environment, and removed all my environmental dependencies, but let me know if I missed anything. Or if anyone wants my git version.

--*SaveFiletypes*app*SaveFiletypes*
-- Function Finder: A Spotlight-like search tool for handlers in AppleScript Script Libraries.
-- Scans ~/Library/Script Libraries/ and /Library/Script Libraries/ for .applescript,
-- .scpt, and .scptd files, parses their handlers, and presents a searchable list.
-- Cmd+Return or Double-click inserts selected function's tell statement at the current cursor position in Script Debugger
-- Cmd+Up/Down navigates results. 

use AppleScript version "2.3.1"
use scripting additions
use framework "Foundation"
use framework "AppKit"

property allHandlers : missing value
property myWindow : missing value
property myTableView : missing value
property myArrayController : missing value
property myDescriptionLabel : missing value
property windowIsOpen : true

-- Check main thread
if not (current application's NSThread's isMainThread()) as boolean then
	display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
	error number -128
end if

-- Scan standard macOS Script Libraries locations
set libFolders to {}
set userLibFolder to (POSIX path of (path to home folder)) & "Library/Script Libraries"
set systemLibFolder to "/Library/Script Libraries"

try
	do shell script "test -d " & quoted form of userLibFolder
	set end of libFolders to userLibFolder
end try
try
	do shell script "test -d " & quoted form of systemLibFolder
	set end of libFolders to systemLibFolder
end try

if (count of libFolders) is 0 then
	display alert "No Script Libraries folders found." & linefeed & "Looked in:" & linefeed & userLibFolder & linefeed & systemLibFolder buttons {"OK"} as critical
	error number -128
end if

-- Parse all libraries from all folders
set allHandlers to {}
repeat with libFolder in libFolders
	set allHandlers to allHandlers & parseAllLibraries(libFolder as text)
end repeat

-- Convert to NSArray of NSDictionary for Cocoa bindings
set nsData to current application's NSMutableArray's new()
repeat with h in allHandlers
	set dict to current application's NSMutableDictionary's new()
	(dict's setObject:(handlerName of h) forKey:"functionName")
	(dict's setObject:(libraryName of h) forKey:"libraryName")
	(dict's setObject:(params of h) forKey:"params")
	(dict's setObject:(handlerDescription of h) forKey:"handlerDescription")
	(nsData's addObject:dict)
end repeat

-- Build and show UI
buildUI(nsData)

-- Event loop - delay yields to the Cocoa run loop so UI stays responsive
repeat while windowIsOpen
	delay 0.5
end repeat

---------- HANDLERS ----------

on parseAllLibraries(libFolder)
	set handlerList to {}
	set whitespaceSet to current application's NSCharacterSet's whitespaceCharacterSet()
	
	-- Collect all library files: .applescript (source), .scpt (compiled), .scptd (bundles)
	set fileList to {}
	try
		set fileList to fileList & paragraphs of (do shell script "ls " & quoted form of libFolder & "/*.applescript 2>/dev/null")
	end try
	try
		set fileList to fileList & paragraphs of (do shell script "ls " & quoted form of libFolder & "/*.scpt 2>/dev/null")
	end try
	try
		set fileList to fileList & paragraphs of (do shell script "ls -d " & quoted form of libFolder & "/*.scptd 2>/dev/null")
	end try
	
	repeat with filePath in fileList
		set filePath to filePath as text
		
		-- Determine library name and file extension
		set fileName to do shell script "basename " & quoted form of filePath
		set fileExt to do shell script "echo " & quoted form of fileName & " | sed 's/.*\\./\\./' "
		set libName to do shell script "basename " & quoted form of filePath & " " & quoted form of fileExt
		
		-- Read source: .applescript files directly, .scpt/.scptd via osadecompile
		set fileSource to missing value
		if fileExt is ".applescript" then
			set fileNSString to (current application's NSString's stringWithContentsOfFile:filePath encoding:(current application's NSUTF8StringEncoding) |error|:(missing value))
			if fileNSString is not missing value then set fileSource to fileNSString as text
		else
			try
				set fileSource to do shell script "osadecompile " & quoted form of filePath & " 2>/dev/null"
			end try
		end if
		
		if fileSource is missing value or fileSource is "" then
			-- skip unreadable files
		else
			set fileNSString to (current application's NSString's stringWithString:fileSource)
			set fileLines to (fileNSString's componentsSeparatedByCharactersInSet:(current application's NSCharacterSet's newlineCharacterSet())) as list
			set lineCount to count of fileLines
			
			repeat with i from 1 to lineCount
				set thisLine to item i of fileLines
				if thisLine is missing value then set thisLine to ""
				set thisLine to thisLine as text
				
				-- Trim leading whitespace
				set trimmedLine to ((current application's NSString's stringWithString:thisLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
				
				-- Check for handler definition
				set handlerKeyword to missing value
				if trimmedLine starts with "on " and (length of trimmedLine) > 3 then
					set handlerKeyword to "on "
				else if trimmedLine starts with "to " and (length of trimmedLine) > 3 then
					set handlerKeyword to "to "
				end if
				
				if handlerKeyword is not missing value then
					set afterKeyword to text 4 thru -1 of trimmedLine
					
					-- Skip standard AppleScript event handlers (error, run, idle, quit, reopen)
					set skipWords to {"error", "run", "idle", "quit", "reopen"}
					set shouldSkip to false
					repeat with skipWord in skipWords
						set skipWord to skipWord as text
						if afterKeyword starts with skipWord then
							set wordLen to length of skipWord
							if (length of afterKeyword) is wordLen then
								set shouldSkip to true
								exit repeat
							else
								set nextChar to character (wordLen + 1) of afterKeyword
								if nextChar is in {" ", return, linefeed, tab} then
									set shouldSkip to true
									exit repeat
								end if
							end if
						end if
					end repeat
					
					-- Also skip ObjC-style callback handlers (e.g. "on colorPicked:")
					if not shouldSkip and afterKeyword does not contain "(" then set shouldSkip to true
					
					if not shouldSkip then
						-- Extract handler name (text before the parenthesis)
						set funcName to ""
						repeat with c from 1 to length of afterKeyword
							set ch to character c of afterKeyword
							if ch is "(" then exit repeat
							set funcName to funcName & ch
						end repeat
						
						-- Trim the handler name
						set funcName to ((current application's NSString's stringWithString:funcName)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
						
						if funcName is not "" then
							-- Extract parameters (text between outermost parentheses)
							set paramStr to ""
							set inParen to false
							set parenDepth to 0
							repeat with c from 1 to length of afterKeyword
								set ch to character c of afterKeyword
								if ch is "(" then
									set parenDepth to parenDepth + 1
									if parenDepth > 1 then set paramStr to paramStr & ch
									set inParen to true
								else if ch is ")" then
									set parenDepth to parenDepth - 1
									if parenDepth > 0 then
										set paramStr to paramStr & ch
									else
										exit repeat
									end if
								else if inParen then
									set paramStr to paramStr & ch
								end if
							end repeat
							
							-- Look backward for preceding comment lines
							set descLines to {}
							repeat with j from (i - 1) to 1 by -1
								set prevLine to item j of fileLines
								if prevLine is missing value then exit repeat
								set prevLine to prevLine as text
								set trimmedPrev to ((current application's NSString's stringWithString:prevLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
								
								if trimmedPrev is "" then
									exit repeat -- blank line stops collection
								else if trimmedPrev starts with "--" then
									-- Skip section dividers (lines of only dashes, equals, asterisks, etc.)
									set commentBody to ""
									if (length of trimmedPrev) > 2 then set commentBody to text 3 thru -1 of trimmedPrev
									set isDivider to true
									if commentBody is not "" then
										repeat with cc from 1 to length of commentBody
											set cch to character cc of commentBody
											if cch is not in {"-", "=", "*", " ", tab, "~"} then
												set isDivider to false
												exit repeat
											end if
										end repeat
									else
										set isDivider to true -- bare "--" is a divider
									end if
									if not isDivider then
										set beginning of descLines to trimmedPrev
									else
										exit repeat
									end if
								else
									exit repeat -- non-comment line stops collection
								end if
							end repeat
							
							-- Build description from collected comment lines
							set descText to ""
							repeat with dLine in descLines
								if descText is not "" then set descText to descText & linefeed
								set descText to descText & (dLine as text)
							end repeat
							
							-- Also capture inline comment on the handler line itself (after closing paren)
							set afterParen to ""
							set foundClose to false
							repeat with c from 1 to length of afterKeyword
								if foundClose then
									set afterParen to afterParen & character c of afterKeyword
								else if character c of afterKeyword is ")" then
									set foundClose to true
								end if
							end repeat
							if afterParen contains "--" then
								set dashPos to offset of "--" in afterParen
								set inlineComment to text dashPos thru -1 of afterParen
								if descText is not "" then
									set descText to inlineComment & linefeed & descText
								else
									set descText to inlineComment
								end if
							end if
							
							set end of handlerList to {handlerName:funcName, params:paramStr, libraryName:libName, handlerDescription:descText}
						end if
					end if
				end if
			end repeat
		end if
	end repeat
	
	return handlerList
end parseAllLibraries

on buildUI(nsData)
	set panelWidth to 800
	set panelHeight to 550
	
	-- Array controller manages data, sorting, and filtering for the table
	set myArrayController to current application's NSArrayController's alloc()'s init()
	myArrayController's setContent:nsData
	set sortDesc to current application's NSSortDescriptor's sortDescriptorWithKey:"functionName" ascending:true selector:"caseInsensitiveCompare:"
	myArrayController's setSortDescriptors:{sortDesc}
	
	-- Utility panel (floating, thin title bar)
	set styleMask to 1 + 2 + 8 + 16 -- titled + closable + resizable + utility
	set myWindow to current application's NSPanel's alloc()'s initWithContentRect:{{200, 200}, {panelWidth, panelHeight}} styleMask:styleMask backing:2 defer:true
	myWindow's setTitle:("Function Finder (" & ((nsData's |count|()) as integer) & " handlers)")
	myWindow's setFloatingPanel:true
	myWindow's setLevel:(current application's NSFloatingWindowLevel)
	myWindow's setMinSize:{500, 350}
	set contentView to myWindow's contentView()
	
	-- Search field at top
	set searchField to current application's NSSearchField's alloc()'s initWithFrame:{{10, panelHeight - 50}, {panelWidth - 20, 26}}
	searchField's setPlaceholderString:"Search functions, libraries, or parameters..."
	searchField's setAutoresizingMask:(2 + 8) -- width sizable + stays at top
	contentView's addSubview:searchField
	
	-- Register for keystroke notifications on the search field
	set notifCenter to current application's NSNotificationCenter's defaultCenter()
	notifCenter's addObserver:me selector:"searchChanged:" |name|:(current application's NSControlTextDidChangeNotification) object:searchField
	
	-- Description label at bottom (monospace, read-only, wrapping)
	set myDescriptionLabel to current application's NSTextField's alloc()'s initWithFrame:{{10, 10}, {panelWidth - 20, 55}}
	myDescriptionLabel's setEditable:false
	myDescriptionLabel's setBezeled:true
	myDescriptionLabel's setDrawsBackground:true
	myDescriptionLabel's setStringValue:"Cmd+Return or double-click to insert at cursor in Script Debugger."
	myDescriptionLabel's setAutoresizingMask:(2 + 32) -- width sizable + stays at bottom
	myDescriptionLabel's cell()'s setLineBreakMode:(current application's NSLineBreakByWordWrapping)
	myDescriptionLabel's cell()'s setUsesSingleLineMode:false
	myDescriptionLabel's setFont:(current application's NSFont's fontWithName:"Menlo" |size|:11)
	contentView's addSubview:myDescriptionLabel
	
	-- Table view
	set myTableView to current application's NSTableView's alloc()'s initWithFrame:{{0, 0}, {panelWidth - 20, panelHeight - 140}}
	myTableView's setUsesAlternatingRowBackgroundColors:true
	myTableView's setColumnAutoresizingStyle:(current application's NSTableViewUniformColumnAutoresizingStyle)
	
	-- Function column (bound to array controller)
	set col1 to current application's NSTableColumn's alloc()'s initWithIdentifier:"functionName"
	col1's setWidth:250
	col1's headerCell()'s setStringValue:"Function"
	col1's setEditable:false
	col1's bind:"value" toObject:myArrayController withKeyPath:"arrangedObjects.functionName" options:(missing value)
	myTableView's addTableColumn:col1
	
	-- Library column
	set col2 to current application's NSTableColumn's alloc()'s initWithIdentifier:"libraryName"
	col2's setWidth:200
	col2's headerCell()'s setStringValue:"Library"
	col2's setEditable:false
	col2's bind:"value" toObject:myArrayController withKeyPath:"arrangedObjects.libraryName" options:(missing value)
	myTableView's addTableColumn:col2
	
	-- Parameters column
	set col3 to current application's NSTableColumn's alloc()'s initWithIdentifier:"params"
	col3's setWidth:310
	col3's headerCell()'s setStringValue:"Parameters"
	col3's setEditable:false
	col3's bind:"value" toObject:myArrayController withKeyPath:"arrangedObjects.params" options:(missing value)
	myTableView's addTableColumn:col3
	
	-- Delegate for selection change, double-click copies to clipboard
	myTableView's setDelegate:me
	myTableView's setTarget:me
	myTableView's setDoubleAction:"tableDoubleClicked:"
	
	-- Scroll view wrapper (required for NSTableView)
	set scrollView to current application's NSScrollView's alloc()'s initWithFrame:{{10, 75}, {panelWidth - 20, panelHeight - 135}}
	scrollView's setDocumentView:myTableView
	scrollView's setHasVerticalScroller:true
	scrollView's setHasHorizontalScroller:false
	scrollView's setAutoresizingMask:(2 + 16) -- width + height sizable
	scrollView's setBorderType:(current application's NSBezelBorder)
	contentView's addSubview:scrollView
	
	-- Watch for window close and app terminate
	notifCenter's addObserver:me selector:"windowClosing:" |name|:(current application's NSWindowWillCloseNotification) object:myWindow
	notifCenter's addObserver:me selector:"appQuitting:" |name|:(current application's NSApplicationWillTerminateNotification) object:(missing value)
	
	-- Build main menu with keyboard shortcuts
	set appMenu to current application's NSMenu's alloc()'s initWithTitle:"Function Finder"
	
	-- Cmd+Return to insert selected function into Script Debugger
	set insertItem to current application's NSMenuItem's alloc()'s initWithTitle:"Insert Function" action:"menuInsertFunction:" keyEquivalent:(character id 13)
	insertItem's setKeyEquivalentModifierMask:(1048576) -- NSEventModifierFlagCommand
	insertItem's setTarget:me
	appMenu's addItem:insertItem
	
	-- Cmd+Down/Up to navigate table from search field
	set downItem to current application's NSMenuItem's alloc()'s initWithTitle:"Next Result" action:"menuMoveDown:" keyEquivalent:(character id 63233)
	downItem's setKeyEquivalentModifierMask:(1048576) -- NSEventModifierFlagCommand
	downItem's setTarget:me
	appMenu's addItem:downItem
	
	set upItem to current application's NSMenuItem's alloc()'s initWithTitle:"Previous Result" action:"menuMoveUp:" keyEquivalent:(character id 63232)
	upItem's setKeyEquivalentModifierMask:(1048576) -- NSEventModifierFlagCommand
	upItem's setTarget:me
	appMenu's addItem:upItem
	
	appMenu's addItem:(current application's NSMenuItem's separatorItem())
	
	-- Cmd+Q to quit
	set quitItem to current application's NSMenuItem's alloc()'s initWithTitle:"Quit Function Finder" action:"terminate:" keyEquivalent:"q"
	quitItem's setTarget:(current application's NSApplication's sharedApplication())
	appMenu's addItem:quitItem
	
	set menuBarItem to current application's NSMenuItem's new()
	menuBarItem's setSubmenu:appMenu
	set mainMenu to current application's NSMenu's new()
	mainMenu's addItem:menuBarItem
	current application's NSApplication's sharedApplication()'s setMainMenu:mainMenu
	
	-- Show the panel and focus the search field
	myWindow's makeKeyAndOrderFront:me
	myWindow's makeFirstResponder:searchField
end buildUI

-- Called on each keystroke in the search field
on searchChanged:aNotification
	set searchText to ((aNotification's object())'s stringValue()) as text
	
	if searchText is "" then
		myArrayController's setFilterPredicate:(missing value)
	else
		set predicate to current application's NSPredicate's predicateWithFormat:"functionName CONTAINS[cd] %@ OR libraryName CONTAINS[cd] %@ OR params CONTAINS[cd] %@" argumentArray:{searchText, searchText, searchText}
		myArrayController's setFilterPredicate:predicate
	end if
	
	-- Update title with visible/total count
	set visibleCount to (myArrayController's arrangedObjects()'s |count|()) as integer
	set totalCount to (myArrayController's content()'s |count|()) as integer
	if searchText is "" then
		myWindow's setTitle:("Function Finder (" & totalCount & " handlers)")
	else
		myWindow's setTitle:("Function Finder (" & visibleCount & " of " & totalCount & ")")
	end if
	
	myDescriptionLabel's setStringValue:""
	
	-- Auto-select first row so arrow keys and Return work immediately
	if visibleCount > 0 then
		myTableView's selectRowIndexes:(current application's NSIndexSet's indexSetWithIndex:0) byExtendingSelection:false
		myTableView's scrollRowToVisible:0
	end if
end searchChanged:

-- Called when table selection changes - shows description of selected handler
on tableViewSelectionDidChange:aNotification
	set selectedRow to (myTableView's selectedRow()) as integer
	if selectedRow is -1 then
		myDescriptionLabel's setStringValue:""
		return
	end if
	
	set selectedObjects to myArrayController's selectedObjects()
	if (selectedObjects's |count|()) as integer is 0 then return
	
	set selectedDict to selectedObjects's objectAtIndex:0
	set descText to (selectedDict's objectForKey:"handlerDescription") as text
	if descText is "" or descText is "missing value" then set descText to "(No description available)"
	myDescriptionLabel's setStringValue:descText
end tableViewSelectionDidChange:

-- Cmd+Return menu action
on menuInsertFunction:sender
	my insertSelectedFunction()
end menuInsertFunction:

-- Cmd+Down arrow menu action
on menuMoveDown:sender
	set currentRow to (myTableView's selectedRow()) as integer
	set rowCount to (myTableView's numberOfRows()) as integer
	if currentRow < rowCount - 1 then
		set newRow to currentRow + 1
		myTableView's selectRowIndexes:(current application's NSIndexSet's indexSetWithIndex:newRow) byExtendingSelection:false
		myTableView's scrollRowToVisible:newRow
	end if
end menuMoveDown:

-- Cmd+Up arrow menu action
on menuMoveUp:sender
	set currentRow to (myTableView's selectedRow()) as integer
	if currentRow > 0 then
		set newRow to currentRow - 1
		myTableView's selectRowIndexes:(current application's NSIndexSet's indexSetWithIndex:newRow) byExtendingSelection:false
		myTableView's scrollRowToVisible:newRow
	end if
end menuMoveUp:

-- Double-click on table row
on tableDoubleClicked:sender
	my insertSelectedFunction()
end tableDoubleClicked:

-- Inserts the selected function's tell statement at the cursor in Script Debugger
on insertSelectedFunction()
	set selectedRow to (myTableView's selectedRow()) as integer
	if selectedRow is -1 then return
	
	set selectedObjects to myArrayController's selectedObjects()
	if (selectedObjects's |count|()) as integer is 0 then return
	
	set selectedDict to selectedObjects's objectAtIndex:0
	set funcName to (selectedDict's objectForKey:"functionName") as text
	set libName to (selectedDict's objectForKey:"libraryName") as text
	set paramStr to (selectedDict's objectForKey:"params") as text
	
	-- Build full tell statement
	set tellString to "tell script \"" & libName & "\" to " & funcName & "(" & paramStr & ")"
	
	-- Insert at cursor position in Script Debugger's frontmost document
	try
		tell application "Script Debugger"
			activate
			set selection of front document to tellString
		end tell
	on error errMsg
		display alert "Could not insert into Script Debugger:" & linefeed & errMsg buttons {"OK"} as warning
	end try
end insertSelectedFunction

-- Called when the window is about to close
on windowClosing:aNotification
	current application's NSNotificationCenter's defaultCenter()'s removeObserver:me
	set my windowIsOpen to false
end windowClosing:

-- Handle app termination (Cmd+Q, Dock quit, etc.)
on appQuitting:aNotification
	set my windowIsOpen to false
end appQuitting:

on quit
	set my windowIsOpen to false
	continue quit
end quit