editorOpenWith.ts 8.4 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
7 8 9 10 11 12 13 14
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
import { Registry } from 'vs/platform/registry/common/platform';
import { ICustomEditorInfo, IEditorService, IOpenEditorOverrideHandler, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService';
import { IEditorInput, IEditorPane, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
import { ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorGroup, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
15 16
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
17 18
import { URI } from 'vs/base/common/uri';
import { extname, basename, isEqual } from 'vs/base/common/resources';
19

20 21 22 23
/**
 * Id of the default editor for open with.
 */
export const DEFAULT_EDITOR_ID = 'default';
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

/**
 * Try to open an resource with a given editor.
 *
 * @param input Resource to open.
 * @param id Id of the editor to use. If not provided, the user is prompted for which editor to use.
 */
export async function openEditorWith(
	input: IEditorInput,
	id: string | undefined,
	options: IEditorOptions | ITextEditorOptions | undefined,
	group: IEditorGroup,
	editorService: IEditorService,
	configurationService: IConfigurationService,
	quickInputService: IQuickInputService,
39
): Promise<IEditorPane | undefined> {
40 41 42 43 44
	const resource = input.resource;
	if (!resource) {
		return;
	}

45 46 47
	const overrideOptions = { ...options, override: id };

	const allEditorOverrides = getAllAvailableEditors(resource, id, overrideOptions, group, editorService);
48 49 50
	if (!allEditorOverrides.length) {
		return;
	}
51

52 53 54 55 56 57
	let overrideToUse;
	if (typeof id === 'string') {
		overrideToUse = allEditorOverrides.find(([_, entry]) => entry.id === id);
	} else if (allEditorOverrides.length === 1) {
		overrideToUse = allEditorOverrides[0];
	}
58
	if (overrideToUse) {
59
		return overrideToUse[0].open(input, overrideOptions, group, OpenEditorContext.NEW_EDITOR)?.override;
60 61 62
	}

	// Prompt
63 64
	const resourceExt = extname(resource);

65
	const items: (IQuickPickItem & { handler: IOpenEditorOverrideHandler })[] = allEditorOverrides.map(([handler, entry]) => {
66
		return {
67 68 69 70 71
			handler: handler,
			id: entry.id,
			label: entry.label,
			description: entry.active ? nls.localize('promptOpenWith.currentlyActive', 'Currently Active') : undefined,
			detail: entry.detail,
72 73 74 75 76 77 78
			buttons: resourceExt ? [{
				iconClass: 'codicon-settings-gear',
				tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt)
			}] : undefined
		};
	});

79
	const picker = quickInputService.createQuickPick<(IQuickPickItem & { handler: IOpenEditorOverrideHandler })>();
80 81 82 83 84 85
	picker.items = items;
	if (items.length) {
		picker.selectedItems = [items[0]];
	}
	picker.placeholder = nls.localize('promptOpenWith.placeHolder', "Select editor for '{0}'", basename(resource));

86
	const pickedItem = await new Promise<(IQuickPickItem & { handler: IOpenEditorOverrideHandler }) | undefined>(resolve => {
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
		picker.onDidAccept(() => {
			resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined);
			picker.dispose();
		});

		picker.onDidTriggerItemButton(e => {
			const pick = e.item;
			const id = pick.id;
			resolve(pick); // open the view
			picker.dispose();

			// And persist the setting
			if (pick && id) {
				const newAssociation: CustomEditorAssociation = { viewType: id, filenamePattern: '*' + resourceExt };
				const currentAssociations = [...configurationService.getValue<CustomEditorsAssociations>(customEditorsAssociationsSettingId)];

				// First try updating existing association
				for (let i = 0; i < currentAssociations.length; ++i) {
					const existing = currentAssociations[i];
					if (existing.filenamePattern === newAssociation.filenamePattern) {
						currentAssociations.splice(i, 1, newAssociation);
						configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations);
						return;
					}
				}

				// Otherwise, create a new one
				currentAssociations.unshift(newAssociation);
				configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations);
			}
		});

		picker.show();
	});

122
	return pickedItem?.handler.open(input, { ...options, override: pickedItem.id }, group, OpenEditorContext.NEW_EDITOR)?.override;
123
}
124

125 126
const builtinProviderDisplayName = nls.localize('builtinProviderDisplayName', "Built-in");

127 128 129
export const defaultEditorOverrideEntry = Object.freeze({
	id: DEFAULT_EDITOR_ID,
	label: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"),
130
	detail: builtinProviderDisplayName
131 132
});

133 134 135 136 137
/**
 * Get a list of all available editors, including the default text editor.
 */
export function getAllAvailableEditors(
	resource: URI,
138
	id: string | undefined,
139 140
	options: IEditorOptions | ITextEditorOptions | undefined,
	group: IEditorGroup,
141
	editorService: IEditorService
142
): Array<[IOpenEditorOverrideHandler, IOpenEditorOverrideEntry]> {
143
	const fileEditorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileEditorInputFactory();
144
	const overrides = editorService.getEditorOverrides(resource, options, group);
145 146 147 148 149 150 151
	if (!overrides.some(([_, entry]) => entry.id === DEFAULT_EDITOR_ID)) {
		overrides.unshift([
			{
				open: (input: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => {
					if (!input.resource) {
						return;
					}
152

153
					const fileEditorInput = editorService.createEditorInput({ resource: input.resource, forceFile: true });
154
					const textOptions: IEditorOptions | ITextEditorOptions = options ? { ...options, override: false } : { override: false };
155 156 157 158
					return { override: editorService.openEditor(fileEditorInput, textOptions, group) };
				}
			},
			{
159
				...defaultEditorOverrideEntry,
160
				active: fileEditorInputFactory.isFileEditorInput(editorService.activeEditor) && isEqual(editorService.activeEditor.resource, resource),
161
			}]);
162
	}
163

164
	return overrides;
165
}
166

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
export const customEditorsAssociationsSettingId = 'workbench.editorAssociations';

export const viewTypeSchamaAddition: IJSONSchema = {
	type: 'string',
	enum: []
};

export type CustomEditorAssociation = {
	readonly viewType: string;
	readonly filenamePattern?: string;
};

export type CustomEditorsAssociations = readonly CustomEditorAssociation[];

export const editorAssociationsConfigurationNode: IConfigurationNode = {
	...workbenchConfigurationNodeBase,
	properties: {
		[customEditorsAssociationsSettingId]: {
			type: 'array',
			markdownDescription: nls.localize('editor.editorAssociations', "Configure which editor to use for specific file types."),
			items: {
				type: 'object',
				defaultSnippets: [{
					body: {
						'viewType': '$1',
						'filenamePattern': '$2'
					}
				}],
				properties: {
					'viewType': {
						anyOf: [
							{
								type: 'string',
								description: nls.localize('editor.editorAssociations.viewType', "The unique id of the editor to use."),
							},
							viewTypeSchamaAddition
						]
					},
					'filenamePattern': {
						type: 'string',
						description: nls.localize('editor.editorAssociations.filenamePattern', "Glob pattern specifying which files the editor should be used for."),
					}
				}
			}
		}
	}
};

export const DEFAULT_CUSTOM_EDITOR: ICustomEditorInfo = {
	id: 'default',
	displayName: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"),
	providerDisplayName: builtinProviderDisplayName
};

export function updateViewTypeSchema(enumValues: string[], enumDescriptions: string[]): void {
	viewTypeSchamaAddition.enum = enumValues;
	viewTypeSchamaAddition.enumDescriptions = enumDescriptions;

	Registry.as<IConfigurationRegistry>(Extensions.Configuration)
		.notifyConfigurationSchemaUpdated(editorAssociationsConfigurationNode);
}