A lot of people on OS X use TextMate, and I respect that. It’s a full featured editor which everyone loves due to it’s ‘project drawer’ which allows you to browse a directory of files right in the sidebar of the window. I have experienced a lot of pain over the years so my likes and dislikes are more at a ‘file’ level than a ‘project’ level. I want an editor, that will read mixed line endings and, if I add a line, will not reformat the line endings in a file. It must be able to switch character encodings at will. It must be stable, it can’t consume a large memory footprint over time and it must open large files. All of these things have led me to SubEthaEdit, which I have been using since the days it was known as ‘Hydra’ It’s a fantastic editor which, through applescript, may be customized as you see fit.
I like SEE’s file-oriented flow and think ‘project managers’ in a text editor is cruft. That being said, when it comes time to compile, this is a *project* oriented task. I’d like, for example, to be able to compile the entire java source to run the class I was just working on.
I realized that, each file is a resident of whatever the source root is, and that the root directory is, by convention, one level above that. Using this knowledge, we can create a set of Applescript targets to work with a build script or to work via the command line on the whole project.
First, we’ll need some support functions:
getParentPath takes a path and climbs up one directory then returns the resulting path, we use this function to walk up directories without using expensive finder operations
on getParentPath(myPath)
(* Andy Bachorski <andyb@APPLE.COM>, with a small modification by Abbey Hawk Sparrow *)
set oldDelimiters to AppleScript's text item delimiters -- always preserve original delimiters
set AppleScript's text item delimiters to {"/"}
set pathItems to text items of (myPath as text)
if last item of pathItems is "" then set pathItems to items 1 thru -2 of pathItems -- its a folder
set parentPath to ((reverse of the rest of reverse of pathItems) as string) & "/"
set AppleScript's text item delimiters to oldDelimiters -- always restore original delimiters
return parentPath
end getParentPath
executeInShell is a wrapper for shell execution that allows output display and logging, etc.
on executeInShell(commandString)
tell application "System Events" to set terminalRunning to ((application processes whose (name is equal to "Terminal")) count)
if outputMode is equal to "verbose" then
if terminalRunning is equal to 0 then
do shell script "open -a Terminal"
end if
tell application "Terminal"
return do script commandString
end tell
else
if outputMode is equal to "logged" then
if terminalRunning is equal to 0 then
do shell script "open -a Terminal"
end if
--empty the last log
try
set log_file_name to rootProjectPath & "run.log"
set log_file to open for access POSIX file log_file_name with write permission
write "" to log_file starting at 0
close access log_file
end try
--open the terminal and tail the file
tell application "Terminal"
do script "tail -c+0 -f " & rootProjectPath & "run.log"
end tell
--do the actual command and trap it's errors
try
set commandResult to do shell script commandString
on error errorString number errorNumber
set commandResult to errorString
end try
--log the results of the command
try
set log_file_name to rootProjectPath & "run.log"
set log_file to open for access POSIX file log_file_name with write permission
write convertMacToUnixLineEndings(commandResult) to log_file starting at 0
close access log_file
end try
return commandResult
else
return do shell script commandString
end if
end if
end executeInShell
convertMacToUnixLineEndings is simply a passthrough conversion
on convertMacToUnixLineEndings(theText)
return replace_string(theText, "
", "
")
end convertMacToUnixLineEndings
statusUpdate This is an alert passthrough which supports growl
on statusUpdate(statusHeader, statusText)
tell application "System Events" to set GrowlRunning to ((application processes whose (name is equal to "GrowlHelperApp")) count)
if GrowlRunning is equal to 0 then
tell application "SubEthaEdit"
display dialog "Project Built." buttons "OK" default button 1 giving up after 10
end tell
else
tell application "GrowlHelperApp"
set the allNotificationsList to {"Build Notification", "Error"}
set the enabledNotificationsList to {"Build Notification"}
register as application "SEE Build Tools" all notifications allNotificationsList default notifications enabledNotificationsList icon of application "SubEthaEdit"
notify with name "Build Notification" title statusHeader description statusText application name "SEE Build Tools"
end tell
end if
end statusUpdate
appendTextToFileis a text aggregator we use for logging
on appendTextToFile(theText, filePath)
try
set log_file_name to filePath
set log_file to open for access POSIX file log_file_name with write permission
write theText to log_file starting at eof
close access log_file
end try
end appendTextToFile
We also need some string management functions, luckily some scripts by Bill Hernandez fit the bill
(* code by Bill Hernandez *)
on implode(aInputArray, delim)
-- join elements in an array --> string
local aInputArray, delim, result_string
--if delim is "" then set delim to ","
set {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delim}
try
set result_string to "" & aInputArray
end try
set AppleScript's text item delimiters to ASTID
return result_string --> text
end implode
(* code by Bill Hernandez *)
on explode(str2split, delim)
-- split a string --> elements in an array
local aResult
if delim is "" then set delim to ","
set {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delim}
try
set aResult to text items of str2split
end try
set AppleScript's text item delimiters to ASTID
return aResult --> list
end explode
(* code by Bill Hernandez *)
on replace_string(str, str2find, str2replace)
local input_string, delim, aArray, result_string
set cf to class of str2find
set cr to class of str2replace
set delim to str2find
set aArray to my explode(str, delim)
set delim to str2replace
set result_string to my implode(aArray, delim)
return result_string
end replace_string
getFilesOfTypeWithContent acts as a file filter
on getFilesOfTypeWithContent(fileType, content, root)
--here we search for all files that have a 'main' function
set scanCommand to "grep -R " & quote & content & quote & " Source|sed -e " & quote & "s/^.*\\.svn.*$//g" & quote & "|sed '/^$/d'|sed " & quote & "s/:.*$//g" & quote
set scanResult to (do shell script "cd " & root & "; " & scanCommand)
set AppleScript's text item delimiters to "
"
set scanResults to text items of scanResult
set AppleScript's text item delimiters to {""}
return scanResults
end getFilesOfTypeWithContent
fileExists allows us to check file existence without involving the finder
on fileExists(filePath)
try
set result to do shell script "ls " & filePath
if result is equal to "" then
return false
else
return true
end if
on error
return false
end try
end fileExists
Next we need to get the path naming conventions for the project so we can figure out what is root
on getSourcePathConvention(filePath)
if "/Source/" is in (the POSIX path of filePath) then
return "Source"
end if
if "/source/" is in (the POSIX path of filePath) then
return "source"
end if
if "/src/" is in (the POSIX path of filePath) then
return "src"
end if
end getSourcePathConvention
on getLibraryPathConvention(rootProjectPath)
if (fileExists(rootProjectPath & "Library") is equal to true) then
return "Library"
end if
if (fileExists(rootProjectPath & "Lib") is equal to true) then
return "Lib"
end if
if (fileExists(rootProjectPath & "lib") is equal to true) then
return "lib"
end if
end getLibraryPathConvention
on getCompilePathConvention(rootProjectPath)
if (fileExists(rootProjectPath & "Classes") is equal to true) then
return "Classes"
end if
if (fileExists(rootProjectPath & "classes") is equal to true) then
return "classes"
end if
if (fileExists(rootProjectPath & "cls") is equal to true) then
return "cls"
end if
return ""
end getCompilePathConvention
getProjectRoot actually walks up the path and determines what is root by keeping going until the source root is not in the path
on getProjectRoot(filePath, sourceFolderName)
repeat until "/" & sourceFolderName & "/" is not in (the POSIX path of filePath)
if filePath is equal to "" then return ""
if filePath is equal to "/" then return ""
set filePath to getParentPath(filePath)
end repeat
return filePath
end getProjectRoot
derivePackagereturns the package for a given source file based on the directory layout
on derivePackage(sourceFolder, filePath)
--set pathString to filePath & ""
set pathString to replace_string(filePath, "!@#@#@!", "") -- replace nothing
set pathString to replace_string(pathString, "Source/", "")
--set pathString to characters (length of sourceFolder) thru 15 of pathString
set pathString to replace_string(pathString, ".java", "")
set pathString to replace_string(pathString, "/", ".")
return pathString
end derivePackage
buildClasspathFromLibDirectory uses the contents of the lib directory to make a classpath for commandline execution
on buildClasspathFromLibDirectory(libraryPath, ClassDirectory)
set directoryItems to directoryListing(libraryPath)
set classpath to ClassDirectory & ":" & libraryPath & "/" & implode(directoryItems, ":" & libraryPath & "/")
return classpath
end buildClasspathFromLibDirectory
directoryListing returns the list of files in a directory.. you guessed it, without interacting with the finder
on directoryListing(directoryPath)
return explode(do shell script "ls " & directoryPath, "
")
end directoryListing
Last we need our actual compile and run wrappers
on compileJavaSourceDirectory(rootDirectory, sourceDirectory, destinationDirectory, sourceFile, classpath)
set compileCommand to "javac -cp " & quote & classpath & quote & " -sourcepath " & quote & sourceDirectory & quote & " -d " & quote & destinationDirectory & quote & " " & sourceFile
--appendTextToFile(compileCommand, rootDirectory & "compile_command.txt")
try
set resultText to executeInShell(compileCommand)
on error errorString number errorNumber
set resultText to errorString
return resultText
end try
return resultText
end compileJavaSourceDirectory
on runJavaSourceDirectory(rootDirectory, sourceDirectory, destinationDirectory, mainClass, classpath)
set compileCommand to "java -cp " & quote & classpath & quote & " " & mainClass
--appendTextToFile(compileCommand, rootDirectory & "compile_command.txt")
try
set resultText to executeInShell(compileCommand)
on error errorString number errorNumber
set resultText to errorString
end try
return resultText
end runJavaSourceDirectory
This allows us to (at long last) build a straightforward script to power this action in SEE
global rootProjectPath
global outputMode
set outputMode to "silent"
--make sure we have a valid SubEthaEdit context within which to work
set theSourcePath to getValidSEEContext()
--get the name of the folder the source is stored in (which can only occur once in it's path)
set sourceFolderName to getSourcePathConvention(theSourcePath) as string
if sourceFolderName is not equal to "" then
--set a bunch of paths and folder names
set rootProjectPath to getProjectRoot(theSourcePath, sourceFolderName)
set compileFolderName to getCompilePathConvention(rootProjectPath)
set libraryFolderName to getLibraryPathConvention(rootProjectPath)
set compileFolderPath to rootProjectPath & compileFolderName
set sourceFolderPath to rootProjectPath & sourceFolderName
--find all the files in the source directory which have a main class
set fileList to getFilesOfTypeWithContent("java", "public static void main", rootProjectPath)
--allow the user to select the class
set selectedFile to item 1 of {choose from list fileList with prompt "Pick your Main Class:" without multiple selections allowed}
--set selectedFile to item 1 of selectedFiles
if selectedFile is not equal to false then
set mainClass to derivePackage(sourceFolderName, selectedFile)
set classpath to buildClasspathFromLibDirectory(rootProjectPath & libraryFolderName, rootProjectPath & compileFolderName)
set compileResults to compileJavaSourceDirectory(rootProjectPath, sourceFolderPath, compileFolderPath, theSourcePath, classpath)
appendTextToFile(compileResults, rootProjectPath & "build.log")
if compileResults is not equal to "" then
do shell script "open " & rootProjectPath & "build.log"
statusUpdate("Build Failure", "Compile Failed")
else
statusUpdate("Build Successful", "Compile Complete")
set outputMode to "logged"
set compileResults to runJavaSourceDirectory(rootProjectPath, sourceFolderPath, compileFolderPath, mainClass, classpath)
end if
end if
else
statusUpdate("Build Failure", "Nothing to do")
end if
-- SEE Configuration
on seescriptsettings()
return {displayName:"Build and Run", shortDisplayName:"Build/Run", keyboardShortcut:"^@r", toolbarIcon:"ToolbarIconBuildAndRun", inDefaultToolbar:"yes", toolbarTooltip:"Build and Run", inContextMenu:"yes"}
end seescriptsettings
That should give you a pretty good idea of how to do that for *any* language. Cheers.
Hi Abbey
First of, great article!
If you like SubEthaEdit you should check out my scripts collection over at http://christoffer.winterkvist.com/groups/christofferwinterkvist/wiki/743d7/SubEthaEdit__Scripts.html
I’ve been working on the for some time and I think they help to extend the app quite a lot.
You can also watch the scripts in action at my youtube channel.
http://www.youtube.com/user/oprahnoodle?feature=mhw5
If you have any questions or suggestions you can just email me at christoffer [at] winterkvist [dot] com
Have a good one!