A shared SharpDevelop addin that manages the Clarion Language Server lifecycle inside the Clarion IDE, and exposes a clean public API (IClarionLanguageClient) so any other addin can query hover text, go-to-definition, find-references, document/workspace symbols, rename, code completion, and diagnostics — without knowing anything about LSP internals.
ClarionLsp.Contracts ← public API dll — reference this from consumer addins
ClarionLsp ← the addin dll — manages server process + JSON-RPC
ClarionLsp.Contracts contains only interfaces and DTOs. Consumer addins reference it at compile time but do not need to reference ClarionLsp.dll. At runtime they use ClarionLspLocator.Current (populated by the addin) to call the language server.
| Capability | LSP Method |
|---|---|
| Hover / tooltip | textDocument/hover |
| Go to definition | textDocument/definition |
| Find all references | textDocument/references |
| Document symbols | textDocument/documentSymbol |
| Workspace symbol search | workspace/symbol |
| Rename (with prepare) | textDocument/prepareRename, textDocument/rename |
| Code completion | textDocument/completion |
| Diagnostics (pull + push) | textDocument/publishDiagnostics |
| Live unsaved-buffer sync | textDocument/didOpen, textDocument/didChange |
- Configured path —
PropertyService.Get("Lsp.ServerPath")in the Clarion IDE preferences - Bundled server —
<addin-dir>\lsp-server\out\server\src\server.js(copy the VS Code extension's builtout/tree here) - Auto-discovered — highest version of
msarson.clarion-extensions-*found in%USERPROFILE%\.vscode\extensions\
If none of these resolve to a real file the addin logs a diagnostic and does nothing — it does not throw or crash the IDE.
LspStartupCommand.Run() is called by SharpDevelop's autostart mechanism when the IDE loads. It:
- Hooks
ProjectService.SolutionLoadedandSolutionClosed - Starts the LSP server immediately (standalone mode if no solution is open)
- Sends
clarion/updatePathswith the version config resolved via reflection fromClarion.Core.Options.Versions - Sets
ClarionLspLocator.Currentso consumer addins can start calling it
On SolutionLoaded it either starts the server (if not running) or re-sends updated paths with the new solution context. On SolutionClosed it sends an empty-project updatePaths so the server's index is cleared.
ClarionVersionService.Detect() uses reflection on Clarion.Core.dll to call Versions.GetVersion(true) — this returns the IDE's live ClarionVersion object containing:
Name— e.g."Clarion11.1"Path— the Clarion bin directoryLibsrc— semicolon-delimited library source pathsRedirectionFile.Name+RedirectionFile.Macros— the active redirection file and its macro table
All of this is forwarded to the language server via clarion/updatePaths so it can resolve %REDIRECTION_MACRO%-expanded paths.
All log output goes to OutputDebugString (visible in DebugView or any kernel debugger). Each line is prefixed with [ClarionLsp].
- Visual Studio 2022 (for MSBuild 17)
- .NET Framework 4.8
- Clarion IDE installed (for the
$(ClarionBin)reference — see below)
Both projects reference Clarion IDE assemblies via $(ClarionBin). Clarion can be installed anywhere, so you must set this for your machine before building:
- Copy
Directory.Build.props.user.template→Directory.Build.props.user(already gitignored) - Edit the path inside it to match your Clarion installation:
<Project>
<PropertyGroup>
<ClarionBin>C:\YourClarion\bin</ClarionBin>
</PropertyGroup>
</Project>Directory.Build.props.user is imported automatically and takes priority over the committed defaults. Never commit it — it's personal to your machine.
# dotnet CLI (simplest)
dotnet build ClarionLsp.slnx -c Debug
# or MSBuild directly
& "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" `
ClarionLsp.slnx /p:Configuration=Debug /v:minimalBoth projects deploy to the same addin folder on build:
C:\Clarion\Clarion11.1\accessory\addins\ClarionLsp\
ClarionLsp.dll
ClarionLsp.Contracts.dll
ClarionLsp.addin
ClarionLsp.addin imports both DLLs so SharpDevelop loads them both into the AppDomain when the addin starts — this is what makes ClarionLspLocator and IClarionLanguageClient visible to consumer addins.
The Clarion IDE must be closed before deploying — DLLs are locked while the IDE runs.
Clone this repo and build it:
git clone https://github.com/msarson/clarion-lsp.git
cd clarion-lsp
dotnet build ClarionLsp.slnx -c DebugThis produces ClarionLsp.Contracts.dll in ClarionLsp.Contracts\bin\Debug\net48\.
Add a reference in your .csproj, pointing to wherever you built or copied the DLL:
<Reference Include="ClarionLsp.Contracts">
<HintPath>..\clarion-lsp\ClarionLsp.Contracts\bin\Debug\net48\ClarionLsp.Contracts.dll</HintPath>
<Private>True</Private>
</Reference><Private>True</Private> copies the DLL into your addin's output folder — this is intentional and explained below.
<Dependency addin="ClarionLsp" version="1.0" coerced="true"/>coerced="true" means your addin still loads even if ClarionLsp is absent — always null-check ClarionLspLocator.Current.
using ClarionLsp.Contracts;
using ClarionLsp.Contracts.Models;
// Always null-check — ClarionLsp may not be installed
var client = ClarionLspLocator.Current;
if (client == null || !client.IsRunning) return;
// Hover (0-based line/character — LSP convention)
HoverResult hover = await client.GetHoverAsync(filePath, line, character);
if (hover != null)
ShowTooltip(hover.Contents);
// Go to definition
LocationResult[] defs = await client.GetDefinitionAsync(filePath, line, character);
foreach (var def in defs)
NavigateTo(def.FilePath, def.Range.Start.Line);
// Find all references
LocationResult[] refs = await client.GetReferencesAsync(filePath, line, character);
// Document symbols (outline)
SymbolResult[] symbols = await client.GetDocumentSymbolsAsync(filePath);
// Workspace symbol search
SymbolResult[] results = await client.FindWorkspaceSymbolAsync("MyClass");Your addin ships its own copy of ClarionLsp.Contracts.dll (because Private=True). That copy is only needed as a fallback. When ClarionLsp is installed:
- ClarionLsp is an autostart addin — it loads first and puts
ClarionLsp.Contracts.dllinto the AppDomain - The CLR never loads the same assembly identity twice — your copy is ignored, the shared instance is used
ClarionLspLocator.Currentis set by ClarionLsp and visible to your addin ✅
When ClarionLsp is not installed:
- Your copy of
ClarionLsp.Contracts.dllloads ClarionLspLocator.Currentis never set → it isnull- Your existing fallback connection takes over — nothing breaks ✅
If your addin already manages its own LSP connection, try the shared service first:
IClarionLanguageClient client = ClarionLspLocator.Current;
if (client == null || !client.IsRunning)
client = MyOwnLspClient.Current; // your existing connection
if (client == null || !client.IsRunning) return;All coordinates are 0-based (LSP convention). The Clarion IDE's SharpDevelop text editor uses 0-based lines internally (IDocument.GetLineSegment) but some UI elements display 1-based. Convert accordingly before calling.
public static class ClarionLspLocator
{
public static IClarionLanguageClient Current { get; set; }
}Set by ClarionLsp on startup. null when the addin is not loaded or the server failed to start.
public interface IClarionLanguageClient
{
bool IsRunning { get; }
Task<HoverResult> GetHoverAsync(string filePath, int line, int character, int timeoutMs = 3000);
Task<LocationResult[]> GetDefinitionAsync(string filePath, int line, int character);
Task<LocationResult[]> GetReferencesAsync(string filePath, int line, int character, bool includeDeclaration = true);
Task<SymbolResult[]> GetDocumentSymbolsAsync(string filePath);
Task<SymbolResult[]> FindWorkspaceSymbolAsync(string query);
// Rename
Task<Range> PrepareRenameAsync(string filePath, int line, int character);
Task<RenameEdit[]> RenameAsync(string filePath, int line, int character, string newName);
// Completion (pass bufferText for scope-aware completion against live/unsaved content)
Task<CompletionResult[]> GetCompletionAsync(string filePath, int line, int character, string bufferText = null, int timeoutMs = 3000);
// Diagnostics — pull: triggers a fresh analysis and waits for the publish (empty = clean file)
Task<DiagnosticResult[]> GetDiagnosticsAsync(string filePath, string bufferText = null, int timeoutMs = 3000);
// Live buffer sync — push unsaved editor text; no-op when unchanged
Task NotifyBufferChangedAsync(string filePath, string bufferText);
// Diagnostics — push: raised on every server publish (for live squiggles)
event Action<string, DiagnosticResult[]> DiagnosticsPublished;
}| Property | Type | Description |
|---|---|---|
Contents |
string |
Markdown or plain-text hover content from the language server |
Range |
Range |
The word range the hover applies to (may be null) |
| Property | Type | Description |
|---|---|---|
FilePath |
string |
Absolute file path (backslash-normalised) |
Range |
Range |
0-based start/end position |
| Property | Type | Description |
|---|---|---|
Name |
string |
Symbol name |
Kind |
string |
Human-readable kind: "Class", "Method", "Variable", etc. |
FilePath |
string |
Absolute file path |
Range |
Range |
Symbol location |
ContainerName |
string |
Enclosing class/procedure name (may be null) |
| Property | Type | Description |
|---|---|---|
Label |
string |
Display text of the completion item |
Kind |
string |
Human-readable kind: "Method", "Variable", "Keyword", etc. |
Detail |
string |
Short type/signature detail (may be null) |
Documentation |
string |
Doc text for the item (may be null) |
InsertText |
string |
Text to insert if it differs from Label (may be null) |
| Property | Type | Description |
|---|---|---|
Severity |
string |
"Error", "Warning", "Information", or "Hint" |
Message |
string |
The diagnostic message |
Source |
string |
Producer of the diagnostic (may be null) |
Range |
Range |
0-based location the diagnostic applies to |
public class Range { public Position Start; public Position End; }
public class Position { public int Line; public int Character; } // 0-basedclarion-lsp/
├── ClarionLsp.slnx solution file
├── ClarionLsp/
│ ├── ClarionLsp.addin SharpDevelop addin manifest
│ ├── ClarionLsp.csproj
│ ├── LspStartupCommand.cs addin entry point, lifecycle management
│ ├── ClarionLspService.cs IClarionLanguageClient implementation + response parsers
│ ├── LspClient.cs raw JSON-RPC over stdio transport
│ ├── ClarionVersionService.cs reflection-based Clarion version detection
│ └── Ods.cs OutputDebugString logging helper
└── ClarionLsp.Contracts/
├── ClarionLsp.Contracts.csproj
├── IClarionLanguageClient.cs public interface
├── ClarionLspLocator.cs service locator
└── Models/
├── HoverResult.cs
├── LocationResult.cs
├── SymbolResult.cs
├── CompletionResult.cs
└── DiagnosticResult.cs
If your addin already has its own LSP connection (e.g. ClarionAssistant), you can adopt the shared service without breaking anything for users who haven't installed ClarionLsp yet:
using ClarionLsp.Contracts;
// Try shared service first; fall back to your own connection
IClarionLanguageClient client = ClarionLspLocator.Current;
if (client == null || !client.IsRunning)
client = MyOwnLspClient.Current; // your existing fallback
if (client == null || !client.IsRunning) return;
var hover = await client.GetHoverAsync(filePath, line, character);This means:
- Users who have ClarionLsp installed share one server process
- Users who don't have it fall back to your addin's own connection, exactly as before
- No breaking change, no hard dependency
To declare a soft dependency in your .addin manifest:
<Dependency addin="ClarionLsp" version="1.0" coerced="true"/>coerced="true" tells SharpDevelop to load your addin even if ClarionLsp is absent.
Since Mark maintains both the VS Code extension (LSP server) and this addin, new server capabilities can be exposed immediately when they land — other addins pick them up without changes.
To add a new capability:
- Add the method signature to
IClarionLanguageClientinClarionLsp.Contracts - Add the result DTO to
ClarionLsp.Contracts/Models/if needed - Implement the method in
ClarionLspServiceby calling the appropriate_client.SendRequest(...)method - Bump the version in
ClarionLsp.addin
Planned future capabilities (not yet implemented):
clarion/findUsages— semantic find-usages distinct from text referencesclarion/callHierarchy— incoming/outgoing call graph
Recently shipped (v1.1.0): code completion, diagnostics (pull + push), and live unsaved-buffer sync.
-
!!!doc comments — The Clarion language server reads!!!doc comments from.inc/.clwfiles. Triple-bang (!!!) marks a doc comment; quadruple-bang (!!!!) is treated as a regular comment. Comments before a declaration attach to that symbol. For methods, the definition's.clwcomment takes priority over the declaration's.inccomment if both exist. -
Inline
!fallback — A regular!comment on the same line as a declaration is auto-wrapped as a<summary>if no!!!comment is present. -
Builtins — The Clarion IDE's native tooltip reads builtin documentation from
builtins2.cln(in the Clarion install). If you bundle an LSP server, ensure it has equivalent coverage for builtins likeGET,SORT,NEXT. -
Language keywords — Hover does not return results for bare language keywords (
IF,LOOP,END,PROCEDURE) — the language server resolves user symbols only.
MIT — © 2025 msarson