{"id":81,"date":"2010-04-13T09:00:25","date_gmt":"2010-04-13T16:00:25","guid":{"rendered":"http:\/\/patternweaver.com\/blog\/?p=81"},"modified":"2010-04-13T09:00:25","modified_gmt":"2010-04-13T16:00:25","slug":"using-applescript-to-enable-projects-in-subethaedit","status":"publish","type":"post","link":"http:\/\/patternweaver.com\/blog\/2010\/04\/using-applescript-to-enable-projects-in-subethaedit\/","title":{"rendered":"Using AppleScript to enable projects in SubEthaEdit"},"content":{"rendered":"<p>A lot of people on OS X use TextMate, and I respect that. It&#8217;s a full featured editor which everyone loves due to it&#8217;s &#8216;project drawer&#8217; 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 &#8216;file&#8217; level than a &#8216;project&#8217; 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&#8217;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 &#8216;Hydra&#8217; It&#8217;s a fantastic editor which, through applescript, may be customized as you see fit.<\/p>\n<p>I like SEE&#8217;s file-oriented flow and think &#8216;project managers&#8217; in a text editor is cruft. That being said, when it comes time to compile, this is a *project* oriented task. I&#8217;d like, for example, to be able to compile the entire java source to run the class I was just working on.<\/p>\n<p>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.<\/p>\n<p>First, we&#8217;ll need some support functions:<\/p>\n<p><b>getParentPath<\/b> 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<\/p>\n<pre><code>on getParentPath(myPath)\n\t(* Andy Bachorski &lt;andyb@APPLE.COM&gt;, with a small modification by Abbey Hawk Sparrow *)\n\tset oldDelimiters to AppleScript's text item delimiters -- always preserve original delimiters\n\tset AppleScript's text item delimiters to {\"\/\"}\n\tset pathItems to text items of (myPath as text)\n\tif last item of pathItems is \"\" then set pathItems to items 1 thru -2 of pathItems -- its a folder\n\tset parentPath to ((reverse of the rest of reverse of pathItems) as string) &amp; \"\/\"\n\tset AppleScript's text item delimiters to oldDelimiters -- always restore original delimiters\n\treturn parentPath\nend getParentPath<\/code><\/pre>\n<p><b>executeInShell<\/b> is a wrapper for shell execution that allows output display and logging, etc.<\/p>\n<pre><code>on executeInShell(commandString)\n\ttell application \"System Events\" to set terminalRunning to ((application processes whose (name is equal to \"Terminal\")) count)\n\tif outputMode is equal to \"verbose\" then\n\t\tif terminalRunning is equal to 0 then\n\t\t\tdo shell script \"open -a Terminal\"\n\t\tend if\n\t\ttell application \"Terminal\"\n\t\t\treturn do script commandString\n\t\tend tell\n\telse\n\t\tif outputMode is equal to \"logged\" then\n\t\t\tif terminalRunning is equal to 0 then\n\t\t\t\tdo shell script \"open -a Terminal\"\n\t\t\tend if\n\t\t\t--empty the last log\n\t\t\ttry\n\t\t\t\tset log_file_name to rootProjectPath &amp; \"run.log\"\n\t\t\t\tset log_file to open for access POSIX file log_file_name with write permission\n\t\t\t\twrite \"\" to log_file starting at 0\n\t\t\t\tclose access log_file\n\t\t\tend try\n\t\t\t--open the terminal and tail the file\n\t\t\ttell application \"Terminal\"\n\t\t\t\tdo script \"tail -c+0 -f \" &amp; rootProjectPath &amp; \"run.log\"\n\t\t\tend tell\n\t\t\t--do the actual command and trap it's errors\n\t\t\ttry\n\t\t\t\tset commandResult to do shell script commandString\n\t\t\ton error errorString number errorNumber\n\t\t\t\tset commandResult to errorString\n\t\t\tend try\n\t\t\t--log the results of the command\n\t\t\ttry\n\t\t\t\tset log_file_name to rootProjectPath &amp; \"run.log\"\n\t\t\t\tset log_file to open for access POSIX file log_file_name with write permission\n\t\t\t\twrite convertMacToUnixLineEndings(commandResult) to log_file starting at 0\n\t\t\t\tclose access log_file\n\t\t\tend try\n\t\t\treturn commandResult\n\t\telse\n\t\t\treturn do shell script commandString\n\t\tend if\n\tend if\nend executeInShell<\/code><\/pre>\n<p><b>convertMacToUnixLineEndings<\/b> is simply a passthrough conversion<\/p>\n<pre><code>on convertMacToUnixLineEndings(theText)\n\treturn replace_string(theText, \"\n\", \"\n\")\nend convertMacToUnixLineEndings<\/code><\/pre>\n<p><b>statusUpdate<\/b> This is an alert passthrough which supports growl<\/p>\n<pre><code>on statusUpdate(statusHeader, statusText)\n\ttell application \"System Events\" to set GrowlRunning to ((application processes whose (name is equal to \"GrowlHelperApp\")) count)\n\tif GrowlRunning is equal to 0 then\n\t\ttell application \"SubEthaEdit\"\n\t\t\tdisplay dialog \"Project Built.\" buttons \"OK\" default button 1 giving up after 10\n\t\tend tell\n\telse\n\t\ttell application \"GrowlHelperApp\"\n\t\t\tset the allNotificationsList to {\"Build Notification\", \"Error\"}\n\t\t\tset the enabledNotificationsList to {\"Build Notification\"}\n\t\t\tregister as application \"SEE Build Tools\" all notifications allNotificationsList default notifications enabledNotificationsList icon of application \"SubEthaEdit\"\n\t\t\tnotify with name \"Build Notification\" title statusHeader description statusText application name \"SEE Build Tools\"\n\t\tend tell\n\tend if\nend statusUpdate<\/code><\/pre>\n<p><b>appendTextToFile<\/b>is a text aggregator we use for logging<\/p>\n<pre><code>on appendTextToFile(theText, filePath)\n\ttry\n\t\tset log_file_name to filePath\n\t\tset log_file to open for access POSIX file log_file_name with write permission\n\t\twrite theText to log_file starting at eof\n\t\tclose access log_file\n\tend try\nend appendTextToFile<\/code><\/pre>\n<p>We also need some string management functions, luckily some scripts by Bill Hernandez fit the bill<\/p>\n<pre><code>(* code by Bill Hernandez *)\non implode(aInputArray, delim)\n\t-- join elements in an array --&gt; string\n\tlocal aInputArray, delim, result_string\n\t--if delim is \"\" then set delim to \",\"\n\n\tset {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delim}\n\ttry\n\t\tset result_string to \"\" &amp; aInputArray\n\tend try\n\tset AppleScript's text item delimiters to ASTID\n\treturn result_string --&gt; text\nend implode\n\n(* code by Bill Hernandez *)\non explode(str2split, delim)\n\t-- split a string --&gt; elements in an array\n\tlocal aResult\n\tif delim is \"\" then set delim to \",\"\n\n\tset {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delim}\n\ttry\n\t\tset aResult to text items of str2split\n\tend try\n\tset AppleScript's text item delimiters to ASTID\n\treturn aResult --&gt; list\nend explode\n\n(* code by Bill Hernandez *)\non replace_string(str, str2find, str2replace)\n\tlocal input_string, delim, aArray, result_string\n\n\tset cf to class of str2find\n\tset cr to class of str2replace\n\tset delim to str2find\n\tset aArray to my explode(str, delim)\n\n\tset delim to str2replace\n\tset result_string to my implode(aArray, delim)\n\treturn result_string\nend replace_string<\/code><\/pre>\n<p><b>getFilesOfTypeWithContent<\/b> acts as a file filter<\/p>\n<pre><code>on getFilesOfTypeWithContent(fileType, content, root)\n\t--here we search for all files that have a 'main' function\n\tset scanCommand to \"grep -R \" &amp; quote &amp; content &amp; quote &amp; \" Source|sed -e \" &amp; quote &amp; \"s\/^.*\\\\.svn.*$\/\/g\" &amp; quote &amp; \"|sed '\/^$\/d'|sed \" &amp; quote &amp; \"s\/:.*$\/\/g\" &amp; quote\n\tset scanResult to (do shell script \"cd \" &amp; root &amp; \"; \" &amp; scanCommand)\n\tset AppleScript's text item delimiters to \"\n\"\n\tset scanResults to text items of scanResult\n\tset AppleScript's text item delimiters to {\"\"}\n\treturn scanResults\nend getFilesOfTypeWithContent<\/code><\/pre>\n<p><b>fileExists<\/b> allows us to check file existence without involving the finder<\/p>\n<pre><code>on fileExists(filePath)\n\ttry\n\t\tset result to do shell script \"ls \" &amp; filePath\n\t\tif result is equal to \"\" then\n\t\t\treturn false\n\t\telse\n\t\t\treturn true\n\t\tend if\n\ton error\n\t\treturn false\n\tend try\nend fileExists<\/code><\/pre>\n<p>Next we need to get the path naming conventions for the project so we can figure out what is root<\/p>\n<pre><code>on getSourcePathConvention(filePath)\n\tif \"\/Source\/\" is in (the POSIX path of filePath) then\n\t\treturn \"Source\"\n\tend if\n\tif \"\/source\/\" is in (the POSIX path of filePath) then\n\t\treturn \"source\"\n\tend if\n\tif \"\/src\/\" is in (the POSIX path of filePath) then\n\t\treturn \"src\"\n\tend if\nend getSourcePathConvention\n\non getLibraryPathConvention(rootProjectPath)\n\tif (fileExists(rootProjectPath &amp; \"Library\") is equal to true) then\n\t\treturn \"Library\"\n\tend if\n\tif (fileExists(rootProjectPath &amp; \"Lib\") is equal to true) then\n\t\treturn \"Lib\"\n\tend if\n\tif (fileExists(rootProjectPath &amp; \"lib\") is equal to true) then\n\t\treturn \"lib\"\n\tend if\nend getLibraryPathConvention\n\non getCompilePathConvention(rootProjectPath)\n\tif (fileExists(rootProjectPath &amp; \"Classes\") is equal to true) then\n\t\treturn \"Classes\"\n\tend if\n\tif (fileExists(rootProjectPath &amp; \"classes\") is equal to true) then\n\t\treturn \"classes\"\n\tend if\n\tif (fileExists(rootProjectPath &amp; \"cls\") is equal to true) then\n\t\treturn \"cls\"\n\tend if\n\treturn \"\"\nend getCompilePathConvention<\/code><\/pre>\n<p><b>getProjectRoot<\/b> actually walks up the path and determines what is root by keeping going until the source root is not in the path<\/p>\n<pre><code>on getProjectRoot(filePath, sourceFolderName)\n\trepeat until \"\/\" &amp; sourceFolderName &amp; \"\/\" is not in (the POSIX path of filePath)\n\t\tif filePath is equal to \"\" then return \"\"\n\t\tif filePath is equal to \"\/\" then return \"\"\n\t\tset filePath to getParentPath(filePath)\n\tend repeat\n\treturn filePath\nend getProjectRoot<\/code><\/pre>\n<p><b>derivePackage<\/b>returns the package for a given source file based on the directory layout<\/p>\n<pre><code>on derivePackage(sourceFolder, filePath)\n\t--set pathString to filePath &amp; \"\"\n\tset pathString to replace_string(filePath, \"!@#@#@!\", \"\") -- replace nothing\n\tset pathString to replace_string(pathString, \"Source\/\", \"\")\n\t--set pathString to characters (length of sourceFolder) thru 15 of pathString\n\tset pathString to replace_string(pathString, \".java\", \"\")\n\tset pathString to replace_string(pathString, \"\/\", \".\")\n\treturn pathString\nend derivePackage<\/code><\/pre>\n<p><b>buildClasspathFromLibDirectory<\/b> uses the contents of the lib directory to make a classpath for commandline execution<\/p>\n<pre><code>on buildClasspathFromLibDirectory(libraryPath, ClassDirectory)\n\tset directoryItems to directoryListing(libraryPath)\n\tset classpath to ClassDirectory &amp; \":\" &amp; libraryPath &amp; \"\/\" &amp; implode(directoryItems, \":\" &amp; libraryPath &amp; \"\/\")\n\treturn classpath\nend buildClasspathFromLibDirectory<\/code><\/pre>\n<p><b>directoryListing<\/b> returns the list of files in a directory.. you guessed it, without interacting with the finder<\/p>\n<pre><code>on directoryListing(directoryPath)\n\treturn explode(do shell script \"ls \" &amp; directoryPath, \"\n\")\nend directoryListing<\/code><\/pre>\n<p>Last we need our actual compile and run wrappers<\/p>\n<pre><code>on compileJavaSourceDirectory(rootDirectory, sourceDirectory, destinationDirectory, sourceFile, classpath)\n\tset compileCommand to \"javac -cp \" &amp; quote &amp; classpath &amp; quote &amp; \" -sourcepath \" &amp; quote &amp; sourceDirectory &amp; quote &amp; \" -d \" &amp; quote &amp; destinationDirectory &amp; quote &amp; \" \" &amp; sourceFile\n\t--appendTextToFile(compileCommand, rootDirectory &amp; \"compile_command.txt\")\n\ttry\n\t\tset resultText to executeInShell(compileCommand)\n\ton error errorString number errorNumber\n\t\tset resultText to errorString\n\t\treturn resultText\n\tend try\n\treturn resultText\nend compileJavaSourceDirectory\n\non runJavaSourceDirectory(rootDirectory, sourceDirectory, destinationDirectory, mainClass, classpath)\n\tset compileCommand to \"java -cp \" &amp; quote &amp; classpath &amp; quote &amp; \" \" &amp; mainClass\n\t--appendTextToFile(compileCommand, rootDirectory &amp; \"compile_command.txt\")\n\ttry\n\t\tset resultText to executeInShell(compileCommand)\n\ton error errorString number errorNumber\n\t\tset resultText to errorString\n\tend try\n\treturn resultText\nend runJavaSourceDirectory<\/code><\/pre>\n<p>This allows us to (at long last) build a straightforward script to power this action in SEE<\/p>\n<pre><code>global rootProjectPath\nglobal outputMode\nset outputMode to \"silent\"\n\n--make sure we have a valid SubEthaEdit context within which to work\nset theSourcePath to getValidSEEContext()\n--get the name of the folder the source is stored in (which can only occur once in it's path)\nset sourceFolderName to getSourcePathConvention(theSourcePath) as string\nif sourceFolderName is not equal to \"\" then\n\t--set a bunch of paths and folder names\n\tset rootProjectPath to getProjectRoot(theSourcePath, sourceFolderName)\n\tset compileFolderName to getCompilePathConvention(rootProjectPath)\n\tset libraryFolderName to getLibraryPathConvention(rootProjectPath)\n\tset compileFolderPath to rootProjectPath &amp; compileFolderName\n\tset sourceFolderPath to rootProjectPath &amp; sourceFolderName\n\t--find all the files in the source directory which have a main class\n\tset fileList to getFilesOfTypeWithContent(\"java\", \"public static void main\", rootProjectPath)\n\t--allow the user to select the class\n\tset selectedFile to item 1 of {choose from list fileList with prompt \"Pick your Main Class:\" without multiple selections allowed}\n\t--set selectedFile to item 1 of selectedFiles\n\n\tif selectedFile is not equal to false then\n\t\tset mainClass to derivePackage(sourceFolderName, selectedFile)\n\t\tset classpath to buildClasspathFromLibDirectory(rootProjectPath &amp; libraryFolderName, rootProjectPath &amp; compileFolderName)\n\t\tset compileResults to compileJavaSourceDirectory(rootProjectPath, sourceFolderPath, compileFolderPath, theSourcePath, classpath)\n\t\tappendTextToFile(compileResults, rootProjectPath &amp; \"build.log\")\n\t\tif compileResults is not equal to \"\" then\n\t\t\tdo shell script \"open \" &amp; rootProjectPath &amp; \"build.log\"\n\t\t\tstatusUpdate(\"Build Failure\", \"Compile Failed\")\n\t\telse\n\t\t\tstatusUpdate(\"Build Successful\", \"Compile Complete\")\n\t\t\tset outputMode to \"logged\"\n\t\t\tset compileResults to runJavaSourceDirectory(rootProjectPath, sourceFolderPath, compileFolderPath, mainClass, classpath)\n\t\tend if\n\tend if\nelse\n\tstatusUpdate(\"Build Failure\", \"Nothing to do\")\nend if\n\n-- SEE Configuration\non seescriptsettings()\n\treturn {displayName:\"Build and Run\", shortDisplayName:\"Build\/Run\", keyboardShortcut:\"^@r\", toolbarIcon:\"ToolbarIconBuildAndRun\", inDefaultToolbar:\"yes\", toolbarTooltip:\"Build and Run\", inContextMenu:\"yes\"}\nend seescriptsettings\n<\/code><\/pre>\n<p>That should give you a pretty good idea of how to do that for *any* language. Cheers.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A lot of people on OS X use TextMate, and I respect that. It&#8217;s a full featured editor which everyone loves due to it&#8217;s &#8216;project drawer&#8217; 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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-81","post","type-post","status-publish","format-standard","hentry","category-applescript"],"_links":{"self":[{"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/posts\/81","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/comments?post=81"}],"version-history":[{"count":0,"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/posts\/81\/revisions"}],"wp:attachment":[{"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/media?parent=81"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/categories?post=81"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/patternweaver.com\/blog\/wp-json\/wp\/v2\/tags?post=81"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}