feat: add landing page at /
Clean HTML page describing iLSP, what LSP is, the WebSocket endpoints, and setup instructions for Neovim, IntelliJ and VS Code. Live stats (Bicep modules, pipeline templates, PyPI packages) are rendered from the in-memory catalog at request time. No emojis. Minimal CSS, no external dependencies.
This commit is contained in:
297
ilsp/server.py
297
ilsp/server.py
@@ -107,11 +107,306 @@ async def _ws_proxy(request: web.Request, host: str, port: int) -> web.WebSocket
|
|||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
_INDEX_HTML = """\
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>iLSP — Language Server Proxy</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after {{ box-sizing: border-box; }}
|
||||||
|
:root {{
|
||||||
|
--bg: #ffffff;
|
||||||
|
--surface: #f6f7f9;
|
||||||
|
--border: #e2e4e8;
|
||||||
|
--text: #1a1d23;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-light: #eff6ff;
|
||||||
|
--code-bg: #f1f3f5;
|
||||||
|
--green: #16a34a;
|
||||||
|
font-size: 16px;
|
||||||
|
}}
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}}
|
||||||
|
header {{
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
}}
|
||||||
|
header h1 {{
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}}
|
||||||
|
header span {{
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}}
|
||||||
|
.badge {{
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--accent-light);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.15rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
}}
|
||||||
|
main {{
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 2rem 4rem;
|
||||||
|
}}
|
||||||
|
h2 {{
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 2.5rem 0 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 0.4rem;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}}
|
||||||
|
p {{ margin: 0.5rem 0 0.75rem; color: var(--text); }}
|
||||||
|
.intro {{ color: var(--muted); margin-top: 0; }}
|
||||||
|
table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}}
|
||||||
|
th {{
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--muted);
|
||||||
|
border-bottom: 2px solid var(--border);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}}
|
||||||
|
td {{
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
vertical-align: top;
|
||||||
|
}}
|
||||||
|
tr:last-child td {{ border-bottom: none; }}
|
||||||
|
td:first-child {{ font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 0.85rem; white-space: nowrap; }}
|
||||||
|
code {{
|
||||||
|
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 0.15em 0.35em;
|
||||||
|
border-radius: 4px;
|
||||||
|
}}
|
||||||
|
pre {{
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0.75rem 0 1.25rem;
|
||||||
|
}}
|
||||||
|
pre code {{ background: none; padding: 0; font-size: inherit; }}
|
||||||
|
.status-row {{
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}}
|
||||||
|
.stat {{
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
min-width: 140px;
|
||||||
|
}}
|
||||||
|
.stat .num {{
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--accent);
|
||||||
|
}}
|
||||||
|
.stat .lbl {{
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}}
|
||||||
|
.note {{
|
||||||
|
background: var(--accent-light);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: #1e3a8a;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}}
|
||||||
|
footer {{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 2rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}}
|
||||||
|
a {{ color: var(--accent); text-decoration: none; }}
|
||||||
|
a:hover {{ text-decoration: underline; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>iLSP</h1>
|
||||||
|
<span>Language Server Proxy</span>
|
||||||
|
<span class="badge">self-hosted</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
iLSP is a WebSocket gateway that exposes language server protocol (LSP)
|
||||||
|
backends over a single HTTP endpoint. Editors connect via WebSocket and
|
||||||
|
receive code intelligence — completions, hover, diagnostics — for Bicep,
|
||||||
|
YAML pipelines, and Python, enriched with organisation-specific catalogs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>What is LSP</h2>
|
||||||
|
<p>
|
||||||
|
The <a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a>
|
||||||
|
is a standard protocol between editors and language analysis tools.
|
||||||
|
An editor sends requests (go-to-definition, completion, hover) and receives
|
||||||
|
structured responses without knowing anything about the language itself.
|
||||||
|
Any editor that speaks LSP — IntelliJ, Neovim, VS Code, Helix — works
|
||||||
|
with any LSP server.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
iLSP wraps existing language servers (Bicep Language Server,
|
||||||
|
yaml-language-server, pylsp) and injects completion items from
|
||||||
|
internal catalogs before responses are returned to the editor.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Live status</h2>
|
||||||
|
<div class="status-row">
|
||||||
|
<div class="stat"><div class="num">{bicep_modules}</div><div class="lbl">Bicep modules</div></div>
|
||||||
|
<div class="stat"><div class="num">{pipeline_templates}</div><div class="lbl">Pipeline templates</div></div>
|
||||||
|
<div class="stat"><div class="num">{pypi_packages}</div><div class="lbl">Python packages</div></div>
|
||||||
|
<div class="stat"><div class="num">254</div><div class="lbl">AzDO tasks</div></div>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:0.82rem;color:var(--muted)">
|
||||||
|
Full JSON status: <a href="/health">/health</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>WebSocket endpoints</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Endpoint</th><th>Language server</th><th>Extra completions</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>wss://ilsp.i80.dk/bicep</td>
|
||||||
|
<td>Bicep Language Server</td>
|
||||||
|
<td>ACR module paths, versions, params</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>wss://ilsp.i80.dk/yaml</td>
|
||||||
|
<td>yaml-language-server</td>
|
||||||
|
<td>AzDO task schema (254 tasks), pipeline templates</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>wss://ilsp.i80.dk/python</td>
|
||||||
|
<td>pylsp (Jedi)</td>
|
||||||
|
<td>Internal PyPI package stubs</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Editor setup</h2>
|
||||||
|
|
||||||
|
<p><strong>Neovim</strong> — add to your LSP config (<code>init.lua</code>):</p>
|
||||||
|
<pre><code>-- Bicep
|
||||||
|
vim.lsp.start({{
|
||||||
|
name = "ilsp-bicep",
|
||||||
|
cmd = {{ "websocat", "wss://ilsp.i80.dk/bicep" }},
|
||||||
|
filetypes = {{ "bicep" }},
|
||||||
|
}})
|
||||||
|
|
||||||
|
-- YAML pipelines
|
||||||
|
vim.lsp.start({{
|
||||||
|
name = "ilsp-yaml",
|
||||||
|
cmd = {{ "websocat", "wss://ilsp.i80.dk/yaml" }},
|
||||||
|
filetypes = {{ "yaml" }},
|
||||||
|
}})</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>IntelliJ IDEA / Rider</strong> — install the
|
||||||
|
<a href="https://plugins.jetbrains.com/plugin/10209-lsp-client">LSP client plugin</a>,
|
||||||
|
then add a server under <em>Settings → Languages & Frameworks → Language Servers</em>:
|
||||||
|
</p>
|
||||||
|
<pre><code>Name: iLSP Bicep
|
||||||
|
Command: websocat wss://ilsp.i80.dk/bicep
|
||||||
|
Pattern: *.bicep
|
||||||
|
|
||||||
|
Name: iLSP YAML
|
||||||
|
Command: websocat wss://ilsp.i80.dk/yaml
|
||||||
|
Pattern: azure-pipelines.yml, *.yaml</code></pre>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<a href="https://github.com/vi/websocat">websocat</a> bridges a WebSocket to
|
||||||
|
stdio so editors that expect a local process can connect to a remote LSP.
|
||||||
|
Install with <code>brew install websocat</code> or
|
||||||
|
<code>cargo install websocat</code>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>VS Code</strong> — use the
|
||||||
|
<a href="https://marketplace.visualstudio.com/items?itemName=mesonbuild.mesonlsp">LSP client</a>
|
||||||
|
or configure the built-in
|
||||||
|
<code>vscode-languageclient</code> in a workspace extension, pointing
|
||||||
|
<code>serverOptions.command</code> at <code>websocat wss://ilsp.i80.dk/bicep</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Updating the catalogs</h2>
|
||||||
|
<p>
|
||||||
|
Catalogs are baked into the Docker image at build time and can also be
|
||||||
|
refreshed at runtime without restarting. Run the sync scripts from the
|
||||||
|
iLSP repository and call the reload endpoint:
|
||||||
|
</p>
|
||||||
|
<pre><code># Sync Bicep module catalog from ACR and push
|
||||||
|
python3 scripts/sync_push_catalogs.py
|
||||||
|
|
||||||
|
# Trigger a hot reload without container restart
|
||||||
|
curl -X POST https://ilsp.i80.dk/reload</code></pre>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
iLSP — <a href="https://gea.i80.dk/hjess/iLSP">source</a> —
|
||||||
|
<a href="/health">health</a>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def _build_app() -> web.Application:
|
async def _build_app() -> web.Application:
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
|
|
||||||
|
async def index(_: web.Request) -> web.Response:
|
||||||
|
html = _INDEX_HTML.format(
|
||||||
|
bicep_modules=len(BicepModuleCatalog._modules),
|
||||||
|
pipeline_templates=PipelineTemplateCatalog.template_count(),
|
||||||
|
pypi_packages=len(PypiCatalog._packages),
|
||||||
|
)
|
||||||
|
return web.Response(text=html, content_type="text/html", charset="utf-8")
|
||||||
|
|
||||||
async def health(_: web.Request) -> web.Response:
|
async def health(_: web.Request) -> web.Response:
|
||||||
import shutil
|
import shutil
|
||||||
|
from .yaml_lsp.proxy import _AZDO_SCHEMA_PATH
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"pypi_packages": len(PypiCatalog._packages),
|
"pypi_packages": len(PypiCatalog._packages),
|
||||||
@@ -119,6 +414,7 @@ async def _build_app() -> web.Application:
|
|||||||
"iac_source_modules": len(BicepModuleCatalog._iac),
|
"iac_source_modules": len(BicepModuleCatalog._iac),
|
||||||
"yaml_lsp": bool(shutil.which("yaml-language-server")),
|
"yaml_lsp": bool(shutil.which("yaml-language-server")),
|
||||||
"pipeline_templates": PipelineTemplateCatalog.template_count(),
|
"pipeline_templates": PipelineTemplateCatalog.template_count(),
|
||||||
|
"azdo_pipeline_schema": _AZDO_SCHEMA_PATH.exists(),
|
||||||
})
|
})
|
||||||
|
|
||||||
async def reload(_: web.Request) -> web.Response:
|
async def reload(_: web.Request) -> web.Response:
|
||||||
@@ -149,6 +445,7 @@ async def _build_app() -> web.Application:
|
|||||||
async def yaml_ws(request: web.Request) -> web.WebSocketResponse:
|
async def yaml_ws(request: web.Request) -> web.WebSocketResponse:
|
||||||
return await yaml_ws_handler(request)
|
return await yaml_ws_handler(request)
|
||||||
|
|
||||||
|
app.router.add_get("/", index)
|
||||||
app.router.add_get("/health", health)
|
app.router.add_get("/health", health)
|
||||||
app.router.add_post("/reload", reload)
|
app.router.add_post("/reload", reload)
|
||||||
app.router.add_get("/python", python_ws)
|
app.router.add_get("/python", python_ws)
|
||||||
|
|||||||
Reference in New Issue
Block a user