Modules (181)

FindInFilesDomain

Description

Dependencies

Functions

addFilesToCache

Adds the list of given files to the project cache. However the files will not be read at this time. We just delete the project cache entry which will trigger a fetch on search.

updateObject Object
function addFilesToCache(updateObject) {
    var fileList = updateObject.fileList || [],
        filesInSearchScope = updateObject.filesInSearchScope || [],
        i = 0,
        changedFilesAlreadyInList = [],
        newFiles = [];
    for (i = 0; i < fileList.length; i++) {
        // We just add a null entry indicating the precense of the file in the project list.
        // The file will be later read when required.
        projectCache[fileList[i]] = null;
    }

    //Now update the search scope
    function isInChangedFileList(path) {
        return (filesInSearchScope.indexOf(path) !== -1) ? true : false;
    }
    changedFilesAlreadyInList = files ? files.filter(isInChangedFileList) : [];
    function isNotAlreadyInList(path) {
        return (changedFilesAlreadyInList.indexOf(path) === -1) ? true : false;
    }
    newFiles = changedFilesAlreadyInList.filter(isNotAlreadyInList);
    files.push.apply(files, newFiles);
}

clearProjectCache

Clears the cached file contents of the project

function clearProjectCache() {
    projectCache = [];
}

countNumMatches

Counts the number of matches matching the queryExpr in the given contents

contents String
The contents to search on
queryExpr Object
Returns: number
number of matches
function countNumMatches(contents, queryExpr) {
    if (!contents) {
        return 0;
    }
    var matches = contents.match(queryExpr);
    return matches ? matches.length : 0;
}

doSearch

Do a search with the searchObject context and return the results

searchObject Object
nextPages boolean
set to true if to indicate that next page of an existing page is being fetched
Returns: Object
search results
function doSearch(searchObject, nextPages) {

    savedSearchObject = searchObject;
    if (!files) {
        console.log("no file object found");
        return {};
    }
    results = {};
    numMatches = 0;
    numFiles = 0;
    foundMaximum = false;
    if (!nextPages) {
        exceedsMaximum = false;
        evaluatedMatches = 0;
    }
    var queryObject = parseQueryInfo(searchObject.queryInfo);
    if (searchObject.files) {
        files = searchObject.files;
    }
    if (searchObject.getAllResults) {
        searchObject.maxResultsToReturn = MAX_TOTAL_RESULTS;
    }
    doSearchInFiles(files, queryObject.queryExpr, searchObject.startFileIndex, searchObject.maxResultsToReturn);
    if (crawlComplete && !nextPages) {
        numMatches = getNumMatches(files, queryObject.queryExpr);
    }
    var send_object = {
        "results":  results,
        "foundMaximum":  foundMaximum,
        "exceedsMaximum":  exceedsMaximum
    };

    if (!nextPages) {
        send_object.numMatches = numMatches;
        send_object.numFiles = numFiles;
    }

    if (searchObject.getAllResults) {
        send_object.allResultsAvailable = true;
    }
    return send_object;
}

doSearchInFiles

Search in the list of files given and populate the results

fileList array
array of file paths
queryExpr Object
startFileIndex number
the start index of the array from which the search has to be done
maxResultsToReturn number
the maximum number of results to return in this search
function doSearchInFiles(fileList, queryExpr, startFileIndex, maxResultsToReturn) {
    var i;
    if (fileList.length === 0) {
        console.log('no files found');
        return;

    } else {
        startFileIndex = startFileIndex || 0;
        for (i = startFileIndex; i < fileList.length && !foundMaximum; i++) {
            doSearchInOneFile(fileList[i], getFileContentsForFile(fileList[i]), queryExpr, maxResultsToReturn);
        }
        lastSearchedIndex = i;
    }
}

// Copied from StringUtils.js
function regexEscape(str) {
    return str.replace(/([.?*+\^$\[\]\\(){}|\-])/g, "\\$1");
}

doSearchInOneFile

Finds search results in the given file and adds them to 'results'

filepath string
text string
contents of the file
queryExpr Object
maxResultsToReturn number
the maximum of results that should be returned in the current search.
function doSearchInOneFile(filepath, text, queryExpr, maxResultsToReturn) {
    var matches = getSearchMatches(text, queryExpr);
    setResults(filepath, {matches: matches}, maxResultsToReturn);
}

documentChanged

Notification function on document changed, we update the cache with the contents

updateObject Object
function documentChanged(updateObject) {
    projectCache[updateObject.filePath] = updateObject.docContents;
}

fileCrawler

Crawls through the files in the project ans stores them in cache. Since that could take a while we do it in batches so that node wont be blocked.

function fileCrawler() {
    if (!files || (files && files.length === 0)) {
        setTimeout(fileCrawler, 1000);
        return;
    }
    var contents = "";
    if (currentCrawlIndex < files.length) {
        contents = getFileContentsForFile(files[currentCrawlIndex]);
        if (contents) {
            cacheSize += contents.length;
        }
        currentCrawlIndex++;
    }
    if (currentCrawlIndex < files.length) {
        crawlComplete = false;
        setImmediate(fileCrawler);
    } else {
        crawlComplete = true;
        if (!crawlEventSent) {
            crawlEventSent = true;
            _domainManager.emitEvent("FindInFiles", "crawlComplete", [files.length, cacheSize]);
        }
        setTimeout(fileCrawler, 1000);
    }
}

getAllResults

Gets all the results for the saved search query if present or empty search results

Returns: Object
The results object
function getAllResults() {
    var send_object = {
        "results":  {},
        "numMatches": 0,
        "foundMaximum":  foundMaximum,
        "exceedsMaximum":  exceedsMaximum
    };
    if (!savedSearchObject) {
        return send_object;
    }
    savedSearchObject.startFileIndex = 0;
    savedSearchObject.getAllResults = true;
    return doSearch(savedSearchObject);
}

getFileContentsForFile

Get the contents of a file from cache given the path. Also adds the file contents to cache from disk if not cached. Will not read/cache files greater than MAX_FILE_SIZE_TO_INDEX in size.

filePath string
full file path
Returns: string
contents or null if no contents
function getFileContentsForFile(filePath) {
    if (projectCache[filePath] || projectCache[filePath] === "") {
        return projectCache[filePath];
    }
    try {
        if (getFilesizeInBytes(filePath) <= MAX_FILE_SIZE_TO_INDEX) {
            projectCache[filePath] = fs.readFileSync(filePath, 'utf8');
        } else {
            projectCache[filePath] = "";
        }
    } catch (ex) {
        console.log(ex);
        projectCache[filePath] = null;
    }
    return projectCache[filePath];
}

getFilesizeInBytes

Gets the file size in bytes.

fileName string
The name of the file to get the size
returns
{Number} the file size in bytes
function getFilesizeInBytes(fileName) {
    try {
        var stats = fs.statSync(fileName);
        return stats.size || 0;
    } catch (ex) {
        console.log(ex);
        return 0;
    }
}

getNextPage

Gets the next page of results of the ongoing search

Returns: Object
search results
function getNextPage() {
    var send_object = {
        "results":  {},
        "numMatches": 0,
        "foundMaximum":  foundMaximum,
        "exceedsMaximum":  exceedsMaximum
    };
    if (!savedSearchObject) {
        return send_object;
    }
    savedSearchObject.startFileIndex = lastSearchedIndex;
    return doSearch(savedSearchObject, true);
}

getNumMatches

Get the total number of matches from all the files in fileList

fileList array
file path array
queryExpr Object
Returns: Number
total number of matches
function getNumMatches(fileList, queryExpr) {
    var i,
        matches = 0;
    for (i = 0; i < fileList.length; i++) {
        var temp = countNumMatches(getFileContentsForFile(fileList[i]), queryExpr);
        if (temp) {
            numFiles++;
            matches += temp;
        }
        if (matches > MAX_TOTAL_RESULTS) {
            exceedsMaximum = true;
            break;
        }
    }
    return matches;
}

getSearchMatches

Searches through the contents and returns an array of matches

contents string
queryExpr RegExp
Returns: !Array.<{start: {line:number,ch:number},end: {line:number,ch:number},line: string}>
function getSearchMatches(contents, queryExpr) {
    if (!contents) {
        return;
    }
    // Quick exit if not found or if we hit the limit
    if (foundMaximum || contents.search(queryExpr) === -1) {
        return [];
    }

    var match, lineNum, line, ch, totalMatchLength, matchedLines, numMatchedLines, lastLineLength, endCh,
        padding, leftPadding, rightPadding, highlightOffset, highlightEndCh,
        lines   = contents.split("\n"),
        matches = [];

    while ((match = queryExpr.exec(contents)) !== null) {
        lineNum          = offsetToLineNum(lines, match.index);
        line             = lines[lineNum];
        ch               = match.index - contents.lastIndexOf("\n", match.index) - 1;  // 0-based index
        matchedLines     = match[0].split("\n");
        numMatchedLines  = matchedLines.length;
        totalMatchLength = match[0].length;
        lastLineLength   = matchedLines[matchedLines.length - 1].length;
        endCh            = (numMatchedLines === 1 ? ch + totalMatchLength : lastLineLength);
        highlightEndCh   = (numMatchedLines === 1 ? endCh : line.length);
        highlightOffset  = 0;

        if (highlightEndCh <= MAX_DISPLAY_LENGTH) {
            // Don't store more than 200 chars per line
            line = line.substr(0, Math.min(MAX_DISPLAY_LENGTH, line.length));
        } else if (totalMatchLength > MAX_DISPLAY_LENGTH) {
            // impossible to display the whole match
            line = line.substr(ch, ch + MAX_DISPLAY_LENGTH);
            highlightOffset = ch;
        } else {
            // Try to have both beginning and end of match displayed
            padding = MAX_DISPLAY_LENGTH - totalMatchLength;
            rightPadding = Math.floor(Math.min(padding / 2, line.length - highlightEndCh));
            leftPadding = Math.ceil(padding - rightPadding);
            highlightOffset = ch - leftPadding;
            line = line.substring(highlightOffset, highlightEndCh + rightPadding);
        }

        matches.push({
            start:       {line: lineNum, ch: ch},
            end:         {line: lineNum + numMatchedLines - 1, ch: endCh},

            highlightOffset: highlightOffset,

            // Note that the following offsets from the beginning of the file are *not* updated if the search
            // results change. These are currently only used for multi-file replacement, and we always
            // abort the replace (by shutting the results panel) if we detect any result changes, so we don't
            // need to keep them up to date. Eventually, we should either get rid of the need for these (by
            // doing everything in terms of line/ch offsets, though that will require re-splitting files when
            // doing a replace) or properly update them.
            startOffset: match.index,
            endOffset:   match.index + totalMatchLength,

            line:        line,
            result:      match,
            isChecked:   true
        });

        // We have the max hits in just this 1 file. Stop searching this file.
        // This fixed issue #1829 where code hangs on too many hits.
        // Adds one over MAX_RESULTS_IN_A_FILE in order to know if the search has exceeded
        // or is equal to MAX_RESULTS_IN_A_FILE. Additional result removed in SearchModel
        if (matches.length > MAX_RESULTS_IN_A_FILE) {
            queryExpr.lastIndex = 0;
            break;
        }

        // Pathological regexps like /^/ return 0-length matches. Ensure we make progress anyway
        if (totalMatchLength === 0) {
            queryExpr.lastIndex++;
        }
    }

    return matches;
}
Public API

init

Initialize the test domain with commands and events related to find in files.

domainManager DomainManager
The DomainManager for the find in files domain "FindInFiles"
function init(domainManager) {
    if (!domainManager.hasDomain("FindInFiles")) {
        domainManager.registerDomain("FindInFiles", {major: 0, minor: 1});
    }
    _domainManager = domainManager;
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "doSearch",    // command name
        doSearch,   // command handler function
        false,          // this command is synchronous in Node
        "Searches in project files and returns matches",
        [{name: "searchObject", // parameters
            type: "object",
            description: "Object containing search data"}],
        [{name: "searchResults", // return values
            type: "object",
            description: "Object containing results of the search"}]
    );
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "nextPage",    // command name
        getNextPage,   // command handler function
        false,          // this command is synchronous in Node
        "get the next page of reults",
        [],
        [{name: "searchResults", // return values
            type: "object",
            description: "Object containing results of the search"}]
    );
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "getAllResults",    // command name
        getAllResults,   // command handler function
        false,          // this command is synchronous in Node
        "get the next page of reults",
        [],
        [{name: "searchResults", // return values
            type: "object",
            description: "Object containing all results of the search"}]
    );
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "collapseResults",    // command name
        setCollapseResults,   // command handler function
        false,          // this command is synchronous in Node
        "get the next page of reults",
        [{name: "collapse", // return values
            type: "boolean",
            description: "true to collapse"}],
        []
    );
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "filesChanged",    // command name
        addFilesToCache,   // command handler function
        false,          // this command is synchronous in Node
        "files in the project has been changed, update cache",
        [{name: "updateObject", // parameters
            type: "object",
            description: "Object containing list of changed files"}],
        []
    );
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "documentChanged",    // command name
        documentChanged,   // command handler function
        false,          // this command is synchronous in Node
        "informs that the document changed and updates the cache",
        [{name: "updateObject", // parameters
            type: "object",
            description: "update with the contents of the object"}],
        []
    );
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "filesRemoved",    // command name
        removeFilesFromCache,   // command handler function
        false,          // this command is synchronous in Node
        "Searches in project files and returns matches",
        [{name: "updateObject", // parameters
            type: "object",
            description: "Object containing list of removed files"}],
        []
    );
    domainManager.registerCommand(
        "FindInFiles",       // domain name
        "initCache",    // command name
        initCache,   // command handler function
        false,          // this command is synchronous in Node
        "Caches the project for find in files in node",
        [{name: "fileList", // parameters
            type: "Array",
            description: "List of all project files - Path only"}],
        []
    );
    domainManager.registerEvent(
        "FindInFiles",     // domain name
        "crawlComplete",   // event name
        [
            {
                name: "numFiles",
                type: "number",
                description: "number of files cached"
            },
            {
                name: "cacheSize",
                type: "number",
                description: "The size of the file cache epressesd as string length of files"
            }
        ]
    );
    setTimeout(fileCrawler, 5000);
}

exports.init = init;

initCache

Init for project, resets the old project cache, and sets the crawler function to restart the file crawl

fileList array
an array of files
function initCache(fileList) {
    files = fileList;
    currentCrawlIndex = 0;
    cacheSize = 0;
    clearProjectCache();
    crawlEventSent = false;
}

offsetToLineNum

Copied from StringUtils.js Returns a line number corresponding to an offset in some text. The text can be specified as a single string or as an array of strings that correspond to the lines of the string.

Specify the text in lines when repeatedly calling the function on the same text in a loop. Use getLines() to divide the text into lines, then repeatedly call this function to compute a line number from the offset.

textOrLines string,Array.<string>
- string or array of lines from which to compute the line number from the offset
offset number
Returns: number
line number
function offsetToLineNum(textOrLines, offset) {
    if (Array.isArray(textOrLines)) {
        var lines = textOrLines,
            total = 0,
            line;
        for (line = 0; line < lines.length; line++) {
            if (total < offset) {
                // add 1 per line since /n were removed by splitting, but they needed to
                // contribute to the total offset count
                total += lines[line].length + 1;
            } else if (total === offset) {
                return line;
            } else {
                return line - 1;
            }
        }

        // if offset is NOT over the total then offset is in the last line
        if (offset <= total) {
            return line - 1;
        } else {
            return undefined;
        }
    } else {
        return textOrLines.substr(0, offset).split("\n").length - 1;
    }
}

parseQueryInfo

Parses the given query into a regexp, and returns whether it was valid or not.

queryInfo {query: string,caseSensitive: boolean,isRegexp: boolean}
Returns: {queryExpr: RegExp,valid: boolean,empty: boolean,error: string}
queryExpr - the regexp representing the query valid - set to true if query is a nonempty string or a valid regexp. empty - set to true if query was empty. error - set to an error string if valid is false and query is nonempty.
function parseQueryInfo(queryInfo) {
    var queryExpr;

    // TODO: only major difference between this one and the one in FindReplace is that
    // this always returns a regexp even for simple strings. Reconcile.
    if (!queryInfo || !queryInfo.query) {
        return {empty: true};
    }

    // For now, treat all matches as multiline (i.e. ^/$ match on every line, not the whole
    // document). This is consistent with how single-file find works. Eventually we should add
    // an option for this.
    var flags = "gm";
    if (!queryInfo.isCaseSensitive) {
        flags += "i";
    }

    // Is it a (non-blank) regex?
    if (queryInfo.isRegexp) {
        try {
            queryExpr = new RegExp(queryInfo.query, flags);
        } catch (e) {
            return {valid: false, error: e.message};
        }
    } else {
        // Query is a plain string. Turn it into a regexp
        queryExpr = new RegExp(regexEscape(queryInfo.query), flags);
    }
    return {valid: true, queryExpr: queryExpr};
}

removeFilesFromCache

Remove the list of given files from the project cache

updateObject Object
function removeFilesFromCache(updateObject) {
    var fileList = updateObject.fileList || [],
        filesInSearchScope = updateObject.filesInSearchScope || [],
        i = 0;
    for (i = 0; i < fileList.length; i++) {
        delete projectCache[fileList[i]];
    }
    function isNotInRemovedFilesList(path) {
        return (filesInSearchScope.indexOf(path) === -1) ? true : false;
    }
    files = files ? files.filter(isNotInRemovedFilesList) : files;
}

setCollapseResults

Sets if the results should be collapsed

collapse boolean
true to collapse
function setCollapseResults(collapse) {
    collapseResults = collapse;
}

setResults

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.

fullpath string
Full path to the file containing the matches.
resultInfo non-nullable {matches: Object, collapsed: boolean=}
Info for the matches to set: matches - Array of matches, in the format returned by FindInFiles.getSearchMatches() collapsed - Optional: whether the results should be collapsed in the UI (default false).
function setResults(fullpath, resultInfo, maxResultsToReturn) {
    if (results[fullpath]) {
        numMatches -= results[fullpath].matches.length;
        delete results[fullpath];
    }

    if (foundMaximum || !resultInfo || !resultInfo.matches || !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 = collapseResults;

    results[fullpath] = resultInfo;
    numMatches += resultInfo.matches.length;
    evaluatedMatches += resultInfo.matches.length;
    maxResultsToReturn = maxResultsToReturn || MAX_RESULTS_TO_RETURN;
    if (numMatches >= maxResultsToReturn || evaluatedMatches > MAX_TOTAL_RESULTS) {
        foundMaximum = true;
    }
}