====== Extension Development ======
Guide to developing new extensions for WvdS FPC RAD Studio.
===== Prerequisites =====
* Development environment set up (see [[.:start|Start]])
* Basic understanding of Pascal and pas2js
* Familiarity with VSCode Extension API
===== Creating a New Extension =====
==== 1. Create Directory Structure ====
cd sources/extensions
mkdir -p wvds.vscode.{name}/pas
mkdir -p wvds.vscode.{name}/dist
mkdir -p wvds.vscode.{name}/images
==== 2. Create package.json ====
{
"name": "wvds-vscode-{name}",
"displayName": "WvdS VSCode {Name}",
"description": "{Description}",
"version": "0.1.0",
"publisher": "wvds",
"license": "MIT",
"icon": "images/icon.png",
"engines": {
"vscode": "^1.85.0"
},
"categories": ["Other"],
"main": "./dist/extension_main.js",
"activationEvents": [
"onCommand:wvds.{name}.{command}"
],
"contributes": {
"commands": [
{
"command": "wvds.{name}.{command}",
"title": "{Title}",
"category": "WvdS"
}
]
},
"extensionDependencies": [
"wvds.wvds-vscode-core"
]
}
==== 3. Create build.cfg ====
[build]
compiler=pas2js
target=vscode-extension
entry=extension_main.pas
[pas2js]
options=-Jc -Jirtl.js -Tbrowser
units=../../common;./pas
output=./dist
==== 4. Create extension_main.pas ====
unit extension_main;
{$mode objfpc}{$H+}
interface
uses
JS,
VSCode.API,
VSCode.Commands,
VSCode.ExtensionExports,
WvdS.VSCode.Strings;
procedure Activate(AContext: TExtensionContext);
procedure Deactivate;
implementation
procedure Handle{Command}(Args: TJSValueDynArray);
begin
ShowInfoMessage(rs{Name}CommandExecuted);
end;
procedure DoActivate(AContext: TExtensionContext);
begin
// Register command
RegisterCommand('wvds.{name}.{command}', @Handle{Command});
// Logging
LogInfo('WvdS {Name} Extension activated');
end;
procedure Activate(AContext: TExtensionContext);
begin
try
DoActivate(AContext);
except
on E: Exception do
begin
LogError(rsActivationFailed, [E.Message]);
ShowErrorMessage(Format(rsActivationFailed, [E.Message]));
end;
end;
end;
procedure Deactivate;
begin
LogInfo('WvdS {Name} Extension deactivated');
end;
initialization
ExportActivateDeactivate(@Activate, @Deactivate);
end.
==== 5. Compile ====
cd sources/extensions/wvds.vscode.{name}
pas2js -Jc -Jirtl.js -Tbrowser \
-Fu../../common \
-Fu./pas \
-FE./dist \
pas/extension_main.pas
==== 6. Test ====
- F5 in VS Code -> Extension Development Host
- Command Palette -> ''WvdS: {Command}''
===== Following Layered Architecture =====
==== Model Layer ====
// {Feature}.Models.pas
unit {Feature}.Models;
{$mode objfpc}{$H+}
interface
type
// Records for data structures
TWvdS{Feature}Config = record
Name: string;
Value: Integer;
Enabled: Boolean;
end;
// Enums
TWvdS{Feature}Status = (
fsIdle,
fsProcessing,
fsCompleted,
fsFailed
);
// Arrays
TWvdS{Feature}ConfigArray = array of TWvdS{Feature}Config;
implementation
end.
==== Service Layer ====
// {Feature}.Service.pas
unit {Feature}.Service;
{$mode objfpc}{$H+}
interface
uses
{Feature}.Models;
// Validation
function ValidateConfig(const AConfig: TWvdS{Feature}Config): Boolean;
// Operations
function ProcessData(const AInput: string): TWvdS{Feature}Config;
// Business logic
procedure ExecuteFeature(const AConfig: TWvdS{Feature}Config);
implementation
uses
SysUtils, WvdS.VSCode.Strings;
function ValidateConfig(const AConfig: TWvdS{Feature}Config): Boolean;
begin
Result := (AConfig.Name <> '') and (AConfig.Value >= 0);
end;
function ProcessData(const AInput: string): TWvdS{Feature}Config;
begin
// Processing...
Result.Name := AInput;
Result.Value := Length(AInput);
Result.Enabled := True;
end;
procedure ExecuteFeature(const AConfig: TWvdS{Feature}Config);
begin
if not ValidateConfig(AConfig) then
raise Exception.Create(rs{Feature}InvalidConfig);
// Execution...
end;
end.
==== Extension Layer ====
// extension_main.pas - UI logic only
procedure HandleCommand(Args: TJSValueDynArray);
var
Config: TWvdS{Feature}Config;
begin
// 1. Get input from UI
Config := GetConfigFromDialog;
// 2. Call service (no logic here!)
if not ValidateConfig(Config) then
begin
ShowWarningMessage(rs{Feature}InvalidInput);
Exit;
end;
// 3. Execution
try
ExecuteFeature(Config);
ShowInfoMessage(rs{Feature}Success);
except
on E: Exception do
ShowErrorMessage(Format(rs{Feature}Failed, [E.Message]));
end;
end;
===== Using VSCode API =====
==== Commands ====
// Register command
RegisterCommand('wvds.{name}.{action}', @Handler);
// Execute command
ExecuteCommand('wvds.other.action');
==== Configuration ====
// Read setting
var Value := GetConfiguration('wvds.{name}').Get('setting', DefaultValue);
// Write setting
GetConfiguration('wvds.{name}').Update('setting', NewValue, True);
==== Output Channel ====
// Use channel (provided by Core)
LogInfo('Information');
LogWarn('Warning');
LogError('Error: %s', [E.Message]);
LogDebug('Debug: %s', [Data]); // Only when logLevel=debug
==== WebView ====
// Create WebView Panel
var Panel := CreateWebviewPanel(
'wvds.{name}.view',
'Title',
ViewColumn.One,
WebviewOptions
);
// Set HTML
Panel.Webview.Html := LoadTemplate('template.html');
// Messaging
Panel.Webview.OnDidReceiveMessage(@HandleMessage);
Panel.Webview.PostMessage(JSObject);
==== StatusBar ====
var StatusItem := CreateStatusBarItem(StatusBarAlignment.Left, 100);
StatusItem.Text := '$(check) Ready';
StatusItem.Show;
===== Resourcestrings for i18n =====
**All strings must be defined in WvdS.VSCode.Strings.pas!**
// WvdS.VSCode.Strings.pas
resourcestring
// {Feature} Extension
rs{Feature}CommandExecuted = '{Feature} command executed';
rs{Feature}InvalidConfig = 'Invalid configuration';
rs{Feature}Success = 'Operation completed successfully';
rs{Feature}Failed = 'Operation failed: %s';
// WvdS.VSCode.Strings.DE.pas
resourcestring
rs{Feature}CommandExecuted = '{Feature}-Befehl ausgefuehrt';
rs{Feature}InvalidConfig = 'Ungueltige Konfiguration';
rs{Feature}Success = 'Vorgang erfolgreich abgeschlossen';
rs{Feature}Failed = 'Vorgang fehlgeschlagen: %s';
===== Error Handling =====
procedure SafeOperation;
begin
try
DoRiskyOperation;
except
on E: EFileNotFoundException do
begin
LogError(rsFileNotFound, [E.FileName]);
ShowErrorMessage(Format(rsFileNotFound, [E.FileName]));
end;
on E: EAccessDenied do
begin
LogError(rsAccessDenied, [E.Path]);
ShowErrorMessage(Format(rsAccessDenied, [E.Path]));
end;
on E: Exception do
begin
LogError(rsUnexpectedError, [E.ClassName, E.Message]);
ShowErrorMessage(Format(rsUnexpectedError, [E.ClassName, E.Message]));
end;
end;
end;
**Never use empty except blocks!** Every exception must be handled or logged.
===== Debug Logging =====
{$IFDEF DEBUG}
procedure LogDebugTrace(const AMessage: string);
begin
if not DebugEnabled then Exit;
WriteDebugLog(Format('[%s] %s at %s', [
FormatDateTime('hh:nn:ss.zzz', Now),
AMessage,
GetCallStack
]));
end;
{$ENDIF}
Activation:
* Compile with ''-dDEBUG''
* Start extension with ''--debug'' parameter
===== Writing Tests =====
// Tests/{Feature}.Tests.pas
unit {Feature}.Tests;
{$mode objfpc}{$H+}
interface
uses
TestFramework, {Feature}.Service, {Feature}.Models;
type
T{Feature}ServiceTest = class(TTestCase)
published
procedure TestValidateConfig_ValidInput_ReturnsTrue;
procedure TestValidateConfig_EmptyName_ReturnsFalse;
procedure TestProcessData_ValidInput_ReturnsConfig;
end;
implementation
procedure T{Feature}ServiceTest.TestValidateConfig_ValidInput_ReturnsTrue;
var
Config: TWvdS{Feature}Config;
begin
Config.Name := 'Test';
Config.Value := 42;
Config.Enabled := True;
CheckTrue(ValidateConfig(Config));
end;
procedure T{Feature}ServiceTest.TestValidateConfig_EmptyName_ReturnsFalse;
var
Config: TWvdS{Feature}Config;
begin
Config.Name := '';
Config.Value := 42;
CheckFalse(ValidateConfig(Config));
end;
end.
===== Checklist Before Commit =====
[ ] SoC followed (Model/Service/Extension separated)
[ ] No hardcoded strings (resourcestrings used)
[ ] Translations present (EN, DE, SL, HR)
[ ] Error handling implemented
[ ] Debug logging added
[ ] Security guidelines checked
[ ] Unit tests written
[ ] README updated
[ ] CHANGELOG updated
===== See Also =====
* [[.:architektur|Architecture Overview]]
* [[.:code-konventionen|Code Conventions]]
* [[.:sicherheit|Security Guidelines]]
* [[.:testing|Testing]]
* [[.:vscode-wrapper|VSCode Wrapper API]]