Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 13 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getJsonSchemaContent, IJSONSchemaCache, JSONSchemaDocumentContentProvid
import { getConflictingExtensions, showUninstallConflictsNotification } from './extensionConflicts';
import { TelemetryErrorHandler, TelemetryOutputChannel } from './telemetry';
import { TextDecoder } from 'util';
import { createJSONSchemaStatusBarItem } from './schema-status-bar-item';

export interface ISchemaAssociations {
[pattern: string]: string[];
Expand Down Expand Up @@ -78,6 +79,12 @@ namespace ResultLimitReachedNotification {
export const type: NotificationType<string> = new NotificationType('yaml/resultLimitReached');
}

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SchemaSelectionRequests {
export const type: NotificationType<void> = new NotificationType('yaml/supportSchemaSelection');
export const schemaStoreInitialized: NotificationType<void> = new NotificationType('yaml/schema/store/initialized');
}

let client: CommonLanguageClient;

const lsName = 'YAML Support';
Expand Down Expand Up @@ -154,6 +161,8 @@ export function startClient(
client.sendNotification(DynamicCustomSchemaRequestRegistration.type);
// Tell the server that the client supports schema requests sent directly to it
client.sendNotification(VSCodeContentRequestRegistration.type);
// Tell the server that the client supports schema selection requests
client.sendNotification(SchemaSelectionRequests.type);
// If the server asks for custom schema content, get it and send it back
client.onRequest(CUSTOM_SCHEMA_REQUEST, (resource: string) => {
return schemaExtensionAPI.requestCustomSchema(resource);
Expand Down Expand Up @@ -190,6 +199,10 @@ export function startClient(
}
}
});

client.onNotification(SchemaSelectionRequests.schemaStoreInitialized, () => {
createJSONSchemaStatusBarItem(context, client);
});
});

return schemaExtensionAPI;
Expand Down
163 changes: 163 additions & 0 deletions src/schema-status-bar-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
ExtensionContext,
window,
commands,
StatusBarAlignment,
TextEditor,
StatusBarItem,
QuickPickItem,
ThemeColor,
workspace,
} from 'vscode';
import { CommonLanguageClient, RequestType } from 'vscode-languageclient/node';

type FileUri = string;
interface JSONSchema {
name?: string;
description?: string;
uri: string;
}

interface MatchingJSONSchema extends JSONSchema {
usedForCurrentFile: boolean;
fromStore: boolean;
}

interface SchemaItem extends QuickPickItem {
schema?: MatchingJSONSchema;
}

// eslint-disable-next-line @typescript-eslint/ban-types
const getJSONSchemas: RequestType<FileUri, MatchingJSONSchema[], {}> = new RequestType('yaml/get/all/jsonSchemas');

// eslint-disable-next-line @typescript-eslint/ban-types
const getSchema: RequestType<FileUri, JSONSchema[], {}> = new RequestType('yaml/get/jsonSchema');

export let statusBarItem: StatusBarItem;

let client: CommonLanguageClient;
export function createJSONSchemaStatusBarItem(context: ExtensionContext, languageclient: CommonLanguageClient): void {
if (statusBarItem) {
updateStatusBar(window.activeTextEditor);
return;
}
const commandId = 'yaml.select.json.schema';
client = languageclient;
commands.registerCommand(commandId, () => {
return showSchemaSelection();
});
statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right);
statusBarItem.command = commandId;
context.subscriptions.push(statusBarItem);

context.subscriptions.push(window.onDidChangeActiveTextEditor(updateStatusBar));
setTimeout(() => updateStatusBar(window.activeTextEditor), 5000);
}

async function updateStatusBar(editor: TextEditor): Promise<void> {
if (editor && editor.document.languageId === 'yaml') {
// get schema info there
const schema = await client.sendRequest(getSchema, editor.document.uri.toString());
if (schema.length === 0) {
statusBarItem.text = 'No JSON Schema';
statusBarItem.tooltip = 'Select JSON Schema';
statusBarItem.backgroundColor = undefined;
} else if (schema.length === 1) {
statusBarItem.text = schema[0].name ?? schema[0].uri;
statusBarItem.tooltip = 'Select JSON Schema';
statusBarItem.backgroundColor = undefined;
} else {
statusBarItem.text = 'Multiple JSON Schemas...';
statusBarItem.tooltip = 'Multiple JSON Schema used to validate this file, click to select one';
statusBarItem.backgroundColor = new ThemeColor('statusBarItem.warningBackground');
}

statusBarItem.show();
} else {
statusBarItem.hide();
}
}

async function showSchemaSelection(): Promise<void> {
const schemas = await client.sendRequest(getJSONSchemas, window.activeTextEditor.document.uri.toString());
const schemasPick = window.createQuickPick<SchemaItem>();
const pickItems: SchemaItem[] = [];

for (const val of schemas) {
const item = {
label: val.name ?? val.uri,
description: val.description,
detail: val.usedForCurrentFile ? 'Used for current file$(check)' : '',
alwaysShow: val.usedForCurrentFile,
schema: val,
};
pickItems.push(item);
}

pickItems.sort((a, b) => {
if (a.schema?.usedForCurrentFile) {
return -1;
}
if (b.schema?.usedForCurrentFile) {
return 1;
}
return a.label.localeCompare(b.label);
});

schemasPick.items = pickItems;
schemasPick.placeholder = 'Search JSON schema';
schemasPick.title = 'Select JSON schema';
schemasPick.onDidHide(() => schemasPick.dispose());

schemasPick.onDidChangeSelection((selection) => {
try {
if (selection.length > 0) {
if (selection[0].schema) {
const settings: Record<string, unknown> = workspace.getConfiguration('yaml').get('schemas');
const fileUri = window.activeTextEditor.document.uri.toString();
const newSettings = Object.assign({}, settings);
deleteExistingFilePattern(newSettings, fileUri);
const schemaURI = selection[0].schema.uri;
const schemaSettings = newSettings[schemaURI];
if (schemaSettings) {
if (Array.isArray(schemaSettings)) {
(schemaSettings as Array<string>).push(fileUri);
} else if (typeof schemaSettings === 'string') {
newSettings[schemaURI] = [schemaSettings, fileUri];
}
} else {
newSettings[schemaURI] = fileUri;
}
workspace.getConfiguration('yaml').update('schemas', newSettings);
}
}
} catch (err) {
console.error(err);
}
schemasPick.hide();
});
schemasPick.show();
}

function deleteExistingFilePattern(settings: Record<string, unknown>, fileUri: string): unknown {
for (const key in settings) {
if (Object.prototype.hasOwnProperty.call(settings, key)) {
const element = settings[key];

if (Array.isArray(element)) {
const filePatterns = element.filter((val) => val !== fileUri);
settings[key] = filePatterns;
}

if (element === fileUri) {
delete settings[key];
}
}
}

return settings;
}
14 changes: 14 additions & 0 deletions test/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import * as vscode from 'vscode';
import * as path from 'path';
import assert = require('assert');
import { CommonLanguageClient } from 'vscode-languageclient/lib/common/commonClient';
import { MessageTransports } from 'vscode-languageclient';

export let doc: vscode.TextDocument;
export let editor: vscode.TextEditor;
Expand Down Expand Up @@ -139,3 +141,15 @@ export class TestMemento implements vscode.Memento {
throw new Error('Method not implemented.');
}
}

export class TestLanguageClient extends CommonLanguageClient {
constructor() {
super('test', 'test', {});
}
protected getLocale(): string {
throw new Error('Method not implemented.');
}
protected createMessageTransports(): Promise<MessageTransports> {
throw new Error('Method not implemented.');
}
}
109 changes: 109 additions & 0 deletions test/json-schema-selection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';
import * as chai from 'chai';
import { createJSONSchemaStatusBarItem } from '../src/schema-status-bar-item';
import { CommonLanguageClient } from 'vscode-languageclient';
import * as vscode from 'vscode';
import { TestLanguageClient } from './helper';
import * as jsonStatusBar from '../src/schema-status-bar-item';
const expect = chai.expect;
chai.use(sinonChai);

describe('Status bar should work in multiple different scenarios', () => {
const sandbox = sinon.createSandbox();
let clock: sinon.SinonFakeTimers;
let clcStub: sinon.SinonStubbedInstance<TestLanguageClient>;
let registerCommandStub: sinon.SinonStub;
let createStatusBarItemStub: sinon.SinonStub;
let onDidChangeActiveTextEditorStub: sinon.SinonStub;

beforeEach(() => {
clcStub = sandbox.stub(new TestLanguageClient());
registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand');
createStatusBarItemStub = sandbox.stub(vscode.window, 'createStatusBarItem');
onDidChangeActiveTextEditorStub = sandbox.stub(vscode.window, 'onDidChangeActiveTextEditor');
sandbox.stub(vscode.window, 'activeTextEditor').returns(undefined);
clock = sandbox.useFakeTimers();
sandbox.stub(jsonStatusBar, 'statusBarItem').returns(undefined);
});

afterEach(() => {
clock.restore();
sandbox.restore();
});

it('Should create status bar item for JSON Schema', () => {
const context: vscode.ExtensionContext = {
subscriptions: [],
} as vscode.ExtensionContext;
createStatusBarItemStub.returns({});

createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);

expect(registerCommandStub).calledOnceWith('yaml.select.json.schema');
expect(createStatusBarItemStub).calledOnceWith(vscode.StatusBarAlignment.Right);
expect(context.subscriptions).has.length(2);
});

it('Should update status bar on editor change', async () => {
const context: vscode.ExtensionContext = {
subscriptions: [],
} as vscode.ExtensionContext;
const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem;
createStatusBarItemStub.returns(statusBar);
onDidChangeActiveTextEditorStub.returns({});
clcStub.sendRequest.resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema' }]);

createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg;
await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } });

expect(statusBar.text).to.equal('bar schema');
expect(statusBar.tooltip).to.equal('Select JSON Schema');
expect(statusBar.backgroundColor).to.be.undefined;
expect(statusBar.show).calledOnce;
});

it('Should inform if there are no schema', async () => {
const context: vscode.ExtensionContext = {
subscriptions: [],
} as vscode.ExtensionContext;
const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem;
createStatusBarItemStub.returns(statusBar);
onDidChangeActiveTextEditorStub.returns({});
clcStub.sendRequest.resolves([]);

createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg;
await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } });

expect(statusBar.text).to.equal('No JSON Schema');
expect(statusBar.tooltip).to.equal('Select JSON Schema');
expect(statusBar.backgroundColor).to.be.undefined;
expect(statusBar.show).calledOnce;
});

it('Should inform if there are more than one schema', async () => {
const context: vscode.ExtensionContext = {
subscriptions: [],
} as vscode.ExtensionContext;
const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem;
createStatusBarItemStub.returns(statusBar);
onDidChangeActiveTextEditorStub.returns({});
clcStub.sendRequest.resolves([{}, {}]);

createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg;
await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } });

expect(statusBar.text).to.equal('Multiple JSON Schemas...');
expect(statusBar.tooltip).to.equal('Multiple JSON Schema used to validate this file, click to select one');
expect(statusBar.backgroundColor).to.eql({ id: 'statusBarItem.warningBackground' });
expect(statusBar.show).calledOnce;
});
});