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.

No comments: