adobe/brackets

View on GitHub
src/search/SearchModel.js

Summary

Maintainability
A
3 hrs
Test Coverage
/*
 * Copyright (c) 2014 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

define(function (require, exports, module) {
    "use strict";

    var FileUtils       = require("file/FileUtils"),
        EventDispatcher = require("utils/EventDispatcher"),
        FindUtils       = require("search/FindUtils"),
        MainViewManager = require("view/MainViewManager");

    /**
     * @constructor
     * Manages a set of search query and result data.
     * Dispatches these events:
     *      "change" - whenever the results have been updated. Note that it's up to people who
     *      edit the model to call fireChange() when necessary - it doesn't automatically fire.
     */
    function SearchModel() {
        this.clear();
    }
    EventDispatcher.makeEventDispatcher(SearchModel.prototype);

    /** @const Constant used to define the maximum results found.
     *  Note that this is a soft limit - we'll likely go slightly over it since
     *  we always add all the searches in a given file.
     */
    SearchModel.MAX_TOTAL_RESULTS = 100000;

    /**
     * The current set of results.
     * @type {Object.<fullPath: string, {matches: Array.<Object>, collapsed: boolean, timestamp: Date}>}
     */
    SearchModel.prototype.results = null;

    /**
     * The query that generated these results.
     * @type {{query: string, caseSensitive: boolean, isRegexp: boolean}}
     */
    SearchModel.prototype.queryInfo = null;

    /**
     * The compiled query, expressed as a regexp.
     * @type {RegExp}
     */
    SearchModel.prototype.queryExpr = null;

    /**
     * Whether this is a find/replace query.
     * @type {boolean}
     */
    SearchModel.prototype.isReplace = false;

    /**
     * The replacement text specified for this query, if any.
     * @type {string}
     */
    SearchModel.prototype.replaceText = null;

    /**
     * The file/folder path representing the scope that this query was performed in.
     * @type {FileSystemEntry}
     */
    SearchModel.prototype.scope = null;

    /**
     * A file filter (as returned from FileFilters) to apply within the main scope.
     * @type {string}
     */
    SearchModel.prototype.filter = null;

    /**
     * The total number of matches in the model.
     * @type {number}
     */
    SearchModel.prototype.numMatches = 0;

    /**
     * Whether or not we hit the maximum number of results for the type of search we did.
     * @type {boolean}
     */
    SearchModel.prototype.foundMaximum = false;

    /**
     * Whether or not we exceeded the maximum number of results in the search we did.
     * @type {boolean}
     */
    SearchModel.prototype.exceedsMaximum = false;

    /**
     * Clears out the model to an empty state.
     */
    SearchModel.prototype.clear = function () {
        var numMatchesBefore = this.numMatches;
        this.results = {};
        this.queryInfo = null;
        this.queryExpr = null;
        this.isReplace = false;
        this.replaceText = null;
        this.scope = null;
        this.numMatches = 0;
        this.foundMaximum = false;
        this.exceedsMaximum = false;
        if (numMatchesBefore !== 0) {
            this.fireChanged();
        }
    };

    /**
     * Sets the given query info and stores a compiled RegExp query in this.queryExpr.
     * @param {{query: string, caseSensitive: boolean, isRegexp: boolean}} queryInfo
     * @return {boolean} true if the query was valid and properly set, false if it was
     *      invalid or empty.
     */
    SearchModel.prototype.setQueryInfo = function (queryInfo) {
        var parsedQuery = FindUtils.parseQueryInfo(queryInfo);
        if (parsedQuery.valid) {
            this.queryInfo = queryInfo;
            this.queryExpr = parsedQuery.queryExpr;
            return true;
        } else {
            return false;
        }
    };

    /**
     * Sets the list of matches for the given path, removing the previous match info, if any, and updating
     * the total match count. Note that for the count to remain accurate, the previous match info must not have
     * been mutated since it was set.
     * @param {string} fullpath Full path to the file containing the matches.
     * @param {!{matches: Object, timestamp: Date, collapsed: boolean=}} resultInfo Info for the matches to set:
     *      matches - Array of matches, in the format returned by FindInFiles._getSearchMatches()
     *      timestamp - The timestamp of the document at the time we searched it.
     *      collapsed - Optional: whether the results should be collapsed in the UI (default false).
     */
    SearchModel.prototype.setResults = function (fullpath, resultInfo) {
        this.removeResults(fullpath);

        if (this.foundMaximum || !resultInfo.matches.length) {
            return;
        }

        // Make sure that the optional `collapsed` property is explicitly set to either true or false,
        // to avoid logic issues later with comparing values.
        resultInfo.collapsed = !!resultInfo.collapsed;

        this.results[fullpath] = resultInfo;
        this.numMatches += resultInfo.matches.length;
        if (this.numMatches >= SearchModel.MAX_TOTAL_RESULTS) {
            this.foundMaximum = true;

            // Remove final result if there have been over MAX_TOTAL_RESULTS found
            if (this.numMatches > SearchModel.MAX_TOTAL_RESULTS) {
                this.results[fullpath].matches.pop();
                this.numMatches--;
                this.exceedsMaximum = true;
            }
        }
    };

    /**
     * Removes the given result's matches from the search results and updates the total match count.
     * @param {string} fullpath Full path to the file containing the matches.
     */
    SearchModel.prototype.removeResults = function (fullpath) {
        if (this.results[fullpath]) {
            this.numMatches -= this.results[fullpath].matches.length;
            delete this.results[fullpath];
        }
    };

    /**
     * @return {boolean} true if there are any results in this model.
     */
    SearchModel.prototype.hasResults = function () {
        return Object.keys(this.results).length > 0;
    };

    /**
     * Counts the total number of matches and files
     * @return {{files: number, matches: number}}
     */
    SearchModel.prototype.countFilesMatches = function () {
        return {files: (this.numFiles || Object.keys(this.results).length), matches: this.numMatches};
    };

    /**
     * Prioritizes the open file and then the working set files to the starting of the list of files
     * If node search is disabled, we sort the files too- Sorting is computation intensive, and our
     * ProjectManager.getAllFiles with the sort flag is not working properly : TODO TOFIX
     * @param {?string} firstFile If specified, the path to the file that should be sorted to the top.
     * @return {Array.<string>}
     */
    SearchModel.prototype.prioritizeOpenFile = function (firstFile) {
        var workingSetFiles = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES),
            workingSetFileFound = {},
            fileSetWithoutWorkingSet = [],
            startingWorkingFileSet = [],
            propertyName = "",
            i = 0;

        if (FindUtils.isNodeSearchDisabled()) {
            return Object.keys(this.results).sort(function (key1, key2) {
                if (firstFile === key1) {
                    return -1;
                } else if (firstFile === key2) {
                    return 1;
                }
                return FileUtils.comparePaths(key1, key2);
            });
        }

        firstFile = firstFile || "";

        // Create a working set path map which indicates if a file in working set is found in file list
        for (i = 0; i < workingSetFiles.length; i++) {
            workingSetFileFound[workingSetFiles[i].fullPath] = false;
        }

        // Remove all the working set files from the filtration list
        fileSetWithoutWorkingSet = Object.keys(this.results).filter(function (key) {
            if (workingSetFileFound[key] !== undefined) {
                workingSetFileFound[key] = true;
                return false;
            }
            return true;
        });

        //push in the first file
        if (workingSetFileFound[firstFile] === true) {
            startingWorkingFileSet.push(firstFile);
            workingSetFileFound[firstFile] = false;
        }
        //push in the rest of working set files already present in file list
        for (propertyName in workingSetFileFound) {
            if (workingSetFileFound.hasOwnProperty(propertyName) && workingSetFileFound[propertyName]) {
                startingWorkingFileSet.push(propertyName);
            }
        }
        return startingWorkingFileSet.concat(fileSetWithoutWorkingSet);
    };

    /**
     * Notifies listeners that the set of results has changed. Must be called after the
     * model is changed.
     * @param {boolean} quickChange Whether this type of change is one that might occur
     *      often, meaning that the view should buffer updates.
     */
    SearchModel.prototype.fireChanged = function (quickChange) {
        this.trigger("change", quickChange);
    };

    // Public API
    exports.SearchModel = SearchModel;
});