Wednesday, March 22, 2006

XMLDoc regions CC#23955

In response to a comment here on this blog, I've uploaded source code of my XMLDoc regions plugin to Code Central.
I hope you'll find it useful.

Wednesday, March 08, 2006

Help Insight

I like Help Insight in Delphi 2006. I think it's going to prove to be very useful. The cool thing is that it's customizable: the popup window is a browser view of an HTML page generated from the XML compiler output by applying HelpInsight.xsl and HelpInsight.css files located in your $(BDS)\Objrepos directory.
If you want to have a look at what the XML compiler output looks like, you can modify your HelpInsight.xsl (back up the original first!) like this:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="/">
<html>
<script language="javascript">
function showsrc() {
window.alert(document.body.innerHTML);
}
</script>
<body>
<a href="javascript:showsrc();">source</a>
<xsl:copy-of select="."/>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Then move your mouse over some identifiers in your source code to bring up the Help Insight browser window, and click on the "source" link to see the original XML produced by the compiler.
The format is basically the following:
<MEMBER DisplayName=|DisplayName| name=|name|>
<SOURCE declaredOn=|declaredOn|
declaredIn=|declaredIn|
declaredInShort=|declaredInShort|></SOURCE>
[|your XML comments|]
<SUMMARY><PARA>|summary|</PARA></SUMMARY>
[|params|]
</MEMBER>
I'm not going to write up a complete description of the elements, here are just a few quick examples. Given the following source code, you get the XML shown below when you invoke Help Insight for Const1, TForm1, Field, Func, Prop, Event and Form1, respectively:
const
Const1 = 42; // the answer to life, the universe and everything

type
TForm1 = class(TForm)
public
Field: Integer;
FEvent: TNotifyEvent;
function Func(X, Y: Integer; const S: string): Boolean;
property Prop: Integer read Field;
property Event: TNotifyEvent read FEvent;
end;

var
Form1: TForm1;

Const1:
<MEMBER DisplayName="Const1 Constant" name="C:Const1">
<SOURCE declaredOn="10,3"
declaredIn="C:\Temp\Unit1.pas"
declaredInShort="Unit1.pas"></SOURCE>
<SUMMARY><PARA>Const1 = 42 - System.Integer</PARA></SUMMARY>
<MEMBER>

TForm1:
<MEMBER DisplayName="TForm1 Type" name="T:Unit1.TForm1">
<SOURCE declaredOn="13,3"
declaredIn="C:\Temp\Unit1.pas"
declaredInShort="Unit1.pas"></SOURCE>
<SUMMARY><PARA>Declared in Unit1</PARA></SUMMARY>
</MEMBER>

Field:
<MEMBER DisplayName="Field Field" name="F:Unit1.TForm1.Field">
<SOURCE declaredOn="15,5"
declaredIn="C:\Temp\Unit1.pas"
declaredInShort="Unit1.pas"></SOURCE>
<SUMMARY><PARA>Field - System.Integer</PARA></SUMMARY>
</MEMBER>

Func:
<MEMBER DisplayName="TForm1.Func(Integer,Integer,string) Method"
name="M:Unit1.TForm1.Func(System.Integer,System.Integer,System.string)">
<SOURCE declaredOn="17,14"
declaredIn="C:\Temp\Unit1.pas"
declaredInShort="Unit1.pas"></SOURCE>
<SUMMARY><PARA>Declared in <SEE DisplayName="Unit1.TForm1"
cref="Unit1|Unit1.TForm1"></SEE></PARA></SUMMARY>
<PARAM name="X">System.Integer
<PARAM name="Y">System.Integer
<PARAM name="S">System.string
<RETURNS><PARA>System.Boolean</PARA></RETURNS>
</MEMBER>

Prop:
<MEMBER DisplayName="Prop Property" name="P:Unit1.TForm1.Prop">
<SOURCE declaredOn="18,14"
declaredIn="C:\Temp\Unit1.pas"
declaredInShort="Unit1.pas"></SOURCE>
<SUMMARY><PARA>Prop - System.Integer</PARA></SUMMARY>
</MEMBER>

Event:
<MEMBER DisplayName="Event Event" name="E:Unit1.TForm1.Event">
<SOURCE declaredOn="19,14"
declaredIn="C:\Temp\Unit1.pas"
declaredInShort="Unit1.pas"></SOURCE>
<SUMMARY><PARA>Declared in <SEE DisplayName="Unit1.TForm1"
cref="Unit1|Unit1.TForm"></SEE></PARA></SUMMARY>
<PARAM name="Sender">System.TObject
</MEMBER>

Form1:
<MEMBER DisplayName="Form1 Field" name="F:Unit1.Form1">
<SOURCE declaredOn="23,3"
declaredIn="C:\Temp\Unit1.pas"
declaredInShort="Unit1.pas"></SOURCE>
<SUMMARY><PARA>Form1 - <SEE DisplayName="Unit1.TForm1"
cref="Unit1|Unit1.TForm"></SEE></PARA></SUMMARY>
</MEMBER>
As you can see, the <SOURCE> node contains information about the source code file name, line number and column of the declaration. Also note the unclosed <PARAM> tags ;-)

The above XML is what you get when the source code doesn't contain any XML comments. If it does, they are simply inserted between Help Insight's <SOURCE> and <SUMMARY> nodes. Whatever your comments are, they all go there. The weird part is that if your comments contain any <PARAM> or <RETURNS> tags, or even any tags starting with these strings, such as <PARAMDEF> or <RETURNSDESC>, you will suppress the Help Insight's own <PARAM> and <RETURNS> sections (they will not be included anymore). If you want to have, for example, a list of parameters from the compiler mixed with your own descriptions of them, you'll have to use different tag names (I use <xparam> and <xreturns>) and write some XSL to produce the combined output.

As one can see in the original HelpInsight.xsl, the Help Insight browser supports some special URLs, e.g. a link with href="helpinsight:typelink:Unit1|Unit1.TForm1" would open Help Insight window for my TForm1 type. Another I've found is href="helpinsight:filelink:C:\Temp\Unit1.pas?10,3" which would make your IDE editor go to Unit1.pas at line 10, character position 3.

My current modifications to HelpInsight.xsl are such that my Help Insight shows me its own parameter list for methods, combined with my own descriptions of them. Summary and remarks are also included. I can also use URLs within my comments; these will produce HTML links which open a new IE window with the URL.
I've also modified HelpInsight.css a little bit to change colors, fonts etc.

Friday, March 03, 2006

XML Documentation in Delphi 2006

Delphi 2006 compiler supports an option to generate XML representation of your source code which can be processed (using XSLT) to produce, for example, HTML or HTML Help output.

To associate a piece of your own documentation with a class or method, you can simply put your comments directly before its declaration. The compiler will copy them into a separate <devnotes> node in the XML output.

Another place where these comments are used is the new, customizable Help Insight which I find very cool.

The only problem I have with writing documentation directly in source code is that the source code becomes cluttered and therefore less readable. (And it seems I'm not alone.)

I think one reasonable workaround for this problem could be this: Enclose all your documentation comments in regions with a special description, say, 'xmldoc', like this:

type
{$region 'xmldoc'}
/// <summary>
/// This is the application's main form.
/// </summary>
{$endregion}
TForm1 = class(TForm)

The important part is to use the 'xmldoc' description only for regions which contain your XML documentation so they can be processed separately from other regions in your code. Then you can write an IDE plugin to elide or unelide all such regions. Since I prefer using keyboard, I've implemented it as a keyboard binding:

procedure TXMLDocRegionKeyBinding.BindKeyboard(const BindingServices: IOTAKeyBindingServices);
begin
BindingServices.AddKeyBinding([ShortCut(Ord('D'), [ssShift, ssCtrl])], Execute, nil);
end;

procedure TXMLDocRegionKeyBinding.Execute(const Context: IOTAKeyContext; KeyCode: TShortcut;
var BindingResult: TKeyBindingResult);
var
AllElided: Boolean;
Row, Col: Integer;
Lines: TStringList;
I: Integer;
ElideActions: IOTAElideActions;
begin
BindingResult := krHandled;

if Supports(Context.EditBuffer.TopView, IOTAElideActions, ElideActions) then
begin
Row := Context.EditBuffer.EditPosition.Row;
Col := Context.EditBuffer.EditPosition.Column;
try
Lines := TStringList.Create;
try
Lines.Text := ReadEditorSource(Context.EditBuffer);
// if all 'xmldoc' regions are elided then we want to unelide all;
// otherwise we want to elide (those which are not)
AllElided := True;
for I := 0 to Lines.Count - 1 do
if AnsiSameText('{$region ''xmldoc''}', TrimLeft(Lines[I])) then
begin
Context.EditBuffer.EditPosition.Move(I + 1, 1);
if not IsRegionElided(Context.EditBuffer.EditPosition, Lines[I]) then
begin
AllElided := False;
Break;
end;
end;
for I := 0 to Lines.Count - 1 do
if AnsiSameText('{$region ''xmldoc''}', TrimLeft(Lines[I])) then
begin
Context.EditBuffer.EditPosition.Move(I + 1, 1);
if AllElided then
ElideActions.UnElideNearestBlock
else if not IsRegionElided(Context.EditBuffer.EditPosition, Lines[I]) then
begin
Context.EditBuffer.EditPosition.Move(I + 1, 1);
ElideActions.ElideNearestBlock;
end;
end;
Context.EditBuffer.TopView.Paint;
finally
Lines.Free;
end;
finally
Context.EditBuffer.EditPosition.Move(Row, Col);
end;
end;
end;

function TXMLDocRegionKeyBinding.IsRegionElided(const EditPosition: IOTAEditPosition; const Line: string): Boolean;
begin
EditPosition.MoveEOL;
Result := EditPosition.Column < Length(Line);
end;

This allows me to quickly show or hide all my XML documentation in the current source code unit with a single keystroke (Ctrl+Shift+D) while keeping the current elision state of other regions.

Update: Note that after the elide/unelide code, I've added a call to IOTAEditView.Paint. This forces the edit view to update itself on the screen. One case when this is necessary is when you're eliding while viewing the bottom part of your unit. After eliding, the portion of the window after the final "end." is not updated/cleared. The Paint call solves this problem.