/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { compare, compareSubstring } from '../../../../base/common/strings.js';
import { Range } from '../../../../editor/common/core/range.js';
import { IModeService } from '../../../../editor/common/services/modeService.js';
import { SnippetParser } from '../../../../editor/contrib/snippet/snippetParser.js';
import { localize } from '../../../../nls.js';
import { ISnippetsService } from './snippets.contribution.js';
import { isPatternInWord } from '../../../../base/common/filters.js';
import { StopWatch } from '../../../../base/common/stopwatch.js';
import { LanguageConfigurationRegistry } from '../../../../editor/common/modes/languageConfigurationRegistry.js';
export class SnippetCompletion {
    constructor(snippet, range) {
        this.snippet = snippet;
        this.label = { label: snippet.prefix, description: snippet.name };
        this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source);
        this.insertText = snippet.codeSnippet;
        this.range = range;
        this.sortText = `${snippet.snippetSource === 3 /* Extension */ ? 'z' : 'a'}-${snippet.prefix}`;
        this.kind = 27 /* Snippet */;
        this.insertTextRules = 4 /* InsertAsSnippet */;
    }
    resolve() {
        this.documentation = new MarkdownString().appendCodeblock('', new SnippetParser().text(this.snippet.codeSnippet));
        return this;
    }
    static compareByLabel(a, b) {
        return compare(a.label.label, b.label.label);
    }
}
let SnippetCompletionProvider = class SnippetCompletionProvider {
    constructor(_modeService, _snippets) {
        this._modeService = _modeService;
        this._snippets = _snippets;
        this._debugDisplayName = 'snippetCompletions';
        //
    }
    provideCompletionItems(model, position, context) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const sw = new StopWatch(true);
            const languageId = this._getLanguageIdAtPosition(model, position);
            const snippets = new Set(yield this._snippets.getSnippets(languageId));
            const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase();
            const suggestions = [];
            const columnOffset = position.column - 1;
            const triggerCharacterLow = (_b = (_a = context.triggerCharacter) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
            for (const snippet of snippets) {
                for (let pos = Math.max(0, columnOffset - snippet.prefixLow.length); pos < lineContentLow.length; pos++) {
                    if (context.triggerKind === 1 /* TriggerCharacter */ && !snippet.prefixLow.startsWith(triggerCharacterLow)) {
                        // strict -> when having trigger characters they must prefix-match
                        continue;
                    }
                    if (!isPatternInWord(lineContentLow, pos, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) {
                        continue;
                    }
                    const prefixRestLen = snippet.prefixLow.length - (columnOffset - pos);
                    const endsWithPrefixRest = compareSubstring(lineContentLow, snippet.prefixLow, columnOffset, columnOffset + prefixRestLen, columnOffset - pos);
                    const startPosition = position.with(undefined, pos + 1);
                    let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column;
                    // First check if there is anything to the right of the cursor
                    if (columnOffset < lineContentLow.length) {
                        const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId);
                        const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContentLow[columnOffset]);
                        // If the character to the right of the cursor is a closing character of an autoclosing pair
                        if (standardAutoClosingPairConditionals === null || standardAutoClosingPairConditionals === void 0 ? void 0 : standardAutoClosingPairConditionals.some(p => 
                        // and the start position is the opening character of an autoclosing pair
                        p.open === lineContentLow[startPosition.column - 1] &&
                            // and the snippet prefix contains the opening and closing pair at its edges
                            snippet.prefix.startsWith(p.open) &&
                            snippet.prefix[snippet.prefix.length - 1] === p.close)) {
                            // Eat the character that was likely inserted because of auto-closing pairs
                            endColumn++;
                        }
                    }
                    const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn });
                    const insert = replace.setEndPosition(position.lineNumber, position.column);
                    suggestions.push(new SnippetCompletion(snippet, { replace, insert }));
                    snippets.delete(snippet);
                    break;
                }
            }
            // add remaing snippets when the current prefix ends in whitespace or when line is empty
            // and when not having a trigger character
            if (!triggerCharacterLow) {
                const endsInWhitespace = /\s/.test(lineContentLow[position.column - 2]);
                if (endsInWhitespace || !lineContentLow /*empty line*/) {
                    for (let snippet of snippets) {
                        const insert = Range.fromPositions(position);
                        const replace = lineContentLow.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert;
                        suggestions.push(new SnippetCompletion(snippet, { replace, insert }));
                    }
                }
            }
            // dismbiguate suggestions with same labels
            suggestions.sort(SnippetCompletion.compareByLabel);
            for (let i = 0; i < suggestions.length; i++) {
                let item = suggestions[i];
                let to = i + 1;
                for (; to < suggestions.length && item.label === suggestions[to].label; to++) {
                    suggestions[to].label.label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[to].label.label, suggestions[to].snippet.name);
                }
                if (to > i + 1) {
                    suggestions[i].label.label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[i].label.label, suggestions[i].snippet.name);
                    i = to;
                }
            }
            return {
                suggestions,
                duration: sw.elapsed()
            };
        });
    }
    resolveCompletionItem(item) {
        return (item instanceof SnippetCompletion) ? item.resolve() : item;
    }
    _getLanguageIdAtPosition(model, position) {
        // 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
        model.tokenizeIfCheap(position.lineNumber);
        let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column);
        languageId = this._modeService.validateLanguageId(languageId);
        if (!languageId || !this._modeService.getLanguageName(languageId)) {
            languageId = model.getLanguageId();
        }
        return languageId;
    }
};
SnippetCompletionProvider = __decorate([
    __param(0, IModeService),
    __param(1, ISnippetsService)
], SnippetCompletionProvider);
export { SnippetCompletionProvider };
