Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {CommandParameters} from './CommandParameters.js';
import {CommandType} from './CommandType.js';
import {AssociateSchemaCommand} from './AssociateSchemaCommand.js';
import {RemoveSchemaCommand} from './RemoveSchemaCommand.js';
import {notifySchemaChange} from '../schema/notifySchemaChange.js';

/**
* Node.js implementation of CommandExecutor with file system support
Expand All @@ -19,6 +20,7 @@ export class CommandExecutor extends CommandExecutorBase {
});

if (result.success) {
this.applySchemaConfigurationChange();
this.connection.window.showInformationMessage(result.message);
} else {
this.connection.window.showErrorMessage(result.message);
Expand All @@ -37,11 +39,20 @@ export class CommandExecutor extends CommandExecutorBase {
});

if (result.success) {
this.applySchemaConfigurationChange();
this.connection.window.showInformationMessage(result.message);
} else {
this.connection.window.showErrorMessage(result.message);
}

return result;
}
}

/**
* Ensure latest schema configuration change is reloaded.
*/
private applySchemaConfigurationChange(): void {
this.documentManager.reloadSchemaConfiguration();
notifySchemaChange(this.connection, this.documentManager);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Connection} from 'vscode-languageserver';
import {KsonDocumentsManager} from '../document/KsonDocumentsManager.js';

export function notifySchemaChange(connection: Connection, documentManager: KsonDocumentsManager): void {
documentManager.refreshDocumentSchemas();
connection.sendNotification('kson/schemaConfigurationChanged');
connection.sendRequest('workspace/diagnostic/refresh');
}
15 changes: 3 additions & 12 deletions tooling/language-server-protocol/src/startKsonServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {SchemaProvider} from './core/schema/SchemaProvider.js';
import {BundledSchemaProvider, BundledSchemaConfig, BundledMetaSchemaConfig} from './core/schema/BundledSchemaProvider.js';
import {CompositeSchemaProvider} from './core/schema/CompositeSchemaProvider.js';
import {SCHEMA_CONFIG_FILENAME} from "./core/schema/SchemaConfig";
import {notifySchemaChange} from "./core/schema/notifySchemaChange.js";
import {CommandExecutorFactory} from "./core/commands/CommandExecutorFactory";

/**
Expand Down Expand Up @@ -256,16 +257,6 @@ export function startKsonServer(
}
});

/**
* Refresh all documents with updated schemas, notify the client,
* and trigger diagnostic refresh.
*/
function notifySchemaChange(): void {
documentManager.refreshDocumentSchemas();
connection.sendNotification('kson/schemaConfigurationChanged');
connection.sendRequest('workspace/diagnostic/refresh');
}

// Handle changes to watched files
connection.onDidChangeWatchedFiles((params) => {
const schemaProvider = documentManager.getSchemaProvider();
Expand All @@ -284,7 +275,7 @@ export function startKsonServer(
}

if (schemaChanged) {
notifySchemaChange();
notifySchemaChange(connection, documentManager);
}
});

Expand All @@ -302,7 +293,7 @@ export function startKsonServer(

if (bundledSchemaProvider && scoped?.enableBundledSchemas !== undefined) {
bundledSchemaProvider.setEnabled(scoped.enableBundledSchemas);
notifySchemaChange();
notifySchemaChange(connection, documentManager);
}

connection.console.info('Configuration updated');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TextDocument} from 'vscode-languageserver-textdocument';
import {beforeEach, describe, it} from 'mocha';
import {afterEach, beforeEach, describe, it} from 'mocha';
import assert from "assert";
import {ConnectionStub} from "../../ConnectionStub";
import {KsonTextDocumentService} from "../../../core/services/KsonTextDocumentService";
Expand All @@ -9,13 +9,21 @@ import {
ExecuteCommandParams,
ApplyWorkspaceEditParams,
TextEdit,
ApplyWorkspaceEditResult, Range
ApplyWorkspaceEditResult, Range,
CompletionList
} from "vscode-languageserver";
import {CommandType, toWireCommandId} from "../../../core/commands/CommandType";
import {FormattingStyle} from "kson";
import {FormattingStyleId, formattingStyleId} from "../../../core/formattingStyle";
import {RemoteWorkspace} from "vscode-languageserver/lib/common/server";
import {createCommandExecutor} from "../../../core/commands/createCommandExecutor.node.js";
import {SchemaProvider} from "../../../core/schema/SchemaProvider";
import {FileSystemSchemaProvider} from "../../../core/schema/FileSystemSchemaProvider";
import {pos} from "../../TestHelpers.js";
import {URI} from "vscode-uri";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

class WorkspaceConnectionStub extends ConnectionStub {
private capturedEdits: ApplyWorkspaceEditParams | undefined;
Expand All @@ -36,12 +44,43 @@ class WorkspaceConnectionStub extends ConnectionStub {
}
}

/**
* Captures the client-facing traffic a schema change produces: the
* `kson/schemaConfigurationChanged` notification, the diagnostic refresh
* request, and the window messages the schema commands report with.
*/
class SchemaConnectionStub extends ConnectionStub {
readonly notifications: string[] = [];
readonly requests: string[] = [];

constructor() {
super();
this.window = {
showInformationMessage: () => Promise.resolve(undefined),
showErrorMessage: () => Promise.resolve(undefined)
} as any;
}

override sendNotification(method: unknown): Promise<void> {
this.notifications.push(String(method));
return Promise.resolve();
}

override sendRequest<R>(method: unknown): Promise<R> {
this.requests.push(String(method));
return Promise.resolve(undefined as R);
}
}

const TEST_DISTRIBUTION_ID = 'test-ns';

function createTestSetup() {
const connection = new ConnectionStub();
const documentsManager = new KsonDocumentsManager();
const service = new KsonTextDocumentService(documentsManager, createCommandExecutor, null, TEST_DISTRIBUTION_ID);
function createTestSetup(
connection: ConnectionStub = new ConnectionStub(),
workspaceRoot: string | null = null,
schemaProvider?: SchemaProvider
) {
const documentsManager = new KsonDocumentsManager(schemaProvider);
const service = new KsonTextDocumentService(documentsManager, createCommandExecutor, workspaceRoot, TEST_DISTRIBUTION_ID);

documentsManager.listen(connection);
service.connect(connection);
Expand Down Expand Up @@ -167,3 +206,93 @@ describe('KSON Command Executor', () => {
});

});

/**
* The schema commands write `.kson-schema.kson` themselves, so the server has to
* pick the change up on its own.
*/
describe('KSON Command Executor schema association', () => {
const SCHEMA_FILENAME = 'status.schema.kson';
const SCHEMA_CONTENT = [
'{',
' type: object',
' properties: {',
' status: {',
' type: string',
' enum: ["active", "inactive", "pending"]',
' }',
' }',
'}'
].join('\n');
const DOCUMENT_CONTENT = '{\n status: "ac"\n}';
// Caret between `ac` and the closing quote: still authoring, so enum completions apply.
const VALUE_POSITION = pos(1, 15);
const SCHEMA_CHANGED_NOTIFICATION = 'kson/schemaConfigurationChanged';

let workspaceRoot: string;
let documentUri: string;
let connection: SchemaConnectionStub;
let documentsManager: KsonDocumentsManager;

beforeEach(() => {
workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kson-test-'));
fs.writeFileSync(path.join(workspaceRoot, SCHEMA_FILENAME), SCHEMA_CONTENT, 'utf-8');
documentUri = URI.file(path.join(workspaceRoot, 'settings.kson')).toString();

connection = new SchemaConnectionStub();
documentsManager = createTestSetup(
connection,
workspaceRoot,
new FileSystemSchemaProvider(URI.file(workspaceRoot))
).documentsManager;

connection.didOpenHandler(createDidOpenParams(documentUri, DOCUMENT_CONTENT));
});

afterEach(() => {
fs.rmSync(workspaceRoot, {recursive: true, force: true});
});

function executeSchemaCommand(command: CommandType, args: object = {}): Promise<any> {
return connection.executeCommandHandler(
{
command: toWireCommandId(command, TEST_DISTRIBUTION_ID),
arguments: [{documentUri, ...args}]
},
{} as any, {} as any, undefined
);
}

async function completionLabels(): Promise<string[] | undefined> {
// The handler's declared type allows a ResponseError; the completion path never returns one.
const completions = await connection.requestCompletion(documentUri, VALUE_POSITION) as CompletionList | null;
return completions?.items.map(item => item.label);
}

it('should apply an associated schema', async () => {
const result = await executeSchemaCommand(CommandType.ASSOCIATE_SCHEMA, {schemaPath: SCHEMA_FILENAME});

assert.strictEqual(result.success, true, result.message);
assert.ok(
documentsManager.get(documentUri)?.getSchemaDocument(),
'the open document should carry the schema it was just associated with'
);
assert.deepStrictEqual(await completionLabels(), ['active', 'inactive', 'pending']);
assert.deepStrictEqual(connection.notifications, [SCHEMA_CHANGED_NOTIFICATION]);
assert.deepStrictEqual(connection.requests, ['workspace/diagnostic/refresh']);
});

it('should drop the schema when the association is removed', async () => {
await executeSchemaCommand(CommandType.ASSOCIATE_SCHEMA, {schemaPath: SCHEMA_FILENAME});

const result = await executeSchemaCommand(CommandType.REMOVE_SCHEMA);

assert.strictEqual(result.success, true, result.message);
assert.strictEqual(documentsManager.get(documentUri)?.getSchemaDocument(), undefined);
assert.strictEqual(await completionLabels(), undefined);
assert.deepStrictEqual(
connection.notifications,
[SCHEMA_CHANGED_NOTIFICATION, SCHEMA_CHANGED_NOTIFICATION]
);
});
});