/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/search/FindUtils.js
552 строки
25 KB
Marcel Gerber
Merge pull request #12027 from ficristo/parseQuery-cleanup
18 авг 2016, 13:06
18 авг 2016, 13:06
b1a24ba
Код
Авторство
О чём код?
/* * 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 Async = require("utils/Async"), DocumentManager = require("document/DocumentManager"), MainViewManager = require("view/MainViewManager"), FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), ProjectManager = require("project/ProjectManager"), PreferencesManager = require("preferences/PreferencesManager"), EventDispatcher = require("utils/EventDispatcher"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), _ = require("thirdparty/lodash"); var nodeSearchDisabled = false, instantSearchDisabled = false, indexingInProgress = false, nodeSearchCount = 0, collapseResults = false; EventDispatcher.makeEventDispatcher(exports); // define preferences for find in files PreferencesManager.definePreference("findInFiles.nodeSearch", "boolean", true, { description: Strings.DESCRIPTION_FIND_IN_FILES_NODE }); PreferencesManager.definePreference("findInFiles.instantSearch", "boolean", true, { description: Strings.DESCRIPTION_FIND_IN_FILES_INSTANT }); /** * returns true if the used disabled node based search in his preferences * @return {boolean} */ function _prefNodeSearchDisabled() { return !PreferencesManager.get("findInFiles.nodeSearch"); } /** * returns true if the used instant search in his preferences * @return {boolean} */ function _prefInstantSearchDisabled() { return !PreferencesManager.get("findInFiles.instantSearch"); } /** * Given a replace string that contains $-expressions, replace them with data from the given * regexp match info. * NOTE: we can't just use the ordinary replace() function here because the string has been * extracted from the original text and so might be missing some context that the regexp matched. * @param {string} replaceWith The string containing the $-expressions. * @param {Object} match The match data from the regexp. * @return {string} The replace text with the $-expressions substituted. */ function parseDollars(replaceWith, match) { replaceWith = replaceWith.replace(/(\$+)(\d{1,2}|&)/g, function (whole, dollars, index) { if (dollars.length % 2 === 1) { // make sure dollar signs don't escape themselves (like $$1, $$$$&) if (index === "&") { // handle $& // slice the first dollar (but leave any others to get unescaped below) and return the // whole match return dollars.substr(1) + (match[0] || ""); } else { // now we're sure index is an integer, so we can parse it var parsedIndex = parseInt(index, 10); if (parsedIndex !== 0) { // handle $n or $nn, but don't handle $0 or $00 // slice the first dollar (but leave any others to get unescaped below) and return the // the corresponding match return dollars.substr(1) + (match[parsedIndex] || ""); } } } // this code gets called if the dollar signs escape themselves or if $0/$00 (not handled) was present return whole; // return everything to get handled below }); // replace escaped dollar signs (i.e. $$, $$$$, ...) with single ones (unescaping) replaceWith = replaceWith.replace(/\$\$/g, "$"); return replaceWith; } /** * Does a set of replacements in a single document in memory. * @param {!Document} doc The document to do the replacements in. * @param {Object} matchInfo The match info for this file, as returned by `_addSearchMatches()`. Might be mutated. * @param {string} replaceText The text to replace each result with. * @param {boolean=} isRegexp Whether the original query was a regexp. * @return {$.Promise} A promise that's resolved when the replacement is finished or rejected with an error if there were one or more errors. */ function _doReplaceInDocument(doc, matchInfo, replaceText, isRegexp) { // Double-check that the open document's timestamp matches the one we recorded. This // should normally never go out of sync, because if it did we wouldn't start the // replace in the first place (due to the fact that we immediately close the search // results panel whenever we detect a filesystem change that affects the results), // but we want to double-check in case we don't happen to get the change in time. // This will *not* handle cases where the document has been edited in memory since // the matchInfo was generated. if (doc.diskTimestamp.getTime() !== matchInfo.timestamp.getTime()) { return new $.Deferred().reject(exports.ERROR_FILE_CHANGED).promise(); } // Do the replacements in reverse document order so the offsets continue to be correct. doc.batchOperation(function () { matchInfo.matches.reverse().forEach(function (match) { if (match.isChecked) { doc.replaceRange(isRegexp ? parseDollars(replaceText, match.result) : replaceText, match.start, match.end); } }); }); return new $.Deferred().resolve().promise(); } /** * Does a set of replacements in a single file on disk. * @param {string} fullPath The full path to the file. * @param {Object} matchInfo The match info for this file, as returned by `_addSearchMatches()`. * @param {string} replaceText The text to replace each result with. * @param {boolean=} isRegexp Whether the original query was a regexp. * @return {$.Promise} A promise that's resolved when the replacement is finished or rejected with an error if there were one or more errors. */ function _doReplaceOnDisk(fullPath, matchInfo, replaceText, isRegexp) { var file = FileSystem.getFileForPath(fullPath); return DocumentManager.getDocumentText(file, true).then(function (contents, timestamp, lineEndings) { if (timestamp.getTime() !== matchInfo.timestamp.getTime()) { // Return a promise that we'll reject immediately. (We can't just return the // error since this is the success handler.) return new $.Deferred().reject(exports.ERROR_FILE_CHANGED).promise(); } // Note that this assumes that the matches are sorted. // TODO: is there a more efficient way to do this in a large string? var result = [], lastIndex = 0; matchInfo.matches.forEach(function (match) { if (match.isChecked) { result.push(contents.slice(lastIndex, match.startOffset)); result.push(isRegexp ? parseDollars(replaceText, match.result) : replaceText); lastIndex = match.endOffset; } }); result.push(contents.slice(lastIndex)); var newContents = result.join(""); // TODO: duplicated logic from Document - should refactor this? if (lineEndings === FileUtils.LINE_ENDINGS_CRLF) { newContents = newContents.replace(/\n/g, "\r\n"); } return Async.promisify(file, "write", newContents); }); } /** * Does a set of replacements in a single file. If the file is already open in a Document in memory, * will do the replacement there, otherwise does it directly on disk. * @param {string} fullPath The full path to the file. * @param {Object} matchInfo The match info for this file, as returned by `_addSearchMatches()`. * @param {string} replaceText The text to replace each result with. * @param {Object=} options An options object: * forceFilesOpen: boolean - Whether to open the file in an editor and do replacements there rather than doing the * replacements on disk. Note that even if this is false, files that are already open in editors will have replacements * done in memory. * isRegexp: boolean - Whether the original query was a regexp. If true, $-substitution is performed on the replaceText. * @return {$.Promise} A promise that's resolved when the replacement is finished or rejected with an error if there were one or more errors. */ function _doReplaceInOneFile(fullPath, matchInfo, replaceText, options) { var doc = DocumentManager.getOpenDocumentForPath(fullPath); options = options || {}; // If we're forcing files open, or if the document is in the working set but not actually open // yet, we want to open the file and do the replacement in memory. if (!doc && (options.forceFilesOpen || MainViewManager.findInWorkingSet(MainViewManager.ALL_PANES, fullPath) !== -1)) { return DocumentManager.getDocumentForPath(fullPath).then(function (newDoc) { return _doReplaceInDocument(newDoc, matchInfo, replaceText, options.isRegexp); }); } else if (doc) { return _doReplaceInDocument(doc, matchInfo, replaceText, options.isRegexp); } else { return _doReplaceOnDisk(fullPath, matchInfo, replaceText, options.isRegexp); } } /** * @private * Returns true if a search result has any checked matches. */ function hasCheckedMatches(result) { return result.matches.some(function (match) { return match.isChecked; }); } /** * Given a set of search results, replaces them with the given replaceText, either on disk or in memory. * Checks timestamps to ensure replacements are not performed in files that have changed on disk since * the original search results were generated. However, does *not* check whether edits have been performed * in in-memory documents since the search; it's up to the caller to guarantee this hasn't happened. * (When called from the standard Find in Files UI, SearchResultsView guarantees this. If called headlessly, * the caller needs to track changes.) * * Replacements in documents that are already open in memory at the start of the replacement are guaranteed to * happen synchronously; replacements in files on disk will return an error if the on-disk file changes between * the time performReplacements() is called and the time the replacement actually happens. * * @param {Object.<fullPath: string, {matches: Array.<{start: {line:number,ch:number}, end: {line:number,ch:number}, startOffset: number, endOffset: number, line: string}>, collapsed: boolean}>} results * The list of results to replace, as returned from _doSearch.. * @param {string} replaceText The text to replace each result with. * @param {?Object} options An options object: * forceFilesOpen: boolean - Whether to open all files in editors and do replacements there rather than doing the * replacements on disk. Note that even if this is false, files that are already open in editors will have replacements * done in memory. * isRegexp: boolean - Whether the original query was a regexp. If true, $-substitution is performed on the replaceText. * @return {$.Promise} A promise that's resolved when the replacement is finished or rejected with an array of errors * if there were one or more errors. Each individual item in the array will be a {item: string, error: string} object, * where item is the full path to the file that could not be updated, and error is either a FileSystem error or one * of the `FindUtils.ERROR_*` constants. */ function performReplacements(results, replaceText, options) { return Async.doInParallel_aggregateErrors(Object.keys(results), function (fullPath) { return _doReplaceInOneFile(fullPath, results[fullPath], replaceText, options); }).done(function () { if (options && options.forceFilesOpen) { // If the currently selected document wasn't modified by the search, or there is no open document, // then open the first modified document. var doc = DocumentManager.getCurrentDocument(); if (!doc || !results[doc.file.fullPath] || !hasCheckedMatches(results[doc.file.fullPath])) { // Figure out the first modified document. This logic is slightly different from // SearchResultsView._getSortedFiles() because it doesn't sort the currently open file to // the top. But if the currently open file were in the search results, we wouldn't be // doing this anyway. var sortedPaths = Object.keys(results).sort(FileUtils.comparePaths), firstPath = _.find(sortedPaths, function (path) { return hasCheckedMatches(results[path]); }); if (firstPath) { var newDoc = DocumentManager.getOpenDocumentForPath(firstPath); // newDoc might be null if the replacement failed. if (newDoc) { // @todo change the `_edit` call to this: // /// CommandManager.execute(Commands.FILE_OPEN, {fullPath: firstPath}); // // The problem with doing that is that the promise returned by this // function has already been resolved by `Async.doInParallel()` and // `CommandManager.execute` is an asynchronous operation. // An asynchronous open can't be waited on (since the promise has been // resolved already) so use the synchronous version so that the next `done` // handler is blocked until the open completes MainViewManager._edit(MainViewManager.ACTIVE_PANE, newDoc); } } } } }); } /** * Returns label text to indicate the search scope. Already HTML-escaped. * @param {?Entry} scope * @return {string} */ function labelForScope(scope) { if (scope) { return StringUtils.format( Strings.FIND_IN_FILES_SCOPED, StringUtils.breakableUrl( ProjectManager.makeProjectRelativeIfPossible(scope.fullPath) ) ); } else { return Strings.FIND_IN_FILES_NO_SCOPE; } } /** * Parses the given query into a regexp, and returns whether it was valid or not. * @param {{query: string, caseSensitive: boolean, isRegexp: boolean}} queryInfo * @return {{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; 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(StringUtils.regexEscape(queryInfo.query), flags); } return {valid: true, queryExpr: queryExpr}; } /** * Prioritizes the open file and then the working set files to the starting of the list of files * @param {Array.<*>} files An array of file paths or file objects to sort * @param {?string} firstFile If specified, the path to the file that should be sorted to the top. * @return {Array.<*>} */ function prioritizeOpenFile(files, firstFile) { var workingSetFiles = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES), workingSetFileFound = {}, fileSetWithoutWorkingSet = [], startingWorkingFileSet = [], propertyName = "", i = 0; 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 = files.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); } /** * Returns the path of the currently open file or null if there isn't one open * @return {?string} */ function getOpenFilePath() { var currentDoc = DocumentManager.getCurrentDocument(); return currentDoc ? currentDoc.file.fullPath : null; } /** * enable/disable instant search * @param {boolean} disable true to disable node based search */ function setInstantSearchDisabled(disable) { instantSearchDisabled = disable; } /** * if instant search is disabled, this will return true we can only do instant search through node * @return {boolean} */ function isInstantSearchDisabled() { return _prefNodeSearchDisabled() || _prefInstantSearchDisabled() || nodeSearchDisabled || instantSearchDisabled; } /** * enable/disable node based search * @param {boolean} disable true to disable node based search */ function setNodeSearchDisabled(disable) { if (disable) { // only set disable. Enabling node earch doesnt mean we have to enable instant search. setInstantSearchDisabled(disable); } nodeSearchDisabled = disable; } /** * if node search is disabled, this will return true * @return {boolean} */ function isNodeSearchDisabled() { return _prefNodeSearchDisabled() || nodeSearchDisabled; } /** * check if a search is progressing in node * @return {Boolean} true if search is processing in node */ function isNodeSearchInProgress() { if (nodeSearchCount === 0) { return false; } else { return true; } } /** * Raises an event when the file filters applied to a search changes */ function notifyFileFiltersChanged() { exports.trigger(exports.SEARCH_FILE_FILTERS_CHANGED); } /** * Raises an event when the search scope changes[say search in a sub drictory in the project] */ function notifySearchScopeChanged() { exports.trigger(exports.SEARCH_SCOPE_CHANGED); } /** * Notifies that a node search has started so that we FindUtils can figure out * if any outstanding node search requests are pendind */ function notifyNodeSearchStarted() { nodeSearchCount++; } /** * Notifies that a node search has finished so that we FindUtils can figure out * if any outstanding node search requests are pendind */ function notifyNodeSearchFinished() { nodeSearchCount--; } /** * Notifies that a node has started indexing the files */ function notifyIndexingStarted() { indexingInProgress = true; exports.trigger(exports.SEARCH_INDEXING_STARTED); } /** * Notifies that a node has finished indexing the files */ function notifyIndexingFinished() { indexingInProgress = false; exports.trigger(exports.SEARCH_INDEXING_FINISHED); } /** * Return true if indexing is in pregress in node * @return {boolean} true if files are being indexed in node */ function isIndexingInProgress() { return indexingInProgress; } /** * Set if we need to collapse all results in the results pane * @param {boolean} collapse true to collapse */ function setCollapseResults(collapse) { collapseResults = collapse; exports.trigger(exports.SEARCH_COLLAPSE_RESULTS); } /** * check if results should be collapsed * @return {boolean} true if results should be collapsed */ function isCollapsedResults() { return collapseResults; } /** * Returns the health data pertaining to Find in files */ function getHealthReport() { return { prefNodeSearchDisabled : _prefNodeSearchDisabled(), prefInstantSearchDisabled : _prefInstantSearchDisabled() }; } exports.parseDollars = parseDollars; exports.hasCheckedMatches = hasCheckedMatches; exports.performReplacements = performReplacements; exports.labelForScope = labelForScope; exports.parseQueryInfo = parseQueryInfo; exports.prioritizeOpenFile = prioritizeOpenFile; exports.getOpenFilePath = getOpenFilePath; exports.setNodeSearchDisabled = setNodeSearchDisabled; exports.isNodeSearchDisabled = isNodeSearchDisabled; exports.setInstantSearchDisabled = setInstantSearchDisabled; exports.isInstantSearchDisabled = isInstantSearchDisabled; exports.isNodeSearchInProgress = isNodeSearchInProgress; exports.isIndexingInProgress = isIndexingInProgress; exports.setCollapseResults = setCollapseResults; exports.isCollapsedResults = isCollapsedResults; exports.getHealthReport = getHealthReport; exports.ERROR_FILE_CHANGED = "fileChanged"; // event notification functions exports.notifyFileFiltersChanged = notifyFileFiltersChanged; exports.notifySearchScopeChanged = notifySearchScopeChanged; exports.notifyNodeSearchStarted = notifyNodeSearchStarted; exports.notifyNodeSearchFinished = notifyNodeSearchFinished; exports.notifyIndexingStarted = notifyIndexingStarted; exports.notifyIndexingFinished = notifyIndexingFinished; // events raised by FindUtils exports.SEARCH_FILE_FILTERS_CHANGED = "fileFiltersChanged"; exports.SEARCH_SCOPE_CHANGED = "searchScopeChanged"; exports.SEARCH_INDEXING_STARTED = "searchIndexingStarted"; exports.SEARCH_INDEXING_FINISHED = "searchIndexingFinished"; exports.SEARCH_COLLAPSE_RESULTS = "searchCollapseResults"; });