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

 

One Response to Customizing Intellisense III

  • Dave Birley says:

    Just checking in to let you know that you efforts are not being ignored, but, rather, are being greatly valued. Thanks a million for removing some of the mystery in this marvellous tool!

Leave a Reply to Dave Birley Cancel reply

Your email address will not be published. Required fields are marked *