Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1f5dc58
feat: add bundled schema support to LSP server
holodorum Feb 3, 2026
411adfb
feat: add VSCode client support for bundled schemas
holodorum Feb 3, 2026
5321e32
fix bundled schema navigation with TextDocumentContentProvider
holodorum Feb 3, 2026
8a77765
refactor: use file extension for bundled schema matching
holodorum Feb 4, 2026
468c337
fix: remove deactivate function
holodorum Feb 4, 2026
45baf22
fix: avoid duplicate diagnostics when schema is present
holodorum Feb 9, 2026
43ca55a
refactor: export KsonInitialization
holodorum Feb 10, 2026
c91e14e
refactor: extract schema resolution helper and remove unused parameter
holodorum Feb 10, 2026
47e10f8
add node to pixi for the `npmInstallProductionLibrary` task
holodorum Feb 11, 2026
78396a2
refactor: introduce KsonSchemaDocument
holodorum Feb 11, 2026
9d17486
refactor: extract notifySchemaChange helper in startKsonServer
holodorum Feb 11, 2026
db7d942
refactor: remove redundant kson/updateBundledSchemaSettings handler
holodorum Feb 11, 2026
dd4e919
refactor: use options object for BundledSchemaProvider constructor
holodorum Feb 11, 2026
6c78a7b
refactor: derive isBundled on client side from URI scheme
holodorum Feb 11, 2026
b186c37
fix: code quality improvements for bundled schema support
holodorum Feb 11, 2026
ee031c4
test: tighten DefinitionService test assertions
holodorum Feb 11, 2026
43078e9
test: replace setTimeout waits with polling
holodorum Feb 11, 2026
5f293d0
refactor: extract waitFor helpers
holodorum Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions kson-lib/pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ version = "0.1.0"

[target.win-64.dependencies]
vs2022_win-64 = ">=19.44.35207,<20"

[dependencies]
nodejs = ">=18"
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Analysis} from 'kson';
import {Analysis, KsonValue, KsonValueType} from 'kson';
import {DocumentUri, TextDocuments, Range, Position} from "vscode-languageserver";
import {TextDocument} from "vscode-languageserver-textdocument";
import {IndexedDocumentSymbols} from "../features/IndexedDocumentSymbols";
Expand Down Expand Up @@ -93,4 +93,32 @@ export class KsonDocument implements TextDocument {
getSchemaDocument(): TextDocument | undefined {
return this.schemaDocument;
}

/**
* Extract the $schema field value from this document's parse analysis.
*
* @returns The $schema string value, or undefined if not present or not a string
*/
getSchemaId(): string | undefined {
return KsonDocument.extractSchemaId(this.parseAnalysis);
}

/**
* Extract the $schema field value from a parsed KSON analysis result.
*
* @param analysis The KSON analysis result
* @returns The $schema string value, or undefined if not present or not a string
*/
static extractSchemaId(analysis: Analysis): string | undefined {
const ksonValue = analysis.ksonValue;
if (!ksonValue || ksonValue.type !== KsonValueType.OBJECT) {
return undefined;
}
const obj = ksonValue as KsonValue.KsonObject;
const schemaValue = obj.properties.asJsReadonlyMapView().get('$schema');
if (!schemaValue || schemaValue.type !== KsonValueType.STRING) {
return undefined;
}
return (schemaValue as KsonValue.KsonString).value;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import {TextDocument} from 'vscode-languageserver-textdocument';
import {Kson} from 'kson';
import {KsonDocument} from "./KsonDocument.js";
import {KsonSchemaDocument} from "./KsonSchemaDocument.js";
import {DocumentUri, TextDocuments, TextDocumentContentChangeEvent} from "vscode-languageserver";
import {SchemaProvider, NoOpSchemaProvider} from "../schema/SchemaProvider.js";

/**
* Resolve the appropriate KsonDocument type for a given document.
*
* First tries URI-based schema resolution. If that fails, tries content-based
* metaschema resolution via $schema — but only returns a KsonSchemaDocument
* in that case, encoding the domain rule that only schema files have metaschemas.
*/
function resolveDocument(
provider: SchemaProvider,
textDocument: TextDocument,
parseResult: ReturnType<ReturnType<typeof Kson.getInstance>['analyze']>
): KsonDocument {
const schema = provider.getSchemaForDocument(textDocument.uri);
if (schema) {
return new KsonDocument(textDocument, parseResult, schema);
}

const schemaId = KsonDocument.extractSchemaId(parseResult);
if (schemaId) {
const metaSchema = provider.getMetaSchemaForId(schemaId);
if (metaSchema) {
return new KsonSchemaDocument(textDocument, parseResult, metaSchema);
}
}

return new KsonDocument(textDocument, parseResult);
}

/**
* Document management for the Kson Language Server.
* The {@link KsonDocumentsManager} keeps track of all {@link KsonDocument}'s that
Expand All @@ -25,12 +54,8 @@ export class KsonDocumentsManager extends TextDocuments<KsonDocument> {
content: string
): KsonDocument => {
const textDocument = TextDocument.create(uri, languageId, version, content);

// Try to get schema from provider
let schemaDocument = provider.getSchemaForDocument(uri);

const parseResult = Kson.getInstance().analyze(content, uri);
return new KsonDocument(textDocument, parseResult, schemaDocument);
return resolveDocument(provider, textDocument, parseResult);
},
update: (
ksonDocument: KsonDocument,
Expand All @@ -43,11 +68,7 @@ export class KsonDocumentsManager extends TextDocuments<KsonDocument> {
version
);
const parseResult = Kson.getInstance().analyze(textDocument.getText(), ksonDocument.uri);
return new KsonDocument(
textDocument,
parseResult,
provider.getSchemaForDocument(ksonDocument.uri)
);
return resolveDocument(provider, textDocument, parseResult);
}
});

Expand Down Expand Up @@ -83,10 +104,8 @@ export class KsonDocumentsManager extends TextDocuments<KsonDocument> {
for (const doc of allDocs) {
const textDocument = doc.textDocument;
const parseResult = doc.getAnalysisResult();
const updatedSchema = this.schemaProvider.getSchemaForDocument(doc.uri);

// Create new document instance with updated schema
const updatedDoc = new KsonDocument(textDocument, parseResult, updatedSchema);
const updatedDoc = resolveDocument(this.schemaProvider, textDocument, parseResult);

// Replace in the internal document cache
// Access the protected _syncedDocuments property from parent class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {Analysis} from 'kson';
import {TextDocument} from 'vscode-languageserver-textdocument';
import {KsonDocument} from './KsonDocument.js';

/**
* A KsonDocument that represents a schema file.
*
* Schema documents have a metaschema (resolved via their $schema field)
* rather than a regular schema association. This distinction prevents
* metaschema resolution from being attempted on regular KSON documents.
*/
export class KsonSchemaDocument extends KsonDocument {
private metaSchemaDocument?: TextDocument;

constructor(textDocument: TextDocument, parseAnalysis: Analysis, metaSchemaDocument?: TextDocument) {
super(textDocument, parseAnalysis);
this.metaSchemaDocument = metaSchemaDocument;
}

/**
* Get the metaschema document for this schema document, if one is configured.
*/
getMetaSchemaDocument(): TextDocument | undefined {
return this.metaSchemaDocument;
}
}

/**
* Type guard to check if a KsonDocument is a KsonSchemaDocument.
*/
export function isKsonSchemaDocument(doc: KsonDocument): doc is KsonSchemaDocument {
return doc instanceof KsonSchemaDocument;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {DefinitionLink, Position, Range} from 'vscode-languageserver';
import {KsonDocument} from '../document/KsonDocument.js';
import {isKsonSchemaDocument} from '../document/KsonSchemaDocument.js';
import {KsonTooling} from 'kson-tooling';

/**
Expand All @@ -19,29 +20,31 @@ export class DefinitionService {
*/
getDefinition(document: KsonDocument, position: Position): DefinitionLink[] {
const tooling = KsonTooling.getInstance();
const results: DefinitionLink[] = [];

// Get the schema for this document
const schemaDocument = document.getSchemaDocument();
// Try $ref resolution within the same document
const refLocations = tooling.resolveRefAtLocation(
document.getText(),
position.line,
position.character
);
results.push(...this.convertRangesToDefinitionLinks(refLocations, document.uri));

// Try document-to-schema navigation first (if schema is configured)
// Try document-to-schema navigation (if schema is configured)
const schemaDocument = isKsonSchemaDocument(document)
? document.getMetaSchemaDocument()
: document.getSchemaDocument();
if (schemaDocument) {
const locations = tooling.getSchemaLocationAtLocation(
document.getText(),
schemaDocument.getText(),
position.line,
position.character
);
return this.convertRangesToDefinitionLinks(locations, schemaDocument.uri);
results.push(...this.convertRangesToDefinitionLinks(locations, schemaDocument.uri));
}

// Try schema $ref resolution (within the same document)
// This handles the case where we're editing a schema file and want to jump to internal refs
const refLocations = tooling.resolveRefAtLocation(
document.getText(),
position.line,
position.character
);
return this.convertRangesToDefinitionLinks(refLocations, document.uri);
return results;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
RelatedFullDocumentDiagnosticReport
} from 'vscode-languageserver';
import {KsonDocument} from '../document/KsonDocument';
import {isKsonSchemaDocument} from '../document/KsonSchemaDocument';
import {Message, Kson, SchemaResult} from 'kson';

/**
Expand All @@ -22,17 +23,23 @@ export class DiagnosticService {
}

private getDiagnostics(document: KsonDocument): Diagnostic[] {
const schema = document.getSchemaDocument()
// Schema validation already includes parse errors, so use it exclusively when
// available to avoid duplicate diagnostics.
const messages = this.getSchemaValidationMessages(document)
?? document.getAnalysisResult().errors.asJsReadonlyArrayView();
return this.loggedMessagesToDiagnostics(messages);
}

const schemaMessages = schema ? (() => {
const parsedSchema = Kson.getInstance().parseSchema(schema.getText())
if(parsedSchema instanceof SchemaResult.Success){
return parsedSchema.schemaValidator.validate(document.getText(), document.uri).asJsReadonlyArrayView()
}
return []
})() : []
const documentMessages = document.getAnalysisResult().errors.asJsReadonlyArrayView()
return this.loggedMessagesToDiagnostics([...schemaMessages, ...documentMessages]);
private getSchemaValidationMessages(document: KsonDocument): readonly Message[] | null {
const schema = isKsonSchemaDocument(document)
? document.getMetaSchemaDocument()
: document.getSchemaDocument();
if (!schema) return null;

const parsedSchema = Kson.getInstance().parseSchema(schema.getText());
if (!(parsedSchema instanceof SchemaResult.Success)) return null;

return parsedSchema.schemaValidator.validate(document.getText(), document.uri).asJsReadonlyArrayView();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Hover, Position, MarkupKind} from 'vscode-languageserver';
import {KsonDocument} from '../document/KsonDocument.js';
import {isKsonSchemaDocument} from '../document/KsonSchemaDocument.js';
import {KsonTooling} from 'kson-tooling';

/**
Expand All @@ -16,7 +17,9 @@ export class HoverService {
*/
getHover(document: KsonDocument, position: Position): Hover | null {
// Get the schema for this document
const schemaDocument = document.getSchemaDocument();
const schemaDocument = isKsonSchemaDocument(document)
? document.getMetaSchemaDocument()
: document.getSchemaDocument();
if (!schemaDocument) {
// No schema configured, no hover info available
return null;
Expand Down
Loading