Initial commit - UCXL VS Code extension
- Added UCXL VS Code extension with syntax highlighting - Implemented language configuration and grammar definitions - Created extension package with examples and documentation - Added syntax highlighting for UCXL code structures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
5
LICENSE.md
Normal file
5
LICENSE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthony Rawlins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy...
|
||||
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# UCXL Syntax + Linter
|
||||
|
||||
Highlights and lints UCXL addresses anywhere in your files.
|
||||
|
||||
## Features
|
||||
- Syntax colouring for UCXL scheme, agent, topic, project, context, temporal segments, and path
|
||||
- Inline linting for invalid UCXL syntax with red squiggly underlines
|
||||
- Hover tooltips showing parsed components
|
||||
|
||||
## Install
|
||||
1. Clone this repo
|
||||
2. Install `vsce` if you don’t have it:
|
||||
```bash
|
||||
npm install -g vsce
|
||||
|
||||
|
||||
Then you can see the syntax-colouring in action right here by reading this file and hovering over:
|
||||
|
||||
|
||||
ucxl://AB001:dev@project:task/~~/policies/linting/details.md
|
||||
|
||||
|
||||
```
|
||||
|
||||
ucxl://F0131:finance@project:any/~~/policies/linting/details.md
|
||||
|
||||
```
|
||||
17
examples/ucxl_example.py
Normal file
17
examples/ucxl_example.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import posixpath
|
||||
|
||||
def ucxl_path_join(*args):
|
||||
"""
|
||||
Join multiple path components into a single UCXL path.
|
||||
|
||||
Args:
|
||||
*args: Path components to join.
|
||||
|
||||
Returns:
|
||||
str: A single UCXL path.
|
||||
"""
|
||||
ucxl = "ucxl://AB001:dev@project:task/~~/policies/linting/details.md"
|
||||
|
||||
return posixpath.join(*args)
|
||||
|
||||
|
||||
5
examples/ucxl_example.rs
Normal file
5
examples/ucxl_example.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
fn main() {
|
||||
let context = ucxl::Context::from(ucxl://any:developer@chorus:website-redesign/#/db/connection/credentials).expect("Failed to create UCXL context");
|
||||
}
|
||||
261
extension.js
Normal file
261
extension.js
Normal file
@@ -0,0 +1,261 @@
|
||||
const vscode = require("vscode");
|
||||
|
||||
/* const UCXL_REGEX = /^ucxl:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)((\/(\^\^|~~))?\/[^\\s]*)?$/; */
|
||||
// Updated regex to match the new UCXL address format with optional temporal and context path
|
||||
// The regex captures:
|
||||
// - Agent and Role: alphanumeric with underscores and hyphens
|
||||
// - Project and Task: alphanumeric with underscores and hyphens
|
||||
// - Temporal: optional, can be `#`, `~*`, `^*`, `~~`, `^^`, `~<number>`, or `^<number>`
|
||||
// - Context Path: optional, can be any string after the temporal part
|
||||
// The regex ensures that the address starts with `ucxl://` and is followed by the correct structure of components.
|
||||
// The regex is designed to be flexible while ensuring that the components are valid according to the UCXL specification.
|
||||
// The regex also allows for the context path to be any valid string, ensuring it captures the full address correctly.
|
||||
// The regex is designed to be used in a VSCode extension for validating and providing hover information
|
||||
// about UCXL addresses in text documents.
|
||||
//const UCXL_REGEX = /^ucxl:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)\/(#|~\*|\^\*|~~|\^\^|~\d+|\^\d+)\/(.+)$/;
|
||||
|
||||
// Agent ID regex for UCXL addresses
|
||||
const UCXL_REGEX = /^ucxl:\/\/([A-Z0-9]{3,5}|any|none):([a-zA-Z0-9_-]+)@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)\/(#|~\*|\^\*|~~|\^\^|~\d+|\^\d+)\/(.+)$/;
|
||||
|
||||
function activate(context) {
|
||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection("ucxl");
|
||||
context.subscriptions.push(diagnosticCollection);
|
||||
|
||||
function updateDiagnostics(doc) {
|
||||
if (!doc) return;
|
||||
const diagnostics = [];
|
||||
const text = doc.getText();
|
||||
const lines = text.split(/\r?\n/);
|
||||
|
||||
lines.forEach((line, idx) => {
|
||||
const matches = line.matchAll(/ucxl:\/\/[^\s]+/g);
|
||||
for (const match of matches) {
|
||||
const addr = match[0];
|
||||
if (!UCXL_REGEX.test(addr)) {
|
||||
diagnostics.push(
|
||||
new vscode.Diagnostic(
|
||||
new vscode.Range(idx, match.index, idx, match.index + addr.length),
|
||||
"Invalid UCXL address syntax",
|
||||
vscode.DiagnosticSeverity.Error
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
diagnosticCollection.set(doc.uri, diagnostics);
|
||||
}
|
||||
|
||||
vscode.workspace.onDidOpenTextDocument(updateDiagnostics);
|
||||
vscode.workspace.onDidChangeTextDocument(e => updateDiagnostics(e.document));
|
||||
vscode.workspace.onDidCloseTextDocument(doc => diagnosticCollection.delete(doc.uri));
|
||||
|
||||
// Hover Provider
|
||||
const hoverProvider = vscode.languages.registerHoverProvider(
|
||||
{ scheme: "file", language: "*" },
|
||||
{
|
||||
provideHover(document, position) {
|
||||
const range = document.getWordRangeAtPosition(position, /ucxl:\/\/[^\s]+/);
|
||||
if (!range) return;
|
||||
const address = document.getText(range);
|
||||
|
||||
const match = address.match(UCXL_REGEX);
|
||||
if (!match) return new vscode.Hover(`$(error) **Invalid UCXL address**`);
|
||||
|
||||
const [, agent, role, project, task, , temporal, , context_path] = match;
|
||||
const md = new vscode.MarkdownString();
|
||||
md.isTrusted = true;
|
||||
md.appendMarkdown(`### UCXL Address Components\n`);
|
||||
md.appendMarkdown(`- **Agent**: \`${agent}\`\n`);
|
||||
md.appendMarkdown(`- **Role**: \`${role}\`\n`);
|
||||
md.appendMarkdown(`- **Project**: \`${project}\`\n`);
|
||||
md.appendMarkdown(`- **Task**: \`${task}\`\n`);
|
||||
if (temporal) md.appendMarkdown(`- **Temporal**: \`${temporal}\`\n`);
|
||||
if (context_path) md.appendMarkdown(`- **Context Path**: \`${context_path}\`\n`);
|
||||
|
||||
return new vscode.Hover(md, range);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
context.subscriptions.push(hoverProvider);
|
||||
}
|
||||
|
||||
function lintUcxlLine(lineText, lineNum, diagnostics) {
|
||||
const match = lineText.match(UCXL_REGEX);
|
||||
if (!match) return;
|
||||
|
||||
const fullAddress = match[0];
|
||||
const agentToken = match[1];
|
||||
|
||||
if (!isValidAgent(agentToken)) {
|
||||
const agentStart = lineText.indexOf(agentToken);
|
||||
const agentEnd = agentStart + agentToken.length;
|
||||
diagnostics.push(
|
||||
new vscode.Diagnostic(
|
||||
new vscode.Range(lineNum, agentStart, lineNum, agentEnd),
|
||||
"Invalid agent token (checksum or field error)",
|
||||
vscode.DiagnosticSeverity.Error
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDiagnostics(doc, diagnosticCollection) {
|
||||
const diagnostics = [];
|
||||
for (let i = 0; i < doc.lineCount; i++) {
|
||||
const lineText = doc.lineAt(i).text;
|
||||
lintUcxlLine(lineText, i, diagnostics);
|
||||
}
|
||||
diagnosticCollection.set(doc.uri, diagnostics);
|
||||
}
|
||||
|
||||
|
||||
vscode.languages.registerHoverProvider("ucxl", {
|
||||
provideHover(document, position) {
|
||||
const wordRange = document.getWordRangeAtPosition(position, /[A-Z0-9]{5}/);
|
||||
if (!wordRange) return null;
|
||||
|
||||
const token = document.getText(wordRange);
|
||||
if (!isValidAgent(token)) return null;
|
||||
|
||||
const agentId = decodeToken(token);
|
||||
return new vscode.Hover(`**AgentID**: Version=${agentId.version}, Host=${agentId.hostId}, GPU=${agentId.gpuSlot}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
const VERSION_BITS = 3;
|
||||
const HOST_ID_BITS = 10;
|
||||
const GPU_SLOT_BITS = 4;
|
||||
const RESERVED_BITS = 2;
|
||||
const CHECKSUM_BITS = 6;
|
||||
|
||||
const PREFIX_BITS = VERSION_BITS + HOST_ID_BITS + GPU_SLOT_BITS + RESERVED_BITS; // 19
|
||||
const TOTAL_BITS = PREFIX_BITS + CHECKSUM_BITS; // 25
|
||||
|
||||
const MAX_HOST_ID = (1 << HOST_ID_BITS) - 1;
|
||||
const MAX_GPU_SLOT = (1 << GPU_SLOT_BITS) - 1;
|
||||
|
||||
// -------------------
|
||||
// Base32 helpers
|
||||
// -------------------
|
||||
function intToBase32(n, length) {
|
||||
let chars = Array(length).fill('0');
|
||||
for (let i = length - 1; i >= 0; i--) {
|
||||
chars[i] = CROCKFORD_ALPHABET[n & 0x1F];
|
||||
n >>= 5;
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
function base32ToInt(s) {
|
||||
const charMap = {};
|
||||
for (let i = 0; i < CROCKFORD_ALPHABET.length; i++) {
|
||||
charMap[CROCKFORD_ALPHABET[i]] = i;
|
||||
}
|
||||
|
||||
if (s.length !== 5) throw new Error(`token length must be 5, got ${s.length}`);
|
||||
|
||||
let n = 0;
|
||||
for (let ch of s.toUpperCase()) {
|
||||
if ("ILOU".includes(ch)) throw new Error(`invalid character ${ch}`);
|
||||
const val = charMap[ch];
|
||||
if (val === undefined) throw new Error(`invalid character ${ch}`);
|
||||
n = (n << 5) | val;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// SHA256 first bits
|
||||
// -------------------
|
||||
function sha256FirstBits(value, bits) {
|
||||
const bytes = Buffer.from([(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]);
|
||||
const hash = crypto.createHash('sha256').update(bytes).digest();
|
||||
return hash[0] >> (8 - bits);
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Pack/Unpack
|
||||
// -------------------
|
||||
function packFields(version, hostId, gpuSlot, reserved) {
|
||||
if (version >= (1 << VERSION_BITS)) throw new Error("version out of range");
|
||||
if (hostId > MAX_HOST_ID) throw new Error("host_id out of range");
|
||||
if (gpuSlot > MAX_GPU_SLOT) throw new Error("gpu_slot out of range");
|
||||
if (reserved >= (1 << RESERVED_BITS)) throw new Error("reserved out of range");
|
||||
|
||||
let bits = 0;
|
||||
bits = (bits << VERSION_BITS) | version;
|
||||
bits = (bits << HOST_ID_BITS) | hostId;
|
||||
bits = (bits << GPU_SLOT_BITS) | gpuSlot;
|
||||
bits = (bits << RESERVED_BITS) | reserved;
|
||||
|
||||
const checksum = sha256FirstBits(bits, CHECKSUM_BITS);
|
||||
bits = (bits << CHECKSUM_BITS) | checksum;
|
||||
|
||||
if (bits >= (1 << TOTAL_BITS)) throw new Error("packed value exceeds allowed bit length");
|
||||
return bits;
|
||||
}
|
||||
|
||||
function unpackFields(packed) {
|
||||
if (packed >= (1 << TOTAL_BITS)) throw new Error("packed value exceeds allowed bit length");
|
||||
|
||||
const checksum = packed & ((1 << CHECKSUM_BITS) - 1);
|
||||
const prefix = packed >> CHECKSUM_BITS;
|
||||
|
||||
let tmp = prefix;
|
||||
const reserved = tmp & ((1 << RESERVED_BITS) - 1); tmp >>= RESERVED_BITS;
|
||||
const gpuSlot = tmp & ((1 << GPU_SLOT_BITS) - 1); tmp >>= GPU_SLOT_BITS;
|
||||
const hostId = tmp & ((1 << HOST_ID_BITS) - 1); tmp >>= HOST_ID_BITS;
|
||||
const version = tmp & ((1 << VERSION_BITS) - 1);
|
||||
|
||||
const expected = sha256FirstBits(prefix, CHECKSUM_BITS);
|
||||
if (expected !== checksum) throw new Error("checksum mismatch");
|
||||
|
||||
return { version, hostId, gpuSlot, reserved, checksum };
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Encode / Decode Token
|
||||
// -------------------
|
||||
function encodeToken(version, hostId, gpuSlot, reserved) {
|
||||
const packed = packFields(version, hostId, gpuSlot, reserved);
|
||||
return intToBase32(packed, 5);
|
||||
}
|
||||
|
||||
function decodeToken(token) {
|
||||
const packed = base32ToInt(token);
|
||||
return unpackFields(packed);
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// AgentID validator
|
||||
// -------------------
|
||||
function isValidAgent(agent) {
|
||||
try {
|
||||
decodeToken(agent);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
// console.log(isValidAgent(encodeToken(1, 42, 3, 0))); // true
|
||||
// console.log(isValidAgent("ABCDE")); // false if checksum fails
|
||||
|
||||
function isValidUCXLAddress(address) {
|
||||
return UCXL_REGEX.test(address);
|
||||
}
|
||||
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
18
language-configuration.json
Normal file
18
language-configuration.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "#",
|
||||
"blockComment": ["/*", "*/"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "'", "close": "'" }
|
||||
]
|
||||
}
|
||||
84
package.json
Normal file
84
package.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "ucxl-syntax-linter",
|
||||
"displayName": "UCXL Syntax + Linter",
|
||||
"description": "Syntax highlighting and linting for UCXL addresses inline in any file.",
|
||||
"version": "0.0.1",
|
||||
"publisher": "Anthony Rawlins - CHORUS.services",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/anthonyrawlins/ucxl-vscode-linter.git"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.70.0"
|
||||
},
|
||||
"categories": [
|
||||
"Linters",
|
||||
"Programming Languages"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:ucxl",
|
||||
"onLanguage:go",
|
||||
"onLanguage:rust",
|
||||
"onLanguage:javascript",
|
||||
"onLanguage:python",
|
||||
"onLanguage:markdown"
|
||||
],
|
||||
"main": "./extension.js",
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "ucxl",
|
||||
"aliases": ["UCXL"],
|
||||
"extensions": [".ucxl"]
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "ucxl",
|
||||
"scopeName": "source.ucxl",
|
||||
"path": "./syntaxes/ucxl.tmLanguage.json",
|
||||
"injectTo": [
|
||||
"source",
|
||||
"text",
|
||||
"source.go",
|
||||
"source.rust",
|
||||
"source.js",
|
||||
"source.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "go",
|
||||
"scopeName": "source.go.ucxl",
|
||||
"path": "./syntaxes/ucxl.tmLanguage.json",
|
||||
"injectTo": ["source.go"]
|
||||
},
|
||||
{
|
||||
"language": "rust",
|
||||
"scopeName": "source.rust.ucxl",
|
||||
"path": "./syntaxes/ucxl.tmLanguage.json",
|
||||
"injectTo": ["source.rust"]
|
||||
},
|
||||
{
|
||||
"language": "javascript",
|
||||
"scopeName": "source.js.ucxl",
|
||||
"path": "./syntaxes/ucxl.tmLanguage.json",
|
||||
"injectTo": ["source.js"]
|
||||
},
|
||||
{
|
||||
"language": "python",
|
||||
"scopeName": "source.python.ucxl",
|
||||
"path": "./syntaxes/ucxl.tmLanguage.json",
|
||||
"injectTo": ["source.py"]
|
||||
},
|
||||
{
|
||||
"language": "markdown",
|
||||
"scopeName": "source.markdown.ucxl",
|
||||
"path": "./syntaxes/ucxl.tmLanguage.json",
|
||||
"injectTo": ["source.md"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"package": "vsce package"
|
||||
}
|
||||
}
|
||||
33
syntaxes/ucxl.tmLanguage.backup.json
Normal file
33
syntaxes/ucxl.tmLanguage.backup.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"scopeName": "source.ucxl",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.scheme.ucxl",
|
||||
"match": "\\bucxl://"
|
||||
},
|
||||
{
|
||||
"name": "entity.name.agent.ucxl",
|
||||
"match": "(?<=ucxl://)[a-zA-Z0-9_-]+(?=:)"
|
||||
},
|
||||
{
|
||||
"name": "support.class.topic.ucxl",
|
||||
"match": "(?<=:)[a-zA-Z0-9_-]+(?=@)"
|
||||
},
|
||||
{
|
||||
"name": "entity.name.project.ucxl",
|
||||
"match": "(?<=@)[a-zA-Z0-9_-]+(?=:)"
|
||||
},
|
||||
{
|
||||
"name": "variable.parameter.context.ucxl",
|
||||
"match": "(?<=:)[a-zA-Z0-9_-]+(?=/)"
|
||||
},
|
||||
{
|
||||
"name": "constant.language.temporal.ucxl",
|
||||
"match": "(?<=/)(#|~\\*|\\^\\*|~~|\\^\\^|~\\d+|\\^\\d+)(?=/)"
|
||||
},
|
||||
{
|
||||
"name": "string.path.ucxl",
|
||||
"match": "(?<=/)[^\\s]+"
|
||||
}
|
||||
]
|
||||
}
|
||||
36
syntaxes/ucxl.tmLanguage.json
Normal file
36
syntaxes/ucxl.tmLanguage.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"scopeName": "source.ucxl",
|
||||
"name": "UCXL",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.ucxl",
|
||||
"match": "^ucxl://([A-Z0-9]{5})" ,
|
||||
"captures": {
|
||||
"1": { "name": "constant.language.agent.ucxl" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "^ucxl://[A-Z0-9]{5}:([a-zA-Z0-9_-]+)",
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.role.ucxl" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)",
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.project.ucxl" },
|
||||
"2": { "name": "entity.name.task.ucxl" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\/(#|~\\*|\\^\\*|~~|\\^\\^|~\\d+|\\^\\d+)\\/",
|
||||
"captures": {
|
||||
"1": { "name": "constant.language.temporal.ucxl" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\/([^\\s]+)$",
|
||||
"name": "string.path.ucxl"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
ucxl-syntax-linter-0.0.1.vsix
Normal file
BIN
ucxl-syntax-linter-0.0.1.vsix
Normal file
Binary file not shown.
Reference in New Issue
Block a user