"use strict";
/********************************************************************************
 * Copyright (C) 2017-2021 Ericsson and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/
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 __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
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());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __values = (this && this.__values) || function(o) {
    var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
    if (m) return m.call(o);
    if (o && typeof o.length === "number") return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
    throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var __read = (this && this.__read) || function (o, n) {
    var m = typeof Symbol === "function" && o[Symbol.iterator];
    if (!m) return o;
    var i = m.call(o), r, ar = [], e;
    try {
        while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
    }
    catch (error) { e = { error: error }; }
    finally {
        try {
            if (r && !r.done && (m = i["return"])) m.call(i);
        }
        finally { if (e) throw e.error; }
    }
    return ar;
};
var __spread = (this && this.__spread) || function () {
    for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
    return ar;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RipgrepSearchInWorkspaceServer = exports.RgPath = void 0;
var fs = require("@theia/core/shared/fs-extra");
var path = require("path");
var core_1 = require("@theia/core");
var node_1 = require("@theia/process/lib/node");
var file_uri_1 = require("@theia/core/lib/node/file-uri");
var uri_1 = require("@theia/core/lib/common/uri");
var inversify_1 = require("@theia/core/shared/inversify");
exports.RgPath = Symbol('RgPath');
function bytesOrTextToString(obj) {
    return 'bytes' in obj ?
        Buffer.from(obj.bytes, 'base64').toString() :
        obj.text;
}
var RipgrepSearchInWorkspaceServer = /** @class */ (function () {
    function RipgrepSearchInWorkspaceServer(logger, rawProcessFactory) {
        this.logger = logger;
        this.rawProcessFactory = rawProcessFactory;
        // List of ongoing searches, maps search id to a the started rg process.
        this.ongoingSearches = new Map();
        // Each incoming search is given a unique id, returned to the client.  This is the next id we will assigned.
        this.nextSearchId = 1;
    }
    RipgrepSearchInWorkspaceServer.prototype.setClient = function (client) {
        this.client = client;
    };
    RipgrepSearchInWorkspaceServer.prototype.getArgs = function (options) {
        var args = new Set();
        args.add('--hidden');
        args.add('--json');
        if (options === null || options === void 0 ? void 0 : options.matchCase) {
            args.add('--case-sensitive');
        }
        else {
            args.add('--ignore-case');
        }
        if (options === null || options === void 0 ? void 0 : options.includeIgnored) {
            args.add('--no-ignore');
        }
        if (options === null || options === void 0 ? void 0 : options.maxFileSize) {
            args.add('--max-filesize=' + options.maxFileSize.trim());
        }
        else {
            args.add('--max-filesize=20M');
        }
        if (options === null || options === void 0 ? void 0 : options.include) {
            this.addGlobArgs(args, options.include, false);
        }
        if (options === null || options === void 0 ? void 0 : options.exclude) {
            this.addGlobArgs(args, options.exclude, true);
        }
        if ((options === null || options === void 0 ? void 0 : options.useRegExp) || (options === null || options === void 0 ? void 0 : options.matchWholeWord)) {
            args.add('--regexp');
        }
        else {
            args.add('--fixed-strings');
            args.add('--');
        }
        return Array.from(args);
    };
    /**
     * Add glob patterns to ripgrep's arguments
     * @param args ripgrep set of arguments
     * @param patterns patterns to include as globs
     * @param exclude whether to negate the glob pattern or not
     */
    RipgrepSearchInWorkspaceServer.prototype.addGlobArgs = function (args, patterns, exclude) {
        var e_1, _a;
        if (exclude === void 0) { exclude = false; }
        var sanitizedPatterns = patterns.map(function (pattern) { return pattern.trim(); }).filter(function (pattern) { return pattern.length > 0; });
        try {
            for (var sanitizedPatterns_1 = __values(sanitizedPatterns), sanitizedPatterns_1_1 = sanitizedPatterns_1.next(); !sanitizedPatterns_1_1.done; sanitizedPatterns_1_1 = sanitizedPatterns_1.next()) {
                var pattern = sanitizedPatterns_1_1.value;
                // make sure the pattern always starts with `**/`
                if (pattern.startsWith('/')) {
                    pattern = '**' + pattern;
                }
                else if (!pattern.startsWith('**/')) {
                    pattern = '**/' + pattern;
                }
                // add the exclusion prefix
                if (exclude) {
                    pattern = '!' + pattern;
                }
                args.add("--glob=" + pattern);
                // add a generic glob cli argument entry to include files inside a given directory
                if (!pattern.endsWith('*')) {
                    // ensure the new pattern ends with `/*`
                    pattern += pattern.endsWith('/') ? '*' : '/*';
                    args.add("--glob=" + pattern);
                }
            }
        }
        catch (e_1_1) { e_1 = { error: e_1_1 }; }
        finally {
            try {
                if (sanitizedPatterns_1_1 && !sanitizedPatterns_1_1.done && (_a = sanitizedPatterns_1.return)) _a.call(sanitizedPatterns_1);
            }
            finally { if (e_1) throw e_1.error; }
        }
    };
    /**
     * Transforms relative patterns to absolute paths, one for each given search path.
     * The resulting paths are not validated in the file system as the pattern keeps glob information.
     *
     * @returns The resulting list may be larger than the received patterns as a relative pattern may
     * resolve to multiple absolute patterns upto the number of search paths.
     */
    RipgrepSearchInWorkspaceServer.prototype.replaceRelativeToAbsolute = function (roots, patterns) {
        var e_2, _a, e_3, _b;
        if (patterns === void 0) { patterns = []; }
        var expandedPatterns = new Set();
        try {
            for (var patterns_1 = __values(patterns), patterns_1_1 = patterns_1.next(); !patterns_1_1.done; patterns_1_1 = patterns_1.next()) {
                var pattern = patterns_1_1.value;
                if (this.isPatternRelative(pattern)) {
                    try {
                        // create new patterns using the absolute form for each root
                        for (var roots_1 = (e_3 = void 0, __values(roots)), roots_1_1 = roots_1.next(); !roots_1_1.done; roots_1_1 = roots_1.next()) {
                            var root = roots_1_1.value;
                            expandedPatterns.add(path.resolve(root, pattern));
                        }
                    }
                    catch (e_3_1) { e_3 = { error: e_3_1 }; }
                    finally {
                        try {
                            if (roots_1_1 && !roots_1_1.done && (_b = roots_1.return)) _b.call(roots_1);
                        }
                        finally { if (e_3) throw e_3.error; }
                    }
                }
                else {
                    expandedPatterns.add(pattern);
                }
            }
        }
        catch (e_2_1) { e_2 = { error: e_2_1 }; }
        finally {
            try {
                if (patterns_1_1 && !patterns_1_1.done && (_a = patterns_1.return)) _a.call(patterns_1);
            }
            finally { if (e_2) throw e_2.error; }
        }
        return Array.from(expandedPatterns);
    };
    /**
     * Tests if the pattern is relative and should/can be made absolute.
     */
    RipgrepSearchInWorkspaceServer.prototype.isPatternRelative = function (pattern) {
        return pattern.replace(/\\/g, '/').startsWith('./');
    };
    /**
     * By default, sets the search directories for the string WHAT to the provided ROOTURIS directories
     * and returns the assigned search id.
     *
     * The include / exclude (options in SearchInWorkspaceOptions) are lists of patterns for files to
     * include / exclude during search (glob characters are allowed).
     *
     * include patterns successfully recognized as absolute paths will override the default search and set
     * the search directories to the ones provided as includes.
     * Relative paths are allowed, the application will attempt to translate them to valid absolute paths
     * based on the applicable search directories.
     */
    RipgrepSearchInWorkspaceServer.prototype.search = function (what, rootUris, options) {
        if (options === void 0) { options = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var searchId, rootPaths, searchPaths, rgArgs, args, processOptions, rgProcess, numResults, databuf, currentSearchResult;
            var _this = this;
            return __generator(this, function (_a) {
                searchId = this.nextSearchId++;
                rootPaths = rootUris.map(function (root) { return file_uri_1.FileUri.fsPath(root); });
                searchPaths = this.extractSearchPathsFromIncludes(rootPaths, options);
                options.include = this.replaceRelativeToAbsolute(searchPaths, options.include);
                options.exclude = this.replaceRelativeToAbsolute(searchPaths, options.exclude);
                rgArgs = this.getArgs(options);
                // If we use matchWholeWord we use regExp internally, so we need
                // to escape regexp characters if we actually not set regexp true in UI.
                if ((options === null || options === void 0 ? void 0 : options.matchWholeWord) && !options.useRegExp) {
                    what = what.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&');
                    if (!/\B/.test(what.charAt(0))) {
                        what = '\\b' + what;
                    }
                    if (!/\B/.test(what.charAt(what.length - 1))) {
                        what = what + '\\b';
                    }
                }
                args = __spread(rgArgs, [what], searchPaths);
                processOptions = {
                    command: this.rgPath,
                    args: args
                };
                rgProcess = this.rawProcessFactory(processOptions);
                this.ongoingSearches.set(searchId, rgProcess);
                rgProcess.onError(function (error) {
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    var errorCode = error.code;
                    // Try to provide somewhat clearer error messages, if possible.
                    if (errorCode === 'ENOENT') {
                        errorCode = 'could not find the ripgrep (rg) binary';
                    }
                    else if (errorCode === 'EACCES') {
                        errorCode = 'could not execute the ripgrep (rg) binary';
                    }
                    var errorStr = "An error happened while searching (" + errorCode + ").";
                    _this.wrapUpSearch(searchId, errorStr);
                });
                numResults = 0;
                databuf = '';
                rgProcess.outputStream.on('data', function (chunk) {
                    var e_4, _a;
                    // We might have already reached the max number of
                    // results, sent a TERM signal to rg, but we still get
                    // the data that was already output in the mean time.
                    // It's not necessary to return early here (the check
                    // for maxResults below would avoid sending extra
                    // results), but it avoids doing unnecessary work.
                    if ((options === null || options === void 0 ? void 0 : options.maxResults) && numResults >= options.maxResults) {
                        return;
                    }
                    databuf += chunk;
                    while (1) {
                        // Check if we have a complete line.
                        var eolIdx = databuf.indexOf('\n');
                        if (eolIdx < 0) {
                            break;
                        }
                        // Get and remove the line from the data buffer.
                        var lineBuf = databuf.slice(0, eolIdx);
                        databuf = databuf.slice(eolIdx + 1);
                        var obj = JSON.parse(lineBuf);
                        if (obj.type === 'begin') {
                            var file = bytesOrTextToString(obj.data.path);
                            if (file) {
                                currentSearchResult = {
                                    fileUri: file_uri_1.FileUri.create(file).toString(),
                                    root: _this.getRoot(file, rootUris).toString(),
                                    matches: []
                                };
                            }
                            else {
                                _this.logger.error('Begin message without path. ' + JSON.stringify(obj));
                            }
                        }
                        else if (obj.type === 'end') {
                            if (currentSearchResult && _this.client) {
                                _this.client.onResult(searchId, currentSearchResult);
                            }
                            currentSearchResult = undefined;
                        }
                        else if (obj.type === 'match') {
                            if (!currentSearchResult) {
                                continue;
                            }
                            var data = obj.data;
                            var file = bytesOrTextToString(data.path);
                            var line = data.line_number;
                            var lineText = bytesOrTextToString(data.lines);
                            if (file === undefined || lineText === undefined) {
                                continue;
                            }
                            var lineInBytes = Buffer.from(lineText);
                            try {
                                for (var _b = (e_4 = void 0, __values(data.submatches)), _c = _b.next(); !_c.done; _c = _b.next()) {
                                    var submatch = _c.value;
                                    var startOffset = lineInBytes.slice(0, submatch.start).toString().length;
                                    var match = bytesOrTextToString(submatch.match);
                                    var lineInfo = lineText.trimRight();
                                    if (lineInfo.length > 300) {
                                        var prefixLength = 25;
                                        var start = Math.max(startOffset - prefixLength, 0);
                                        var length_1 = prefixLength + match.length + 70;
                                        var prefix = '';
                                        if (start >= prefixLength) {
                                            prefix = '...';
                                        }
                                        var character = (start < prefixLength ? start : prefixLength) + prefix.length + 1;
                                        lineInfo = {
                                            text: prefix + lineInfo.substr(start, length_1),
                                            character: character
                                        };
                                    }
                                    currentSearchResult.matches.push({
                                        line: line,
                                        character: startOffset + 1,
                                        length: match.length,
                                        lineText: lineInfo
                                    });
                                    numResults++;
                                    // Did we reach the maximum number of results?
                                    if ((options === null || options === void 0 ? void 0 : options.maxResults) && numResults >= options.maxResults) {
                                        rgProcess.kill();
                                        if (currentSearchResult && _this.client) {
                                            _this.client.onResult(searchId, currentSearchResult);
                                        }
                                        currentSearchResult = undefined;
                                        _this.wrapUpSearch(searchId);
                                        break;
                                    }
                                }
                            }
                            catch (e_4_1) { e_4 = { error: e_4_1 }; }
                            finally {
                                try {
                                    if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
                                }
                                finally { if (e_4) throw e_4.error; }
                            }
                        }
                    }
                });
                rgProcess.outputStream.on('end', function () {
                    // If we reached maxResults, we should have already
                    // wrapped up the search.  Returning early avoids
                    // logging a warning message in wrapUpSearch.
                    if ((options === null || options === void 0 ? void 0 : options.maxResults) && numResults >= options.maxResults) {
                        return;
                    }
                    _this.wrapUpSearch(searchId);
                });
                return [2 /*return*/, searchId];
            });
        });
    };
    /**
     * The default search paths are set to be the root paths associated to a workspace
     * however the search scope can be further refined with the include paths available in the search options.
     * This method will replace the searching paths to the ones specified in the 'include' options but as long
     * as the 'include' paths can be successfully validated as existing.
     *
     * Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
     * derived from the include options which can be used to perform the search.
     *
     * Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
     * provided as an equivalent search path instead.
     */
    RipgrepSearchInWorkspaceServer.prototype.extractSearchPathsFromIncludes = function (rootPaths, options) {
        var _this = this;
        if (!options.include) {
            return rootPaths;
        }
        var resolvedPaths = new Set();
        options.include = options.include.filter(function (pattern) {
            var e_5, _a;
            var keep = true;
            try {
                for (var rootPaths_1 = __values(rootPaths), rootPaths_1_1 = rootPaths_1.next(); !rootPaths_1_1.done; rootPaths_1_1 = rootPaths_1.next()) {
                    var root = rootPaths_1_1.value;
                    var absolutePath = _this.getAbsolutePathFromPattern(root, pattern);
                    // undefined means the pattern cannot be converted into an absolute path
                    if (absolutePath) {
                        resolvedPaths.add(absolutePath);
                        keep = false;
                    }
                }
            }
            catch (e_5_1) { e_5 = { error: e_5_1 }; }
            finally {
                try {
                    if (rootPaths_1_1 && !rootPaths_1_1.done && (_a = rootPaths_1.return)) _a.call(rootPaths_1);
                }
                finally { if (e_5) throw e_5.error; }
            }
            return keep;
        });
        return resolvedPaths.size > 0
            ? Array.from(resolvedPaths)
            : rootPaths;
    };
    /**
     * Transform include/exclude option patterns from relative patterns to absolute patterns.
     * E.g. './abc/foo.*' to '${root}/abc/foo.*', the transformation does not validate the
     * pattern against the file system as glob suffixes remain.
     *
     * @returns undefined if the pattern cannot be converted into an absolute path.
     */
    RipgrepSearchInWorkspaceServer.prototype.getAbsolutePathFromPattern = function (root, pattern) {
        pattern = pattern.replace(/\\/g, '/');
        // The pattern is not referring to a single file or folder, i.e. not to be converted
        if (!path.isAbsolute(pattern) && !pattern.startsWith('./')) {
            return undefined;
        }
        // remove the `/**` suffix if present
        if (pattern.endsWith('/**')) {
            pattern = pattern.substr(0, pattern.length - 3);
        }
        // if `pattern` is absolute then `root` will be ignored by `path.resolve()`
        var targetPath = path.resolve(root, pattern);
        if (fs.existsSync(targetPath)) {
            return targetPath;
        }
        return undefined;
    };
    /**
     * Returns the root folder uri that a file belongs to.
     * In case that a file belongs to more than one root folders, returns the root folder that is closest to the file.
     * If the file is not from the current workspace, returns empty string.
     * @param filePath string path of the file
     * @param rootUris string URIs of the root folders in the current workspace
     */
    RipgrepSearchInWorkspaceServer.prototype.getRoot = function (filePath, rootUris) {
        var roots = rootUris.filter(function (root) { return new uri_1.default(root).withScheme('file').isEqualOrParent(file_uri_1.FileUri.create(filePath).withScheme('file')); });
        if (roots.length > 0) {
            return file_uri_1.FileUri.create(file_uri_1.FileUri.fsPath(roots.sort(function (r1, r2) { return r2.length - r1.length; })[0]));
        }
        return new uri_1.default();
    };
    // Cancel an ongoing search.  Trying to cancel a search that doesn't exist isn't an
    // error, otherwise we'd have to deal with race conditions, where a client cancels a
    // search that finishes normally at the same time.
    RipgrepSearchInWorkspaceServer.prototype.cancel = function (searchId) {
        var process = this.ongoingSearches.get(searchId);
        if (process) {
            process.kill();
            this.wrapUpSearch(searchId);
        }
        return Promise.resolve();
    };
    // Send onDone to the client and clean up what we know about search searchId.
    RipgrepSearchInWorkspaceServer.prototype.wrapUpSearch = function (searchId, error) {
        if (this.ongoingSearches.delete(searchId)) {
            if (this.client) {
                this.logger.debug('Sending onDone for ' + searchId, error);
                this.client.onDone(searchId, error);
            }
            else {
                this.logger.debug('Wrapping up search ' + searchId + ' but no client');
            }
        }
        else {
            this.logger.debug("Trying to wrap up a search we don't know about " + searchId);
        }
    };
    RipgrepSearchInWorkspaceServer.prototype.dispose = function () {
    };
    __decorate([
        inversify_1.inject(exports.RgPath),
        __metadata("design:type", String)
    ], RipgrepSearchInWorkspaceServer.prototype, "rgPath", void 0);
    RipgrepSearchInWorkspaceServer = __decorate([
        inversify_1.injectable(),
        __param(0, inversify_1.inject(core_1.ILogger)),
        __param(1, inversify_1.inject(node_1.RawProcessFactory)),
        __metadata("design:paramtypes", [Object, Function])
    ], RipgrepSearchInWorkspaceServer);
    return RipgrepSearchInWorkspaceServer;
}());
exports.RipgrepSearchInWorkspaceServer = RipgrepSearchInWorkspaceServer;
//# sourceMappingURL=ripgrep-search-in-workspace-server.js.map