As noted below, the entries in the LaTeX mode menu for SubEthaEdit need to be put into a sensible order. After some thinking, I decided that there are really three groups of scripts: scripts for interacting with the LaTeX system, scripts for simplifying typing, and a script for interacting with the LaTeX mode itself.
Ideally, we'd put the three into groups divided by a horizontal rule. Unfortunately, SEE does not (yet) allow dividing lines to be inserted into the menu. Regardless, let's organize the scripts into the three groups, and - with one exception- just alphabetize within the groups. The exception is for the "Typeset and View" menu item; it strikes me as natural to put that first in the list.
End result is a menu ordered as:
Typeset and View
Clean Up Auxiliary Files
Run BibTeX
View
Complete Citation
Inline Math
Insert Environment...
Un/Comment Selected Lines
Customize Mode...
Although I've broken the groups apart with spacing, they'll just run together in the menu. Perhaps a later version of SEE will allow some more structure to be added.
One final change is that I've renamed the "Mode Environment..." menu item to "Customize Mode...". The two distinct meanings of "environment" struck me as confusing, and "Insert Environment..." is simply too appropriate for LaTeX to change.
I'll add another entry to the menu as soon as possible. It will go into the third group, with a title something like "Mode Help". I still have to write the mode help, first.
Monday, January 7, 2008
SEEing LaTeX 25: Putting Things in Order
Sunday, January 6, 2008
SEEing LaTeX 24: For Completeness, BibTeX
I guess the right solution is to add a menu item for running
Here's the AppleScript:
And here's the shell script that it calls:
bibtex
, and that's it. Since I don't use makeindex
myself, I'll leave that aside, unless someone wants to contribute scripts or just examples of use. I'd guess that the scripts would be easy enough, just adapt the ones I'll present for BibTeX. Here's the AppleScript:
checkSaveStatus without updating
set bibScript to join of {modeEnvironment(), quotedForm for "$SEE_MODE_RESOURCES/bin/runbibtex.sh", quotedForm for documentPath()} by space
do shell script bibScript
on seescriptsettings()
return {displayName:"Run BibTeX"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
set bibScript to join of {modeEnvironment(), quotedForm for "$SEE_MODE_RESOURCES/bin/runbibtex.sh", quotedForm for documentPath()} by space
do shell script bibScript
on seescriptsettings()
return {displayName:"Run BibTeX"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
And here's the shell script that it calls:
#!/bin/sh
#$Id: runbibtex.sh,v 1.1 2008/01/06 19:09:49 mjb Exp $
PATH="$PATH:/usr/texbin:/usr/local/bin"
export PATH
BIBTEX=${SEE_BIBTEX:-'bibtex "$(basename $FILE .tex)"'}
FILE="$(basename "$1")"
DIRNAME="$(dirname "$1")"
cd "$DIRNAME"
eval $BIBTEX
#$Id: runbibtex.sh,v 1.1 2008/01/06 19:09:49 mjb Exp $
PATH="$PATH:/usr/texbin:/usr/local/bin"
export PATH
BIBTEX=${SEE_BIBTEX:-'bibtex "$(basename $FILE .tex)"'}
FILE="$(basename "$1")"
DIRNAME="$(dirname "$1")"
cd "$DIRNAME"
eval $BIBTEX
Labels:
AppleScript,
BibTeX,
LaTeX,
SEEing LaTeX,
shell,
SubEthaEdit
SEEing LaTeX 23: Am I Done?
Last July, I listed some desirable features for the LaTeX mode for SubEthaEdit. The mentioned features were integrating with a PDF viewer using pdfsync, enabling insertion of citation keys in bibtex format, allow typesetting by calling
I've accomplished all of that. As well, I have introduced a mechanism for customizing the shell script environment for the mode and have developed a set of AppleScript handlers useful for scripting SEE. I'm quite pleased with how all of that has worked out. Not only do I now have a LaTeX mode that covers my main needs, but I've got a solid foundation on which I - and hopefully others! - can build support scripts for other modes.
That said, the LaTeX mode is not quite finished. It's clear that I should write some documentation, and add a "Mode Help" menu item. I should also better document SubEthaEditTools. Beyond that, there are two additional tasks.
First, the scripts in the LaTeX mode menu should be put into some sort of reasonable order, rather than the haphazard order that currently is there. The items appear in order based on the names of the scripts, which can differ from the entry in the menu. What is the right order to use?
Second, I've omitted some important elements of a LaTeX system, because
pdflatex
from within SEE, cleaning up auxiliary files, and commenting out selected lines. I also mentioned that it might be nice, if inessential, to be able to insert environments and formatting. I've accomplished all of that. As well, I have introduced a mechanism for customizing the shell script environment for the mode and have developed a set of AppleScript handlers useful for scripting SEE. I'm quite pleased with how all of that has worked out. Not only do I now have a LaTeX mode that covers my main needs, but I've got a solid foundation on which I - and hopefully others! - can build support scripts for other modes.
That said, the LaTeX mode is not quite finished. It's clear that I should write some documentation, and add a "Mode Help" menu item. I should also better document SubEthaEditTools. Beyond that, there are two additional tasks.
First, the scripts in the LaTeX mode menu should be put into some sort of reasonable order, rather than the haphazard order that currently is there. The items appear in order based on the names of the scripts, which can differ from the entry in the menu. What is the right order to use?
Second, I've omitted some important elements of a LaTeX system, because
latexmk
handles them for me. For example, I have not provided a way to run bibtex
. Should additional elements be added? If so, which? bibtex
? makeindex
? Something else?Saturday, January 5, 2008
SEEing LaTeX 22: Inline Math
Environments aren't the only thing common LaTeX constructs that are awkward to type. The delimiters for inline math are pretty awkward, too. Let's add those, too:
set mathText to selectionText()
set wrappedText to "\\( " & mathText & " \\)"
setSelectionText to wrappedText
if (length of mathText) equals 0 then
set {startChar, nextChar} to selectionRange without extendingFront or extendingEnd
setSelectionRange to (startChar + length of mathText + 3)
end if
on seescriptsettings()
{displayName:"Inline Math", keyboardShortcut:"@~^m", inContextMenu:"yes"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
set wrappedText to "\\( " & mathText & " \\)"
setSelectionText to wrappedText
if (length of mathText) equals 0 then
set {startChar, nextChar} to selectionRange without extendingFront or extendingEnd
setSelectionRange to (startChar + length of mathText + 3)
end if
on seescriptsettings()
{displayName:"Inline Math", keyboardShortcut:"@~^m", inContextMenu:"yes"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
Labels:
AppleScript,
LaTeX,
SEEing LaTeX,
SubEthaEdit
SEEing LaTeX 21: Inserting Environments
Environments again? Yes, but this time we're going to look at environments in LaTeX, not the shell environment. Environments are a bit of a pain to type, but the repetitive structure makes them suitable to automation: we just get the name of the environment, and then have a more-or-less standard form:
Indentation of the body can be a little unclear. For example, I usually indent equation environments, but would never dream of indenting the document environment.
To add environment insertion into the LaTeX mode for SubEthaEdit, we begin by getting the environment name using
The
After that, there is just some fiddly work getting the formatting of the environment correct. It needs to intelligently insert newlines and tabs to keep the document readable. As well, something needs to be done with the selection text. There are two possibilities as I see it: (1) treat the selection as the name of the environment and (2) treat the selection as the body of the environment. I went with the latter. Finally, it would be nice to place the insertion point somewhere reasonable; I think it works nicely at the end of the body, especially since the insertion point is positioned to start typing immediately if the body is empty. Putting it all together, we have:
The
That's all there is to it, apart from the boilerplate:
Update: There is an interesting possibility for adjusting the default environment. I had used "equation" as the default, but it was pretty arbitrary. Another approach would be to just repeat whichever environment was last given. We just add a property to the script that holds the default environment, use its value in making the dialog, and update the property based on the dialog result:
Is this actually a good idea? or will it just be annoying? Hard to say without using it, so I guess I'll try it out for a while.
\begin{environmentname}
body
\end{environmentname}
body
\end{environmentname}
Indentation of the body can be a little unclear. For example, I usually indent equation environments, but would never dream of indenting the document environment.
To add environment insertion into the LaTeX mode for SubEthaEdit, we begin by getting the environment name using
display dialog
:try
display dialog "Enter environment name:" with title "Insert Environment" default answer "equation"
on error number -128 -- user canceled
return
end try
set envName to text returned of result
display dialog "Enter environment name:" with title "Insert Environment" default answer "equation"
on error number -128 -- user canceled
return
end try
set envName to text returned of result
The
try
block is to handle when the user cancels instead of entering and environment name. Somewhat arbitrarily, I set the default answer to be "equation", since my guess is that equations are probably the most common environment. After that, there is just some fiddly work getting the formatting of the environment correct. It needs to intelligently insert newlines and tabs to keep the document readable. As well, something needs to be done with the selection text. There are two possibilities as I see it: (1) treat the selection as the name of the environment and (2) treat the selection as the body of the environment. I went with the latter. Finally, it would be nice to place the insertion point somewhere reasonable; I think it works nicely at the end of the body, especially since the insertion point is positioned to start typing immediately if the body is empty. Putting it all together, we have:
set {startSelected, nextSelected} to selectionRange without extendingFront or extendingEnd
set {startExtended, nextExtended} to selectionRange with extendingFront and extendingEnd
set prefix to selectByComparing(startSelected, startExtended, "", "\n")
set suffix to selectByComparing(nextSelected, nextExtended, "", "\n")
set indent to selectByComparing(startSelected, nextSelected, "\t", "")
set beforeInsertion to (join of {prefix, "\\begin{", envName, "}\n", indent, selectionText()} by "")
set afterInsertion to (join of {suffix, "\\end{", envName, "}\n" } by "")
setSelectionText to (beforeInsertion & afterInsertion)
setSelectionRange to startSelected + (count of beforeInsertion) - 1 + (count of suffix)
Note that I've made repeated use of a convenience function:set {startExtended, nextExtended} to selectionRange with extendingFront and extendingEnd
set prefix to selectByComparing(startSelected, startExtended, "", "\n")
set suffix to selectByComparing(nextSelected, nextExtended, "", "\n")
set indent to selectByComparing(startSelected, nextSelected, "\t", "")
set beforeInsertion to (join of {prefix, "\\begin{", envName, "}\n", indent, selectionText()} by "")
set afterInsertion to (join of {suffix, "\\end{", envName, "}\n" } by "")
setSelectionText to (beforeInsertion & afterInsertion)
setSelectionRange to startSelected + (count of beforeInsertion) - 1 + (count of suffix)
to selectByComparing(val1, val2, sameVal, diffVal)
if val1 equals val2 then
sameVal
else
diffVal
end if
end selectByComparing
if val1 equals val2 then
sameVal
else
diffVal
end if
end selectByComparing
The
selectByComparing
handler is not part of SubEthaEditTools - maybe it should be?That's all there is to it, apart from the boilerplate:
on seescriptsettings()
{displayName:"Insert Environment...", keyboardShortcut:"@^e", inContextMenu:"yes"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
{displayName:"Insert Environment...", keyboardShortcut:"@^e", inContextMenu:"yes"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
Update: There is an interesting possibility for adjusting the default environment. I had used "equation" as the default, but it was pretty arbitrary. Another approach would be to just repeat whichever environment was last given. We just add a property to the script that holds the default environment, use its value in making the dialog, and update the property based on the dialog result:
property defaultEnvironment: "equation"
try
display dialog "Enter environment name:" with title "Insert Environment" default answer defaultEnvironment
on error number -128 -- user canceled
return
end try
set envName to text returned of result
set defaultEnvironment to envName
try
display dialog "Enter environment name:" with title "Insert Environment" default answer defaultEnvironment
on error number -128 -- user canceled
return
end try
set envName to text returned of result
set defaultEnvironment to envName
Is this actually a good idea? or will it just be annoying? Hard to say without using it, so I guess I'll try it out for a while.
Labels:
AppleScript,
LaTeX,
SEEing LaTeX,
SubEthaEdit
Friday, January 4, 2008
SEEing LaTeX 20: Getting Citations Right
I started enhancing the LaTeX mode for SubEthaEdit by looking at citations. The original approach, based on just using the input manager supplied with BibDesk, proved to be unsatisfactory. Let's fix that now.
The general approach seems clear enough. We need to determine a search term based on the cursor position in the LaTeX document, pass that to BibDesk to get matching documents, let the user pick which documents are relevant, format the selected documents, and insert the result into the document. Using SubEthaEditTools our SEE scripting abstraction layer, it proves to be fairly straightforward.
The first step, determining the search term, is probably the trickiest. What I envision is that the user can enter a partial citation and have it finished by the script. Thus, we'll need to examine the text immediately before the insertion point and determine a partial citation key. We should not cross an open brace "{", as we don't want to include the macro. Additionally, we shouldn't cross a comma, since we might be looking at multiple citations within one macro. Let's not check the calling macro; this allows the completion to be invoked at inappropriate points, but also allows the completion to be invoked for user-defined macros.
One final issue is what we do when there is a range of text selected. Since the default behavior should be to have no text selected, we should treat a non-empty selection as meaningful. Let's take it to mean that the search should be constrained to only include the selected text. Therefore, we determine a search term based either on the selected text or all the text on the line preceding the insertion point.
Putting all that together, I came up with:
I've used a couple of the new handlers from SubEthaEditTools. These are hopefully self-explanatory, but check the implementation in case they are not.
Using the two
The ungrammatical
We next present the list of completions to the user, in order to narrow the list down to just the appropriate citations. To present a list, we use
We now have a non-empty list of completions. We need to split those apart to get the cite keys, then join the cite keys with commas. I used
Pretty ugly! Let's reformat the core of the
In this form, it's clear enough, keeping in mind that we use " % " as the field separator: we just print out the first field, corresponding to the cite key, with the first record treated specially to get the number of commas right.
Finally, we insert the formatted citation back into the document. I also move the insertion point to the end of the formatted citation, as a typing convenience. I had considered closing the brace for the citation macro, but that would make multi-citation lists more awkward, so decided against it. This is none too difficult:
Again, I've used new handlers from the SubEthaEditTools library.
Putting it all together, and adding a
The general approach seems clear enough. We need to determine a search term based on the cursor position in the LaTeX document, pass that to BibDesk to get matching documents, let the user pick which documents are relevant, format the selected documents, and insert the result into the document. Using SubEthaEditTools our SEE scripting abstraction layer, it proves to be fairly straightforward.
The first step, determining the search term, is probably the trickiest. What I envision is that the user can enter a partial citation and have it finished by the script. Thus, we'll need to examine the text immediately before the insertion point and determine a partial citation key. We should not cross an open brace "{", as we don't want to include the macro. Additionally, we shouldn't cross a comma, since we might be looking at multiple citations within one macro. Let's not check the calling macro; this allows the completion to be invoked at inappropriate points, but also allows the completion to be invoked for user-defined macros.
One final issue is what we do when there is a range of text selected. Since the default behavior should be to have no text selected, we should treat a non-empty selection as meaningful. Let's take it to mean that the search should be constrained to only include the selected text. Therefore, we determine a search term based either on the selected text or all the text on the line preceding the insertion point.
Putting all that together, I came up with:
set {startChar, nextChar} to selectionRange without extendingFront or extendingEnd
if startChar equals nextChar then
-- empty selection, try the whole line
set selectionContents to extendedSelectionText with extendingFront without extendingEnd
else
set selectionContents to selectionText()
end if
set macroArgument to the last item of the (tokens of the selectionContents between "{")
set searchTerm to the last item of the (tokens of the macroArgument between ",")
if startChar equals nextChar then
-- empty selection, try the whole line
set selectionContents to extendedSelectionText with extendingFront without extendingEnd
else
set selectionContents to selectionText()
end if
set macroArgument to the last item of the (tokens of the selectionContents between "{")
set searchTerm to the last item of the (tokens of the macroArgument between ",")
I've used a couple of the new handlers from SubEthaEditTools. These are hopefully self-explanatory, but check the implementation in case they are not.
Using the two
tokens
calls, we obtain a partial citation key. We pass that to BibDesk:tell application "BibDesk"
set citeMatches to search for searchTerm with for completion
end tell
set citeMatches to search for searchTerm with for completion
end tell
The ungrammatical
with for completion
will give us a list of cite keys, not just document titles. Each completion is given as a string containing both the cite key and some document information, separated by a " % " string. We next present the list of completions to the user, in order to narrow the list down to just the appropriate citations. To present a list, we use
choose from list
from the AppleScript StandardAdditions. There are three cases worth considering. First, if there is only one publication, we can select it by default, so the user just confirms whether it is correct. Second, if there are multiple possible publications, we just show the list, declining to guess. Finally, if there aren't any matches, we just inform the user with display alert
; in this case, we'll set the list of publications to the empty list. If there aren't any publications returned, that means the user canceled, so we should just exit and leave the document unchanged. Putting it all together, we arrive at:if (count of citeMatches) equals 1 then
choose from list citeMatches with title "Citation Matches" with prompt "One matching publication:" default items citeMatches
set pubs to result
else if (count of citeMatches) > 1
choose from list citeMatches with title "Citation Matches" with prompt "Please select publications:" with multiple selections allowed
set pubs to result
else
display alert "No matches found for partial citation \"" & searchTerm & "\""
set pubs to {}
end if
if (count of pubs) equals 0 then
-- user canceled, do nothing
return
end
choose from list citeMatches with title "Citation Matches" with prompt "One matching publication:" default items citeMatches
set pubs to result
else if (count of citeMatches) > 1
choose from list citeMatches with title "Citation Matches" with prompt "Please select publications:" with multiple selections allowed
set pubs to result
else
display alert "No matches found for partial citation \"" & searchTerm & "\""
set pubs to {}
end if
if (count of pubs) equals 0 then
-- user canceled, do nothing
return
end
We now have a non-empty list of completions. We need to split those apart to get the cite keys, then join the cite keys with commas. I used
awk
:set citation to shellTransform of (join of pubs by "\n") for "" through "awk -F' % ' 'NR == 1 { printf(\"%s\", $1) } NR > 1 { printf(\",%s\", $1) }'" without alteringLineEndings
Pretty ugly! Let's reformat the core of the
awk
program to make it clearer:NR == 1 { printf("%s", $1) }
NR > 1 { printf(",%s", $1) }
NR > 1 { printf(",%s", $1) }
In this form, it's clear enough, keeping in mind that we use " % " as the field separator: we just print out the first field, corresponding to the cite key, with the first record treated specially to get the number of commas right.
Finally, we insert the formatted citation back into the document. I also move the insertion point to the end of the formatted citation, as a typing convenience. I had considered closing the brace for the citation macro, but that would make multi-citation lists more awkward, so decided against it. This is none too difficult:
setSelectionRange to {nextChar - (length of searchTerm), nextChar - 1}
setSelectionText to citation
setSelectionRange to (nextChar - (length of searchTerm) + (length of citation))
setSelectionText to citation
setSelectionRange to (nextChar - (length of searchTerm) + (length of citation))
Again, I've used new handlers from the SubEthaEditTools library.
Putting it all together, and adding a
seescriptsettings
handler to integrate it into SEE, we get:-- $Id: BibDeskCompletions.applescript,v 1.2 2008/01/04 18:40:16 mjb Exp mjb $
(*
Need to figure out the search term. Treat a selection as meaning to constrain the search
term to lie within the selection, and an empty selection as meaning to get the search term
from the preceding text on the line. We don't cross an opening brace, so that the search term
comes from a call to a macro. However, we don't check to see if the macro is one of the
standard citation macros, since we do want to allow user macros.
*)
set {startChar, nextChar} to selectionRange without extendingFront or extendingEnd
if startChar equals nextChar then
-- empty selection, try the whole line
set selectionContents to extendedSelectionText with extendingFront without extendingEnd
else
set selectionContents to selectionText()
end if
set macroArgument to the last item of the (tokens of the selectionContents between "{")
set searchTerm to the last item of the (tokens of the macroArgument between ",")
tell application "BibDesk"
set citeMatches to search for searchTerm with for completion
end tell
-- get list of publications, customizing user interaction based on number of matches
if (count of citeMatches) equals 1 then
choose from list citeMatches with title "Citation Matches" with prompt "One matching publication:" default items citeMatches
set pubs to result
else if (count of citeMatches) > 1
choose from list citeMatches with title "Citation Matches" with prompt "Please select publications:" with multiple selections allowed
set pubs to result
else
display alert "No matches found for partial citation \"" & searchTerm & "\""
set pubs to {}
end if
if (count of pubs) equals 0 then
-- user canceled, do nothing
return
end
(*
At this point, there is a non-empty list of matches, which replaces the search term. By
construction, the search term always immediately precedes the end of the selection.
Call out to the shell to format the publication list into a LaTeX citation, insert the citation,
and then move the insertion point just after the citation.
*)
set citation to shellTransform of (join of pubs by "\n") for "" through "awk -F' % ' 'NR == 1 { printf(\"%s\", $1) } NR > 1 { printf(\",%s\", $1) }'" without alteringLineEndings
setSelectionRange to {nextChar - (length of searchTerm), nextChar - 1}
setSelectionText to citation
setSelectionRange to (nextChar - (length of searchTerm) + (length of citation))
on seescriptsettings()
{displayName:"Complete Citation", shortDisplayName:"Citation", keyboardShortcut:"@^j", toolbarIcon:"ToolbarBibDesk.png", inDefaultToolbar:"yes", toolbarTooltip:"Complete citation using BibDesk", inContextMenu:"yes"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
(*
Need to figure out the search term. Treat a selection as meaning to constrain the search
term to lie within the selection, and an empty selection as meaning to get the search term
from the preceding text on the line. We don't cross an opening brace, so that the search term
comes from a call to a macro. However, we don't check to see if the macro is one of the
standard citation macros, since we do want to allow user macros.
*)
set {startChar, nextChar} to selectionRange without extendingFront or extendingEnd
if startChar equals nextChar then
-- empty selection, try the whole line
set selectionContents to extendedSelectionText with extendingFront without extendingEnd
else
set selectionContents to selectionText()
end if
set macroArgument to the last item of the (tokens of the selectionContents between "{")
set searchTerm to the last item of the (tokens of the macroArgument between ",")
tell application "BibDesk"
set citeMatches to search for searchTerm with for completion
end tell
-- get list of publications, customizing user interaction based on number of matches
if (count of citeMatches) equals 1 then
choose from list citeMatches with title "Citation Matches" with prompt "One matching publication:" default items citeMatches
set pubs to result
else if (count of citeMatches) > 1
choose from list citeMatches with title "Citation Matches" with prompt "Please select publications:" with multiple selections allowed
set pubs to result
else
display alert "No matches found for partial citation \"" & searchTerm & "\""
set pubs to {}
end if
if (count of pubs) equals 0 then
-- user canceled, do nothing
return
end
(*
At this point, there is a non-empty list of matches, which replaces the search term. By
construction, the search term always immediately precedes the end of the selection.
Call out to the shell to format the publication list into a LaTeX citation, insert the citation,
and then move the insertion point just after the citation.
*)
set citation to shellTransform of (join of pubs by "\n") for "" through "awk -F' % ' 'NR == 1 { printf(\"%s\", $1) } NR > 1 { printf(\",%s\", $1) }'" without alteringLineEndings
setSelectionRange to {nextChar - (length of searchTerm), nextChar - 1}
setSelectionText to citation
setSelectionRange to (nextChar - (length of searchTerm) + (length of citation))
on seescriptsettings()
{displayName:"Complete Citation", shortDisplayName:"Citation", keyboardShortcut:"@^j", toolbarIcon:"ToolbarBibDesk.png", inDefaultToolbar:"yes", toolbarTooltip:"Complete citation using BibDesk", inContextMenu:"yes"}
end seescriptsettings
include(`SubEthaEditTools.applescript')
Labels:
AppleScript,
BibDesk,
LaTeX,
SEEing LaTeX,
SubEthaEdit
SEEing LaTeX 19: SubEthaEditTools, or, Eating My Own Dogfood
Let's take the arguments in the last three posts seriously. We move all of the handlers developed in the course of the SEEing LaTeX series into a separate file called SubEthaEditTools.applescript, and rewrite all of the AppleScripts to include the file using
This requires relatively minor changes to the makefile I've been using, but nothing too serious. Also, I construct SubEthaEditTools itself using
A concept that is made explicit in the SubEthaEditTools is that of the extended selection. That is, the selection modified either so that the beginning is extended to the start of the first line of the selection, or to the end of the last line or the selection, or both. The extended selection has appeared implicitly several times, so it seems worthwhile to make it explicit. Further, there are now handlers both for extending the selection (forward or backward) and for referring to the extended selection without modifying the actual selection.
Update: I've replaced the
Also, I've added a license. It's an MIT-style license, so should be suitably permissive for use by others.
Update 2 (2008/04/23): I've added a few more handlers. Specifically,
m4
. While we're at it, we also add some more handlers to the SubEthaEditTools to try to make a more complete set, with the goal being to write scripts for the mode by using the handlers without directly interacting with SubEthaEdit in the AppleScript.This requires relatively minor changes to the makefile I've been using, but nothing too serious. Also, I construct SubEthaEditTools itself using
m4
, with more general purpose handlers for, e.g., string manipulation placed in their own files. With the more modular structure, some additional minor rewrites of some handlers seems appropriate, which do not warrant specific comment. A concept that is made explicit in the SubEthaEditTools is that of the extended selection. That is, the selection modified either so that the beginning is extended to the start of the first line of the selection, or to the end of the last line or the selection, or both. The extended selection has appeared implicitly several times, so it seems worthwhile to make it explicit. Further, there are now handlers both for extending the selection (forward or backward) and for referring to the extended selection without modifying the actual selection.
Update: I've replaced the
quotedForm
handler with a doubleQuotedForm
handler, to help prevent confusion with the quoted form of
action for AppleScript strings. I've also worked in a few usages of quoted form of
in the various scripts for the LaTeX mode (I didn't know about quoted form of
until recently).Also, I've added a license. It's an MIT-style license, so should be suitably permissive for use by others.
Update 2 (2008/04/23): I've added a few more handlers. Specifically,
modeSetting
, selectionIsEmpty
, and documentIsAvailable
. -- SubEthaEdit Tools
(*
Copyright (c) 2008, Michael J. Barber
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject
to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*)
-- Environment management
on modeEnvironment()
join of {"export __CF_USER_TEXT_ENCODING=0x1F5:0x8000100:0x8000100;", "export SEE_MODE_RESOURCES=", doubleQuotedForm for modeResources(), "; ", readEnvironment out of environmentFilePath()} by ""
end modeEnvironment
on modeSetting for envVar
try
tell application "System Events"
tell property list file (my environmentFilePath())
get value of the property list item envVar
end tell
end tell
on error
missing value
end try
end modeEnvironmentSetting
to openEnvironmentSettings()
openEnvironment at environmentFilePath() with settingDefaultEnvironment
end openEnvironment
on environmentFilePath()
tell application "SubEthaEdit" to set modeName to name of the mode of the front document
join of {path to preferences from user domain as string, "de.codingmonkeys.SubEthaEdit.", modeName, "_environment.plist"} by ""
end environmentFileName
-- Manipulation of document text
on documentText()
tell the front document of application "SubEthaEdit" to get the contents
end documentText
on selectionIsEmpty()
tell the front document of application "SubEthaEdit" to get the length of the selection
result is equal to 0
end selectionIsEmpty
to completeSelectedLines()
extendSelection with extendingFront and extendingEnd
end completeSelectedLines
on selectionText()
tell the front document of application "SubEthaEdit" to get the contents of the selection
end selectionText
to setSelectionText to newText
tell application "SubEthaEdit" to set the contents of the selection of the front document to the newText
end setSelectionText
on selectionRange given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
tell the front document of application "SubEthaEdit"
if shouldExtendFront and shouldExtendEnd then
get {startCharacterIndex of the first paragraph of the selection, nextCharacterIndex of the last paragraph of the selection}
else if shouldExtendFront then
get {startCharacterIndex of the first paragraph of the selection, nextCharacterIndex of the selection}
else if shouldExtendEnd then
get {startCharacterIndex of the selection, nextCharacterIndex of the last paragraph of the selection}
else
get {startCharacterIndex of the selection, nextCharacterIndex of the selection}
end if
end tell
end selectionRange
to setSelectionRange to newRange
tell the front document of application "SubEthaEdit"
set selection to newRange
end tell
end setSelectionRange
on extendedSelectionText given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
set {startChar, nextChar} to selectionRange given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
tell the front document of application "SubEthaEdit"
get the contents of characters startChar through (nextChar - 1) as text
end tell
end extendedSelectionText
to extendSelection given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
set {startChar, nextChar} to (selectionRange given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd)
setSelectionRange to {startChar, nextChar-1}
end extendSelection
-- Manipulation of document properties
to checkSaveStatus given updating:shouldSave
tell application "SubEthaEdit"
if not (exists path of front document) then
error "You have to save the document first"
end if
if shouldSave and (modified of front document) then
try
save front document
end try
end if
end tell
end checkSaveStatus
on documentIsAvailable()
tell application "SubEthaEdit" to get the count of the documents
result > 0
end documentIsAvailable
to requireNewlineAtEOF()
tell the front document of application "SubEthaEdit"
if "" is equal to the contents of the last paragraph then
-- final line terminated, do nothing
else
set the contents of the last insertion point of the last paragraph to return
end if
end tell
end requireNewlineAtEOF
on documentPath()
tell application "SubEthaEdit" to get the path of the front document
end documentPath
on documentLine()
tell application "SubEthaEdit" to get the startLineNumber of selection of front document
end documentLine
on modeResources()
tell application "SubEthaEdit" to get the resource path of the mode of the front document
end modeResources
-- String Utilities
on replacement of oldDelim by newDelim for sourceString
return join of (tokens of sourceString between oldDelim) by newDelim
end replacement
on tokens of str between delimiters
set oldTIDs to text item delimiters of AppleScript
set text item delimiters of AppleScript to delimiters
set strtoks to text items of str
set text item delimiters of AppleScript to oldTIDs
return strtoks
end tokens
on join of tokenList by delimiter
set oldTIDs to text item delimiters of AppleScript
set text item delimiters of AppleScript to delimiter
set joinedString to tokenList as string
set text item delimiters of AppleScript to oldTIDs
return joinedString
end join
on doubleQuotedForm for baseString
quote & baseString & quote
end doubleQuotedForm
on shellTransform of inText for envString through pipeline given alteringLineEndings:altEnds
set shellscript to join of {envString, "pbpaste", "|", pipeline} by space
set the oldClipboard to the clipboard
set the clipboard to the inText
try
set shellresponse to do shell script shellscript altering line endings altEnds
on error errMsg number errNum from badObject
set the clipboard to the oldClipboard
error errMsg number errNum from badObject
end try
set the clipboard to the oldClipboard
shellresponse
end shellTransform
-- Handling of environment settings using a plist file
to writeDefaultEnvironment at envPath
set savedClipboard to the clipboard
set the clipboard to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict/>
</plist>"
try
do shell script "pbpaste > " & (POSIX path of envPath)
on error errMsg number errNum from badObject
set the clipboard to the savedClipboard
error errMsg number errNum from badObject
end try
set the clipboard to the savedClipboard
end writeDefaultEnvironment
to openEnvironment at envFilePath given settingDefaultEnvironment:shouldSetDefault
tell application "System Events"
if not exists file envFilePath
if shouldSetDefault
my writeDefaultEnvironment at envFilePath
else
error ("Can't get environment file " & quote & envFilePath & quote) number -1728
end if
end if
open file envFilePath
end tell
end openEnvironment
to readEnvironment out of plist
readListPair out of plist
environmentString from result
end readEnvironment
to readListPair out of plist
tell application "System Events"
if exists file plist then
tell property list file plist
get {name, value} of every property list item
end tell
else
{{}, {}}
end if
end tell
end readPlist
on environmentString from keyValueListPair
set {plistKeys, plistValues} to keyValueListPair
set accumulator to {}
set oldTIDs to text item delimiters of AppleScript
set text item delimiters of AppleScript to ""
repeat with i from 1 to number of items in plistKeys
set tokens to {"export ", item i of plistKeys, "=", item i of plistValues, ";"}
copy (tokens as string) to the end of the accumulator
end repeat
set AppleScript's text item delimiters to space
set envString to accumulator as string
set AppleScript's text item delimiters to oldTIDs
envString
end environmentString
(*
Copyright (c) 2008, Michael J. Barber
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject
to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*)
-- Environment management
on modeEnvironment()
join of {"export __CF_USER_TEXT_ENCODING=0x1F5:0x8000100:0x8000100;", "export SEE_MODE_RESOURCES=", doubleQuotedForm for modeResources(), "; ", readEnvironment out of environmentFilePath()} by ""
end modeEnvironment
on modeSetting for envVar
try
tell application "System Events"
tell property list file (my environmentFilePath())
get value of the property list item envVar
end tell
end tell
on error
missing value
end try
end modeEnvironmentSetting
to openEnvironmentSettings()
openEnvironment at environmentFilePath() with settingDefaultEnvironment
end openEnvironment
on environmentFilePath()
tell application "SubEthaEdit" to set modeName to name of the mode of the front document
join of {path to preferences from user domain as string, "de.codingmonkeys.SubEthaEdit.", modeName, "_environment.plist"} by ""
end environmentFileName
-- Manipulation of document text
on documentText()
tell the front document of application "SubEthaEdit" to get the contents
end documentText
on selectionIsEmpty()
tell the front document of application "SubEthaEdit" to get the length of the selection
result is equal to 0
end selectionIsEmpty
to completeSelectedLines()
extendSelection with extendingFront and extendingEnd
end completeSelectedLines
on selectionText()
tell the front document of application "SubEthaEdit" to get the contents of the selection
end selectionText
to setSelectionText to newText
tell application "SubEthaEdit" to set the contents of the selection of the front document to the newText
end setSelectionText
on selectionRange given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
tell the front document of application "SubEthaEdit"
if shouldExtendFront and shouldExtendEnd then
get {startCharacterIndex of the first paragraph of the selection, nextCharacterIndex of the last paragraph of the selection}
else if shouldExtendFront then
get {startCharacterIndex of the first paragraph of the selection, nextCharacterIndex of the selection}
else if shouldExtendEnd then
get {startCharacterIndex of the selection, nextCharacterIndex of the last paragraph of the selection}
else
get {startCharacterIndex of the selection, nextCharacterIndex of the selection}
end if
end tell
end selectionRange
to setSelectionRange to newRange
tell the front document of application "SubEthaEdit"
set selection to newRange
end tell
end setSelectionRange
on extendedSelectionText given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
set {startChar, nextChar} to selectionRange given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
tell the front document of application "SubEthaEdit"
get the contents of characters startChar through (nextChar - 1) as text
end tell
end extendedSelectionText
to extendSelection given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd
set {startChar, nextChar} to (selectionRange given extendingFront:shouldExtendFront, extendingEnd:shouldExtendEnd)
setSelectionRange to {startChar, nextChar-1}
end extendSelection
-- Manipulation of document properties
to checkSaveStatus given updating:shouldSave
tell application "SubEthaEdit"
if not (exists path of front document) then
error "You have to save the document first"
end if
if shouldSave and (modified of front document) then
try
save front document
end try
end if
end tell
end checkSaveStatus
on documentIsAvailable()
tell application "SubEthaEdit" to get the count of the documents
result > 0
end documentIsAvailable
to requireNewlineAtEOF()
tell the front document of application "SubEthaEdit"
if "" is equal to the contents of the last paragraph then
-- final line terminated, do nothing
else
set the contents of the last insertion point of the last paragraph to return
end if
end tell
end requireNewlineAtEOF
on documentPath()
tell application "SubEthaEdit" to get the path of the front document
end documentPath
on documentLine()
tell application "SubEthaEdit" to get the startLineNumber of selection of front document
end documentLine
on modeResources()
tell application "SubEthaEdit" to get the resource path of the mode of the front document
end modeResources
-- String Utilities
on replacement of oldDelim by newDelim for sourceString
return join of (tokens of sourceString between oldDelim) by newDelim
end replacement
on tokens of str between delimiters
set oldTIDs to text item delimiters of AppleScript
set text item delimiters of AppleScript to delimiters
set strtoks to text items of str
set text item delimiters of AppleScript to oldTIDs
return strtoks
end tokens
on join of tokenList by delimiter
set oldTIDs to text item delimiters of AppleScript
set text item delimiters of AppleScript to delimiter
set joinedString to tokenList as string
set text item delimiters of AppleScript to oldTIDs
return joinedString
end join
on doubleQuotedForm for baseString
quote & baseString & quote
end doubleQuotedForm
on shellTransform of inText for envString through pipeline given alteringLineEndings:altEnds
set shellscript to join of {envString, "pbpaste", "|", pipeline} by space
set the oldClipboard to the clipboard
set the clipboard to the inText
try
set shellresponse to do shell script shellscript altering line endings altEnds
on error errMsg number errNum from badObject
set the clipboard to the oldClipboard
error errMsg number errNum from badObject
end try
set the clipboard to the oldClipboard
shellresponse
end shellTransform
-- Handling of environment settings using a plist file
to writeDefaultEnvironment at envPath
set savedClipboard to the clipboard
set the clipboard to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict/>
</plist>"
try
do shell script "pbpaste > " & (POSIX path of envPath)
on error errMsg number errNum from badObject
set the clipboard to the savedClipboard
error errMsg number errNum from badObject
end try
set the clipboard to the savedClipboard
end writeDefaultEnvironment
to openEnvironment at envFilePath given settingDefaultEnvironment:shouldSetDefault
tell application "System Events"
if not exists file envFilePath
if shouldSetDefault
my writeDefaultEnvironment at envFilePath
else
error ("Can't get environment file " & quote & envFilePath & quote) number -1728
end if
end if
open file envFilePath
end tell
end openEnvironment
to readEnvironment out of plist
readListPair out of plist
environmentString from result
end readEnvironment
to readListPair out of plist
tell application "System Events"
if exists file plist then
tell property list file plist
get {name, value} of every property list item
end tell
else
{{}, {}}
end if
end tell
end readPlist
on environmentString from keyValueListPair
set {plistKeys, plistValues} to keyValueListPair
set accumulator to {}
set oldTIDs to text item delimiters of AppleScript
set text item delimiters of AppleScript to ""
repeat with i from 1 to number of items in plistKeys
set tokens to {"export ", item i of plistKeys, "=", item i of plistValues, ";"}
copy (tokens as string) to the end of the accumulator
end repeat
set AppleScript's text item delimiters to space
set envString to accumulator as string
set AppleScript's text item delimiters to oldTIDs
envString
end environmentString
Labels:
abstraction,
AppleScript,
m4,
SEEing LaTeX,
SubEthaEdit
What Role Should the Script Editor Play in Writing AppleScript?
I think it is safe to say that Script Editor is the most widely used application for writing AppleScripts. But, should it be? I'm coming to the conclusion that it brings so many limitations along with it that it can only play a limited role.
Before going any further with this line of thought, I'd like to make clear that I'm not making any recommendation for or against the various commercial AppleScript development environments. Looking at the SEEing LaTeX series, you might get the impression that I use AppleScript a fair amount. I don't. I use it as little as possible, which is why in that series I do most of the real work not in AppleScript, but in shell scripts called from AppleScript. Paying for a commercial environment may make sense if you want to or have to write a lot of AppleScript, but would be a waste of money for me.
However, our choices are not limited to Script Editor and the commercial development environments. We have an excellent development environment that comes for free with Mac OS X: it's called Unix. We can use
Within AppleScript, the usual approach is to put common functionality in a script library and use
Consider now using
At this point, we still can use Script Editor to edit the script sources. However, we still have a lot of boilerplate to write for little gain. We need to set up the scripts correctly so that
Alternatively, instead of emulating how Python loads modules, let's take a look at how something like C would do things. In C, you just use the preprocessor to include the various files. We can do that for AppleScript using, e.g.,
Just like in C, this approach includes everything into the namespace of the script. And, just like in C, this could lead to namespace conflicts. So far, I've only used this in a few scripts, all of which are relatively small. I've not encountered any difficulties. But, should trouble arise, there is a solution. Instead of including the text at the top level of the script, include it inside of a script object:
I'm not at all sure that this approach would scale to larger scripts well. You'd really like to be able to have scripts import what they need, and not worry about whether they will themselves be imported into others. Since I'm not planning to write any large AppleScripts, I'm not going to worry about that until it becomes a problem.
Where does this leave the Script Editor? It's no longer useful for scripts of any length, since it can't handle the
Before going any further with this line of thought, I'd like to make clear that I'm not making any recommendation for or against the various commercial AppleScript development environments. Looking at the SEEing LaTeX series, you might get the impression that I use AppleScript a fair amount. I don't. I use it as little as possible, which is why in that series I do most of the real work not in AppleScript, but in shell scripts called from AppleScript. Paying for a commercial environment may make sense if you want to or have to write a lot of AppleScript, but would be a waste of money for me.
However, our choices are not limited to Script Editor and the commercial development environments. We have an excellent development environment that comes for free with Mac OS X: it's called Unix. We can use
make
to manage a project, of which AppleScript can play a useful part. Unfortunately, Script Editor is tied too strongly to AppleScript, which means we're more or less stuck with AppleScript's mechanisms for modularity. Since modularity goes hand in hand with reuse, this is a key point.Within AppleScript, the usual approach is to put common functionality in a script library and use
load script
from the StandardAdditions. This has a number of problems. First, there isn't a standard search path from which to load the script library, so you need to introduce a new convention. Second, if a library is changed, there is no automated mechanism to update scripts that depend on it. You can load the script library, store it in a property, and just check to see if the library has been updated whenever the script is called; this roughly emulates how, e.g., import
works in Python. There are enough steps to this process that you're looking at a lot of copy and paste programming. Also, you depend on whatever convention you've established to load the script library. Consider now using
make
. You can use osacompile
to compile your text source into a compiled script. You could also use various shell methods to manipulate your script to define the search path for the script libraries, but how is the Script Editor to work with that? You can also just forget about reloading the script libraries when they change, since make
will take care of that for us. We just load the script library once, and forget about it. Since we're just loading the script library once, why not just forget about the search path while we're at it? Just compile the library source into your working directory, and have the script load it from that same directory. At this point, we still can use Script Editor to edit the script sources. However, we still have a lot of boilerplate to write for little gain. We need to set up the scripts correctly so that
make
can run them once to load the library. We need to add extra steps to make
in order to load the libraries. We need to refer to the script libraries by using the property in which we stored them, whenever we want to use a handler in the library. Alternatively, instead of emulating how Python loads modules, let's take a look at how something like C would do things. In C, you just use the preprocessor to include the various files. We can do that for AppleScript using, e.g.,
m4
. Assuming you're working in a script called demo.applescript
, and want to use some handlers you've written for manipulating text. Just include the relevant library source with something likeinclude(`StringTools.applescript')
and compile with m4 demo.applescript | osacompile -o demo.scpt
Just like in C, this approach includes everything into the namespace of the script. And, just like in C, this could lead to namespace conflicts. So far, I've only used this in a few scripts, all of which are relatively small. I've not encountered any difficulties. But, should trouble arise, there is a solution. Instead of including the text at the top level of the script, include it inside of a script object:
script StringTools
include(`StringTools.applescript')
end script
This could be further automated by writing an include(`StringTools.applescript')
end script
m4
macro, such as:define(`import', `script `$2'
include(`$1')
end script
')dnl
include(`$1')
end script
')dnl
I'm not at all sure that this approach would scale to larger scripts well. You'd really like to be able to have scripts import what they need, and not worry about whether they will themselves be imported into others. Since I'm not planning to write any large AppleScripts, I'm not going to worry about that until it becomes a problem.
Where does this leave the Script Editor? It's no longer useful for scripts of any length, since it can't handle the
include
statements. We do want it in order to look at scripting dictionaries, that much is clear, but is it needed for any editing at all? It is a reasonable environment in which to try out little code snippets, but we can even dispose of that, since Script Editor offers that functionality through the services menu.
Subscribe to:
Posts (Atom)