[hand with pencil]
Stuff For Sale
2004 Summer Tour
About
Blog
Class Stuff
Email Me
Events
Gallery
Home
In The Press
Newsletter
Services
Smalltalk
Veggie Van Gogh

Credits
© 2002,
Bytesmiths

[this is simply a banner and menu bar]

Please patronize sponsors of this page!

Bytesmiths no longer is involved in software consulting. Maintenance of this web site is currently subsidised by unrelated business activities. Please pass the word to other interested folks, so I can continue to host this page!
Your site could be listed here, for as little as $12 per month! Go to Bytesmiths Press for details.

This site has been selected by PC Webopaedia as one of the best on this topic!
This site has been awarded a Links2Go Key Resource Award in the Smalltalk category!

Originally published in The Smalltalk Report, September 1995.

Managing Project Documents, Part 2

by Jan Steinman

In the June issue, we made a case for "continuous documentation," and outlined what that entails. We also promised to give you some concrete examples and source code, so you could begin to implement a continuous documentation process.

There are at least five widely differing Smalltalk dialects out there, augmented by two major and numerous minor code management systems. Rather than attempt the impossible task of embracing such diversity, we're presenting stuff that is actually implemented and working in VisualWorks® 2.0 under ENVY/Developer® R1.43.

Many of these things can be done in other environments. However, much of the following assumes you can associate storage with arbitrary software components, which might be difficult if your code manager is simply a layer on top of source code files.

  • Principle 1: Conceptual Integrity -- Documentation must be at the same level as that which it describes. There is simply no way you can cram all your documentation needs into class and method specifications. ENVY provides nestable modules called Applications and SubApplications that have a specification field, and a module binding component called a "configuration map" that also has a specification field.
  • Principle 2: Constant Accuracy -- Documentation must be stored with that which it describes. If you want your developers to maintain their documents, you've got to make it easy for them to do so.
  • Principle 3: Accessibility -- Documentation must be available quickly and efficiently from other, related documentation. This does not mean sending a reference number via email to your organization's technical library!

A Simple Macro Facility

The marriage of these three principles demands some way of linking related parts of documentation together. VisualWorks has a simple but efficient tagged character class, Text , that allows you to associate arbitrary objects with each character in a string. (If your Smalltalk has no such thing, you will need to either add it, or come up with some other linking mechanism.)

This tagged character capability suggests a simple "not quite hypertext" linking facility. [951223-JWS: See also our marketing blurb on a much-improved full hypertext implementation that is available with our consulting services.] First off, we need to fix a bug; add the following instance method to TextStream :

TextStream :
  nextPutAll: aText
 
    "Place each of the elements of aText on myself, starting at my
 current position. If I'm fed an instance of Text, keep its emphasis.
 Answer aText."
 
    (aText respondsTo: #runs)
      ifFalse: [^super nextPutAll: aText].
 
    1 to: aText size do: [:i |
      "I know, it's brute-force. Someday, this should be optimized by
       examining runs."
      self
        emphasis: (aText emphasisAt: i);
        nextPut: (aText string at: i)].
    ^aText

Without this bugfix, Text fed to a TextStream loses its emphasis, which sorta defeats the purpose! Readers of our last column may recognize a "signature testing" pattern of managing system changes, and may recall that an override is indeed a base image change. Be aware that code that expects the superclass behavior will now be "broken" by this bugfix!

Now implement the following three methods in a class extension of Text . We try to put system additions in a parallel application with a similar name. In our repository, the following extensions are in a subapp called DevelopmentBytesmiths , since they relate to the development process, and not particularly to Text in its full generality. Note also that these extensions are dependent on Compiler ; simply adding these extensions to the subapp Collections , where Text is defined, would create a circular dependency between the apps Kernel and Compilation .

Text :
  withInclusions
    "Answer a copy of me in which all strings with the emphasis
  #Smalltalk are evaluated and replaced with a String or Text
  representation of the result."
 
    ^self withInclusionsIn: nil
 
  withInclusionsIn: codeOwner
    "Answer a copy of me in which all strings with the emphasis
  #Smalltalk are evaluated and replaced with a String or Text
  representation of the result. The context of evaluation is the
  object codeOwner."
 
    ^(self
      withInclusionsIn: codeOwner
      on: (TextStream on: (String new: self size * 2
        "Guess that inclusions may double size."))) contents
 
  withInclusionsIn: codeOwner on: textStream
    "Place on textStream a copy of me in which all strings with the
  emphasis #Smalltalk are evaluated and replaced with a String or Text
  representation of the result. The context of evaluation is the object
  codeOwner. Answer textStream."
 
    | whereAmI whereWasI |
    ^(self size > 0 and: [runs values includes: #Smalltalk])
      ifFalse: [textStream nextPutAll: self]
      ifTrue:
        [whereAmI := whereWasI := 1.
        [whereAmI := whereAmI + (self runLengthFor: whereAmI).
        (self emphasisAt: whereWasI) == #Smalltalk
          ifFalse: [textStream nextPutAll: (self copyFrom: whereWasI to: whereAmI-1)]
          ifTrue: [(Object errorSignal
            handle: [:ex |
              textStream
                emphasis: #bold;
                nextPutAll: '*** Cannot include the following expression!! ***';
                emphasis: nil;
                cr;
                nextPutAll: (string copyFrom: whereWasI to: whereAmI-1).
              ex returnWith: '']
            do: [Compiler
              silentEvaluate: (self copyFrom: whereWasI to: whereAmI-1)
              for: codeOwner
              logged: false]) withInclusionsIn: codeOwner on: textStream].
        whereWasI := whereAmI.
        whereAmI = 0 or: [whereAmI > self size]] whileFalse: [].
        textStream]

The VisualWorks 2.0 compiler is in many ways as old as Smalltalk itself, and has not been fully integrated with signals and exceptions. We fixed that by adding silentEvaluate:for:logged: , which always raises an exception when compilation breaks. You can do the same, or you can change this to evaluate:for:logged: and put up with the occasional syntax error window when expanding Text that has bad expressions to expand.

This facility is recursive; if a section of Text has the emphasis #Smalltalk, the resulting expansion is itself scanned for inclusions, so any object that understands withInclusionsIn:on: can implement its own expansion scheme. In fact, without someone putting an end to this recursion, there can be real trouble! Implement the following two methods in extensions to Object and String , respectively:

Object :
  withInclusionsIn: ignored on: textStream
    "Place on textStream a printable representation of me suitable for
  use in documentation. Answer textStream.
    This is a 'bug catch' message in this class, and should normally
  only be sent to Texts or Strings. Subclasses should not normally
  override simply to change presentation."
 
    self printOn: textStream.
    ^textStream
String :
  withInclusionsIn: ignored on: textStream
    "Place myself on textStream for use in documentation. Answer
  textStream."
 
    textStream nextPutAll: self.
    ^textStream

For even more flexibility, add the following to an extension of BlockClosure . If the Text being expanded is a block, the block will be evaluated, and the result will be inserted into the expanded output. The block can optionally take the "code owner" and the active TextStream as arguments, which allows included source code blocks to query their environment and directly manipulate the resulting expanded output.

BlockClosure :
  withInclusionsIn: codeOwner on: textStream
    "Place on textStream a printable representation of my evaluation
  suitable for use in documentation. Depending on the number of
  arguments I take, pass me codeOwner and textStream on evaluation.
  Answer textStream."
 
    | args |
    args := Array new: self numArgs.
    1 <= args size ifTrue: [args at: 1 put: codeOwner].
    2 <= args size ifTrue: [args at: 2 put: textStream].
    ^(self valueWithArguments: args)
      withInclusionsIn: codeOwner
      on: textStream

These methods add the basic "hypertext" behavior to Text . Now your app/subapp comments can have things like MyClass comment with the emphasis #Smalltalk, and sending withInclusions to that Text will embed the comment for MyClass . But by itself, this inclusion facility is not terribly useful for two reasons:

  1. ENVY strips the per-character attributes from Text before storing it.
  2. Basic VisualWorks provides no user interface for applying custom character attributes to a Text .

ENVY Text storage

If your hypertext goes away when your image quits, it won't improve your team's productivity one bit! When you press the "source" button in an ENVY browser to switch to "comment" mode, any changes you make are stored as Strings in "inherited user fields." These are arbitrary key value storage locations, in which both key and value must be a String . If we can trick ENVY into storing Text (or even arbitrary objects) in these fields, it saves us from changing each of the many places where these fields are accessed.

This gets a little difficult, because the source code for methods that access the repository has been removed, and your ENVY/Developer license keeps you from decompiling or otherwise reverse engineering those methods. The following technique allows you to copy the hidden methods and associate that copy with a new method selector, allowing you to provide an original implementation in its place that conditionally sends the old method.

OTI has no legal objections to this technique, but it can be dangerous if misused! We've been using the following for some time, but if there is a typo, or if you use this technique to intercept and modify other hidden methods, you may damage your ENVY repository. Be sure to follow our suggestions in last month's column for managing base image changes, and consider making and testing these changes in a separate repository until you are certain they are safe.

Do this in a workspace to copy and rename the two hidden methods that need to be intercepted:

  | meth oldRecord |
  meth := (UserFieldRecord compiledMethodAt: #contents) copy.
  oldRecord := meth record.
  meth selector: #contentsFromVendor.
  UserFieldRecord
    updateEditionsRecordIn: LibraryManagement
    with: [:record | record
      addMethod: meth
      source: nil
      basedOn: oldRecord
      changeCategoryTo: 'intercepted methods']
    ifUnable: [].
  meth := (Record class compiledMethodAt: #libraryFormatFor:) copy.
  oldRecord := meth record.
  meth selector: #libraryFormatForFromVendor:.
  Record class
    updateEditionsRecordIn: LibraryManagement
    with: [:record | record
      addMethod: meth
      source: nil
      basedOn: oldRecord
      changeCategoryTo: 'intercepted methods']
    ifUnable: []

Before we go any further, we need to establish the predicate we are going to use for switching between the original implementation and our Text capable implementation. Put the following two methods in the same Application or SubApplication where you put the other class extensions:

Text class:
  canReadFrom: chars
    "Does the String (or streamed String) chars contain information
  that can be used to create an instance of me via #readFrom:?"
 
    | signatureChars |
    ^chars size >= "String new asText storeString size" 56 and:
      ['(Text string: '''
        occursIn: (chars isSequenceable
          ifTrue: [chars]
          ifFalse:
            [signatureChars := chars next: "'(Text string: ''' size" 15.
            chars position: chars position - 15.
            signatureChars])
        at: 1]
Text :
  libraryFormat
    "Answer a representation of myself suitable for storing in ENVY
  user fields."
 
    self storeString

Now replace those two hidden methods that we copied with original implementations that conditionally send the renamed method:

UserFieldRecord :
  contents
    "Answer my contents, decoding them if necessary."
 
    | charStream decoder |
    charStream := self collection readStream position: self startPosition - 1.
    ^((Text respondsTo: #canReadFrom:) and: [Text canReadFrom: charStream])
      ifTrue: [Text readFrom: charStream]
      ifFalse: [self contentsFromVendor]
Record class:
  libraryFormatFor: anObject
    "Answer a format suitable for storing anObject in the library."
 
    ^(anObject respondsTo: #libraryFormat)
      ifFalse: [self libraryFormatForFromVendor: anObject]
      ifTrue: [anObject libraryFormat]

Notice the conditional nature of these changes. These changes can safely replace those in the base image, because they will not break if the Application or SubApplication containing the Text extensions is not present.

With these changes, character emphases that you change in any comment or notes field will be preserved in ENVY. More importantly, the newly emphasized commentary does not "break" if these changes are not present; since they are in the storeString format, they are a simply a bit difficult to read.

You now have all the "modelling" changes you need to implement hypertext based on arbitrary Smalltalk expressions embedded in Text objects -- that's always the hardest thing to get right. Now all you need is a UI!


Setting #Smalltalk Text Emphasis

We've added extensive support for viewing, searching, and modifying these embedded expressions; a magazine column format cannot do such things justice. However, there are a few simple things you can do to get started, which may be enough to fill your needs, or it may inspire you to greater accomplishments.

(You're missing a comment template with 
hyper-links.)

Figure 1. An application comment template, showing hyper-links.

ParagraphEditor is the class that generates and modifies Text , including character emphasis. Unfortunately, there is no simple way to fully support new emphasis types without changing existing ParagraphEditor code, making new TextAttributes and CharacterAttributes instances, and then managing those instances as long lived system resources.

We're going to cheat by adding a simple facility for setting and removing the new #Smalltalk emphasis we've specified in a less general way. To be able to set or remove an arbitrary emphasis, add the following to an extension of ParagraphEditor :

ParagraphEditor :
  addEmphasis: emphasis
    "Add the given emphasis to the emphasis of the current selection."
 
    | thisText |
    thisText := self selectionStartIndex = self selectionStopIndex
      ifTrue: [Text string: 'x' emphasis: emphasisHere]
      ifFalse: [self selection].
    thisText addEmphasis: emphasis
      removeEmphasis: #()
      allowDuplicates: false.
    self selectionStartIndex = self selectionStopIndex
      ifTrue: [emphasisHere := thisText emphasisAt: 1]
      ifFalse: [self replaceSelectionWith: thisText].
    view topComponent refresh
 
  removeEmphasis: emphasis
    "Remove the given emphasis from the emphasis of the current
  selection."
 
    | thisText |
    thisText := self selectionStartIndex = self selectionStopIndex
      ifTrue: [Text string: 'x' emphasis: emphasisHere]
      ifFalse: [self selection].
    thisText addEmphasis: #()
      removeEmphasis: emphasis
      allowDuplicates: false.
    self selectionStartIndex = self selectionStopIndex
      ifTrue: [emphasisHere := thisText emphasisAt: 1]
      ifFalse: [self replaceSelectionWith: thisText].
    view topComponent refresh

Now, wire it to a pair of "hot" keys. This method works like the other emphasis hot keys, such as "ESC b" to add bold and "ESC B" to remove bold, but unlike the method that implements other emphasis changes, this one is not hard wired to a particular key.

ParagraphEditor :
  changeInclusionKey: aChar 
    "Add or remove 'inclusion' emphasis of the current selection, depending on the 
case of aChar. Uppercase removes, lowercase adds."
 
    aChar keyValue isUppercase
      ifTrue: [self removeEmphasis: #(Smalltalk)]
      ifFalse: [self addEmphasis: #(Smalltalk)].
    ^true

Finally, evaluate the following statements to bind this emphasis change to a "hot" key of your choice. (We chose "h" for "hyper," and because it was not already used.) These statements should go in the loaded method of the Application or SubApplication where you have been putting all this code. (While you're at it, add a removing method that undoes these key bindings.)

  (ParagraphEditor classPool at: #Keyboard)
    bindValue: #changeInclusionKey: to: ESC followedBy: $h;
    bindValue: #changeInclusionKey: to: ESC followedBy: $H.
  ParagraphEditor withAllSubclasses do: [:peClass |
    peClass allInstancesDo: [:pe |
      pe flushKeyboardMap]]

Viewing The Results

Now you are able to create and store run time inclusions in your project documentation. This allows detailed documentation to be linked to abstract documentation, yet be maintained at the detailed level, while not distracting the reader of the abstract information. But what if the reader wants to be so distracted? How do we see this stuff?

The simple way is to select a #Smalltalk-emphasized expression and inspect it. You get a basic inspector on a Text , which allows you to see the plain ASCII expansion. This suggests a simple, yet effective alternative: implement an Inspector subclass for Text that allows you a WYSIWYG view of Text .

Rather than use up precious column space with an entire implementation, we'll tease you with some salient bits:

  Inspector subclass: #TextInspector
    instanceVariableNames: ' '
    classVariableNames: ''
    poolDictionaries: ''
    category: 'BaseToolsBytesmiths'
 
  TextInspector comment: 'This class allows normal ParagraphEditor
    style editing of a Text just by inspecting it. It has no list view,
    since you don''''t really need one.'
TextInspector class:
  view: anInspector in: area of: superView
    "Create a text view on anInspector in area of superView."
 
    superView
      add: 
        (LookPreferences edgeDecorator on:
          (anInspector class textViewClass
            on: anInspector 
            aspect: #text
            change: #acceptText:from:
            menu: #textMenu initialSelection: nil))
      in: area

The rest is eight methods that all override Inspector methods.

This works great as the "developer" interface -- your developers will be so glad to be rid of keeping class comments in synch with app/subapp comments, that they won't mind that their hyperlink to embedded documentation is an inspector. But something better is needed for the "linear reader" and for hardcopy.

Unfortunately, any presentation change that is available from existing browsers is going to be a base image change. We considered wiring inclusion expansion to a new ParagraphEditor menu item, but we didn't feel the facility was general enough, and we certainly didn't want it slipping through to the end user!

Instead, we dug into the EnvyBrowser hierarchy, adding yet another mode to the code pane, with a menu item in the status area to expand all inclusions. This isn't the entire story, but it should get you started:

AbstractMethodsBrowser :
  expandComment
    "I assume that my text view is in comment mode. Cause the comment
  to be re-generated, with all Text of #Smalltalk style evaluated and
  inserted in-place."
 
    | oldState |
    [oldState := self textSelector.
    self changedTextTo: #textShowingCommentWithInclusions]
      valueNowOrOnUnwindDo: [self textSelector: oldState]
 
  textShowingCommentWithInclusions
    "Answer the expanded comment."
 
    ^Cursor read showWhile: [self commentString asText withInclusions]

(You're missing 
the example shown in the previous figure, with the hyperlinks expanded.)

Figure 2. Example of fully expanded documentation.


Conclusion

Getting project documentation to flow out of the development process can greatly increase productivity, and if it is well done, the developers actually begin to enjoy producing and maintaining their documentation!

In two columns, we've outlined the requirements and principles of a continuous documentation process, and provided some concrete examples of how it can be accomplished. In the future, we'll periodically revisit the topic with further design sketches and examples.


Go to the previous column in the series, or the next column in the series, or check out our marketing blurb on our SmallDoc system that we give to our clients.

160 Sharp Road, Salt Spring Island, British Columbia, V8K 2P6, Canada