Skip to content

Commit c82041d

Browse files
evidolobJPinkney
andauthored
Add status bar item for JSON schema selection (#643)
* Add status bar item for JSON schema selection Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * fix review comments Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * Update test/json-schema-selection.test.ts Co-authored-by: Josh Pinkney <jpinkney@redhat.com> * Fix build when yaml-ls linked with 'yarn link' Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * Upgrade to new yaml-ls version Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * Update ts version Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> Co-authored-by: Josh Pinkney <jpinkney@redhat.com>
1 parent ca93afa commit c82041d

File tree

8 files changed

+316
-21
lines changed

8 files changed

+316
-21
lines changed

.yarnrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--ignore-engines true

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@
237237
"sinon-chai": "^3.5.0",
238238
"ts-loader": "^9.2.5",
239239
"ts-node": "^3.3.0",
240-
"typescript": "4.1.2",
240+
"typescript": "4.4.3",
241241
"umd-compat-loader": "^2.1.2",
242242
"url": "^0.11.0",
243243
"util": "^0.12.4",

src/extension.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getJsonSchemaContent, IJSONSchemaCache, JSONSchemaDocumentContentProvid
2020
import { getConflictingExtensions, showUninstallConflictsNotification } from './extensionConflicts';
2121
import { TelemetryErrorHandler, TelemetryOutputChannel } from './telemetry';
2222
import { TextDecoder } from 'util';
23+
import { createJSONSchemaStatusBarItem } from './schema-status-bar-item';
2324

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

82+
// eslint-disable-next-line @typescript-eslint/no-namespace
83+
export namespace SchemaSelectionRequests {
84+
export const type: NotificationType<void> = new NotificationType('yaml/supportSchemaSelection');
85+
export const schemaStoreInitialized: NotificationType<void> = new NotificationType('yaml/schema/store/initialized');
86+
}
87+
8188
let client: CommonLanguageClient;
8289

8390
const lsName = 'YAML Support';
@@ -154,6 +161,8 @@ export function startClient(
154161
client.sendNotification(DynamicCustomSchemaRequestRegistration.type);
155162
// Tell the server that the client supports schema requests sent directly to it
156163
client.sendNotification(VSCodeContentRequestRegistration.type);
164+
// Tell the server that the client supports schema selection requests
165+
client.sendNotification(SchemaSelectionRequests.type);
157166
// If the server asks for custom schema content, get it and send it back
158167
client.onRequest(CUSTOM_SCHEMA_REQUEST, (resource: string) => {
159168
return schemaExtensionAPI.requestCustomSchema(resource);
@@ -190,6 +199,10 @@ export function startClient(
190199
}
191200
}
192201
});
202+
203+
client.onNotification(SchemaSelectionRequests.schemaStoreInitialized, () => {
204+
createJSONSchemaStatusBarItem(context, client);
205+
});
193206
});
194207

195208
return schemaExtensionAPI;

src/schema-status-bar-item.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import {
6+
ExtensionContext,
7+
window,
8+
commands,
9+
StatusBarAlignment,
10+
TextEditor,
11+
StatusBarItem,
12+
QuickPickItem,
13+
ThemeColor,
14+
workspace,
15+
} from 'vscode';
16+
import { CommonLanguageClient, RequestType } from 'vscode-languageclient/node';
17+
18+
type FileUri = string;
19+
interface JSONSchema {
20+
name?: string;
21+
description?: string;
22+
uri: string;
23+
}
24+
25+
interface MatchingJSONSchema extends JSONSchema {
26+
usedForCurrentFile: boolean;
27+
fromStore: boolean;
28+
}
29+
30+
interface SchemaItem extends QuickPickItem {
31+
schema?: MatchingJSONSchema;
32+
}
33+
34+
// eslint-disable-next-line @typescript-eslint/ban-types
35+
const getJSONSchemas: RequestType<FileUri, MatchingJSONSchema[], {}> = new RequestType('yaml/get/all/jsonSchemas');
36+
37+
// eslint-disable-next-line @typescript-eslint/ban-types
38+
const getSchema: RequestType<FileUri, JSONSchema[], {}> = new RequestType('yaml/get/jsonSchema');
39+
40+
export let statusBarItem: StatusBarItem;
41+
42+
let client: CommonLanguageClient;
43+
export function createJSONSchemaStatusBarItem(context: ExtensionContext, languageclient: CommonLanguageClient): void {
44+
if (statusBarItem) {
45+
updateStatusBar(window.activeTextEditor);
46+
return;
47+
}
48+
const commandId = 'yaml.select.json.schema';
49+
client = languageclient;
50+
commands.registerCommand(commandId, () => {
51+
return showSchemaSelection();
52+
});
53+
statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right);
54+
statusBarItem.command = commandId;
55+
context.subscriptions.push(statusBarItem);
56+
57+
context.subscriptions.push(window.onDidChangeActiveTextEditor(updateStatusBar));
58+
setTimeout(() => updateStatusBar(window.activeTextEditor), 5000);
59+
}
60+
61+
async function updateStatusBar(editor: TextEditor): Promise<void> {
62+
if (editor && editor.document.languageId === 'yaml') {
63+
// get schema info there
64+
const schema = await client.sendRequest(getSchema, editor.document.uri.toString());
65+
if (schema.length === 0) {
66+
statusBarItem.text = 'No JSON Schema';
67+
statusBarItem.tooltip = 'Select JSON Schema';
68+
statusBarItem.backgroundColor = undefined;
69+
} else if (schema.length === 1) {
70+
statusBarItem.text = schema[0].name ?? schema[0].uri;
71+
statusBarItem.tooltip = 'Select JSON Schema';
72+
statusBarItem.backgroundColor = undefined;
73+
} else {
74+
statusBarItem.text = 'Multiple JSON Schemas...';
75+
statusBarItem.tooltip = 'Multiple JSON Schema used to validate this file, click to select one';
76+
statusBarItem.backgroundColor = new ThemeColor('statusBarItem.warningBackground');
77+
}
78+
79+
statusBarItem.show();
80+
} else {
81+
statusBarItem.hide();
82+
}
83+
}
84+
85+
async function showSchemaSelection(): Promise<void> {
86+
const schemas = await client.sendRequest(getJSONSchemas, window.activeTextEditor.document.uri.toString());
87+
const schemasPick = window.createQuickPick<SchemaItem>();
88+
const pickItems: SchemaItem[] = [];
89+
90+
for (const val of schemas) {
91+
const item = {
92+
label: val.name ?? val.uri,
93+
description: val.description,
94+
detail: val.usedForCurrentFile ? 'Used for current file$(check)' : '',
95+
alwaysShow: val.usedForCurrentFile,
96+
schema: val,
97+
};
98+
pickItems.push(item);
99+
}
100+
101+
pickItems.sort((a, b) => {
102+
if (a.schema?.usedForCurrentFile) {
103+
return -1;
104+
}
105+
if (b.schema?.usedForCurrentFile) {
106+
return 1;
107+
}
108+
return a.label.localeCompare(b.label);
109+
});
110+
111+
schemasPick.items = pickItems;
112+
schemasPick.placeholder = 'Search JSON schema';
113+
schemasPick.title = 'Select JSON schema';
114+
schemasPick.onDidHide(() => schemasPick.dispose());
115+
116+
schemasPick.onDidChangeSelection((selection) => {
117+
try {
118+
if (selection.length > 0) {
119+
if (selection[0].schema) {
120+
const settings: Record<string, unknown> = workspace.getConfiguration('yaml').get('schemas');
121+
const fileUri = window.activeTextEditor.document.uri.toString();
122+
const newSettings = Object.assign({}, settings);
123+
deleteExistingFilePattern(newSettings, fileUri);
124+
const schemaURI = selection[0].schema.uri;
125+
const schemaSettings = newSettings[schemaURI];
126+
if (schemaSettings) {
127+
if (Array.isArray(schemaSettings)) {
128+
(schemaSettings as Array<string>).push(fileUri);
129+
} else if (typeof schemaSettings === 'string') {
130+
newSettings[schemaURI] = [schemaSettings, fileUri];
131+
}
132+
} else {
133+
newSettings[schemaURI] = fileUri;
134+
}
135+
workspace.getConfiguration('yaml').update('schemas', newSettings);
136+
}
137+
}
138+
} catch (err) {
139+
console.error(err);
140+
}
141+
schemasPick.hide();
142+
});
143+
schemasPick.show();
144+
}
145+
146+
function deleteExistingFilePattern(settings: Record<string, unknown>, fileUri: string): unknown {
147+
for (const key in settings) {
148+
if (Object.prototype.hasOwnProperty.call(settings, key)) {
149+
const element = settings[key];
150+
151+
if (Array.isArray(element)) {
152+
const filePatterns = element.filter((val) => val !== fileUri);
153+
settings[key] = filePatterns;
154+
}
155+
156+
if (element === fileUri) {
157+
delete settings[key];
158+
}
159+
}
160+
}
161+
162+
return settings;
163+
}

test/helper.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import * as vscode from 'vscode';
88
import * as path from 'path';
99
import assert = require('assert');
10+
import { CommonLanguageClient } from 'vscode-languageclient/lib/common/commonClient';
11+
import { MessageTransports } from 'vscode-languageclient';
1012

1113
export let doc: vscode.TextDocument;
1214
export let editor: vscode.TextEditor;
@@ -139,3 +141,15 @@ export class TestMemento implements vscode.Memento {
139141
throw new Error('Method not implemented.');
140142
}
141143
}
144+
145+
export class TestLanguageClient extends CommonLanguageClient {
146+
constructor() {
147+
super('test', 'test', {});
148+
}
149+
protected getLocale(): string {
150+
throw new Error('Method not implemented.');
151+
}
152+
protected createMessageTransports(): Promise<MessageTransports> {
153+
throw new Error('Method not implemented.');
154+
}
155+
}

test/json-schema-selection.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as sinon from 'sinon';
7+
import * as sinonChai from 'sinon-chai';
8+
import * as chai from 'chai';
9+
import { createJSONSchemaStatusBarItem } from '../src/schema-status-bar-item';
10+
import { CommonLanguageClient } from 'vscode-languageclient';
11+
import * as vscode from 'vscode';
12+
import { TestLanguageClient } from './helper';
13+
import * as jsonStatusBar from '../src/schema-status-bar-item';
14+
const expect = chai.expect;
15+
chai.use(sinonChai);
16+
17+
describe('Status bar should work in multiple different scenarios', () => {
18+
const sandbox = sinon.createSandbox();
19+
let clock: sinon.SinonFakeTimers;
20+
let clcStub: sinon.SinonStubbedInstance<TestLanguageClient>;
21+
let registerCommandStub: sinon.SinonStub;
22+
let createStatusBarItemStub: sinon.SinonStub;
23+
let onDidChangeActiveTextEditorStub: sinon.SinonStub;
24+
25+
beforeEach(() => {
26+
clcStub = sandbox.stub(new TestLanguageClient());
27+
registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand');
28+
createStatusBarItemStub = sandbox.stub(vscode.window, 'createStatusBarItem');
29+
onDidChangeActiveTextEditorStub = sandbox.stub(vscode.window, 'onDidChangeActiveTextEditor');
30+
sandbox.stub(vscode.window, 'activeTextEditor').returns(undefined);
31+
clock = sandbox.useFakeTimers();
32+
sandbox.stub(jsonStatusBar, 'statusBarItem').returns(undefined);
33+
});
34+
35+
afterEach(() => {
36+
clock.restore();
37+
sandbox.restore();
38+
});
39+
40+
it('Should create status bar item for JSON Schema', () => {
41+
const context: vscode.ExtensionContext = {
42+
subscriptions: [],
43+
} as vscode.ExtensionContext;
44+
createStatusBarItemStub.returns({});
45+
46+
createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
47+
48+
expect(registerCommandStub).calledOnceWith('yaml.select.json.schema');
49+
expect(createStatusBarItemStub).calledOnceWith(vscode.StatusBarAlignment.Right);
50+
expect(context.subscriptions).has.length(2);
51+
});
52+
53+
it('Should update status bar on editor change', async () => {
54+
const context: vscode.ExtensionContext = {
55+
subscriptions: [],
56+
} as vscode.ExtensionContext;
57+
const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem;
58+
createStatusBarItemStub.returns(statusBar);
59+
onDidChangeActiveTextEditorStub.returns({});
60+
clcStub.sendRequest.resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema' }]);
61+
62+
createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
63+
const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg;
64+
await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } });
65+
66+
expect(statusBar.text).to.equal('bar schema');
67+
expect(statusBar.tooltip).to.equal('Select JSON Schema');
68+
expect(statusBar.backgroundColor).to.be.undefined;
69+
expect(statusBar.show).calledOnce;
70+
});
71+
72+
it('Should inform if there are no schema', async () => {
73+
const context: vscode.ExtensionContext = {
74+
subscriptions: [],
75+
} as vscode.ExtensionContext;
76+
const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem;
77+
createStatusBarItemStub.returns(statusBar);
78+
onDidChangeActiveTextEditorStub.returns({});
79+
clcStub.sendRequest.resolves([]);
80+
81+
createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
82+
const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg;
83+
await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } });
84+
85+
expect(statusBar.text).to.equal('No JSON Schema');
86+
expect(statusBar.tooltip).to.equal('Select JSON Schema');
87+
expect(statusBar.backgroundColor).to.be.undefined;
88+
expect(statusBar.show).calledOnce;
89+
});
90+
91+
it('Should inform if there are more than one schema', async () => {
92+
const context: vscode.ExtensionContext = {
93+
subscriptions: [],
94+
} as vscode.ExtensionContext;
95+
const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem;
96+
createStatusBarItemStub.returns(statusBar);
97+
onDidChangeActiveTextEditorStub.returns({});
98+
clcStub.sendRequest.resolves([{}, {}]);
99+
100+
createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
101+
const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg;
102+
await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } });
103+
104+
expect(statusBar.text).to.equal('Multiple JSON Schemas...');
105+
expect(statusBar.tooltip).to.equal('Multiple JSON Schema used to validate this file, click to select one');
106+
expect(statusBar.backgroundColor).to.eql({ id: 'statusBarItem.warningBackground' });
107+
expect(statusBar.show).calledOnce;
108+
});
109+
});

webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const serverWeb = {
142142
},
143143
plugins: [
144144
new webpack.ProvidePlugin({
145-
process: 'process/browser.js', // provide a shim for the global `process` variable
145+
process: path.resolve(path.join(__dirname, 'node_modules/process/browser.js')), // provide a shim for the global `process` variable
146146
}),
147147
],
148148
module: {},

0 commit comments

Comments
 (0)