Skip to content

Commit 045b21f

Browse files
authored
Add setting to disable automatic schema detection for matching YAML files and add "No JSON Schema" to schema picker (#1248)
* implement schemaDetectionDisableFor setting Signed-off-by: Morgan Chang <shin19991207@gmail.com> * added No JSON Schema option to schema picker Signed-off-by: Morgan Chang <shin19991207@gmail.com> --------- Signed-off-by: Morgan Chang <shin19991207@gmail.com>
1 parent 3d5bfd8 commit 045b21f

3 files changed

Lines changed: 196 additions & 13 deletions

File tree

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@
140140
},
141141
"markdownDescription": "Associate schemas to YAML files in the current workspace. The expected value of this configuration option is a string to string map:\n- **Key**: The path or URL of the schema to use\n- **Value**: A glob pattern or list of glob patterns specifying which files the schema should be used on"
142142
},
143+
"yaml.disableSchemaDetection": {
144+
"anyOf": [
145+
{
146+
"type": "string"
147+
},
148+
{
149+
"type": "array",
150+
"items": {
151+
"type": "string"
152+
}
153+
}
154+
],
155+
"default": [],
156+
"markdownDescription": "Disable schema detection for YAML files matching the configured glob pattern or glob patterns. The expected value is a glob pattern or list of glob patterns. Modelines still apply."
157+
},
143158
"yaml.kubernetesCRDStore.enable": {
144159
"type": "boolean",
145160
"description": "Enable/disable validation of Kubernetes custom resources using schemas from well-known Custom Resource Definitions (CRDs)",

src/schema-status-bar-item.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface MatchingJSONSchema extends JSONSchema {
3131

3232
interface SchemaItem extends QuickPickItem {
3333
schema?: MatchingJSONSchema;
34+
disableSchemaDetection?: boolean;
3435
}
3536

3637
interface SchemaVersionItem extends QuickPickItem {
@@ -50,6 +51,7 @@ let client: CommonLanguageClient;
5051
let versionSelection: SchemaItem = undefined;
5152

5253
const selectVersionLabel = 'Select Different Version';
54+
const noSchemaLabel = 'No JSON Schema';
5355

5456
export function createJSONSchemaStatusBarItem(context: ExtensionContext, languageclient: CommonLanguageClient): void {
5557
if (statusBarItem) {
@@ -73,10 +75,11 @@ export function createJSONSchemaStatusBarItem(context: ExtensionContext, languag
7375
async function updateStatusBar(editor: TextEditor): Promise<void> {
7476
if (editor && editor.document.languageId === 'yaml') {
7577
versionSelection = undefined;
78+
const fileUri = editor.document.uri.toString();
7679
// get schema info there
77-
const schema = await client.sendRequest(getSchemaRequestType, editor.document.uri.toString());
80+
const schema = await client.sendRequest(getSchemaRequestType, fileUri);
7881
if (!schema || schema.length === 0) {
79-
statusBarItem.text = 'No JSON Schema';
82+
statusBarItem.text = noSchemaLabel;
8083
statusBarItem.tooltip = 'Select JSON Schema';
8184
statusBarItem.backgroundColor = undefined;
8285
} else if (schema.length === 1) {
@@ -85,7 +88,7 @@ async function updateStatusBar(editor: TextEditor): Promise<void> {
8588
if (schema[0].versions) {
8689
version = findUsedVersion(schema[0].versions, schema[0].uri);
8790
} else {
88-
const schemas = await client.sendRequest(getJSONSchemasRequestType, window.activeTextEditor.document.uri.toString());
91+
const schemas = await client.sendRequest(getJSONSchemasRequestType, fileUri);
8992
let versionSchema: JSONSchema;
9093
const schemaStoreItem = findSchemaStoreItem(schemas, schema[0].uri);
9194
if (schemaStoreItem) {
@@ -113,7 +116,8 @@ async function updateStatusBar(editor: TextEditor): Promise<void> {
113116
}
114117

115118
async function showSchemaSelection(): Promise<void> {
116-
const schemas = await client.sendRequest(getJSONSchemasRequestType, window.activeTextEditor.document.uri.toString());
119+
const fileUri = window.activeTextEditor.document.uri.toString();
120+
const schemas = await client.sendRequest(getJSONSchemasRequestType, fileUri);
117121
const schemasPick = window.createQuickPick<SchemaItem>();
118122
let pickItems: SchemaItem[] = [];
119123

@@ -151,6 +155,12 @@ async function showSchemaSelection(): Promise<void> {
151155
return a.label.localeCompare(b.label);
152156
});
153157

158+
pickItems.unshift({
159+
label: noSchemaLabel,
160+
alwaysShow: true,
161+
disableSchemaDetection: true,
162+
});
163+
154164
schemasPick.items = pickItems;
155165
schemasPick.placeholder = 'Search JSON schema';
156166
schemasPick.title = 'Select JSON schema';
@@ -160,9 +170,11 @@ async function showSchemaSelection(): Promise<void> {
160170
try {
161171
if (selection.length > 0) {
162172
if (selection[0].label === selectVersionLabel) {
163-
handleSchemaVersionSelection(selection[0].schema);
173+
handleSchemaVersionSelection(selection[0].schema, fileUri);
174+
} else if (selection[0].disableSchemaDetection) {
175+
writeDisableSchemaDetectionMapping(fileUri);
164176
} else if (selection[0].schema) {
165-
writeSchemaUriMapping(selection[0].schema.uri);
177+
writeSchemaUriMapping(selection[0].schema.uri, fileUri);
166178
}
167179
}
168180
} catch (err) {
@@ -192,6 +204,34 @@ function deleteExistingFilePattern(settings: Record<string, unknown>, fileUri: s
192204
return settings;
193205
}
194206

207+
function removeFilePatternFromSetting(setting: unknown, fileUri: string): string | string[] {
208+
if (Array.isArray(setting)) {
209+
return setting.filter((value): value is string => typeof value === 'string' && value !== fileUri);
210+
}
211+
212+
if (setting === fileUri) {
213+
return [];
214+
}
215+
216+
return typeof setting === 'string' ? setting : [];
217+
}
218+
219+
function addFilePatternToSetting(setting: unknown, fileUri: string): string | string[] {
220+
if (Array.isArray(setting)) {
221+
const filePatterns = setting.filter((value): value is string => typeof value === 'string');
222+
if (!filePatterns.includes(fileUri)) {
223+
filePatterns.push(fileUri);
224+
}
225+
return filePatterns;
226+
}
227+
228+
if (typeof setting === 'string') {
229+
return setting === fileUri ? setting : [setting, fileUri];
230+
}
231+
232+
return [fileUri];
233+
}
234+
195235
function createSelectVersionItem(version: string, schema: MatchingJSONSchema): SchemaItem {
196236
return {
197237
label: selectVersionLabel,
@@ -212,9 +252,11 @@ function findSchemaStoreItem(schemas: JSONSchema[], url: string): [string, JSONS
212252
}
213253
}
214254

215-
function writeSchemaUriMapping(schemaUrl: string): void {
216-
const settings: Record<string, unknown> = workspace.getConfiguration('yaml').get('schemas');
217-
const fileUri = window.activeTextEditor.document.uri.toString();
255+
function writeSchemaUriMapping(schemaUrl: string, fileUri: string): void {
256+
const yamlConfiguration = workspace.getConfiguration('yaml');
257+
const settings: Record<string, unknown> = yamlConfiguration.get('schemas');
258+
const disableSchemaDetection = yamlConfiguration.get('disableSchemaDetection');
259+
yamlConfiguration.update('disableSchemaDetection', removeFilePatternFromSetting(disableSchemaDetection, fileUri));
218260
const newSettings = Object.assign({}, settings);
219261
deleteExistingFilePattern(newSettings, fileUri);
220262
const schemaSettings = newSettings[schemaUrl];
@@ -227,10 +269,16 @@ function writeSchemaUriMapping(schemaUrl: string): void {
227269
} else {
228270
newSettings[schemaUrl] = fileUri;
229271
}
230-
workspace.getConfiguration('yaml').update('schemas', newSettings);
272+
yamlConfiguration.update('schemas', newSettings);
273+
}
274+
275+
function writeDisableSchemaDetectionMapping(fileUri: string): void {
276+
const yamlConfiguration = workspace.getConfiguration('yaml');
277+
const disableSchemaDetection = yamlConfiguration.get('disableSchemaDetection');
278+
yamlConfiguration.update('disableSchemaDetection', addFilePatternToSetting(disableSchemaDetection, fileUri));
231279
}
232280

233-
function handleSchemaVersionSelection(schema: MatchingJSONSchema): void {
281+
function handleSchemaVersionSelection(schema: MatchingJSONSchema, fileUri: string): void {
234282
const versionPick = window.createQuickPick<SchemaVersionItem>();
235283
const versionItems: SchemaVersionItem[] = [];
236284
const usedVersion = findUsedVersion(schema.versions, schema.uri);
@@ -249,7 +297,7 @@ function handleSchemaVersionSelection(schema: MatchingJSONSchema): void {
249297

250298
versionPick.onDidChangeSelection((items) => {
251299
if (items && items.length === 1) {
252-
writeSchemaUriMapping(items[0].url);
300+
writeSchemaUriMapping(items[0].url, fileUri);
253301
}
254302
versionPick.hide();
255303
});

test/json-schema-selection.test.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,27 @@ import * as jsonStatusBar from '../src/schema-status-bar-item';
1515
const expect = chai.expect;
1616
chai.use(sinonChai);
1717

18+
interface TestSchemaItem extends vscode.QuickPickItem {
19+
schema?: unknown;
20+
}
21+
1822
describe('Status bar should work in multiple different scenarios', () => {
1923
const sandbox = sinon.createSandbox();
2024
let clcStub: sinon.SinonStubbedInstance<TestLanguageClient>;
2125
let registerCommandStub: sinon.SinonStub;
2226
let createStatusBarItemStub: sinon.SinonStub;
2327
let onDidChangeActiveTextEditorStub: sinon.SinonStub;
28+
let createQuickPickStub: sinon.SinonStub;
29+
let activeTextEditor: vscode.TextEditor | undefined;
2430

2531
beforeEach(() => {
2632
clcStub = sandbox.stub(new TestLanguageClient());
2733
registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand');
2834
createStatusBarItemStub = sandbox.stub(vscode.window, 'createStatusBarItem');
35+
createQuickPickStub = sandbox.stub(vscode.window, 'createQuickPick');
2936
onDidChangeActiveTextEditorStub = sandbox.stub(vscode.window, 'onDidChangeActiveTextEditor');
30-
sandbox.stub(vscode.window, 'activeTextEditor').returns(undefined);
37+
activeTextEditor = undefined;
38+
sandbox.stub(vscode.window, 'activeTextEditor').get(() => activeTextEditor);
3139
sandbox.stub(jsonStatusBar, 'statusBarItem').returns(undefined);
3240
});
3341

@@ -154,4 +162,116 @@ describe('Status bar should work in multiple different scenarios', () => {
154162
expect(statusBar.backgroundColor).to.be.undefined;
155163
expect(statusBar.show).calledOnce;
156164
});
165+
166+
it('Should include No JSON Schema in schema selection', async () => {
167+
const context: vscode.ExtensionContext = {
168+
subscriptions: [],
169+
} as vscode.ExtensionContext;
170+
const statusBar = ({ show: sandbox.stub(), hide: sandbox.stub() } as unknown) as vscode.StatusBarItem;
171+
const quickPick = createQuickPickStubValue<TestSchemaItem>();
172+
createStatusBarItemStub.returns(statusBar);
173+
createQuickPickStub.returns(quickPick);
174+
clcStub.sendRequest.resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema' }]);
175+
activeTextEditor = ({
176+
document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') },
177+
} as unknown) as vscode.TextEditor;
178+
179+
createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
180+
const command = registerCommandStub.firstCall.args[1];
181+
await command();
182+
183+
expect(quickPick.items[0].label).to.equal('No JSON Schema');
184+
expect(quickPick.show).calledOnce;
185+
});
186+
187+
it('Should write disableSchemaDetection when No JSON Schema is selected', async () => {
188+
const context: vscode.ExtensionContext = {
189+
subscriptions: [],
190+
} as vscode.ExtensionContext;
191+
const statusBar = ({ show: sandbox.stub(), hide: sandbox.stub() } as unknown) as vscode.StatusBarItem;
192+
const quickPick = createQuickPickStubValue<TestSchemaItem>();
193+
const update = sandbox.stub();
194+
createStatusBarItemStub.returns(statusBar);
195+
createQuickPickStub.returns(quickPick);
196+
clcStub.sendRequest.resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema' }]);
197+
activeTextEditor = ({
198+
document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') },
199+
} as unknown) as vscode.TextEditor;
200+
sandbox
201+
.stub(vscode.workspace, 'getConfiguration')
202+
.withArgs('yaml')
203+
.returns(({
204+
get: sandbox.stub().withArgs('disableSchemaDetection').returns([]),
205+
update,
206+
} as unknown) as vscode.WorkspaceConfiguration);
207+
208+
createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
209+
const command = registerCommandStub.firstCall.args[1];
210+
await command();
211+
quickPick.select([quickPick.items[0]]);
212+
213+
expect(update).calledWith('disableSchemaDetection', ['file:///foo.yaml']);
214+
expect(quickPick.hide).calledOnce;
215+
});
216+
217+
it('Should clear disableSchemaDetection when a schema is selected', async () => {
218+
const context: vscode.ExtensionContext = {
219+
subscriptions: [],
220+
} as vscode.ExtensionContext;
221+
const statusBar = ({ show: sandbox.stub(), hide: sandbox.stub() } as unknown) as vscode.StatusBarItem;
222+
const quickPick = createQuickPickStubValue<TestSchemaItem>();
223+
const update = sandbox.stub();
224+
const get = sandbox.stub();
225+
get.withArgs('disableSchemaDetection').returns(['file:///foo.yaml', 'file:///other.yaml']);
226+
get.withArgs('schemas').returns({});
227+
createStatusBarItemStub.returns(statusBar);
228+
createQuickPickStub.returns(quickPick);
229+
clcStub.sendRequest.resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema' }]);
230+
activeTextEditor = ({
231+
document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') },
232+
} as unknown) as vscode.TextEditor;
233+
sandbox
234+
.stub(vscode.workspace, 'getConfiguration')
235+
.withArgs('yaml')
236+
.returns(({
237+
get,
238+
update,
239+
} as unknown) as vscode.WorkspaceConfiguration);
240+
241+
createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient);
242+
const command = registerCommandStub.firstCall.args[1];
243+
await command();
244+
const schemaItem = quickPick.items.find((item) => item.schema);
245+
expect(schemaItem).to.exist;
246+
quickPick.select([schemaItem as TestSchemaItem]);
247+
248+
expect(update).calledWith('disableSchemaDetection', ['file:///other.yaml']);
249+
expect(update).calledWith('schemas', { 'https://foo.com/bar.json': 'file:///foo.yaml' });
250+
expect(quickPick.hide).calledOnce;
251+
});
157252
});
253+
254+
function createQuickPickStubValue<T extends vscode.QuickPickItem>(): vscode.QuickPick<T> & { select: (items: T[]) => void } {
255+
let selectionHandler: (items: readonly T[]) => void;
256+
let hideHandler: () => void;
257+
const quickPick = {
258+
items: [] as readonly T[],
259+
placeholder: '',
260+
title: '',
261+
show: sinon.stub(),
262+
hide: sinon.stub(),
263+
dispose: sinon.stub(),
264+
onDidHide: sinon.stub(),
265+
onDidChangeSelection: sinon.stub().callsFake((handler: (items: readonly T[]) => void) => {
266+
selectionHandler = handler;
267+
return { dispose: sinon.stub() };
268+
}),
269+
select: (items: T[]) => selectionHandler(items),
270+
};
271+
quickPick.hide.callsFake(() => hideHandler?.());
272+
quickPick.onDidHide.callsFake((handler: () => void) => {
273+
hideHandler = handler;
274+
return { dispose: sinon.stub() };
275+
});
276+
return (quickPick as unknown) as vscode.QuickPick<T> & { select: (items: T[]) => void };
277+
}

0 commit comments

Comments
 (0)