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

2 Likes

How about inserting a reference to the script library needed as a property?

Very interesting to me in particular. I’m mid dev on the same idea. No AI. My tack is a little different because I’m bundling the libraries ( also 19 or so ) , I offer two different modes of using the handlers ( inserting code in the target script or creating a custom library for the target script ) but its crazy parallel to your project.

I’m gonna try yours out and see how it compares. What AI did you use for this?

I used Claude, my work provides me with the highest level of Claude Code.

I’ve previously tried the highest paid level of GPT and Gemini on Applescript/ASObjC, and they were mostly only somewhat helpful with Applescript, but both made plenty of mistakes, and both were basically completely useless with ASObjC. I was basically blown away by this. There were plenty of mistakes and I spent about 2.5 hours on this total, doing some myself, some was just telling Cluade what wasn’t working and letting it find and correct its own bugs, some was me reviewing the code and finding Claude bugs to tell it to fix, and a little bit was me actually finding and fixing bugs myself, but most of this was Claude writing it and Claude revising it based on my testing and just telling it the result. I’ve done 100’s of thousands of lines of Applescript and maintain a huge codebase it in used by hundreds of people daily, but I never really invested the time in ASObjC and don’t even have the ObjC ability to even do this myself from scratch. I’ve just looked up stuff as needed and gotten help on the forums and bludgeoned my way through the ASObj stuff I’ve done in the past. It’s not overstating it to say Claude Code, which I’ve only had access to for a couple weeks, is a “game changer” for me.

Claude’s very first crack at it was a “basically functioning” version whose only real deal-killer bug on version 1.0 was that the paste into Script Debugger was failing. Which was one of the things it wasn’t managing to fix on its own that I had to find the solution to on my own… not that it was difficult or time consuming. I never bothered to figure out why it was failing, but it was using a delay followed by a “tell System Events to tell process “Script Debugger” to keystroke command + V” thing, where when I saw it I instantly thought “Script Debugger has a great AS dictionary, there’s no way this should be done with UI Scripting,” which was correct.

Our libraries are all at a location with a shortcut to it in the user libraries location, so all the functions can be addressed with
tell script “[library name]” to [function name]
I’ve never used a reference to the script as a property… not sure when this would be useful/come into play.
Feel free to modify away for your own purposes.

I’ve got a newer version with some new features, including that command+shift+return will open the Library and reveal the function, instead of inserting a call to it in the current script. Also, added some more keyboard controls. I have a lot of functions that take a single record as input, and that record can contain a lot of different keys/values. I use a standard format for comments on functions, where the comment immediately precedes the functions, and the last line of the comment is always an example call to the function, with those record arguments spelled out. Now, if the script can find such an example call in the comment preceding the function, it will insert the example call rather than the actual function definition, so I get all the keys spelled out.

This one isn’t adapted for general use like the above, like where it’s looking for the libraries. It’s got stuff specific to my environment, like looking for the libraries on git. But if anyone wants this version, it’s easy to grab the few relevant bits to generalize it out of the former. And maybe someone wants to keep it looking on git and just update it to their local environment.

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

-- Locate Script Libraries by reading the git path from Push Compiled Script's ScriptDataFile
set myPath to POSIX path of (path to me)
if myPath ends with "/" then set myPath to text 1 thru -2 of myPath
set parentFolder to do shell script "dirname " & quoted form of myPath
set dataFilePath to parentFolder & "/Push Compiled Script.scptd/Contents/Resources/ScriptDataFile.txt"

try
	set dataFileAlias to (dataFilePath as POSIX file) as alias
	set userGitFolders to read dataFileAlias as list
	set userName to short user name of (system info)
	set gitDirectory to false
	repeat with anEntry in userGitFolders
		if item 1 of anEntry is userName then set gitDirectory to item 2 of anEntry
	end repeat
	if gitDirectory is false then error "No git path found for user \"" & userName & "\" in ScriptDataFile.txt"
	set libFolder to gitDirectory & "Script Libraries"
	set libAlias to (libFolder as POSIX file) as alias -- verify it exists
	if libFolder ends with "/" then set libFolder to text 1 thru -2 of libFolder
on error errMsg
	display alert "Could not locate Script Libraries:" & linefeed & errMsg buttons {"OK"} as critical
	error number -128
end try

-- Parse all libraries
set allHandlers to parseAllLibraries(libFolder)

-- 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")
	(dict's setObject:(exampleCall of h) forKey:"exampleCall")
	(dict's setObject:(filePath of h) forKey:"filePath")
	(nsData's addObject:dict)
end repeat

-- Build and show UI
buildUI(nsData)

-- Hand control to Cocoa's run loop (handles all events including Dock/Cmd+Tab quit)
current application's NSApplication's sharedApplication()'s performSelector:"run"

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

on parseAllLibraries(libFolder)
	set handlerList to {}
	set whitespaceSet to current application's NSCharacterSet's whitespaceAndNewlineCharacterSet()
	
	-- Get .applescript files
	try
		set fileList to paragraphs of (do shell script "ls " & quoted form of libFolder & "/*.applescript 2>/dev/null")
	on error
		return handlerList
	end try
	
	repeat with filePath in fileList
		set filePath to filePath as text
		set libName to do shell script "basename " & quoted form of filePath & " .applescript"
		
		-- Read file via NSString for reliability
		set fileNSString to (current application's NSString's stringWithContentsOfFile:filePath encoding:(current application's NSUTF8StringEncoding) |error|:(missing value))
		if fileNSString is missing value then
			-- skip unreadable files
		else
			set fileLines to (fileNSString's componentsSeparatedByCharactersInSet:(current application's NSCharacterSet's newlineCharacterSet())) as list
			set lineCount to count of fileLines
			
			-- Forward scan: track comments as we go, assign to next handler
			set pendingComment to ""
			set inBlockComment to false
			set blockLines to {}
			
			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
				set trimmedLine to ((current application's NSString's stringWithString:thisLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
				
				if inBlockComment then
					-- Inside a (* *) block comment - check for closing *)
					if trimmedLine contains "*)" then
						-- Last line of block - strip closing *)
						set closeLine to trimmedLine
						if closeLine ends with "*)" then
							if (length of closeLine) > 2 then
								set closeLine to text 1 thru -3 of closeLine
								set closeLine to ((current application's NSString's stringWithString:closeLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
							else
								set closeLine to ""
							end if
						end if
						if closeLine is not "" then set end of blockLines to closeLine
						-- Build the pending comment from collected block lines
						set pendingComment to ""
						repeat with bLine in blockLines
							if pendingComment is not "" then set pendingComment to pendingComment & linefeed
							set pendingComment to pendingComment & (bLine as text)
						end repeat
						set inBlockComment to false
						set blockLines to {}
					else
						-- Middle line of block comment
						if trimmedLine is not "" then
							set end of blockLines to trimmedLine
						end if
					end if
					
				else if trimmedLine starts with "(*" then
					-- Start of a block comment
					set inBlockComment to true
					set blockLines to {}
					-- Strip opening (* from first line
					set firstLine to ""
					if (length of trimmedLine) > 2 then
						set firstLine to text 3 thru -1 of trimmedLine
					end if
					-- Check for single-line block comment: (* text *)
					if firstLine contains "*)" then
						if firstLine ends with "*)" then
							if (length of firstLine) > 2 then
								set firstLine to text 1 thru -3 of firstLine
							else
								set firstLine to ""
							end if
						end if
						set firstLine to ((current application's NSString's stringWithString:firstLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
						if firstLine is not "" then set pendingComment to firstLine
						set inBlockComment to false
						set blockLines to {}
					else
						set firstLine to ((current application's NSString's stringWithString:firstLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
						if firstLine is not "" then set end of blockLines to firstLine
					end if
					
				else if trimmedLine starts with "--" then
					-- Line comment: check if it's a divider or real comment
					set commentBody to ""
					if (length of trimmedLine) > 2 then set commentBody to text 3 thru -1 of trimmedLine
					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
					end if
					if not isDivider then
						-- Append to pending comment (for consecutive -- lines)
						if pendingComment is not "" then
							set pendingComment to pendingComment & linefeed & trimmedLine
						else
							set pendingComment to trimmedLine
						end if
					else
						-- Divider line clears the pending comment
						set pendingComment to ""
					end if
					
				else if trimmedLine is "" then
					-- Blank lines are OK between comment and handler, don't clear
					
				else
					-- 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 event handlers
						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
						
						if not shouldSkip and afterKeyword does not contain "(" then set shouldSkip to true
						
						if not shouldSkip then
							-- Extract handler name
							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
							set funcName to ((current application's NSString's stringWithString:funcName)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
							
							if funcName is not "" then
								-- Extract parameters
								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
								
								-- Capture inline comment on handler line
								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 pendingComment is not "" then
										set pendingComment to inlineComment & linefeed & pendingComment
									else
										set pendingComment to inlineComment
									end if
								end if
								
								-- Extract example call from comment (line after "Example:")
								set exampleCall to ""
								if pendingComment contains "Example:" then
									set commentNS to (current application's NSString's stringWithString:pendingComment)
									set commentLines to (commentNS's componentsSeparatedByString:(linefeed)) as list
									set foundExample to false
									repeat with cLine in commentLines
										set cLine to cLine as text
										set trimCLine to ((current application's NSString's stringWithString:cLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
										if foundExample then
											if trimCLine is not "" then
												set exampleCall to trimCLine
												exit repeat
											end if
										else if trimCLine is "Example:" or trimCLine is "Example" then
											set foundExample to true
										end if
									end repeat
								end if
								
								set end of handlerList to {handlerName:funcName, params:paramStr, libraryName:libName, handlerDescription:pendingComment, exampleCall:exampleCall, filePath:filePath}
							end if
						end if
					end if
					-- Any non-blank, non-comment, non-handler line clears the pending comment
					if handlerKeyword is missing value then set pendingComment to ""
				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
	
	-- "Reveal In Library" button at bottom-right
	set gotoButton to current application's NSButton's alloc()'s initWithFrame:{{panelWidth - 140, 10}, {130, 24}}
	gotoButton's setTitle:"Reveal In Library"
	gotoButton's setBezelStyle:1 -- NSBezelStyleRounded
	gotoButton's setTarget:me
	gotoButton's setAction:"gotoDefinitionClicked:"
	gotoButton's setAutoresizingMask:(4) -- anchored right
	contentView's addSubview:gotoButton
	
	-- Description label at bottom (monospace, read-only, wrapping)
	set myDescriptionLabel to current application's NSTextField's alloc()'s initWithFrame:{{10, 10}, {panelWidth - 155, 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 inserts function
	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 mainMenu to current application's NSMenu's new()
	
	-- Edit menu (enables Cmd+A, Cmd+V, Cmd+C, Cmd+X in text fields)
	set editMenu to current application's NSMenu's alloc()'s initWithTitle:"Edit"
	editMenu's addItemWithTitle:"Cut" action:"cut:" keyEquivalent:"x"
	editMenu's addItemWithTitle:"Copy" action:"copy:" keyEquivalent:"c"
	editMenu's addItemWithTitle:"Paste" action:"paste:" keyEquivalent:"v"
	editMenu's addItemWithTitle:"Select All" action:"selectAll:" keyEquivalent:"a"
	set editMenuItem to current application's NSMenuItem's new()
	editMenuItem's setSubmenu:editMenu
	mainMenu's addItem:editMenuItem
	
	-- App menu
	set appMenu to current application's NSMenu's alloc()'s initWithTitle:"Function Finder"
	
	-- Cmd+Return to insert selected function
	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+Shift+Return to reveal in library in Script Debugger
	set gotoItem to current application's NSMenuItem's alloc()'s initWithTitle:"Reveal in Library" action:"menuGoToDefinition:" keyEquivalent:(character id 13)
	gotoItem's setKeyEquivalentModifierMask:(1048576 + 131072) -- NSEventModifierFlagCommand + NSEventModifierFlagShift
	gotoItem's setTarget:me
	appMenu's addItem:gotoItem
	
	-- 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 appMenuItem to current application's NSMenuItem's new()
	appMenuItem's setSubmenu:appMenu
	mainMenu's addItem:appMenuItem
	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:

-- Button click for Go to Definition
on gotoDefinitionClicked:sender
	my goToDefinition()
end gotoDefinitionClicked:

-- Cmd+Shift+Return menu action
on menuGoToDefinition:sender
	my goToDefinition()
end menuGoToDefinition:

-- Opens the library file in Script Debugger, compiles, and jumps to the handler
on goToDefinition()
	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 sourceFile to (selectedDict's objectForKey:"filePath") as text
	
	try
		tell application "Script Debugger"
			activate
			set theDoc to open (sourceFile as POSIX file)
			compile theDoc
			-- Find the handler's position in the source text
			set srcText to source text of theDoc
			set handlerSig to "on " & funcName & "("
			set charPos to offset of handlerSig in srcText
			if charPos is 0 then
				-- Try "to " form
				set handlerSig to "to " & funcName & "("
				set charPos to offset of handlerSig in srcText
			end if
			if charPos > 0 then
				set character range of selection of theDoc to {charPos, length of handlerSig}
			end if
		end tell
		-- Nudge cursor to force Script Debugger to scroll to the selection
		delay 0.1
		tell application "System Events"
			tell process "Script Debugger"
				key code 124 -- right arrow
			end tell
		end tell
	on error errMsg
		display alert "Could not open in Script Debugger:" & linefeed & errMsg buttons {"OK"} as warning
	end try
end goToDefinition

-- 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
	set exampleStr to (selectedDict's objectForKey:"exampleCall") as text
	
	-- Prefer the example call from the comment block; fall back to generic tell statement
	if exampleStr is not "" and exampleStr is not "missing value" then
		set tellString to exampleStr
	else
		set tellString to "tell script \"" & libName & "\" to " & funcName & "(" & paramStr & ")"
	end if
	
	-- 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
	current application's NSApplication's sharedApplication()'s terminate:me
end windowClosing:

-- Called via NSApplicationWillTerminateNotification for all quit paths
on appQuitting:aNotification
	set my windowIsOpen to false
end appQuitting:

on quit
	set my windowIsOpen to false
	continue quit
end quit

Oh, I also have an icon for it, if anyone wants that. The “upload” button wouldn’t let me select is, so I’ve got it at a Dropbox link for now.

https://www.dropbox.com/scl/fi/6xgi7wwpjx8kob4vnkk9t/Function-Finder-Icon.icns?rlkey=vyk6evwx724vfofdfkdhpkquq&dl=0

I want to love the interface window. It’s far better than a choose from list in concept. Here, it is unusably laggy (M1 MacBook Pro on Tahoe) but understandable when it’s got an 805 item list. I removed all libraries except my string handlers and with only 83 items it’s still laggy. Unexpected.

It’s simple and effective, I’ll give it that. I’m spending a lot of time and bytes on UI polish, features, and usability. But for prompt-generated code this is darn impressive.

If I can sort out the performance issues I may steal and mod this dialog code.

For me, I’ve got way more than 800 items, and it’s laggy when it first fires. But I just leave it open. It’s faster after it’s been open for a minute and stays fast, for me. I can click it in the dock, command tab to it, or the key command I have it on launches when closed, but activates when it’s already open. I only quit and relaunch it when I want it to update its database.

So I know what you mean, but for me the results are nearly instantaneous once it’s been open for a bit.

Hi t.spoon

Thank you for the script.
On my mac (Power Book 26.3.1) I get the error message
“This script must be run from the main thread.”
How do I do that?

Regrads
Sven

Not t.spoon but you just need to save and run the script as an application.

Hi paulskinneer,

The script runs now but shows an error massage that it does not find any script libraries:
image
I changed the folder name in the script to:

set systemLibFolder to “/Library/Script”

My Script folder looks like:
image

Where does the script finds the library?

Hello Sven (@Bluesbear) :wave:

That’s wrong …

The Location has to be ~/Library/Script Libraries/ for the user and for all users on one Mac /Library/Script Libraries/. I remember that there is also the possibility for sharing Libraries beween multiple Macs, but I don’t remember the path …

Greetings from Germany :de:

Tobias

That gives an error too on Tahoe 26.3.1
image

That would prevent the script from locating Libraries inside of “/Library/Script Libraries/” which is where they are normally located. The script will only read top-level libraries, if they are in sub-folders you’ll need to add each path to the ‘libFolders’ variable.

However, your libraries appear to be in subfolders inside the ~Library/Scripts/ folder. If you do get the script to read that directory and its subfolders you’re going to load ALL SCRIPTS into this script as your libraries.

If you put your scripts that are intended to be used as libraries at the top level of ~/Library/Script Libraries/ then set systemLibFolder to “/Library/Script Libraries/” you should load them properly.

FYI ~ in file paths just indicates ( the path leading to a user folder named… ). It’s not needed in the code above defining systemLibFolder.

Another big update. This was working so well, I thought “why not also use this to search my entire code base.” Maybe it’s too big to be performant (≈100,000 lines), but I could give it a shot.
It now has two search window panes, they can be moved between with command keys. Still opens the script in Script Debugger and reveals the relevant line when chosen off the results list.
Startup time for the app is definitely slower, maybe ≈8 seconds or so now. But since I leave it open all the time except for restarts to make it re-index, that doesn’t really effect me. The actual search results through ≈100,000 lines of code is insantaneous.

Again, this copy is not adapted for general purpose use, there are a few hardcoded paths and such. Also, I store 100% of code as .applescript in git, .scpts, .apps, etc get built and deployed via script in my environment. So this is only a plain-text indexing/search. It will not OSA Decompile everything to text - if you store as .scpt, .scptd, .app, it won’t be that helpful. It could do that, but expect startup time with indexing would become painful.

Just thought I’d share for anyone else who want live code search for .applescript files.

I first made a mistake and pasted in the wrong code, then tried to edit, but go an error the post was too long. Trying again, I’ll try the code in a separate post, see if I can get it down. If not, I’ll post main body and handlers separately.

Code was too long to post. First half:

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 mySearchField : missing value
property mySegmentedControl : missing value
property windowIsOpen : true
property searchMode : 1 -- 1 = Functions, 2 = Code Search
property gitDirectory : missing value
property nsData : missing value -- pre-parsed handler list for Functions mode

-- 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

-- Locate Script Libraries by reading the git path from Push Compiled Script's ScriptDataFile
set myPath to POSIX path of (path to me)
if myPath ends with "/" then set myPath to text 1 thru -2 of myPath
set parentFolder to do shell script "dirname " & quoted form of myPath
set dataFilePath to parentFolder & "/Push Compiled Script.scptd/Contents/Resources/ScriptDataFile.txt"

try
	set dataFileAlias to (dataFilePath as POSIX file) as alias
	set userGitFolders to read dataFileAlias as list
	set userName to short user name of (system info)
	set foundGit to false
	repeat with anEntry in userGitFolders
		if item 1 of anEntry is userName then set foundGit to item 2 of anEntry
	end repeat
	if foundGit is false then error "No git path found for user \"" & userName & "\" in ScriptDataFile.txt"
	set my gitDirectory to foundGit
	set libFolder to (my gitDirectory) & "Script Libraries"
	set libAlias to (libFolder as POSIX file) as alias -- verify it exists
	if libFolder ends with "/" then set libFolder to text 1 thru -2 of libFolder
on error errMsg
	display alert "Could not locate Script Libraries:" & linefeed & errMsg buttons {"OK"} as critical
	error number -128
end try

-- Parse all libraries
set allHandlers to parseAllLibraries(libFolder)

-- Convert to NSArray of NSDictionary for Cocoa bindings
set my 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")
	(dict's setObject:(exampleCall of h) forKey:"exampleCall")
	(dict's setObject:(filePath of h) forKey:"filePath")
	((my nsData)'s addObject:dict)
end repeat

-- Build and show UI
buildUI(my nsData)

-- Hand control to Cocoa's run loop (handles all events including Dock/Cmd+Tab quit)
current application's NSApplication's sharedApplication()'s performSelector:"run"

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

on parseAllLibraries(libFolder)
	set handlerList to {}
	set whitespaceSet to current application's NSCharacterSet's whitespaceAndNewlineCharacterSet()
	
	-- Get .applescript files
	try
		set fileList to paragraphs of (do shell script "ls " & quoted form of libFolder & "/*.applescript 2>/dev/null")
	on error
		return handlerList
	end try
	
	repeat with filePath in fileList
		set filePath to filePath as text
		set libName to do shell script "basename " & quoted form of filePath & " .applescript"
		
		-- Read file via NSString for reliability
		set fileNSString to (current application's NSString's stringWithContentsOfFile:filePath encoding:(current application's NSUTF8StringEncoding) |error|:(missing value))
		if fileNSString is missing value then
			-- skip unreadable files
		else
			set fileLines to (fileNSString's componentsSeparatedByCharactersInSet:(current application's NSCharacterSet's newlineCharacterSet())) as list
			set lineCount to count of fileLines
			
			-- Forward scan: track comments as we go, assign to next handler
			set pendingComment to ""
			set inBlockComment to false
			set blockLines to {}
			
			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
				set trimmedLine to ((current application's NSString's stringWithString:thisLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
				
				if inBlockComment then
					-- Inside a (* *) block comment - check for closing *)
					if trimmedLine contains "*)" then
						-- Last line of block - strip closing *)
						set closeLine to trimmedLine
						if closeLine ends with "*)" then
							if (length of closeLine) > 2 then
								set closeLine to text 1 thru -3 of closeLine
								set closeLine to ((current application's NSString's stringWithString:closeLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
							else
								set closeLine to ""
							end if
						end if
						if closeLine is not "" then set end of blockLines to closeLine
						-- Build the pending comment from collected block lines
						set pendingComment to ""
						repeat with bLine in blockLines
							if pendingComment is not "" then set pendingComment to pendingComment & linefeed
							set pendingComment to pendingComment & (bLine as text)
						end repeat
						set inBlockComment to false
						set blockLines to {}
					else
						-- Middle line of block comment
						if trimmedLine is not "" then
							set end of blockLines to trimmedLine
						end if
					end if
					
				else if trimmedLine starts with "(*" then
					-- Start of a block comment
					set inBlockComment to true
					set blockLines to {}
					-- Strip opening (* from first line
					set firstLine to ""
					if (length of trimmedLine) > 2 then
						set firstLine to text 3 thru -1 of trimmedLine
					end if
					-- Check for single-line block comment: (* text *)
					if firstLine contains "*)" then
						if firstLine ends with "*)" then
							if (length of firstLine) > 2 then
								set firstLine to text 1 thru -3 of firstLine
							else
								set firstLine to ""
							end if
						end if
						set firstLine to ((current application's NSString's stringWithString:firstLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
						if firstLine is not "" then set pendingComment to firstLine
						set inBlockComment to false
						set blockLines to {}
					else
						set firstLine to ((current application's NSString's stringWithString:firstLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
						if firstLine is not "" then set end of blockLines to firstLine
					end if
					
				else if trimmedLine starts with "--" then
					-- Line comment: check if it's a divider or real comment
					set commentBody to ""
					if (length of trimmedLine) > 2 then set commentBody to text 3 thru -1 of trimmedLine
					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
					end if
					if not isDivider then
						-- Append to pending comment (for consecutive -- lines)
						if pendingComment is not "" then
							set pendingComment to pendingComment & linefeed & trimmedLine
						else
							set pendingComment to trimmedLine
						end if
					else
						-- Divider line clears the pending comment
						set pendingComment to ""
					end if
					
				else if trimmedLine is "" then
					-- Blank lines are OK between comment and handler, don't clear
					
				else
					-- 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 event handlers
						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
						
						if not shouldSkip and afterKeyword does not contain "(" then set shouldSkip to true
						
						if not shouldSkip then
							-- Extract handler name
							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
							set funcName to ((current application's NSString's stringWithString:funcName)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
							
							if funcName is not "" then
								-- Extract parameters
								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
								
								-- Capture inline comment on handler line
								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 pendingComment is not "" then
										set pendingComment to inlineComment & linefeed & pendingComment
									else
										set pendingComment to inlineComment
									end if
								end if
								
								-- Extract example call from comment (line after "Example:")
								set exampleCall to ""
								if pendingComment contains "Example:" then
									set commentNS to (current application's NSString's stringWithString:pendingComment)
									set commentLines to (commentNS's componentsSeparatedByString:(linefeed)) as list
									set foundExample to false
									repeat with cLine in commentLines
										set cLine to cLine as text
										set trimCLine to ((current application's NSString's stringWithString:cLine)'s stringByTrimmingCharactersInSet:whitespaceSet) as text
										if foundExample then
											if trimCLine is not "" then
												set exampleCall to trimCLine
												exit repeat
											end if
										else if trimCLine is "Example:" or trimCLine is "Example" then
											set foundExample to true
										end if
									end repeat
								end if
								
								set end of handlerList to {handlerName:funcName, params:paramStr, libraryName:libName, handlerDescription:pendingComment, exampleCall:exampleCall, filePath:filePath}
							end if
						end if
					end if
					-- Any non-blank, non-comment, non-handler line clears the pending comment
					if handlerKeyword is missing value then set pendingComment to ""
				end if
			end repeat
		end if
	end repeat
	
	return handlerList
end parseAllLibraries

on buildUI(nsData)
	set panelWidth to 800
	set panelHeight to 580 -- +30 for segmented control
	
	-- 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:("Search Scripts (" & ((nsData's |count|()) as integer) & " handlers)")
	myWindow's setFloatingPanel:true
	myWindow's setLevel:(current application's NSFloatingWindowLevel)
	myWindow's setMinSize:{500, 380}
	set contentView to myWindow's contentView()
	
	-- Segmented control at top: mode switcher ("Functions" | "Code Search")
	set mySegmentedControl to current application's NSSegmentedControl's alloc()'s initWithFrame:{{10, panelHeight - 40}, {panelWidth - 20, 24}}
	mySegmentedControl's setSegmentCount:2
	(mySegmentedControl's setLabel:"Functions" forSegment:0)
	(mySegmentedControl's setLabel:"Code Search" forSegment:1)
	(mySegmentedControl's setWidth:0 forSegment:0) -- 0 = auto/equal
	(mySegmentedControl's setWidth:0 forSegment:1)
	mySegmentedControl's setSegmentStyle:(current application's NSSegmentStyleRounded)
	mySegmentedControl's setSelectedSegment:0
	mySegmentedControl's setTarget:me
	mySegmentedControl's setAction:"modeChanged:"
	mySegmentedControl's setAutoresizingMask:(2 + 8) -- width sizable + stays at top
	contentView's addSubview:mySegmentedControl
	
	-- Search field below segmented control
	set mySearchField to current application's NSSearchField's alloc()'s initWithFrame:{{10, panelHeight - 75}, {panelWidth - 20, 26}}
	mySearchField's setPlaceholderString:"Search functions, libraries, or parameters..."
	mySearchField's setAutoresizingMask:(2 + 8) -- width sizable + stays at top
	contentView's addSubview:mySearchField
	
	-- 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:mySearchField
	
	-- "Copy Name" button (top-right, stacked above Reveal)
	set copyButton to current application's NSButton's alloc()'s initWithFrame:{{panelWidth - 140, 36}, {130, 24}}
	copyButton's setTitle:"Copy Name"
	copyButton's setBezelStyle:1 -- NSBezelStyleRounded
	copyButton's setTarget:me
	copyButton's setAction:"copyNameClicked:"
	copyButton's setAutoresizingMask:(4) -- anchored right
	contentView's addSubview:copyButton
	
	-- "Reveal In Library" button at bottom-right
	set gotoButton to current application's NSButton's alloc()'s initWithFrame:{{panelWidth - 140, 10}, {130, 24}}
	gotoButton's setTitle:"Reveal In Library"
	gotoButton's setBezelStyle:1 -- NSBezelStyleRounded
	gotoButton's setTarget:me
	gotoButton's setAction:"gotoDefinitionClicked:"
	gotoButton's setAutoresizingMask:(4) -- anchored right
	contentView's addSubview:gotoButton
	
	-- Description label at bottom (monospace, read-only, wrapping)
	set myDescriptionLabel to current application's NSTextField's alloc()'s initWithFrame:{{10, 10}, {panelWidth - 155, 55}}
	myDescriptionLabel's setEditable:false
	myDescriptionLabel's setBezeled:true
	myDescriptionLabel's setDrawsBackground:true
	myDescriptionLabel's setStringValue:"Cmd+Return: insert   Cmd+Shift+Return: reveal   Opt+Return: copy name"
	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 - 170}}
	myTableView's setUsesAlternatingRowBackgroundColors:true
	myTableView's setColumnAutoresizingStyle:(current application's NSTableViewUniformColumnAutoresizingStyle)
	
	-- Install columns for the initial (Functions) mode
	my installColumnsForMode(1)
	
	-- Delegate for selection change, double-click inserts function
	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 - 165}}
	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 mainMenu to current application's NSMenu's new()
	
	-- Edit menu (enables Cmd+A, Cmd+V, Cmd+C, Cmd+X in text fields)
	set editMenu to current application's NSMenu's alloc()'s initWithTitle:"Edit"
	editMenu's addItemWithTitle:"Cut" action:"cut:" keyEquivalent:"x"
	editMenu's addItemWithTitle:"Copy" action:"copy:" keyEquivalent:"c"
	editMenu's addItemWithTitle:"Paste" action:"paste:" keyEquivalent:"v"
	editMenu's addItemWithTitle:"Select All" action:"selectAll:" keyEquivalent:"a"
	set editMenuItem to current application's NSMenuItem's new()
	editMenuItem's setSubmenu:editMenu
	mainMenu's addItem:editMenuItem
	
	-- App menu
	set appMenu to current application's NSMenu's alloc()'s initWithTitle:"Search Scripts"
	
	-- Cmd+Return to insert selected function
	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+Shift+Return to reveal in library in Script Debugger
	set gotoItem to current application's NSMenuItem's alloc()'s initWithTitle:"Reveal in Library" action:"menuGoToDefinition:" keyEquivalent:(character id 13)
	gotoItem's setKeyEquivalentModifierMask:(1048576 + 131072) -- NSEventModifierFlagCommand + NSEventModifierFlagShift
	gotoItem's setTarget:me
	appMenu's addItem:gotoItem
	
	-- Option+Return to copy "LibraryName FunctionName" to clipboard
	set copyItem to current application's NSMenuItem's alloc()'s initWithTitle:"Copy Name" action:"menuCopyName:" keyEquivalent:(character id 13)
	copyItem's setKeyEquivalentModifierMask:(524288) -- NSEventModifierFlagOption
	copyItem's setTarget:me
	appMenu's addItem:copyItem
	
	-- 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
	
	-- Cmd+Left to switch to Functions mode
	set funcModeItem to current application's NSMenuItem's alloc()'s initWithTitle:"Functions Mode" action:"menuFunctionsMode:" keyEquivalent:(character id 63234)
	funcModeItem's setKeyEquivalentModifierMask:(1048576) -- NSEventModifierFlagCommand
	funcModeItem's setTarget:me
	appMenu's addItem:funcModeItem
	
	-- Cmd+Right to switch to Code Search mode
	set codeModeItem to current application's NSMenuItem's alloc()'s initWithTitle:"Code Search Mode" action:"menuCodeSearchMode:" keyEquivalent:(character id 63235)
	codeModeItem's setKeyEquivalentModifierMask:(1048576) -- NSEventModifierFlagCommand
	codeModeItem's setTarget:me
	appMenu's addItem:codeModeItem
	
	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 Search Scripts" action:"terminate:" keyEquivalent:"q"
	quitItem's setTarget:(current application's NSApplication's sharedApplication())
	appMenu's addItem:quitItem
	
	set appMenuItem to current application's NSMenuItem's new()
	appMenuItem's setSubmenu:appMenu
	mainMenu's addItem:appMenuItem
	current application's NSApplication's sharedApplication()'s setMainMenu:mainMenu
	
	-- Show the panel and focus the search field
	myWindow's makeKeyAndOrderFront:me
	myWindow's makeFirstResponder:mySearchField
end buildUI```

Part 2:

-- Remove existing columns and install the set appropriate for the given mode
on installColumnsForMode(aMode)
	-- Remove all existing columns
	repeat while ((myTableView's tableColumns()'s |count|()) as integer) > 0
		set existingCol to (myTableView's tableColumns()'s objectAtIndex:0)
		existingCol's unbind:"value"
		myTableView's removeTableColumn:existingCol
	end repeat
	
	if aMode is 1 then
		-- Functions mode: Function / Library / Parameters
		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
		
		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
		
		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
	else
		-- Code Search mode: File / Line / Code
		set col1 to current application's NSTableColumn's alloc()'s initWithIdentifier:"fileName"
		col1's setWidth:150
		col1's headerCell()'s setStringValue:"File"
		col1's setEditable:false
		col1's bind:"value" toObject:myArrayController withKeyPath:"arrangedObjects.fileName" options:(missing value)
		myTableView's addTableColumn:col1
		
		set col2 to current application's NSTableColumn's alloc()'s initWithIdentifier:"lineNumber"
		col2's setWidth:50
		col2's headerCell()'s setStringValue:"Line"
		col2's setEditable:false
		col2's bind:"value" toObject:myArrayController withKeyPath:"arrangedObjects.lineNumber" options:(missing value)
		myTableView's addTableColumn:col2
		
		set col3 to current application's NSTableColumn's alloc()'s initWithIdentifier:"lineText"
		col3's setWidth:560
		col3's headerCell()'s setStringValue:"Code"
		col3's setEditable:false
		col3's bind:"value" toObject:myArrayController withKeyPath:"arrangedObjects.lineText" options:(missing value)
		myTableView's addTableColumn:col3
	end if
end installColumnsForMode

-- Called on each keystroke in the search field
on searchChanged:aNotification
	set searchText to ((aNotification's object())'s stringValue()) as text
	my runSearch(searchText)
end searchChanged:

-- Runs the current search against the current mode's data source
on runSearch(searchText)
	if searchMode is 1 then
		-- Functions mode: in-memory NSPredicate filter
		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
		
		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:("Search Scripts (" & totalCount & " handlers)")
		else
			myWindow's setTitle:("Search Scripts (" & visibleCount & " of " & totalCount & ")")
		end if
	else
		-- Code Search mode: live shell grep
		myArrayController's setFilterPredicate:(missing value)
		if searchText is "" then
			myArrayController's setContent:(current application's NSMutableArray's new())
			myWindow's setTitle:"Code Search (type to search)"
		else
			my codeSearchFor(searchText)
		end if
	end if
	
	-- Only clear description in Functions mode; Code Search uses it for status/debug
	if searchMode is 1 then myDescriptionLabel's setStringValue:""
	
	-- Auto-select first row so arrow keys and Return work immediately
	set visibleCount to (myArrayController's arrangedObjects()'s |count|()) as integer
	if visibleCount > 0 then
		myTableView's selectRowIndexes:(current application's NSIndexSet's indexSetWithIndex:0) byExtendingSelection:false
		myTableView's scrollRowToVisible:0
	end if
end runSearch

-- Performs a fixed-string recursive grep over .applescript files in gitDirectory
-- Populates array controller with dicts {fileName, filePath, lineNumber, lineText}. Capped at 500 rows.
on codeSearchFor(searchText)
	set resultLimit to 500
	set results to current application's NSMutableArray's new()
	
	-- -F fixed-string, -n line numbers, -i case-insensitive, --include limits extensions, --exclude-dir skips .git
	set shellCmd to "grep -rn -F -i --include='*.applescript' --exclude-dir='.git' -- " & quoted form of searchText & " " & quoted form of gitDirectory & " | head -" & resultLimit
	set grepOutput to ""
	try
		set grepOutput to do shell script shellCmd
	on error
		set grepOutput to ""
	end try
	
	if grepOutput is not "" then
		set outNS to current application's NSString's stringWithString:grepOutput
		-- do shell script separates lines with CR, not LF; newlineCharacterSet handles CR/LF/CRLF uniformly
		-- NOTE: can't use `lines` as a var name — AppleScript reserved word (every line of ...)
		set grepLines to (outNS's componentsSeparatedByCharactersInSet:(current application's NSCharacterSet's newlineCharacterSet())) as list
		repeat with aLine in grepLines
			set aLine to aLine as text
			if aLine is not "" then
				-- Parse "<filepath>:<linenum>:<linetext>" (filepath may contain colons, but : is used with a line number so we split on first two)
				set colon1 to offset of ":" in aLine
				if colon1 > 0 then
					set restAfter1 to text (colon1 + 1) thru -1 of aLine
					set colon2 to offset of ":" in restAfter1
					if colon2 > 0 then
						set filePath to text 1 thru (colon1 - 1) of aLine
						set lineNumber to text 1 thru (colon2 - 1) of restAfter1
						set lineText to ""
						if colon2 < (length of restAfter1) then set lineText to text (colon2 + 1) thru -1 of restAfter1
						-- Trim leading whitespace from lineText for display
						set lineText to ((current application's NSString's stringWithString:lineText)'s stringByTrimmingCharactersInSet:(current application's NSCharacterSet's whitespaceAndNewlineCharacterSet())) as text
						
						-- fileName = basename of filePath minus .applescript (native NS ops, avoids per-row subshell)
						set pathNS to (current application's NSString's stringWithString:filePath)
						set fileName to ((pathNS's lastPathComponent())'s stringByDeletingPathExtension()) as text
						
						set dict to current application's NSMutableDictionary's new()
						(dict's setObject:fileName forKey:"fileName")
						(dict's setObject:filePath forKey:"filePath")
						(dict's setObject:lineNumber forKey:"lineNumber")
						(dict's setObject:lineText forKey:"lineText")
						(results's addObject:dict)
					end if
				end if
			end if
		end repeat
	end if
	
	myArrayController's setContent:results
	
	set foundCount to (results's |count|()) as integer
	if foundCount ≥ resultLimit then
		myWindow's setTitle:("Code Search (" & foundCount & "+ results, showing first " & resultLimit & ")")
	else
		myWindow's setTitle:("Code Search (" & foundCount & " results)")
	end if
end codeSearchFor

-- Called when table selection changes - shows description of selected handler / full line text
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
	if searchMode is 1 then
		set descText to (selectedDict's objectForKey:"handlerDescription") as text
		if descText is "" or descText is "missing value" then set descText to "(No description available)"
	else
		set descText to (selectedDict's objectForKey:"lineText") as text
	end if
	myDescriptionLabel's setStringValue:descText
end tableViewSelectionDidChange:

-- Segmented control action: user clicked a segment
on modeChanged:sender
	set newMode to ((sender's selectedSegment()) as integer) + 1
	my switchToMode(newMode)
end modeChanged:

-- Cmd+Left menu action
on menuFunctionsMode:sender
	my switchToMode(1)
end menuFunctionsMode:

-- Cmd+Right menu action
on menuCodeSearchMode:sender
	my switchToMode(2)
end menuCodeSearchMode:

-- Switch between Functions (1) and Code Search (2) modes
on switchToMode(newMode)
	if newMode is searchMode then return
	set my searchMode to newMode
	mySegmentedControl's setSelectedSegment:(newMode - 1)
	
	-- Swap columns and reset data source
	my installColumnsForMode(newMode)
	
	if newMode is 1 then
		myArrayController's setContent:(my nsData)
		set sortDesc to current application's NSSortDescriptor's sortDescriptorWithKey:"functionName" ascending:true selector:"caseInsensitiveCompare:"
		myArrayController's setSortDescriptors:{sortDesc}
		mySearchField's setPlaceholderString:"Search functions, libraries, or parameters..."
		myDescriptionLabel's setStringValue:"Cmd+Return: insert   Cmd+Shift+Return: reveal   Opt+Return: copy name"
	else
		myArrayController's setSortDescriptors:{}
		myArrayController's setContent:(current application's NSMutableArray's new())
		mySearchField's setPlaceholderString:"Search all .applescript files in codebase..."
		myDescriptionLabel's setStringValue:"Cmd+Shift+Return: reveal   Opt+Return: copy line   (insert disabled)"
	end if
	
	-- Re-run search in the new mode with current query
	set currentQuery to (mySearchField's stringValue()) as text
	my runSearch(currentQuery)
	
	-- Keep focus in search field
	myWindow's makeFirstResponder:mySearchField
end switchToMode

-- 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
	if searchMode is 2 then return -- insert disabled in Code Search
	my insertSelectedFunction()
end tableDoubleClicked:

-- Button click for Go to Definition
on gotoDefinitionClicked:sender
	my goToDefinition()
end gotoDefinitionClicked:

-- Cmd+Shift+Return menu action
on menuGoToDefinition:sender
	my goToDefinition()
end menuGoToDefinition:

-- Button click for Copy Name
on copyNameClicked:sender
	my copySelectedName()
end copyNameClicked:

-- Option+Return menu action
on menuCopyName:sender
	my copySelectedName()
end menuCopyName:

-- Copies "LibraryName FunctionName" (Functions mode) or matched line text (Code Search) to clipboard
on copySelectedName()
	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
	
	if searchMode is 1 then
		set funcName to (selectedDict's objectForKey:"functionName") as text
		set libName to (selectedDict's objectForKey:"libraryName") as text
		set copyText to libName & " " & funcName
	else
		set copyText to (selectedDict's objectForKey:"lineText") as text
	end if
	
	set pboard to current application's NSPasteboard's generalPasteboard()
	pboard's clearContents()
	pboard's setString:copyText forType:(current application's NSPasteboardTypeString)
	
	myDescriptionLabel's setStringValue:("Copied: " & copyText)
end copySelectedName

-- Opens the source file in Script Debugger and jumps to either the handler (Functions mode) or the line (Code Search mode)
on goToDefinition()
	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 sourceFile to (selectedDict's objectForKey:"filePath") as text
	
	try
		if searchMode is 1 then
			-- Functions mode: jump to handler signature
			set funcName to (selectedDict's objectForKey:"functionName") as text
			tell application "Script Debugger"
				activate
				set theDoc to open (sourceFile as POSIX file)
				compile theDoc
				set srcText to source text of theDoc
				set handlerSig to "on " & funcName & "("
				set charPos to offset of handlerSig in srcText
				if charPos is 0 then
					set handlerSig to "to " & funcName & "("
					set charPos to offset of handlerSig in srcText
				end if
				if charPos > 0 then
					set character range of selection of theDoc to {charPos, length of handlerSig}
				end if
			end tell
		else
			-- Code Search mode: jump to matched line number
			set lineNumStr to (selectedDict's objectForKey:"lineNumber") as text
			set targetLine to lineNumStr as integer
			tell application "Script Debugger"
				activate
				set theDoc to open (sourceFile as POSIX file)
				-- Don't compile — code may be mid-edit or contain search matches in strings that don't parse alone
				set srcText to source text of theDoc
				-- Compute character offset of start of targetLine
				set charPos to 1
				set currentLine to 1
				set srcLen to length of srcText
				set i to 1
				repeat while i ≤ srcLen and currentLine < targetLine
					if character i of srcText is return or character i of srcText is linefeed then
						set currentLine to currentLine + 1
						set charPos to i + 1
					end if
					set i to i + 1
				end repeat
				-- Find end of that line to select the whole line
				set lineEnd to charPos
				repeat while lineEnd ≤ srcLen and character lineEnd of srcText is not return and character lineEnd of srcText is not linefeed
					set lineEnd to lineEnd + 1
				end repeat
				set lineLen to lineEnd - charPos
				if lineLen < 1 then set lineLen to 1
				set character range of selection of theDoc to {charPos, lineLen}
			end tell
		end if
		-- Nudge cursor to force Script Debugger to scroll to the selection
		delay 0.1
		tell application "System Events"
			tell process "Script Debugger"
				key code 124 -- right arrow
			end tell
		end tell
	on error errMsg
		display alert "Could not open in Script Debugger:" & linefeed & errMsg buttons {"OK"} as warning
	end try
end goToDefinition

-- Inserts the selected function's tell statement at the cursor in Script Debugger
on insertSelectedFunction()
	if searchMode is 2 then return -- disabled in Code Search mode
	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
	set exampleStr to (selectedDict's objectForKey:"exampleCall") as text
	
	-- Prefer the example call from the comment block; fall back to generic tell statement
	if exampleStr is not "" and exampleStr is not "missing value" then
		set tellString to exampleStr
	else
		set tellString to "tell script \"" & libName & "\" to " & funcName & "(" & paramStr & ")"
	end if
	
	-- 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
	current application's NSApplication's sharedApplication()'s terminate:me
end windowClosing:

-- Called via NSApplicationWillTerminateNotification for all quit paths
on appQuitting:aNotification
	set my windowIsOpen to false
end appQuitting:

on quit
	set my windowIsOpen to false
	continue quit
end quit