Skip to content

msarson/clarion-lsp

Repository files navigation

ClarionLsp

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.


Overview

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.


Features

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

Architecture

Server discovery (in priority order)

  1. Configured pathPropertyService.Get("Lsp.ServerPath") in the Clarion IDE preferences
  2. Bundled server<addin-dir>\lsp-server\out\server\src\server.js (copy the VS Code extension's built out/ tree here)
  3. 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.

Startup flow

LspStartupCommand.Run() is called by SharpDevelop's autostart mechanism when the IDE loads. It:

  1. Hooks ProjectService.SolutionLoaded and SolutionClosed
  2. Starts the LSP server immediately (standalone mode if no solution is open)
  3. Sends clarion/updatePaths with the version config resolved via reflection from Clarion.Core.Options.Versions
  4. Sets ClarionLspLocator.Current so 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.

Version detection

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 directory
  • Libsrc — semicolon-delimited library source paths
  • RedirectionFile.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.

Logging

All log output goes to OutputDebugString (visible in DebugView or any kernel debugger). Each line is prefixed with [ClarionLsp].


Build & deploy

Prerequisites

  • Visual Studio 2022 (for MSBuild 17)
  • .NET Framework 4.8
  • Clarion IDE installed (for the $(ClarionBin) reference — see below)

ClarionBin property

Both projects reference Clarion IDE assemblies via $(ClarionBin). Clarion can be installed anywhere, so you must set this for your machine before building:

  1. Copy Directory.Build.props.user.templateDirectory.Build.props.user (already gitignored)
  2. 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.

Build

# 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:minimal

Automatic deploy on build

Both 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.


Consuming the API from another addin

Step 1 — Build this repo and get the DLL

Clone this repo and build it:

git clone https://github.com/msarson/clarion-lsp.git
cd clarion-lsp
dotnet build ClarionLsp.slnx -c Debug

This produces ClarionLsp.Contracts.dll in ClarionLsp.Contracts\bin\Debug\net48\.

Step 2 — Reference ClarionLsp.Contracts.dll in your addin project

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.

Step 3 — Declare a soft dependency in your .addin manifest

<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.

Step 4 — Call the API

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");

How the runtime sharing works

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.dll into the AppDomain
  • The CLR never loads the same assembly identity twice — your copy is ignored, the shared instance is used
  • ClarionLspLocator.Current is set by ClarionLsp and visible to your addin ✅

When ClarionLsp is not installed:

  • Your copy of ClarionLsp.Contracts.dll loads
  • ClarionLspLocator.Current is never set → it is null
  • Your existing fallback connection takes over — nothing breaks ✅

Soft-dependency fallback pattern

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;

Line/character numbering

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 API reference

ClarionLspLocator

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.

IClarionLanguageClient

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;
}

HoverResult

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)

LocationResult

Property Type Description
FilePath string Absolute file path (backslash-normalised)
Range Range 0-based start/end position

SymbolResult

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)

CompletionResult

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)

DiagnosticResult

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

Range / Position

public class Range    { public Position Start; public Position End; }
public class Position { public int Line; public int Character; }  // 0-based

Project structure

clarion-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

Soft-dependency fallback pattern

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.


Roadmap / extending the interface

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:

  1. Add the method signature to IClarionLanguageClient in ClarionLsp.Contracts
  2. Add the result DTO to ClarionLsp.Contracts/Models/ if needed
  3. Implement the method in ClarionLspService by calling the appropriate _client.SendRequest(...) method
  4. Bump the version in ClarionLsp.addin

Planned future capabilities (not yet implemented):

  • clarion/findUsages — semantic find-usages distinct from text references
  • clarion/callHierarchy — incoming/outgoing call graph

Recently shipped (v1.1.0): code completion, diagnostics (pull + push), and live unsaved-buffer sync.


Notes for Clarion IDE addin developers

  • !!! doc comments — The Clarion language server reads !!! doc comments from .inc/.clw files. 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 .clw comment takes priority over the declaration's .inc comment 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 like GET, SORT, NEXT.

  • Language keywords — Hover does not return results for bare language keywords (IF, LOOP, END, PROCEDURE) — the language server resolves user symbols only.


Licence

MIT — © 2025 msarson

About

Shared Clarion IDE addin — manages the Clarion Language Server (LSP) lifecycle and exposes IClarionLanguageClient for hover, definition, references, and symbol queries

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages