settingsDocumentHelper.ts 9.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 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
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { getLocation, Location } from 'jsonc-parser';
import * as nls from 'vscode-nls';

const localize = nls.loadMessageBundle();

export class SettingsDocument {

	constructor(private document: vscode.TextDocument) { }

	public provideCompletionItems(position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CompletionItem[]> {
		const location = getLocation(this.document.getText(), this.document.offsetAt(position));
		const range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position);

		// window.title
		if (location.path[0] === 'window.title') {
			return this.provideWindowTitleCompletionItems(location, range);
		}

		// files.association
		if (location.path[0] === 'files.associations') {
			return this.provideFilesAssociationsCompletionItems(location, range);
		}

		// files.exclude, search.exclude
		if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude') {
			return this.provideExcludeCompletionItems(location, range);
		}

		return this.provideLanguageOverridesCompletionItems(location, position);
	}

	private provideWindowTitleCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult<vscode.CompletionItem[]> {
		const completions: vscode.CompletionItem[] = [];

		completions.push(this.newSimpleCompletionItem('${activeEditorName}', range, localize('activeEditorName', "e.g. myFile.txt")));
		completions.push(this.newSimpleCompletionItem('${activeFilePath}', range, localize('activeFilePath', "e.g. /Users/Development/myProject/myFile.txt")));
		completions.push(this.newSimpleCompletionItem('${rootName}', range, localize('rootName', "e.g. myProject")));
		completions.push(this.newSimpleCompletionItem('${rootPath}', range, localize('rootPath', "e.g. /Users/Development/myProject")));
		completions.push(this.newSimpleCompletionItem('${appName}', range, localize('appName', "e.g. VS Code")));
		completions.push(this.newSimpleCompletionItem('${dirty}', range, localize('dirty', "a dirty indicator if the active editor is dirty")));
		completions.push(this.newSimpleCompletionItem('${separator}', range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values")));

		return Promise.resolve(completions);
	}

	private provideFilesAssociationsCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult<vscode.CompletionItem[]> {
		const completions: vscode.CompletionItem[] = [];

		// Key
		if (location.path.length === 1) {
			completions.push(this.newSnippetCompletionItem({
				label: localize('assocLabelFile', "Files with Extension"),
				documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier."),
				snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }',
				range
			}));

			completions.push(this.newSnippetCompletionItem({
				label: localize('assocLabelPath', "Files with Path"),
				documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier."),
				snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }',
				range
			}));
		}

		// Value
		else if (location.path.length === 2 && !location.isAtPropertyKey) {
			return this.provideLanguageCompletionItems(location, range);
		}

		return Promise.resolve(completions);
	}

	private provideExcludeCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult<vscode.CompletionItem[]> {
		const completions: vscode.CompletionItem[] = [];

		// Key
		if (location.path.length === 1) {
			completions.push(this.newSnippetCompletionItem({
				label: localize('fileLabel', "Files by Extension"),
				documentation: localize('fileDescription', "Match all files of a specific file extension."),
				snippet: location.isAtPropertyKey ? '"**/*.${1:extension}": true' : '{ "**/*.${1:extension}": true }',
				range
			}));

			completions.push(this.newSnippetCompletionItem({
				label: localize('filesLabel', "Files with Multiple Extensions"),
				documentation: localize('filesDescription', "Match all files with any of the file extensions."),
				snippet: location.isAtPropertyKey ? '"**/*.{ext1,ext2,ext3}": true' : '{ "**/*.{ext1,ext2,ext3}": true }',
				range
			}));

			completions.push(this.newSnippetCompletionItem({
				label: localize('derivedLabel', "Files with Siblings by Name"),
				documentation: localize('derivedDescription', "Match files that have siblings with the same name but a different extension."),
				snippet: location.isAtPropertyKey ? '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }' : '{ "**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" } }',
				range
			}));

			completions.push(this.newSnippetCompletionItem({
				label: localize('topFolderLabel', "Folder by Name (Top Level)"),
				documentation: localize('topFolderDescription', "Match a top level folder with a specific name."),
				snippet: location.isAtPropertyKey ? '"${1:name}": true' : '{ "${1:name}": true }',
				range
			}));

			completions.push(this.newSnippetCompletionItem({
				label: localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"),
				documentation: localize('topFoldersDescription', "Match multiple top level folders."),
				snippet: location.isAtPropertyKey ? '"{folder1,folder2,folder3}": true' : '{ "{folder1,folder2,folder3}": true }',
				range
			}));

			completions.push(this.newSnippetCompletionItem({
				label: localize('folderLabel', "Folder by Name (Any Location)"),
				documentation: localize('folderDescription', "Match a folder with a specific name in any location."),
				snippet: location.isAtPropertyKey ? '"**/${1:name}": true' : '{ "**/${1:name}": true }',
				range
			}));
		}

		// Value
		else {
			completions.push(this.newSimpleCompletionItem('false', range, localize('falseDescription', "Disable the pattern.")));
			completions.push(this.newSimpleCompletionItem('true', range, localize('trueDescription', "Enable the pattern.")));

			completions.push(this.newSnippetCompletionItem({
				label: localize('derivedLabel', "Files with Siblings by Name"),
				documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension."),
				snippet: '{ "when": "$(basename).${1:extension}" }',
				range
			}));
		}

		return Promise.resolve(completions);
	}

	private provideLanguageCompletionItems(location: Location, range: vscode.Range, stringify: boolean = true): vscode.ProviderResult<vscode.CompletionItem[]> {
		return vscode.languages.getLanguages().then(languages => {
			return languages.map(l => {
				return this.newSimpleCompletionItem(stringify ? JSON.stringify(l) : l, range);
			});
		});
	}

	private provideLanguageOverridesCompletionItems(location: Location, position: vscode.Position): vscode.ProviderResult<vscode.CompletionItem[]> {
		let range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position);
		const text = this.document.getText(range);

		if (location.path.length === 0) {

			let snippet = '"[${1:language}]": {\n\t"$0"\n}';

			// Suggestion model word matching includes quotes,
			// hence exclude the starting quote from the snippet and the range
			// ending quote gets replaced
			if (text.startsWith('"')) {
				range = new vscode.Range(new vscode.Position(range.start.line, range.start.character + 1), range.end);
				snippet = snippet.substring(1);
			}

			return Promise.resolve([this.newSnippetCompletionItem({
				label: localize('languageSpecificEditorSettings', "Language specific editor settings"),
				documentation: localize('languageSpecificEditorSettingsDescription', "Override editor settings for language"),
				snippet,
				range
			})]);
		}

		if (location.path.length === 1 && location.previousNode && location.previousNode.value.startsWith('[')) {

			// Suggestion model word matching includes starting quote and open sqaure bracket
			// Hence exclude them from the proposal range
			range = new vscode.Range(new vscode.Position(range.start.line, range.start.character + 2), range.end);

			return vscode.languages.getLanguages().then(languages => {
				return languages.map(l => {

					// Suggestion model word matching includes closed sqaure bracket and ending quote
					// Hence include them in the proposal to replace
					return this.newSimpleCompletionItem(l, range, '', l + ']"');
				});
			});
		}
		return Promise.resolve([]);
	}

	private newSimpleCompletionItem(text: string, range: vscode.Range, description?: string, insertText?: string): vscode.CompletionItem {
		const item = new vscode.CompletionItem(text);
		item.kind = vscode.CompletionItemKind.Value;
		item.detail = description;
		item.insertText = insertText ? insertText : text;
		item.range = range;
		return item;
	}

	private newSnippetCompletionItem(o: { label: string; documentation?: string; snippet: string; range: vscode.Range; }): vscode.CompletionItem {
		const item = new vscode.CompletionItem(o.label);
		item.kind = vscode.CompletionItemKind.Value;
		item.documentation = o.documentation;
		item.insertText = new vscode.SnippetString(o.snippet);
		item.range = o.range;
		return item;
	}
}