snippetsService.ts 5.8 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.
 *--------------------------------------------------------------------------------------------*/
'use strict';

J
Johannes Rieken 已提交
7
import { localize } from 'vs/nls';
8
import * as strings from 'vs/base/common/strings';
9
import { IModel, IPosition } from 'vs/editor/common/editorCommon';
A
Alex Dima 已提交
10
import { ISuggestion, LanguageIdentifier, LanguageId } from 'vs/editor/common/modes';
11 12 13
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/common/suggest';
14
import { IModeService } from 'vs/editor/common/services/modeService';
15

16
export const ISnippetsService = createDecorator<ISnippetsService>('snippetService');
17

18 19 20
export interface ISnippetsService {

	_serviceBrand: any;
21

A
Alex Dima 已提交
22
	registerSnippets(languageIdentifier: LanguageIdentifier, snippets: ISnippet[], owner?: string): void;
23

A
Alex Dima 已提交
24
	visitSnippets(languageId: LanguageId, accept: (snippet: ISnippet) => void): void;
25 26 27
}

export interface ISnippet {
28
	name: string;
29
	owner: string;
30 31 32 33 34
	prefix: string;
	description: string;
	codeSnippet: string;
}

35 36 37 38
interface ISnippetSuggestion extends ISuggestion {
	disambiguateLabel: string;
}

39 40 41
class SnippetsService implements ISnippetsService {

	_serviceBrand: any;
42

A
Alex Dima 已提交
43
	private _snippets: { [owner: string]: ISnippet[] }[] = [];
44

45 46 47
	constructor(
		@IModeService private _modeService: IModeService
	) {
48 49
		setSnippetSuggestSupport({
			provideCompletionItems: (model, position) => {
50
				const suggestions = this._getSnippetCompletions(<any>model, position);
51 52 53 54 55
				return { suggestions };
			}
		});
	}

A
Alex Dima 已提交
56
	public registerSnippets(languageIdentifier: LanguageIdentifier, snippets: ISnippet[], owner = ''): void {
57
		let snippetsByMode = this._snippets[languageIdentifier.id];
58
		if (!snippetsByMode) {
59
			this._snippets[languageIdentifier.id] = snippetsByMode = {};
60 61 62 63
		}
		snippetsByMode[owner] = snippets;
	}

A
Alex Dima 已提交
64 65
	public visitSnippets(languageId: LanguageId, accept: (snippet: ISnippet) => boolean): void {
		let snippetsByMode = this._snippets[languageId];
66 67 68 69 70 71 72 73 74
		if (snippetsByMode) {
			for (let s in snippetsByMode) {
				let result = snippetsByMode[s].every(accept);
				if (!result) {
					return;
				}
			}
		}
	}
75

76 77 78 79 80 81 82 83 84 85 86 87 88 89
	private _getLanguageIdAtPosition(model: IModel, position: IPosition): LanguageId {
		// validate the `languageId` to ensure this is a user
		// facing language with a name and the chance to have
		// snippets, else fall back to the outer language
		let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column);
		let { language } = this._modeService.getLanguageIdentifier(languageId);
		if (!this._modeService.getLanguageName(language)) {
			languageId = model.getLanguageIdentifier().id;
		}
		return languageId;
	}

	private _getSnippetCompletions(model: IModel, position: IPosition): ISuggestion[] {
		const languageId = this._getLanguageIdAtPosition(model, position);
A
Alex Dima 已提交
90
		if (!this._snippets[languageId]) {
M
Matt Bierner 已提交
91
			return undefined;
92
		}
93 94 95

		const result: ISnippetSuggestion[] = [];

96 97 98
		const word = model.getWordAtPosition(position);
		const currentWord = word ? word.word.substring(0, position.column - word.startColumn).toLowerCase() : '';
		const currentFullWord = getNonWhitespacePrefix(model, position).toLowerCase();
99

A
Alex Dima 已提交
100
		this.visitSnippets(languageId, s => {
101 102 103 104 105
			let overwriteBefore: number;
			if (currentWord.length === 0 && currentFullWord.length === 0) {
				// if there's no prefix, only show snippets at the beginning of the line, or after a whitespace
				overwriteBefore = 0;
			} else {
106
				const label = s.prefix.toLowerCase();
107 108 109 110 111 112 113 114 115
				// force that the current word or full word matches with the snippet prefix
				if (currentWord.length > 0 && strings.startsWith(label, currentWord)) {
					overwriteBefore = currentWord.length;
				} else if (currentFullWord.length > currentWord.length && strings.startsWith(label, currentFullWord)) {
					overwriteBefore = currentFullWord.length;
				} else {
					return true;
				}
			}
116

117 118
			// store in result
			result.push({
119 120
				type: 'snippet',
				label: s.prefix,
121
				get disambiguateLabel() { return localize('snippetSuggest.longLabel', "{0}, {1}", s.prefix, s.name); },
122
				detail: s.owner,
123 124 125
				documentation: s.description,
				insertText: s.codeSnippet,
				noAutoAccept: true,
126
				snippetType: 'textmate',
127
				overwriteBefore
128
			});
129

130 131
			return true;
		});
132 133 134

		// dismbiguate suggestions with same labels
		let lastSuggestion: ISnippetSuggestion;
135
		for (const suggestion of result.sort(SnippetsService._compareSuggestionsByLabel)) {
136 137 138 139 140 141 142 143 144 145 146
			if (lastSuggestion && lastSuggestion.label === suggestion.label) {
				// use the disambiguateLabel instead of the actual label
				lastSuggestion.label = lastSuggestion.disambiguateLabel;
				suggestion.label = suggestion.disambiguateLabel;
			}
			lastSuggestion = suggestion;
		}

		return result;
	}

J
Johannes Rieken 已提交
147
	private static _compareSuggestionsByLabel(a: ISuggestion, b: ISuggestion): number {
148
		return strings.compare(a.label, b.label);
149
	}
150 151
}

152 153
registerSingleton(ISnippetsService, SnippetsService);

154 155 156 157
export interface ISimpleModel {
	getLineContent(lineNumber): string;
}

J
Johannes Rieken 已提交
158
export function getNonWhitespacePrefix(model: ISimpleModel, position: IPosition): string {
159 160 161 162 163
	/**
	 * Do not analyze more characters
	 */
	const MAX_PREFIX_LENGTH = 100;

164
	let line = model.getLineContent(position.lineNumber).substr(0, position.column - 1);
165 166 167 168 169 170 171 172

	let minChIndex = Math.max(0, line.length - MAX_PREFIX_LENGTH);
	for (let chIndex = line.length - 1; chIndex >= minChIndex; chIndex--) {
		let ch = line.charAt(chIndex);

		if (/\s/.test(ch)) {
			return line.substr(chIndex + 1);
		}
173
	}
174 175 176 177 178

	if (minChIndex === 0) {
		return line;
	}

179 180 181
	return '';
}