Roll Your Own Features in HomeSite and Studio
By Dan Hulse
HomeSite/Studio Development
Macromedia, Inc.
There's something about the anticipation of that shiny new release
of your favorite software package. The mystery surrounding the infamous
"Beta" testing, the rumor of the latest features in the newsgroups, the
final announcement on the Web site, the FedEx truck turning the corner
of your street...
Okay, so maybe that's a bit dramatic for most of us. Still, it's
satisfying to find that must-have feature finally implemented in the
latest and greatest version.
HomeSite (and ColdFusion and JRun Studios) includes a built-in
scripting language based on Microsoft's ActiveScripting engine. This
scripting engine allows you to use either JScript (a JavaScript-derived
language) or VBScript as a language base and utilize all of the native
statements, functions, and objects of these languages. On top of this
base-language functionality, the HomeSite and Studio products add
additional objects which allow you to control features specific to
these applications. These additional objects comprise the Visual Tools
Object Model (VTOM) and allow the advanced user to manipulate and
extend the product beyond its core feature set. So, instead of waiting
for that next release, we can begin to evolve and tailor the software
right away.
Introduction
In this article, I'll offer some examples of adding two new features
to HomeSite through VTOM. I'll be using JScript, although VBScript can
be used just as well. Although this article isn't meant to be a
tutorial on JScript, I'll explain the major points of the sample
scripts, and the end of the article lists some additional resources for
more information on scripting. The intent here is to give advanced
developers some code fragments to generate ideas for their own
customizations. Novice users can still use the sample scripts to add
real functionality to HomeSite, and hopefully get a taste for the power
of VTOM. The samples will work equally well in HomeSite, ColdFusion
Studio, and JRun Studio.
Converting Text to Special Character Entities
When writing documents destined for the Web, I often need to insert
HTML coding samples which need to be viewed in their "source" form,
with all the tags intact and displayed in the browser. This involves
the use of special character entities in the HTML source, such as
< for the "<" symbol. While HomeSite offers the Special
Character tool window for entering these entities, it can be awkward to
use if you dislike mixing keyboard and mouse usage. Our first script
(ConvSpecChars.js) allows you to type in the most common characters
directly, and then convert them automatically to their character entity
equivalents.
/*
ConvSpecChars.js
Takes the current selection, and replaces special characters
with their general entity equivalent
E.g.,
<html> ..becomes.. >html<
*/
function Main() {
var aChars = new Array(4);
var aRepls = new Array(4);
var SelStr = '';
var NewStr = '';
var s = '';
var i;
// Initialize search/replace pairs
// Note that order is important (e.g., replace '&' first!!)
aChars[0] = '&';
aRepls[0] = '&';
aChars[1] = '<';
aRepls[1] = '<';
aChars[2] = '>';
aRepls[2] = '>';
aChars[3] = '"';
aRepls[3] = """;
with (Application){
with (ActiveDocument){
if (SelLength == 0){
MessageBox('One or more characters must be selected.', 'Error', 0);
return;
}
SelStr = SelText;
for (i = 0; i < SelStr.length; i++){
s = SelStr.substr(i, 1);
iItem = ArrFind(aChars, s);
if (iItem > -1)
NewStr = NewStr + aRepls[iItem]
else
NewStr = NewStr + s;
}
SelText = NewStr;
}
}
}
function ArrFind(aArray, sSearch){
var i;
var result = -1;
var s;
for (i = 0; i < aArray.length; i++){
s = aArray[i];
if (s == sSearch){
result = i;
break;
}
}
return(result);
}
The single character symbols and their equivalent character entities
are stored in two arrays (JScript does not support multidimensional
arrays). Application and ActiveDocument are objects which are provided
through VTOM. Application is the HomeSite or Studio application itself,
and ActiveDocument is the currently displayed document in the code
editor. The with () statement establishes a default object, so instead
of having to write Application.ActiveDocument.SelText, we can nest the
Application and ActiveDocument objects in two with statements and just
use the property or method names without the object identifiers.
This script simply iterates through all the characters of the
current text selection, and performs a lookup of the character (via the
ArrFind function). We build up a new string with any replacement
entities, and set the selected text to this new string.
The best way to utilize the above script is through a keystroke
combination. HomeSite versions 4.5 and up allow you to assign a script
to a keyboard shortcut. To do this, perform the following steps:
- Select the Options ->Customize menu item.
- In the Customize window, select the Script Shortcuts tab.
- Click the Add button and select your script from the file dialog.
- Back in the Customize window, make sure the script is highlighted
in the list, and then click in the shortcut box in the bottom left of
the dialog.
- Type in the key combination you want to assign to this script and click Apply.
- Click Close to exit the Customize dialog.
The next time you want to convert characters, simply select the desired text and press the key combination you specified.
Saving and Loading Editor Bookmarks
The code editor in HomeSite allows you to create bookmarks for
quickly moving to specific lines in your code. CTRL+K toggles a
bookmark for the current line on and off, and SHIFT+CTRL+K moves the
cursor to each bookmarked line in sequence. One feature that has been
requested by users is the ability to save and reload bookmarks for
individual files. This is not a trivial matter using VTOM, but the next
set of scripts will show you how this can be done.
We'll accomplish this by writing out the bookmarks to a simple text
file. Each line of the file will contain the bookmarks for a single
document, with the filename first, followed by '=' and then the list of
bookmarks in comma-delimited format (e.g.,
'C:\InetPub\wwwroot\Content\Index.htm=12,27,102'). Since we're dealing
with external files, the next set of scripts give some insight on using
the FileSystemObject. This object is provided by the Microsoft
scripting engine, and provides access to the Windows file system.
We'll start with the SaveBookMarks.js script:
/*
SaveBookMarks.js
Save current bookmarks to a file...
File is an "INI" style format, with a file name
matched to bookmarks in a comma-delimited format. E.g.:
C:\Inetpub\wwwroot\catalog.htm=12,20,55
*/
function Main() {
var iSelStart;
var iSelLen;
var sMarkFile = 'C:\\Program Files\\Allaire\\ColdFusion Studio 4.5\\UserData\\BookMarks.dat';
with (Application.ActiveDocument){
// Save current selection
iSelStart = SelStart;
iSelLen = SelLength;
try{
// Prevent editor updates
BeginUpdate();
SaveBookMarks(sMarkFile);
}
finally{
// Restore selection
SelStart = iSelStart;
SelLength = iSelLen;
// Restore editor updates
EndUpdate();
}
}
}
function SaveBookMarks(sFileName){
var aMarkSets;
var aMarks = new Array();
var sMarkLine;
var iPos;
var i;
var iStartPos;
var iLine;
var sLine;
with (Application.ActiveDocument){
sMarkLine = Filename + '=';
aMarkSets = FileToArray(sFileName);
// Check if this file is currently in the bookmarks file and delete it
for (iPos = 0; iPos < aMarkSets.length; iPos++){
sLine = aMarkSets[iPos];
if (sLine.indexOf(sMarkLine) >= 0){
// Shift remaining entries up one element
for (i = iPos; i < aMarkSets.length - 1; i++){
aMarkSets[i] = aMarkSets[i + 1];
}
// Adjust array size so the last element is deleted
aMarkSets.length = aMarkSets.length - 1;
break;
}
}
try{
CursorDocStart(false);
CursorLineEnd(false);
iStartPos = SelStart;
// Go to next bookmark
Application.ExecCommand(60, 0);
// If the cursor hasn't moved, then no bookmarks present?
if (SelStart == iStartPos)
return;
iLine = LineNo(Text, SelStart);
while (true){
aMarks.length = aMarks.length + 1;
aMarks[aMarks.length - 1] = iLine;
// Go to next bookmark
Application.ExecCommand(60, 0);
iLine = LineNo(Text, SelStart);
// if we are back at the first bookmark, then we are done
if (aMarks[0] == iLine)
break;
}
// append the comma-delimited line numbers
sMarkLine = sMarkLine + aMarks.join(',');
// add current bookmark set to existing ones
aMarkSets.length = aMarkSets.length + 1;
aMarkSets[aMarkSets.length - 1] = sMarkLine;
}
finally{
ArrayToFile(sFileName, aMarkSets);
}
}
}
function ArrayToFile(sFileName, aArray){
var aFileObj = new ActiveXObject("Scripting.FileSystemObject");
var ForWriting = 2;
var aTextStream;
if (!aFileObj.FileExists(sFileName))
aFileObj.CreateTextFile(sFileName, true);
aTextStream = aFileObj.OpenTextFile(sFileName, ForWriting);
aTextStream.Write(aArray.join('\r\n'));
aTextStream.Close();
}
function FileToArray(sFileName){
var aFileObj = new ActiveXObject("Scripting.FileSystemObject");
var ForReading = 1;
var aTextStream;
var aFile;
var sText;
var aArray;
if (!aFileObj.FileExists(sFileName))
return(new Array());
aFile = aFileObj.GetFile(sFileName);
if (aFile.Size == 0)
return(new Array());
aTextStream = aFileObj.OpenTextFile(sFileName, ForReading);
try{
sText = aTextStream.ReadAll();
aArray = sText.split('\r\n');
}
finally{
aTextStream.Close()
}
return(aArray);
}
function LineNo(sText, iOffSet){
var iPos;
var iLine;
iPos = 0;
iLine = 1;
while (iPos < iOffSet){
if (sText.substr(iPos, 2) == '\r\n'){
iLine++;
// Increment the offset to account for line feeds
iOffSet++;
// skip past the \n
iPos++;
}
iPos++;
}
return iLine;
}
Before running this script, ensure that the variable sMarkFile
points to a valid location for your system. Note that in JScript, the
backslash used for the path separator needs to be "escaped" out in a
JScript string by preceding it with another backslash character.
Let's look at the support functions first. The LineNo function is
actually a workaround for an error in the JScript implementation of the
ActiveDocument.GetCaretPos method, which would normally be used to get
the current line containing the cursor. In VBScript, this method takes
two parameters which are passed by reference (the variables passed in
will be modified by the method). JScript only allows passing of
variables by value, not by reference, so this method will not work
correctly in this language. Our LineNo function takes a string and an
offset, and calculates which line the offset is located on based on the
count of carriage returns and line feeds ('\r\n') preceding the offset.
As used in the above script, we pass it the text of the document
(ActiveDocument.Text) and the start of the selection in the editor
(ActiveDocument.SelStart), and the function's return value tells us
which line the cursor is on.
The ArrayToFile and FileToArray functions provide a way to store
JScript arrays to an external file and to load the lines of a text file
back into an array. The FileSystemObject provides numerous file access
functions, such as testing if a file exists, getting a file's size on
disk, etc. This object also allows us to read and write files by
creating a FileStream object. Check the Microsoft scripting links at
the end of this article to get more information on the features of
these objects.
The SaveBookMarks function is the core routine for this script. We
first use the FileToArray function to load in the current bookmark file
to an array variable and delete any item which references the current
document. We position the editor cursor at the top of the document, and
then call the Application.ExecCommand method to move to each
consecutive bookmark in the document, and store the line number into an
array. ExecCommand is a generic method used for performing many tasks
inside HomeSite. The first parameter of this method is an integer value
which corresponds to a specific application command. These values are
listed in the help documentation, under the Scripting the Visual Tools
Object Model topic section. The second parameter determines if text is
selected during any cursor movement operations.
Once we find a bookmarked line that we have already detected, we
know that we have looped through all of the bookmarks. We then add the
current document's name and comma-delimited bookmarked line numbers to
the bookmark file array, and make a call to ArrayToFile to save it back
to disk.
One point worth noting is the use of the BeginUpdate and EndUpdate
methods, which are part of the ActiveDocument object. BeginUpdate will
suspend any updates to the code editor in HomeSite. Its used in these
scripts so that the user doesn't see text being selected or the cursor
scrolling through the document as the script is executing. EndUpdate
will resume proper refreshing of the editor. While this provides a
visually cleaner running script, be aware that if the script halts on
an error before the EndUpdate is called, the editor will be left in a
suspended state, and you will need to restart HomeSite (or create a
script with just an Application.ActiveDocument.EndUpdate within the
Main() procedure). It's a good idea to comment out the editor update
commands until you are satisfied that the script is working as expected.
Now that we can successfully save a document's bookmarks, we need to
be able to load them back in. The LoadBookMarks.js script accomplishes
this:
/*
LoadBookMarks.js
Load current bookmarks from a file...
File is an "INI" style format, with a file name
matched to bookmarks in a comma-delimited format. E.g.:
C:\Inetpub\wwwroot\catalog.htm=12,20,55
*/
function Main() {
var iSelStart;
var iSelLen;
var sMarkFile = 'C:\\Program Files\\Allaire\\ColdFusion Studio 4.5\\UserData\\BookMarks.dat';
with (Application.ActiveDocument){
// Save current selection
iSelStart = SelStart;
iSelLen = SelLength;
try{
// Prevent editor updates
BeginUpdate();
LoadBookMarks(sMarkFile);
}
finally{
// Restore selection
SelStart = iSelStart;
SelLength = iSelLen;
// Restore editor updates
EndUpdate();
}
}
}
function LoadBookMarks(sFileName){
var aMarkSets;
var aMarks;
var iCount;
var sMarkLine;
var iPos;
var sCurrDoc = Application.ActiveDocument.Filename;
aMarkSets = FileToArray(sFileName);
if (aMarkSets.length == 0)
return;
for (iCount = 0; iCount < aMarkSets.length; iCount++){
sMarkLine = aMarkSets[iCount];
iPos = sMarkLine.indexOf(sCurrDoc + '=');
if (iPos >= 0)
break;
}
if (iPos < 0)
return;
iPos = (sMarkLine.indexOf('=') + 1);
sMarkLine = sMarkLine.substring(iPos, sMarkLine.length);
aMarks = sMarkLine.split(',');
with (Application.ActiveDocument){
for (iCount = 0; iCount < aMarks.length; iCount++){
SelectLine(aMarks[iCount]);
Application.ExecCommand(59, 0);
}
}
}
function FileToArray(sFileName){
var aFileObj = new ActiveXObject("Scripting.FileSystemObject");
var ForReading = 1;
var aTextStream;
var aFile;
var sText;
var aArray;
if (!aFileObj.FileExists(sFileName))
return(new Array());
aFile = aFileObj.GetFile(sFileName);
if (aFile.Size == 0)
return(new Array());
aTextStream = aFileObj.OpenTextFile(sFileName, ForReading);
try{
sText = aTextStream.ReadAll();
aArray = sText.split('\r\n');
}
finally{
aTextStream.Close()
}
return(aArray);
}
Again, before running the script, check that the sMarkFile variable points to a valid path on your system.
This script uses the same FileToArray procedure as the
SaveBookMarks.js script. The LoadBookMarks procedure uses this function
to read in the current bookmark file. We iterate through the resulting
array to see if the current document is listed. If it is, we convert
its comma-delimited bookmark string to an array and loop through the
array to send the cursor to the appropriate line
(ActiveDocument.SelectLine), We then issue an ExecCommand to set a
bookmark.
You might decide that you want to include the saving or loading of
the document along with its bookmarks. In SaveBookMarks.js, you would
just add the following line just before the SaveBookMarks() call in the
Main() method:
Save();
To load a file together with its bookmarks, add the following line
before the LoadBookBookMarks() call in the LoadBookMarks.js script:
if (OpenFile(''));
You can tie the bookmark scripts to a keyboard shortcut as we did
for the ConvSpecChars.js script, or you might find it more convenient
to access them via a custom toolbar button. To create a custom toolbar
icon, do the following:
- Select Options -> Customize from the main menu.
- From the Visible Toolbars list, select the destination toolbar for the new buttons.
- Click Add Custom Button to display the Custom Toolbutton window.
- Click the Execute an ActiveScript file radio button, and enter the appropriate script filename in the Script File edit box.
- Enter a caption for the button (you can also set a bitmap image and a hint as well).
- Click Ok to close the Custom Toolbutton window.
- Click Close to exit the Customize dialog.
Hopefully you've seen that the VTOM scripting features in HomeSite
can do a lot more than just execute repetitive tasks. You can add major
functionality to the software in order to customize it to your own
needs and extend it beyond its out-of-the-box capabilities.
Resources
Scripting the Visual Tools Object Model
HomeSite/ColdFusion Studio/JRun Studio help file.
Microsoft Scripting Technologies
msdn.microsoft.com/scripting/default.htm?/scripting/start.htm
Microsoft Scripting Clinic
msdn.microsoft.com/voices/scripting05142001.asp
Microsoft JScript Documentation (HTML help file)
msdn.microsoft.com/scripting/jscript/download/jsdoc.exe
HomeSite Dev Center
www.allaire.com/developer/hsreferencedesk/