Friday, April 10, 2020

Customizing Intellisense III

In the final part of this little series we'll look at scripting in Intellisense.

The FoxCode Object
While the FoxCode table handles the metadata that lies at the heart of the IntelliSense system, the FoxCode Object is what enables us, as developers, to really customize the behavior of IntelliSense. The FoxCode object is an instance of the foxcodescript class that is defined (on the session base class) in foxcode.prg  that defines the behavior for the various IntelliSense operations and provides hooks that allow us to extend that functionality as needed. The key methods of interest are listed at Table 2:
Table 3: FoxCodeScript Methods
Method
Purpose
Main
Template method, called from Start – allows custom code to be inserted
Start
Main set-up method for the object. Should be called explicitly in scripts
DefaultScript
Handler for “space” character – the default trigger for IntelliSense
HandleMRU
Handler for Most Recently Used Lists
HandleCOps
Handler for C-Operator Expansion
HandleCustomScripts
Handler for custom scripts
AdjustCase
Adjust the case of a keyword according to setting of the CASE Field, or the default if not specified
ReplaceWord
Replaces the last “word” typed with the specified “word”

In addition to these methods, the object exposes the set of properties shown below. First, every field in the FoxCode table has a corresponding property in the FoxCode object. The reason, of course, is so that the entire content of the record from the FoxCode table can be passed to whatever script is executing. There are, however, a number of additional properties control other aspects of the implementation:
Table 4: Properties of the FoxCode Object
Property
Description
Abbrev
Contents of FoxCode field
Case
Contents of FoxCode field
Cmd
Contents of FoxCode field
Cursorlocchar
The character used to indicate the location of cursor on completion (Default is "~")
Data
Contents of FoxCode field
Defaultcase
Case to use if nothing specified for this record (from the “V” record in FoxCode table)
Expanded
Contents of FoxCode field
Filename
Fully qualified path and file name of the currently open file
Fullline
The entire contents of the current line (includes spaces, tabs etc)
Icon
Icon for use in items array
Items
Array used when generating lists. Two columns, but only column 1 is required.
·          Items[1,1] – text to display in list
·          Items[1,2] – value tip for item
Items are sorted on Column 1 by default. Clear ItemSort flag to disable sorting
Itemscript
Name of the script to run after an item has been selected from a list (Optional)
Itemsort
Determine whether the items array is sorted or not (Default = .T. )
Location
Current editing window – allows control over whether a script is appropriate:
·          0      Command Window
·          1      Program
·          8      Menu Snippet
·          10    Code Snippet
·          12    Stored Procedure
Menuitem
The item that was selected from the list (empty unless a follow-up script is running)
Paramnum
Defined in help as: “Parameter number of the function for script call made within a function”
Save
Contents of FoxCode field
Source
Contents of FoxCode field
Timestamp
Contents of FoxCode field
Tip
Contents of FoxCode field
Type
Contents of FoxCode field
Uniqueid
Contents of FoxCode field
User
Contents of FoxCode field
Usertyped
Text that user typed (excludes leading spaces, tabs and the triggering keystroke)
Valuetip
Quick Info tip to display (when FoxCode.ValueType = "T")
Valuetype
Define how the return value from the script should be interpreted
·          V     Value:      Action depends upon the script – may be used to replace the typed text or add to it
·          L      List:         Displays the contents of the FoxCode.Items array as a list
·          T      Tip:         Displays the contents of the FoxCode.ValueTip property as a Quick Info Tip
Creating Scripts
Probably the most exciting thing that IntelliSense brings to Visual FoxPro is the ability to create custom scripts that allow us to greatly improve our productivity. A script has two distinct components:
·         A Preamble            This is IntelliSense specific and is responsible for setting up the FoxCode object and the environment in which the script is to run. Whenever a script is called, an instance of the FoxCode object is passed to it as a parameter.
·         The Code               This is standard FoxPro code that generates a result. Scripts must return something and the way in which their return value is interpreted is controlled by the Valuetype property.
All the scripts illustrated here have these two components and the capabilities of scripts are restricted only by what you can do in FoxPro code itself. The easiest way to describe scripts is to show some….
Insert a block of text
To insert a block of text we need to create a script that will return a formatted string to replace the abbreviation that triggered it. This is clearly not a generic script, so we can create it directly in the Data field of the foxcode.dbf record that defines the abbreviation like this:

Type
Abbrev
Expanded
Cmd
Tip
Data
Case
Save
U
hdr

{}
memo
Memo
M
T
  
The actual script, in the Data field, looks like this. As you can see, the preamble starts out by receiving a reference to the FoxCode object and checking the location to make sure that we are not in the command window ( a program header would not really be relevant there!). The preamble ends by setting the ValueType property to “V” on the FoxCode object to indicate that the return will be a value to replace the abbreviation:
LPARAMETER toFCObject
LOCAL lcOwner, lcName, lcVersion, lcPos, lcDesc, lcRets
lcDevName = [Andy Kramek]
lcOwner = "Tightline Computers, Inc"

IF toFCObject.Location = 0
  *** Not in the command window
   RETURN toFCObject.UserTyped
ELSE
  *** We will need textmerge ON for this script
  lcMerge = SET('TextMerge')
  SET TEXTMERGE ON
ENDIF

*** This script will return a string which we want to use to
*** replace the triggering text. Set valuetype accordingly:
toFCObject.ValueType = "V"
The actual work of the script is handled here. This is standard Visual FoxPro code that defines some variables and builds a formatted return string. There are several ways of doing this – the new TEXTMERGE functionality offers some excellent opportunities in this area.
LOCAL lcTxt, lcName, lcComment, lnPos
STORE "" TO lcTxt, lcName, lcComment
#DEFINE CRLF CHR(13)+CHR(10)

lcName = ALLTRIM( WONTOP() )
lcVersion = VERSION(1)
lnPos = AT( "[", lcVersion ) - 1
lcVersion = LEFT( lcVersion, lnPos )
lcDesc = ALLTRIM( INPUTBOX( 'Describe this Program or Procedure', lcOwner ))
lcRets = ALLTRIM( INPUTBOX( 'What does it return?', lcOwner, 'Logical' ))

*** Find out where on the line the insertion point is and set the indent accordingly
lnspaces = AT(  toFCObject.UserTyped, toFCObject.FullLine ) -1

*** And generate the actual text here
TEXT TO lcText NOSHOW
********************************************************************
<<SPACE(lnSpaces)>>*** Name.....: <<UPPER( lcName )>>
<<SPACE(lnSpaces)>>*** Author...: <<lcDevName>>
<<SPACE(lnSpaces)>>*** Date.....: <<DATE()>>
<<SPACE(lnSpaces)>>*** Notice...: Copyright (c) <<TRANSFORM( YEAR(DATE()))>> <<lcOwner>>
<<SPACE(lnSpaces)>>*** Compiler.: <<lcVersion>>
<<SPACE(lnSpaces)>>*** Function.: <<lcDesc>>
<<SPACE(lnSpaces)>>*** Returns..: <<lcRets>>
<<SPACE(lnSpaces)>>********************************************************************
<<SPACE(lnSpaces)>>~
ENDTEXT

*** Restore original textmerge setting
IF NOT EMPTY( lcMerge )
  SET TEXTMERGE &lcMerge
ENDIF 
RETURN lcText
Notice that this script uses the FoxCode object’s FullLine property to work out the indentation. This will only work as long as you use replace tabs with spaces in your editing windows. If you use tab characters (CHR(9)) only one space per tab will be inserted and the indentation will not be correct.
The final string is simply returned from the script in exactly the same way as any value is returned from a method or function. The same pattern can be used to define any script that inserts a text string.
Generating lists
The IntelliSense engine handles the generation of  “most-recently used” and “member lists” automatically, and there is nothing more that we need to do about those. The lists generated for native commands and functions are actually generated from a table named “FoxCode2”. A copy of this table is included with the source code, but, unlike the main foxcode.dbf, it is not exposed to developers for modification. So (unless we want to re-write the entire FoxCode application) we cannot easily alter these lists anyway.
However, that still leaves us with an awful lot of potential and we can certainly define additional lists to make our own lives easier. However, dealing with lists is a little more complex than merely returning a block of code because it is actually a two-part process. First we have to generate the list, and second we have to respond to the selection that was made.
Defining the contents for the list
IntelliSense lists are created by populating an array property (named “Items”) on the FoxCode object. This array must be dimensioned with two columns, the first is used to hold the text for the list items and the second to hold the Tip text to be associated with the item. In addition to specifying the content of a list, a script must tell the IntelliSense engine that a list is required. We have already seen that the ValueType property of the FoxCode object is used to communicate how the result of running a script should be interpreted and, to generate a list, all that is needed is to set this property to “L”.
The simplest way to generate a list is to create a foxcode.dbf record (Type = “U”) in which the data field defines a script that explicitly populates the required FoxCode object properties directly, as follows:

Type
Abbrev
Expanded
Cmd
Data
Save
U
olist
lcChoice =
{}
LPARAMETER toFoxCode
WITH toFoxCode
    .ValueType = "L"
    DIMENSION .items[3,2]
    .items[1,1] = "'First Option'"
    .items[1,2] = "Tip for option 1"
    .items[2,1] = "'Second Option'"
    .items[2,2] = "Tip for option 2"
    .items[3,1] = "'Third Option'"
    .items[3,2] = "Tip for option 3"
    RETURN ALLTRIM( .Expanded ) ENDWITH
.T.

Typing “olist” followed by a space in an editing window pops up a list containing the three options defined in the script (Figure 10). By returning the content of the FoxCode object’s Expanded property we can replace the keyword with some meaningful text and add on whatever is selected from the list. While this is pretty cool, it is not really very flexible since we have to hard-code the list options directly into the script. We could create a table to hold the content of lists that we want to generate and create a separate record for each abbreviation.
Of course, we don’t want to have to repeat the code that does the look up in each record. This is where the “Script” record type comes in. As we have already seen, we can call scripts from the CMD field of a foxcode.dbf record by including the script name in braces like this {scriptname}. So we can create a generic script to handle the lookup, generate the list and take the appropriate action when an item is selected.
How to define the action when a selection is made in a list
The problem in defining the action to take when an item is selected in a list is that the code that actually creates the list is not exposed to us. So, while we can specify the content of the list, we cannot directly control the consequential action. Instead the IntelliSense engine relies on two properties of the FoxCode object. The Itemscript property is used to specify a “handler” script that will be run after the list is closed and the MenuItem property is used to store the text of the selected item. If the list is closed without any entry being selected, this property will, of course, be empty. So in order to specify how IntelliSense should respond to a selection we need to create our own handler script.
We have already seen that generic Scripts require their own record (Type = “S”) in Foxcode.dbf and since they are called from another record, they must be constructed accordingly. The secret to such scripts lies in the FoxCodeScript class, which is defined in the IntelliSense Manager. (Note: the source code for this class can be found as FoxCode.prg in the FoxCode source directory).
To create a generic script, define a subclass of the FoxCodeScript class to provide whatever functionality you require and instantiate it. All the necessary code is, as usual, stored in the Data field of the Foxcode.dbf record. The easiest way to explain is to show it working - and it really is much easier than it sounds.
How to create a table driven list
The objective is to create a generic script that will:
·         Be triggered by a simple abbreviation (the keyword)
·         Replace the keyword with the specified expanded text
·         Use that keyword to retrieve a list of items from a local table
·         Display a list of the items
·         Append the selected item to the expanded text
The first thing that is needed is a table. For this example we will use ListOptions.dbf, which has only two fields as shown.

Ckey
Coption
ol1
'Number One'
ol1
'Number Two'
ol1
'Number Three'
ol2
'Apples'
ol2
'Bananas'
ol2
'Cherries'

One obvious improvement to this table would be to add a column to include some tip text for our menu items, and another would be a ‘Sequence’ column so that we can order our menus however we want. However, these refinements do not affect the principles here and to keep it simple we will leave them as an exercise for the reader.
As you can see our table recognizes two keywords “ol1” and “ol2”. First we need to add a record to foxcode.dbf for each of these keywords. These records must define the expanded text to replace the keyword and call the generic handler script for everything else. In this example the script is named ‘lkups’ so a total of three records have to be added to foxcode.dbf as follows:
       
Type
Abbrev
Expanded
Cmd
Data
Save
U
ol1
lcChoice =
{lkups}

.T.
U
ol2
lcFruit =
{lkups}

.T.
S
lkups


<Script Code here>


The content of the lkups script is described in detail below. The first part of the script receives a reference to the FoxCode object, instantiates the custom ‘ScriptHandler’ object (which is a custom class, based on the foxcodescript class, defined right within the script) and passes on the reference to the FoxCode object to the handler’s custom Start() method. (_CodeSense is a new VFP system variable that stores the name of the application that provides IntelliSense functionality; by default it is FoxCode.app).
This part of the code is completely standard and you will find it repeated (with minor variations in names) in several script records. The Start() method in the FoxCodeScript base class, populates a number of properties that are needed on the FoxCodeScript object and then calls a template method named ‘Main()’. That is where you place your custom code.
LPARAMETER toFoxcode
IF FILE( _CODESENSE)
  *** The IntelliSense manager can be located
  *** Declare the local variables that we need
  LOCAL luRetVal, loHandler
  SET PROCEDURE TO (_CODESENSE ) ADDITIVE
  *** Create an instance of the custom class
  loHandler = CreateObject( "ScriptHandler" )
  *** Call Start() and pass foxcode object ref
  luRetVal  = loHandler.Start( toFoxCode )
  *** Tidy up and return result
  loHandler = NULL
  IF ATC( _CODESENSE, SET( "PROC" ) )# 0
    RELEASE PROCEDURE ( _CODESENSE )
  ENDIF
  RETURN luRetVal
ELSE
  *** Do nothing at all
ENDIF
However, the most important thing to remember is that this script will actually be called twice!
The first time will be when the specified keyword is typed, because the record in the foxcode.dbf table specifically invokes it. On this pass, the FoxCode object’s MenuItem property will be empty – we have not yet displayed a list, and so nothing can have been selected. So we must first tell IntelliSense that we want it to display a list. To do so, we set foxcode.valuetype to “L”.
Next we call on the custom GetList() method to populate the Items property of the FoxCode object. Then we set foxcode.ItemScript to point back to this same script so that it is called again when a selection has been made. Finally, for this pass, we tell the IntelliSense engine to replace the keyword with the contents of the Expanded field.
DEFINE CLASS ScriptHandler as FoxCodeScript
  PROCEDURE Main()
    WITH This.oFoxCode
      IF EMPTY( .MenuItem )
        *** This is the first time this script is called,
        *** by typing in the abbreviation. First tell the
        *** IntelliSense Engine that we want a List
        .ValueType = "L"
        *** Now, pass the key to the List Builder Method
        *** This returns T when one or more items are found
        IF This.GetList( .UserTyped )
          *** We have a list, so set the ItemScript Property
          *** to re-call this script when a selection is made
          .ItemScript = 'lkups'
          *** And replace the key with the expanded text
          RETURN ALLTRIM( .Expanded )
        ELSE
          *** No items found, just return what the user typed
          RETURN .UserTyped
        ENDIF
You could, if you wished, make use of the Case field to determine how to format the return value instead of explicitly returning the contents of the Expanded field ‘as is’. To do that call the FoxCodeScript.AdjustCase() method in the RETURN statement instead, no parameters are needed. This method applies the appropriate formatting command to the content of the Expanded field before returning it.
The GetList() method is very simple indeed. It is called with the FoxCode.UserTyped property as a parameter. (Obviously we have defined the abbreviation that triggers the script to be identical to the lookup key in the table). It then executes a select statement into a local array using that value as the filter. If any values are found, the foxcode.items array is sized accordingly and the values copied to it. The method returns a logical value indicating whether any items were found.
  PROCEDURE GetList( tcKey )
    LOCAL llRetVal
    LOCAL ARRAY laTemp[1]
    *** Get any matching records from the option list
    SELECT cOption, .F. FROM myopts ;
     WHERE cKey = tcKey ;
      INTO ARRAY laTemp
    *** Set the return value and close table
    STORE (_TALLY > 0) TO llRetVal
    USE IN myopts
    IF llRetVal
      *** Populate the foxcode ITEMS array
      DIMENSION This.oFoxCode.Items[ _TALLY, 2 ]
      ACOPY( laTemp, This.oFoxCode.Items )
    ENDIF
    RETURN llRetVal   
  ENDPROC
When an item is selected from the list, the script is called once more, but this time the MenuItem property of the FoxCode object will contain whatever was selected which means that on the second pass the ‘ELSE’ condition of the Main() method gets executed.  This sets the foxcode.valuetype property to “V” and returns whatever is contained in the foxcode.MenuItem property.
  ELSE
   *** We have a selection so what we need to do is simply return the selected item
   .ValueType = "V"
   RETURN ALLTRIM( .MenuItem )
  ENDIF
ENDWITH
By setting the ValueType property to “V” we also tell the IntelliSense engine to insert the return value at the current insertion point so it will appear after the expanded text  Note that although, in this case, we are not actually changing the return value, this methodology does allow us to intercept the result after an item has been chosen, but before it is inserted into the code. While not essential, this additional flexibility can be very useful.
By implementing a generic script like this we can create as many shortcut lists as we like by simply adding the appropriate items to the listoptions.dbf table and a new record to foxcode.dbf  to identify the key and call the generic script.
Getting a list of files
Our first thought when this subject came up was – but we already have an automated list of “Most Recently Used files”. It is configurable, (on the ‘General’ tab of Options Dialog is a spinner for setting the number of files to hold in MRU Lists) and so it can display as many entries as we want. However, we then realized that in order to get a file into the MRU List we have to use it at least once (obviously)! Furthermore unless we make the MRU list very large indeed, it really is only useful for the most recently used files. This is because the number of entries in the list is fixed, so once that number is reached, each new file that we open forces an existing entry out of the list. In fact we still don’t really have a good way of getting a list of all files without going through the GetFile() dialog.
A little more thought gave us the idea of creating a script which would retrieve a listing of all files in the current directory, and all first level sub-directories, of a specified type. Since we want to be able to specify the file type, we need to make this script generic and call it from several different shortcuts by adding records to the foxcode.dbf table as follows:

Type
Abbrev
Expanded
Cmd
Case
Save
U
mop
modify command
{shofile}
U
T
U
dop
do
{shofile}
U
T
U
mof
modify form
{shofile}
U
T
U
dof
do form
{shofile}
U
T
U
mor
modify report
{shofile}
U
T
U
dor
report form
{shofile}
U
T

As you can see, these shortcuts expand to the appropriate command to either run, or modify a program, form or report. You may add other things (e.g. Classes, Labels, Menus, XML and Text Files) as you need them. All of these call the same generic ShoFile script which looks like this:
LPARAMETER oFoxcode
IF FILE(_CODESENSE)
  LOCAL eRetVal, loFoxCodeLoader
  SET PROCEDURE TO (_CODESENSE) ADDITIVE
  loFoxCodeLoader = CreateObject("FoxCodeLoader")
  eRetVal = loFoxCodeLoader.Start(m.oFoxCode)
  loFoxCodeLoader = NULL
  IF ATC(_CODESENSE,SET("PROC"))#0
    RELEASE PROCEDURE (_CODESENSE)
  ENDIF
  RETURN m.eRetVal
ENDIF
This block of code is the standard FoxCodeScriptLoader that you will find used in all of the scripts that utilize this class. The second part of the script defines the custom subclass and adds the Main() method (which is called from Start()).
DEFINE CLASS FoxCodeLoader as FoxCodeScript
  PROCEDURE Main()
    LOCAL lcMenu, lcKey
    lcMenu = THIS.oFoxcode.MenuItem
    IF EMPTY( lcMenu )
      *** Nothing selected, so display list
      lcKey  = UPPER( THIS.oFoxcode.UserTyped )
      *** What sort of files do we want
      DO CASE
        CASE INLIST( lcKey, "MOP","DOP" )
          lcFiles = '*.prg'
        CASE INLIST( lcKey, "MOF", "DOF" )
          lcFiles = '*.scx'
        CASE INLIST( lcKey, "MOR", "DOR" )
          lcFiles = '*.frx'
        OTHERWISE
          lcFiles = ""
      ENDCASE
      *** Populate the Items Array for display
      This.GetItemList( lcFiles )
      *** Return the Expanded item
      RETURN This.AdjustCase()
    ELSE
      *** Return the Selected item
      This.oFoxCode.ValueType = "V"
      RETURN lcMenu
    ENDIF
  ENDPROC
ENDDEFINE
The Main() method merely defines the file type skeleton using the keyword which was typed and calls the custom GetItemList() method. This is standard FoxPro code which uses the ADIR() function to retrieve a list of directories and then retrieves the list of files that match the specified skeleton from the current root directory and each first level sub-directory found. (Of course, the code could easily be modified to handle additional directory levels). The only IntelliSense related code in the method is right at the end where the contents of the file list array are copied to the Items collection on the FoxCode object and the ValueType and ItemScript Properties are set to generate the list, and define this script as the selection handler.
*** If we got something, display the list
IF lnFiles > 0
  THIS.oFoxcode.ValueType = "L"
  THIS.oFoxcode.ItemScript = "ShoFile"
  *** Copy items to temporary array
  DIMENSION THIS.oFoxcode.Items[lnFiles ,2]
  ACOPY(laFiles,THIS.oFoxcode.Items)
ENDIF
This paper barely touches the surface of the capabilities of IntelliSense scripting, but hopefully it will give you the impetus to try a few things of your own, if you haven’t already done so and maybe it will even give you a few new ideas if you have.
I would love to see any scripts that people develop and there is a page on the FoxPro Wiki for Intellisense scripts at http://fox.wikis.com/wc.dll?Wiki~IntelliSenseCustomScripts~VFP

Published Sunday, April 17, 2005 3:45 PM by andykr

Customizing Intellisense III

In the final part of this little series we'll look at scripting in Intellisense.
The FoxCode Object
While the FoxCode table handles the metadata that lies at the heart of the IntelliSense system, the FoxCode Object is what enables us, as developers, to really customize the behavior of IntelliSense. The FoxCode object is an instance of the foxcodescript class that is defined (on the session base class) in foxcode.prg  that defines the behavior for the various IntelliSense operations and provides hooks that allow us to extend that functionality as needed. The key methods of interest are listed at Table 2:
Table 3: FoxCodeScript Methods
Method
Purpose
Main
Template method, called from Start – allows custom code to be inserted
Start
Main set-up method for the object. Should be called explicitly in scripts
DefaultScript
Handler for “space” character – the default trigger for IntelliSense
HandleMRU
Handler for Most Recently Used Lists
HandleCOps
Handler for C-Operator Expansion
HandleCustomScripts
Handler for custom scripts
AdjustCase
Adjust the case of a keyword according to setting of the CASE Field, or the default if not specified
ReplaceWord
Replaces the last “word” typed with the specified “word”

In addition to these methods, the object exposes the set of properties shown below. First, every field in the FoxCode table has a corresponding property in the FoxCode object. The reason, of course, is so that the entire content of the record from the FoxCode table can be passed to whatever script is executing. There are, however, a number of additional properties control other aspects of the implementation:
Table 4: Properties of the FoxCode Object
Property
Description
Abbrev
Contents of FoxCode field
Case
Contents of FoxCode field
Cmd
Contents of FoxCode field
Cursorlocchar
The character used to indicate the location of cursor on completion (Default is "~")
Data
Contents of FoxCode field
Defaultcase
Case to use if nothing specified for this record (from the “V” record in FoxCode table)
Expanded
Contents of FoxCode field
Filename
Fully qualified path and file name of the currently open file
Fullline
The entire contents of the current line (includes spaces, tabs etc)
Icon
Icon for use in items array
Items
Array used when generating lists. Two columns, but only column 1 is required.
·          Items[1,1] – text to display in list
·          Items[1,2] – value tip for item
Items are sorted on Column 1 by default. Clear ItemSort flag to disable sorting
Itemscript
Name of the script to run after an item has been selected from a list (Optional)
Itemsort
Determine whether the items array is sorted or not (Default = .T. )
Location
Current editing window – allows control over whether a script is appropriate:
·          0      Command Window
·          1      Program
·          8      Menu Snippet
·          10    Code Snippet
·          12    Stored Procedure
Menuitem
The item that was selected from the list (empty unless a follow-up script is running)
Paramnum
Defined in help as: “Parameter number of the function for script call made within a function”
Save
Contents of FoxCode field
Source
Contents of FoxCode field
Timestamp
Contents of FoxCode field
Tip
Contents of FoxCode field
Type
Contents of FoxCode field
Uniqueid
Contents of FoxCode field
User
Contents of FoxCode field
Usertyped
Text that user typed (excludes leading spaces, tabs and the triggering keystroke)
Valuetip
Quick Info tip to display (when FoxCode.ValueType = "T")
Valuetype
Define how the return value from the script should be interpreted
·          V     Value:      Action depends upon the script – may be used to replace the typed text or add to it
·          L      List:         Displays the contents of the FoxCode.Items array as a list
·          T      Tip:         Displays the contents of the FoxCode.ValueTip property as a Quick Info Tip
Creating Scripts
Probably the most exciting thing that IntelliSense brings to Visual FoxPro is the ability to create custom scripts that allow us to greatly improve our productivity. A script has two distinct components:
·         A Preamble            This is IntelliSense specific and is responsible for setting up the FoxCode object and the environment in which the script is to run. Whenever a script is called, an instance of the FoxCode object is passed to it as a parameter.
·         The Code               This is standard FoxPro code that generates a result. Scripts must return something and the way in which their return value is interpreted is controlled by the Valuetype property.
All the scripts illustrated here have these two components and the capabilities of scripts are restricted only by what you can do in FoxPro code itself. The easiest way to describe scripts is to show some….
Insert a block of text
To insert a block of text we need to create a script that will return a formatted string to replace the abbreviation that triggered it. This is clearly not a generic script, so we can create it directly in the Data field of the foxcode.dbf record that defines the abbreviation like this:

Type
Abbrev
Expanded
Cmd
Tip
Data
Case
Save
U
hdr

{}
memo
Memo
M
T
  
The actual script, in the Data field, looks like this. As you can see, the preamble starts out by receiving a reference to the FoxCode object and checking the location to make sure that we are not in the command window ( a program header would not really be relevant there!). The preamble ends by setting the ValueType property to “V” on the FoxCode object to indicate that the return will be a value to replace the abbreviation:
LPARAMETER toFCObject
LOCAL lcOwner, lcName, lcVersion, lcPos, lcDesc, lcRets
lcDevName = [Andy Kramek]
lcOwner = "Tightline Computers, Inc"

IF toFCObject.Location = 0
  *** Not in the command window
   RETURN toFCObject.UserTyped
ELSE
  *** We will need textmerge ON for this script
  lcMerge = SET('TextMerge')
  SET TEXTMERGE ON
ENDIF

*** This script will return a string which we want to use to
*** replace the triggering text. Set valuetype accordingly:
toFCObject.ValueType = "V"
The actual work of the script is handled here. This is standard Visual FoxPro code that defines some variables and builds a formatted return string. There are several ways of doing this – the new TEXTMERGE functionality offers some excellent opportunities in this area.
LOCAL lcTxt, lcName, lcComment, lnPos
STORE "" TO lcTxt, lcName, lcComment
#DEFINE CRLF CHR(13)+CHR(10)

lcName = ALLTRIM( WONTOP() )
lcVersion = VERSION(1)
lnPos = AT( "[", lcVersion ) - 1
lcVersion = LEFT( lcVersion, lnPos )
lcDesc = ALLTRIM( INPUTBOX( 'Describe this Program or Procedure', lcOwner ))
lcRets = ALLTRIM( INPUTBOX( 'What does it return?', lcOwner, 'Logical' ))

*** Find out where on the line the insertion point is and set the indent accordingly
lnspaces = AT(  toFCObject.UserTyped, toFCObject.FullLine ) -1

*** And generate the actual text here
TEXT TO lcText NOSHOW
********************************************************************
<<SPACE(lnSpaces)>>*** Name.....: <<UPPER( lcName )>>
<<SPACE(lnSpaces)>>*** Author...: <<lcDevName>>
<<SPACE(lnSpaces)>>*** Date.....: <<DATE()>>
<<SPACE(lnSpaces)>>*** Notice...: Copyright (c) <<TRANSFORM( YEAR(DATE()))>> <<lcOwner>>
<<SPACE(lnSpaces)>>*** Compiler.: <<lcVersion>>
<<SPACE(lnSpaces)>>*** Function.: <<lcDesc>>
<<SPACE(lnSpaces)>>*** Returns..: <<lcRets>>
<<SPACE(lnSpaces)>>********************************************************************
<<SPACE(lnSpaces)>>~
ENDTEXT

*** Restore original textmerge setting
IF NOT EMPTY( lcMerge )
  SET TEXTMERGE &lcMerge
ENDIF 
RETURN lcText
Notice that this script uses the FoxCode object’s FullLine property to work out the indentation. This will only work as long as you use replace tabs with spaces in your editing windows. If you use tab characters (CHR(9)) only one space per tab will be inserted and the indentation will not be correct.
The final string is simply returned from the script in exactly the same way as any value is returned from a method or function. The same pattern can be used to define any script that inserts a text string.
Generating lists
The IntelliSense engine handles the generation of  “most-recently used” and “member lists” automatically, and there is nothing more that we need to do about those. The lists generated for native commands and functions are actually generated from a table named “FoxCode2”. A copy of this table is included with the source code, but, unlike the main foxcode.dbf, it is not exposed to developers for modification. So (unless we want to re-write the entire FoxCode application) we cannot easily alter these lists anyway.
However, that still leaves us with an awful lot of potential and we can certainly define additional lists to make our own lives easier. However, dealing with lists is a little more complex than merely returning a block of code because it is actually a two-part process. First we have to generate the list, and second we have to respond to the selection that was made.
Defining the contents for the list
IntelliSense lists are created by populating an array property (named “Items”) on the FoxCode object. This array must be dimensioned with two columns, the first is used to hold the text for the list items and the second to hold the Tip text to be associated with the item. In addition to specifying the content of a list, a script must tell the IntelliSense engine that a list is required. We have already seen that the ValueType property of the FoxCode object is used to communicate how the result of running a script should be interpreted and, to generate a list, all that is needed is to set this property to “L”.
The simplest way to generate a list is to create a foxcode.dbf record (Type = “U”) in which the data field defines a script that explicitly populates the required FoxCode object properties directly, as follows:

Type
Abbrev
Expanded
Cmd
Data
Save
U
olist
lcChoice =
{}
LPARAMETER toFoxCode
WITH toFoxCode
    .ValueType = "L"
    DIMENSION .items[3,2]
    .items[1,1] = "'First Option'"
    .items[1,2] = "Tip for option 1"
    .items[2,1] = "'Second Option'"
    .items[2,2] = "Tip for option 2"
    .items[3,1] = "'Third Option'"
    .items[3,2] = "Tip for option 3"
    RETURN ALLTRIM( .Expanded ) ENDWITH
.T.

Typing “olist” followed by a space in an editing window pops up a list containing the three options defined in the script (Figure 10). By returning the content of the FoxCode object’s Expanded property we can replace the keyword with some meaningful text and add on whatever is selected from the list. While this is pretty cool, it is not really very flexible since we have to hard-code the list options directly into the script. We could create a table to hold the content of lists that we want to generate and create a separate record for each abbreviation.
Of course, we don’t want to have to repeat the code that does the look up in each record. This is where the “Script” record type comes in. As we have already seen, we can call scripts from the CMD field of a foxcode.dbf record by including the script name in braces like this {scriptname}. So we can create a generic script to handle the lookup, generate the list and take the appropriate action when an item is selected.
How to define the action when a selection is made in a list
The problem in defining the action to take when an item is selected in a list is that the code that actually creates the list is not exposed to us. So, while we can specify the content of the list, we cannot directly control the consequential action. Instead the IntelliSense engine relies on two properties of the FoxCode object. The Itemscript property is used to specify a “handler” script that will be run after the list is closed and the MenuItem property is used to store the text of the selected item. If the list is closed without any entry being selected, this property will, of course, be empty. So in order to specify how IntelliSense should respond to a selection we need to create our own handler script.
We have already seen that generic Scripts require their own record (Type = “S”) in Foxcode.dbf and since they are called from another record, they must be constructed accordingly. The secret to such scripts lies in the FoxCodeScript class, which is defined in the IntelliSense Manager. (Note: the source code for this class can be found as FoxCode.prg in the FoxCode source directory).
To create a generic script, define a subclass of the FoxCodeScript class to provide whatever functionality you require and instantiate it. All the necessary code is, as usual, stored in the Data field of the Foxcode.dbf record. The easiest way to explain is to show it working - and it really is much easier than it sounds.
How to create a table driven list
The objective is to create a generic script that will:
·         Be triggered by a simple abbreviation (the keyword)
·         Replace the keyword with the specified expanded text
·         Use that keyword to retrieve a list of items from a local table
·         Display a list of the items
·         Append the selected item to the expanded text
The first thing that is needed is a table. For this example we will use ListOptions.dbf, which has only two fields as shown.

Ckey
Coption
ol1
'Number One'
ol1
'Number Two'
ol1
'Number Three'
ol2
'Apples'
ol2
'Bananas'
ol2
'Cherries'

One obvious improvement to this table would be to add a column to include some tip text for our menu items, and another would be a ‘Sequence’ column so that we can order our menus however we want. However, these refinements do not affect the principles here and to keep it simple we will leave them as an exercise for the reader.
As you can see our table recognizes two keywords “ol1” and “ol2”. First we need to add a record to foxcode.dbf for each of these keywords. These records must define the expanded text to replace the keyword and call the generic handler script for everything else. In this example the script is named ‘lkups’ so a total of three records have to be added to foxcode.dbf as follows:
       
Type
Abbrev
Expanded
Cmd
Data
Save
U
ol1
lcChoice =
{lkups}

.T.
U
ol2
lcFruit =
{lkups}

.T.
S
lkups


<Script Code here>


The content of the lkups script is described in detail below. The first part of the script receives a reference to the FoxCode object, instantiates the custom ‘ScriptHandler’ object (which is a custom class, based on the foxcodescript class, defined right within the script) and passes on the reference to the FoxCode object to the handler’s custom Start() method. (_CodeSense is a new VFP system variable that stores the name of the application that provides IntelliSense functionality; by default it is FoxCode.app).
This part of the code is completely standard and you will find it repeated (with minor variations in names) in several script records. The Start() method in the FoxCodeScript base class, populates a number of properties that are needed on the FoxCodeScript object and then calls a template method named ‘Main()’. That is where you place your custom code.
LPARAMETER toFoxcode
IF FILE( _CODESENSE)
  *** The IntelliSense manager can be located
  *** Declare the local variables that we need
  LOCAL luRetVal, loHandler
  SET PROCEDURE TO (_CODESENSE ) ADDITIVE
  *** Create an instance of the custom class
  loHandler = CreateObject( "ScriptHandler" )
  *** Call Start() and pass foxcode object ref
  luRetVal  = loHandler.Start( toFoxCode )
  *** Tidy up and return result
  loHandler = NULL
  IF ATC( _CODESENSE, SET( "PROC" ) )# 0
    RELEASE PROCEDURE ( _CODESENSE )
  ENDIF
  RETURN luRetVal
ELSE
  *** Do nothing at all
ENDIF
However, the most important thing to remember is that this script will actually be called twice!
The first time will be when the specified keyword is typed, because the record in the foxcode.dbf table specifically invokes it. On this pass, the FoxCode object’s MenuItem property will be empty – we have not yet displayed a list, and so nothing can have been selected. So we must first tell IntelliSense that we want it to display a list. To do so, we set foxcode.valuetype to “L”.
Next we call on the custom GetList() method to populate the Items property of the FoxCode object. Then we set foxcode.ItemScript to point back to this same script so that it is called again when a selection has been made. Finally, for this pass, we tell the IntelliSense engine to replace the keyword with the contents of the Expanded field.
DEFINE CLASS ScriptHandler as FoxCodeScript
  PROCEDURE Main()
    WITH This.oFoxCode
      IF EMPTY( .MenuItem )
        *** This is the first time this script is called,
        *** by typing in the abbreviation. First tell the
        *** IntelliSense Engine that we want a List
        .ValueType = "L"
        *** Now, pass the key to the List Builder Method
        *** This returns T when one or more items are found
        IF This.GetList( .UserTyped )
          *** We have a list, so set the ItemScript Property
          *** to re-call this script when a selection is made
          .ItemScript = 'lkups'
          *** And replace the key with the expanded text
          RETURN ALLTRIM( .Expanded )
        ELSE
          *** No items found, just return what the user typed
          RETURN .UserTyped
        ENDIF
You could, if you wished, make use of the Case field to determine how to format the return value instead of explicitly returning the contents of the Expanded field ‘as is’. To do that call the FoxCodeScript.AdjustCase() method in the RETURN statement instead, no parameters are needed. This method applies the appropriate formatting command to the content of the Expanded field before returning it.
The GetList() method is very simple indeed. It is called with the FoxCode.UserTyped property as a parameter. (Obviously we have defined the abbreviation that triggers the script to be identical to the lookup key in the table). It then executes a select statement into a local array using that value as the filter. If any values are found, the foxcode.items array is sized accordingly and the values copied to it. The method returns a logical value indicating whether any items were found.
  PROCEDURE GetList( tcKey )
    LOCAL llRetVal
    LOCAL ARRAY laTemp[1]
    *** Get any matching records from the option list
    SELECT cOption, .F. FROM myopts ;
     WHERE cKey = tcKey ;
      INTO ARRAY laTemp
    *** Set the return value and close table
    STORE (_TALLY > 0) TO llRetVal
    USE IN myopts
    IF llRetVal
      *** Populate the foxcode ITEMS array
      DIMENSION This.oFoxCode.Items[ _TALLY, 2 ]
      ACOPY( laTemp, This.oFoxCode.Items )
    ENDIF
    RETURN llRetVal   
  ENDPROC
When an item is selected from the list, the script is called once more, but this time the MenuItem property of the FoxCode object will contain whatever was selected which means that on the second pass the ‘ELSE’ condition of the Main() method gets executed.  This sets the foxcode.valuetype property to “V” and returns whatever is contained in the foxcode.MenuItem property.
  ELSE
   *** We have a selection so what we need to do is simply return the selected item
   .ValueType = "V"
   RETURN ALLTRIM( .MenuItem )
  ENDIF
ENDWITH
By setting the ValueType property to “V” we also tell the IntelliSense engine to insert the return value at the current insertion point so it will appear after the expanded text  Note that although, in this case, we are not actually changing the return value, this methodology does allow us to intercept the result after an item has been chosen, but before it is inserted into the code. While not essential, this additional flexibility can be very useful.
By implementing a generic script like this we can create as many shortcut lists as we like by simply adding the appropriate items to the listoptions.dbf table and a new record to foxcode.dbf  to identify the key and call the generic script.
Getting a list of files
Our first thought when this subject came up was – but we already have an automated list of “Most Recently Used files”. It is configurable, (on the ‘General’ tab of Options Dialog is a spinner for setting the number of files to hold in MRU Lists) and so it can display as many entries as we want. However, we then realized that in order to get a file into the MRU List we have to use it at least once (obviously)! Furthermore unless we make the MRU list very large indeed, it really is only useful for the most recently used files. This is because the number of entries in the list is fixed, so once that number is reached, each new file that we open forces an existing entry out of the list. In fact we still don’t really have a good way of getting a list of all files without going through the GetFile() dialog.
A little more thought gave us the idea of creating a script which would retrieve a listing of all files in the current directory, and all first level sub-directories, of a specified type. Since we want to be able to specify the file type, we need to make this script generic and call it from several different shortcuts by adding records to the foxcode.dbf table as follows:

Type
Abbrev
Expanded
Cmd
Case
Save
U
mop
modify command
{shofile}
U
T
U
dop
do
{shofile}
U
T
U
mof
modify form
{shofile}
U
T
U
dof
do form
{shofile}
U
T
U
mor
modify report
{shofile}
U
T
U
dor
report form
{shofile}
U
T

As you can see, these shortcuts expand to the appropriate command to either run, or modify a program, form or report. You may add other things (e.g. Classes, Labels, Menus, XML and Text Files) as you need them. All of these call the same generic ShoFile script which looks like this:
LPARAMETER oFoxcode
IF FILE(_CODESENSE)
  LOCAL eRetVal, loFoxCodeLoader
  SET PROCEDURE TO (_CODESENSE) ADDITIVE
  loFoxCodeLoader = CreateObject("FoxCodeLoader")
  eRetVal = loFoxCodeLoader.Start(m.oFoxCode)
  loFoxCodeLoader = NULL
  IF ATC(_CODESENSE,SET("PROC"))#0
    RELEASE PROCEDURE (_CODESENSE)
  ENDIF
  RETURN m.eRetVal
ENDIF
This block of code is the standard FoxCodeScriptLoader that you will find used in all of the scripts that utilize this class. The second part of the script defines the custom subclass and adds the Main() method (which is called from Start()).
DEFINE CLASS FoxCodeLoader as FoxCodeScript
  PROCEDURE Main()
    LOCAL lcMenu, lcKey
    lcMenu = THIS.oFoxcode.MenuItem
    IF EMPTY( lcMenu )
      *** Nothing selected, so display list
      lcKey  = UPPER( THIS.oFoxcode.UserTyped )
      *** What sort of files do we want
      DO CASE
        CASE INLIST( lcKey, "MOP","DOP" )
          lcFiles = '*.prg'
        CASE INLIST( lcKey, "MOF", "DOF" )
          lcFiles = '*.scx'
        CASE INLIST( lcKey, "MOR", "DOR" )
          lcFiles = '*.frx'
        OTHERWISE
          lcFiles = ""
      ENDCASE
      *** Populate the Items Array for display
      This.GetItemList( lcFiles )
      *** Return the Expanded item
      RETURN This.AdjustCase()
    ELSE
      *** Return the Selected item
      This.oFoxCode.ValueType = "V"
      RETURN lcMenu
    ENDIF
  ENDPROC
ENDDEFINE
The Main() method merely defines the file type skeleton using the keyword which was typed and calls the custom GetItemList() method. This is standard FoxPro code which uses the ADIR() function to retrieve a list of directories and then retrieves the list of files that match the specified skeleton from the current root directory and each first level sub-directory found. (Of course, the code could easily be modified to handle additional directory levels). The only IntelliSense related code in the method is right at the end where the contents of the file list array are copied to the Items collection on the FoxCode object and the ValueType and ItemScript Properties are set to generate the list, and define this script as the selection handler.
*** If we got something, display the list
IF lnFiles > 0
  THIS.oFoxcode.ValueType = "L"
  THIS.oFoxcode.ItemScript = "ShoFile"
  *** Copy items to temporary array
  DIMENSION THIS.oFoxcode.Items[lnFiles ,2]
  ACOPY(laFiles,THIS.oFoxcode.Items)
ENDIF
This paper barely touches the surface of the capabilities of IntelliSense scripting, but hopefully it will give you the impetus to try a few things of your own, if you haven’t already done so and maybe it will even give you a few new ideas if you have.
I would love to see any scripts that people develop and there is a page on the FoxPro Wiki for Intellisense scripts at http://fox.wikis.com/wc.dll?Wiki~IntelliSenseCustomScripts~VFP

Published Sunday, April 17, 2005 3:45 PM by andykr

No comments:

Post a Comment

Writing better code (Part 1)

Writing better code (Part 1) As we all know, Visual FoxPro provides an extremely rich and varied development environment but sometimes to...