Skip to content

API Reference

Server

create_server

Create and configure the CDL language server.

from cdl_lsp import create_server

server = create_server()
server.start_io()

cdl_lsp.create_server()

Create and configure the LSP server.

Source code in src/cdl_lsp/server.py
def create_server() -> "LanguageServer":
    """Create and configure the LSP server."""
    if not PYGLS_AVAILABLE:
        raise ImportError(
            "pygls is required for the LSP server. Install with: pip install pygls lsprotocol"
        )

    server = LanguageServer(name=SERVER_NAME, version=SERVER_VERSION)

    # Document storage
    documents: dict = {}

    # ==========================================================================
    # Lifecycle Events
    # ==========================================================================

    @server.feature(types.INITIALIZE)
    def initialize(params: types.InitializeParams) -> types.InitializeResult:
        """Handle initialization request."""
        logger.info(f"Initializing CDL Language Server {SERVER_VERSION}")
        logger.info(f"Root URI: {params.root_uri}")

        return types.InitializeResult(
            capabilities=types.ServerCapabilities(
                text_document_sync=types.TextDocumentSyncOptions(
                    open_close=True,
                    change=types.TextDocumentSyncKind.Full,
                    save=types.SaveOptions(include_text=True),
                ),
                completion_provider=types.CompletionOptions(
                    trigger_characters=["{", "[", ":", "@", "+", "|", "(", ",", "$"],
                    resolve_provider=False,
                ),
                hover_provider=types.HoverOptions(),
                definition_provider=types.DefinitionOptions(),
                diagnostic_provider=types.DiagnosticOptions(
                    inter_file_dependencies=False, workspace_diagnostics=False
                ),
                # New capabilities
                signature_help_provider=types.SignatureHelpOptions(trigger_characters=["(", ","]),
                code_action_provider=types.CodeActionOptions(
                    code_action_kinds=[types.CodeActionKind.QuickFix]
                ),
                document_symbol_provider=True,
                document_formatting_provider=True,
                # Note: execute_command_provider is auto-populated by @server.command() decorators
            ),
            server_info=types.ServerInfo(name=SERVER_NAME, version=SERVER_VERSION),
        )

    @server.feature(types.INITIALIZED)
    def initialized(params: types.InitializedParams) -> None:
        """Handle initialized notification."""
        logger.info("CDL Language Server initialized")

    @server.feature(types.SHUTDOWN)
    def shutdown(params: None) -> None:
        """Handle shutdown request."""
        logger.info("CDL Language Server shutting down")

    # ==========================================================================
    # Document Synchronization
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_DID_OPEN)
    def did_open(params: types.DidOpenTextDocumentParams) -> None:
        """Handle document open notification."""
        uri = params.text_document.uri
        text = params.text_document.text

        logger.debug(f"Document opened: {uri}")
        documents[uri] = text

        # Publish diagnostics
        diagnostics = get_diagnostics(text)
        server.text_document_publish_diagnostics(
            types.PublishDiagnosticsParams(uri=uri, diagnostics=diagnostics)
        )

    @server.feature(types.TEXT_DOCUMENT_DID_CHANGE)
    def did_change(params: types.DidChangeTextDocumentParams) -> None:
        """Handle document change notification."""
        uri = params.text_document.uri

        # Full sync - get the complete new content
        for change in params.content_changes:
            if hasattr(change, "text"):
                documents[uri] = change.text
                break

        text = documents.get(uri, "")
        logger.debug(f"Document changed: {uri}, length: {len(text)}")

        # Publish diagnostics
        diagnostics = get_diagnostics(text)
        server.text_document_publish_diagnostics(
            types.PublishDiagnosticsParams(uri=uri, diagnostics=diagnostics)
        )

    @server.feature(types.TEXT_DOCUMENT_DID_CLOSE)
    def did_close(params: types.DidCloseTextDocumentParams) -> None:
        """Handle document close notification."""
        uri = params.text_document.uri
        logger.debug(f"Document closed: {uri}")

        if uri in documents:
            del documents[uri]

        # Clear diagnostics
        server.text_document_publish_diagnostics(
            types.PublishDiagnosticsParams(uri=uri, diagnostics=[])
        )

    @server.feature(types.TEXT_DOCUMENT_DID_SAVE)
    def did_save(params: types.DidSaveTextDocumentParams) -> None:
        """Handle document save notification."""
        uri = params.text_document.uri
        text = params.text if params.text else documents.get(uri, "")

        logger.debug(f"Document saved: {uri}")

        if text:
            documents[uri] = text
            diagnostics = get_diagnostics(text)
            server.text_document_publish_diagnostics(
                types.PublishDiagnosticsParams(uri=uri, diagnostics=diagnostics)
            )

    # ==========================================================================
    # Completion
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_COMPLETION)
    def completion(params: types.CompletionParams) -> types.CompletionList | None:
        """Handle completion request."""
        uri = params.text_document.uri
        position = params.position

        text = documents.get(uri, "")
        if not text:
            return None

        # Get the current line
        lines = text.split("\n")
        if position.line >= len(lines):
            return None

        line = lines[position.line]
        col = position.character

        logger.debug(f"Completion at {uri}:{position.line}:{col}")

        trigger_char = None
        if params.context and params.context.trigger_character:
            trigger_char = params.context.trigger_character

        items = get_completions(line, col, trigger_char, document_text=text)

        return types.CompletionList(is_incomplete=False, items=items)

    # ==========================================================================
    # Hover
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_HOVER)
    def hover(params: types.HoverParams) -> types.Hover | None:
        """Handle hover request."""
        uri = params.text_document.uri
        position = params.position

        text = documents.get(uri, "")
        if not text:
            return None

        lines = text.split("\n")
        if position.line >= len(lines):
            return None

        line = lines[position.line]
        col = position.character

        logger.debug(f"Hover at {uri}:{position.line}:{col}")

        return get_hover_info(line, col, position.line)

    # ==========================================================================
    # Go to Definition
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_DEFINITION)
    def definition(params: types.DefinitionParams) -> types.Location | None:
        """Handle go to definition request."""
        uri = params.text_document.uri
        position = params.position

        text = documents.get(uri, "")
        if not text:
            return None

        lines = text.split("\n")
        if position.line >= len(lines):
            return None

        line = lines[position.line]
        col = position.character

        logger.debug(f"Definition at {uri}:{position.line}:{col}")

        return get_definition(line, col, position.line, uri, text)

    # ==========================================================================
    # Diagnostics (Pull Model)
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_DIAGNOSTIC)
    def diagnostic(params: types.DocumentDiagnosticParams) -> types.DocumentDiagnosticReport:
        """Handle diagnostic request (pull model)."""
        uri = params.text_document.uri
        text = documents.get(uri, "")

        logger.debug(f"Diagnostic request for {uri}")

        if not text:
            return types.RelatedFullDocumentDiagnosticReport(
                kind=types.DocumentDiagnosticReportKind.Full, items=[]
            )

        diagnostics = get_diagnostics(text)

        return types.RelatedFullDocumentDiagnosticReport(
            kind=types.DocumentDiagnosticReportKind.Full, items=diagnostics
        )

    # ==========================================================================
    # Code Actions
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_CODE_ACTION)
    def code_action(params: types.CodeActionParams) -> list[types.CodeAction] | None:
        """Handle code action request."""
        uri = params.text_document.uri
        diagnostics = params.context.diagnostics

        logger.debug(f"Code action at {uri}")

        if not diagnostics:
            return None

        actions = get_code_actions(uri, params.range, diagnostics)
        return actions if actions else None

    # ==========================================================================
    # Signature Help
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_SIGNATURE_HELP)
    def signature_help(params: types.SignatureHelpParams) -> types.SignatureHelp | None:
        """Handle signature help request."""
        uri = params.text_document.uri
        position = params.position

        text = documents.get(uri, "")
        if not text:
            return None

        lines = text.split("\n")
        if position.line >= len(lines):
            return None

        line = lines[position.line]
        col = position.character

        logger.debug(f"Signature help at {uri}:{position.line}:{col}")

        return get_signature_help(line, col)

    # ==========================================================================
    # Document Symbols
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
    def document_symbol(params: types.DocumentSymbolParams) -> list[types.DocumentSymbol]:
        """Handle document symbol request."""
        uri = params.text_document.uri
        text = documents.get(uri, "")

        logger.debug(f"Document symbols for {uri}")

        if not text:
            return []

        return get_document_symbols(text)

    # ==========================================================================
    # Formatting
    # ==========================================================================

    @server.feature(types.TEXT_DOCUMENT_FORMATTING)
    def formatting(params: types.DocumentFormattingParams) -> list[types.TextEdit]:
        """Handle document formatting request."""
        uri = params.text_document.uri
        text = documents.get(uri, "")

        logger.debug(f"Formatting {uri}")

        if not text:
            return []

        return format_cdl(text, params.options)

    # ==========================================================================
    # Execute Commands (Explain, Preview)
    # ==========================================================================

    @server.command("cdl.explain")
    def cmd_explain(ls, *args) -> dict:
        """Execute cdl.explain command."""
        logger.debug(f"cdl.explain command, args: {args}")

        if args:
            uri = args[0]
            text = documents.get(uri, "")
            if text:
                return get_explain_result(text)
            else:
                return {"content": "Document not found or empty", "kind": "markdown"}
        return {"content": "No document URI provided", "kind": "markdown"}

    @server.command("cdl.preview")
    def cmd_preview(ls, *args) -> dict:
        """Execute cdl.preview command."""
        logger.debug(f"cdl.preview command, args: {args}")

        if args:
            uri = args[0]
            width = args[1] if len(args) > 1 else 600
            height = args[2] if len(args) > 2 else 500
            text = documents.get(uri, "")
            if text:
                return render_cdl_preview(text, width, height)
            else:
                return {"success": False, "error": "Document not found or empty", "svg": ""}
        return {"success": False, "error": "No document URI provided", "svg": ""}

    @server.command("cdl.preview3d")
    def cmd_preview_3d(ls, *args) -> dict:
        """Execute cdl.preview3d command - returns glTF for 3D preview."""
        logger.debug(f"cdl.preview3d command, args: {args}")

        if args:
            uri = args[0]
            text = documents.get(uri, "")
            if text:
                return render_cdl_preview_3d(text)
            else:
                return {"success": False, "error": "Document not found or empty", "gltf": None}
        return {"success": False, "error": "No document URI provided", "gltf": None}

    @server.command("cdl.previewCapabilities")
    def cmd_preview_capabilities(ls, *args) -> dict:
        """Execute cdl.previewCapabilities command."""
        logger.debug("cdl.previewCapabilities command")
        return get_preview_capabilities()

    return server

SERVER_NAME

The server name constant.

from cdl_lsp import SERVER_NAME

print(SERVER_NAME)  # 'cdl-lsp'

SERVER_VERSION

The server version constant.

from cdl_lsp import SERVER_VERSION

print(SERVER_VERSION)  # '1.0.0'

Constants

CRYSTAL_SYSTEMS

Set of valid crystal system names.

from cdl_lsp.constants import CRYSTAL_SYSTEMS

print(CRYSTAL_SYSTEMS)
# {'cubic', 'tetragonal', 'orthorhombic', 'hexagonal', 'trigonal', 'monoclinic', 'triclinic'}

POINT_GROUPS

Dictionary mapping crystal systems to their valid point groups.

from cdl_lsp.constants import POINT_GROUPS

print(POINT_GROUPS['cubic'])
# {'m3m', '432', '-43m', 'm-3', '23'}

ALL_POINT_GROUPS

Set of all 32 crystallographic point groups.

from cdl_lsp.constants import ALL_POINT_GROUPS

print(len(ALL_POINT_GROUPS))  # 32

TWIN_LAWS

Set of recognized twin law names.

from cdl_lsp.constants import TWIN_LAWS

print(TWIN_LAWS)
# {'spinel', 'brazil', 'japan', 'fluorite', 'iron_cross', ...}

NAMED_FORMS

Dictionary mapping common form names to Miller indices.

from cdl_lsp.constants import NAMED_FORMS

print(NAMED_FORMS['octahedron'])  # (1, 1, 1)
print(NAMED_FORMS['cube'])        # (1, 0, 0)

MODIFICATIONS

Set of available modification names.

from cdl_lsp.constants import MODIFICATIONS

print(MODIFICATIONS)
# {'elongate', 'compress', 'twin', 'habit', ...}

LSP Features

The language server implements the following LSP features:

Text Document Synchronization

  • textDocument/didOpen - Document opened
  • textDocument/didChange - Document changed
  • textDocument/didClose - Document closed
  • textDocument/didSave - Document saved

Language Features

  • textDocument/completion - Auto-completion
  • textDocument/hover - Hover information
  • textDocument/signatureHelp - Signature help
  • textDocument/definition - Go to definition
  • textDocument/documentSymbol - Document symbols
  • textDocument/formatting - Document formatting
  • textDocument/codeAction - Code actions

Diagnostics

The server provides real-time diagnostics for:

  • Syntax errors in CDL notation
  • Invalid crystal systems
  • Invalid point groups
  • Invalid Miller indices
  • Unknown twin laws
  • Unknown named forms

Completion Items

Auto-completion is provided for:

Context Items
System position Crystal system names
Point group position Valid point groups for current system
Form position Named forms, Miller index templates
Modification position Available modifications
Twin law position Twin law names

Hover Information

Hover provides documentation for:

  • Crystal systems (description, point groups)
  • Point groups (symmetry operations)
  • Miller indices (face orientation)
  • Named forms (equivalent Miller index)
  • Twin laws (description, examples)

Server Configuration

Initialization Options

{
  "cdl": {
    "preview": {
      "enabled": true,
      "format": "svg"
    },
    "validation": {
      "strict": false
    },
    "completion": {
      "showDeprecated": false
    }
  }
}

Workspace Configuration

The server reads configuration from:

  1. Initialization options
  2. workspace/configuration requests
  3. .cdl-lsp.json in workspace root

Extending the Server

Custom Handlers

from cdl_lsp import create_server

server = create_server()

@server.feature('custom/myFeature')
def my_custom_handler(params):
    return {'result': 'custom response'}

server.start_io()

Adding Completions

from cdl_lsp.completion import register_completion_provider

def my_completion_provider(context):
    if context.trigger == 'custom':
        return [
            {'label': 'custom1', 'detail': 'Custom item 1'},
            {'label': 'custom2', 'detail': 'Custom item 2'},
        ]
    return []

register_completion_provider(my_completion_provider)