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.
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);
}
Clears the cached file contents of the project
function clearProjectCache() {
projectCache = [];
}
Counts the number of matches matching the queryExpr in the given contents
function countNumMatches(contents, queryExpr) {
if (!contents) {
return 0;
}
var matches = contents.match(queryExpr);
return matches ? matches.length : 0;
}
Do a search with the searchObject context and return the 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;
}
Search in the list of files given and populate the results
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");
}
Finds search results in the given file and adds them to 'results'
function doSearchInOneFile(filepath, text, queryExpr, maxResultsToReturn) {
var matches = getSearchMatches(text, queryExpr);
setResults(filepath, {matches: matches}, maxResultsToReturn);
}
Notification function on document changed, we update the cache with the contents
function documentChanged(updateObject) {
projectCache[updateObject.filePath] = updateObject.docContents;
}
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);
}
}
Gets all the results for the saved search query if present or empty search results
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);
}
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.
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];
}
Gets 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;
}
}
Gets the next page of results of the ongoing search
function getNextPage() {
var send_object = {
"results": {},
"numMatches": 0,
"foundMaximum": foundMaximum,
"exceedsMaximum": exceedsMaximum
};
if (!savedSearchObject) {
return send_object;
}
savedSearchObject.startFileIndex = lastSearchedIndex;
return doSearch(savedSearchObject, true);
}
Get the total number of matches from all the files in fileList
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;
}
Searches through the contents and returns an array of matches
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;
}
Initialize the test domain with commands and events related to find in files.
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;
Init for project, resets the old project cache, and sets the crawler function to restart the file crawl
function initCache(fileList) {
files = fileList;
currentCrawlIndex = 0;
cacheSize = 0;
clearProjectCache();
crawlEventSent = false;
}
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.
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;
}
}
Parses the given query into a regexp, and returns whether it was valid or not.
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};
}
Remove the list of given files from the project cache
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;
}
Sets if the results should be collapsed
function setCollapseResults(collapse) {
collapseResults = collapse;
}
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.
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;
}
}