@@ -47,6 +47,7 @@ define(function (require, exports, module) {
4747 Commands = require ( "command/Commands" ) ,
4848 Strings = require ( "strings" ) ,
4949 StringUtils = require ( "utils/StringUtils" ) ,
50+ ProjectManager = require ( "project/ProjectManager" ) ,
5051 DocumentManager = require ( "document/DocumentManager" ) ,
5152 EditorManager = require ( "editor/EditorManager" ) ,
5253 FileIndexManager = require ( "project/FileIndexManager" ) ,
@@ -86,6 +87,21 @@ define(function (require, exports, module) {
8687 return new RegExp ( query , "gi" ) ;
8788 }
8889
90+ /**
91+ * Returns label text to indicate the search scope. Already HTML-escaped.
92+ * @param {?Entry } scope
93+ */
94+ function _labelForScope ( scope ) {
95+ var projName = ProjectManager . getProjectRoot ( ) . name ;
96+ if ( scope ) {
97+ var displayPath = StringUtils . htmlEscape ( ProjectManager . makeProjectRelativeIfPossible ( scope . fullPath ) ) ;
98+ return StringUtils . format ( Strings . FIND_IN_FILES_SCOPED , displayPath ) ;
99+ } else {
100+ return Strings . FIND_IN_FILES_NO_SCOPE ;
101+ }
102+ }
103+
104+
89105 // This dialog class was mostly copied from QuickOpen. We should have a common dialog
90106 // class that everyone can use.
91107
@@ -126,12 +142,14 @@ define(function (require, exports, module) {
126142 /**
127143 * Shows the search dialog
128144 * @param {?string } initialString Default text to prepopulate the search field with
145+ * @param {?Entry } scope Search scope, or null to search whole proj
129146 * @returns {$.Promise } that is resolved with the string to search for
130147 */
131- FindInFilesDialog . prototype . showDialog = function ( initialString ) {
132- var dialogHTML = Strings . CMD_FIND_IN_FILES +
133- ": <input type='text' id='findInFilesInput' style='width: 10em'> <span style='color: #888'>(" +
134- Strings . SEARCH_REGEXP_INFO + ")</span>" ;
148+ FindInFilesDialog . prototype . showDialog = function ( initialString , scope ) {
149+ // Note the prefix label is a simple "Find:" - the "in ..." part comes after the text field
150+ var dialogHTML = Strings . CMD_FIND +
151+ ": <input type='text' id='findInFilesInput' style='width: 10em'> <span id='findInFilesScope'></span> " +
152+ "<span style='color: #888'>(" + Strings . SEARCH_REGEXP_INFO + ")</span>" ;
135153 this . result = new $ . Deferred ( ) ;
136154 this . _createDialogDiv ( dialogHTML ) ;
137155 var $searchField = $ ( "input#findInFilesInput" ) ;
@@ -140,6 +158,8 @@ define(function (require, exports, module) {
140158 $searchField . attr ( "value" , initialString || "" ) ;
141159 $searchField . get ( 0 ) . select ( ) ;
142160
161+ $ ( "#findInFilesScope" ) . html ( _labelForScope ( scope ) ) ;
162+
143163 $searchField . bind ( "keydown" , function ( event ) {
144164 if ( event . keyCode === KeyEvent . DOM_VK_RETURN || event . keyCode === KeyEvent . DOM_VK_ESCAPE ) { // Enter/Return key or Esc key
145165 event . stopPropagation ( ) ;
@@ -209,7 +229,7 @@ define(function (require, exports, module) {
209229 return matches ;
210230 }
211231
212- function _showSearchResults ( searchResults , query ) {
232+ function _showSearchResults ( searchResults , query , scope ) {
213233 var $searchResultsDiv = $ ( "#search-results" ) ;
214234
215235 if ( searchResults && searchResults . length ) {
@@ -229,17 +249,19 @@ define(function (require, exports, module) {
229249 }
230250 numMatchesStr += String ( numMatches ) ;
231251
252+ // This text contains some formatting, so all the strings are assumed to be already escaped
232253 var summary = StringUtils . format (
233254 Strings . FIND_IN_FILES_TITLE ,
234255 numMatchesStr ,
235256 ( numMatches > 1 ) ? Strings . FIND_IN_FILES_MATCHES : Strings . FIND_IN_FILES_MATCH ,
236257 searchResults . length ,
237258 ( searchResults . length > 1 ? Strings . FIND_IN_FILES_FILES : Strings . FIND_IN_FILES_FILE ) ,
238- query
259+ query ,
260+ scope ? _labelForScope ( scope ) : ""
239261 ) ;
240262
241263 $ ( "#search-result-summary" )
242- . text ( summary +
264+ . html ( summary +
243265 ( numMatches > FIND_IN_FILES_MAX ? StringUtils . format ( Strings . FIND_IN_FILES_MAX , FIND_IN_FILES_MAX ) : "" ) )
244266 . prepend ( " " ) ; // putting a normal space before the "-" is not enough
245267
@@ -251,19 +273,16 @@ define(function (require, exports, module) {
251273 return $ ( "<td/>" ) . html ( content ) ;
252274 } ;
253275
254- var esc = function ( str ) {
255- str = str . replace ( / < / g, "<" ) ;
256- str = str . replace ( / > / g, ">" ) ;
257- return str ;
258- } ;
276+ // shorthand function name
277+ var esc = StringUtils . htmlEscape ;
259278
260279 var highlightMatch = function ( line , start , end ) {
261280 return esc ( line . substr ( 0 , start ) ) + "<span class='highlight'>" + esc ( line . substring ( start , end ) ) + "</span>" + esc ( line . substr ( end ) ) ;
262281 } ;
263282
264283 // Add row for file name
265284 $ ( "<tr class='file-section' />" )
266- . append ( "<td colspan='3'>" + StringUtils . format ( Strings . FIND_IN_FILES_FILE_PATH , StringUtils . breakableUrl ( item . fullPath ) ) + "</td>" )
285+ . append ( "<td colspan='3'>" + StringUtils . format ( Strings . FIND_IN_FILES_FILE_PATH , StringUtils . breakableUrl ( esc ( item . fullPath ) ) ) + "</td>" )
267286 . click ( function ( ) {
268287 // Clicking file section header collapses/expands result rows for that file
269288 var $fileHeader = $ ( this ) ;
@@ -313,11 +332,30 @@ define(function (require, exports, module) {
313332 EditorManager . resizeEditor ( ) ;
314333 }
315334
335+ /**
336+ * @param {!FileInfo } fileInfo File in question
337+ * @param {?Entry } scope Search scope, or null if whole project
338+ * @return {boolean }
339+ */
340+ function inScope ( fileInfo , scope ) {
341+ if ( scope ) {
342+ if ( scope . isDirectory ) {
343+ // Dirs always have trailing slash, so we don't have to worry about being
344+ // a substring of another dir name
345+ return fileInfo . fullPath . indexOf ( scope . fullPath ) === 0 ;
346+ } else {
347+ return fileInfo . fullPath === scope . fullPath ;
348+ }
349+ }
350+ return true ;
351+ }
352+
316353 /**
317354 * Displays a non-modal embedded dialog above the code mirror editor that allows the user to do
318355 * a find operation across all files in the project.
356+ * @param {?Entry } scope Project file/subfolder to search within; else searches whole project.
319357 */
320- function doFindInFiles ( ) {
358+ function doFindInFiles ( scope ) {
321359
322360 var dialog = new FindInFilesDialog ( ) ;
323361
@@ -328,7 +366,7 @@ define(function (require, exports, module) {
328366 searchResults = [ ] ;
329367 maxHitsFoundInFile = false ;
330368
331- dialog . showDialog ( initialString )
369+ dialog . showDialog ( initialString , scope )
332370 . done ( function ( query ) {
333371 if ( query ) {
334372 var queryExpr = _getQueryRegExp ( query ) ;
@@ -341,28 +379,33 @@ define(function (require, exports, module) {
341379 Async . doInParallel ( fileListResult , function ( fileInfo ) {
342380 var result = new $ . Deferred ( ) ;
343381
344- DocumentManager . getDocumentForPath ( fileInfo . fullPath )
345- . done ( function ( doc ) {
346- var matches = _getSearchMatches ( doc . getText ( ) , queryExpr ) ;
347-
348- if ( matches && matches . length ) {
349- searchResults . push ( {
350- fullPath : fileInfo . fullPath ,
351- matches : matches
352- } ) ;
353- }
354- result . resolve ( ) ;
355- } )
356- . fail ( function ( error ) {
357- // Error reading this file. This is most likely because the file isn't a text file.
358- // Resolve here so we move on to the next file.
359- result . resolve ( ) ;
360- } ) ;
361-
382+ if ( ! inScope ( fileInfo , scope ) ) {
383+ result . resolve ( ) ;
384+ } else {
385+ // Search one file
386+ DocumentManager . getDocumentForPath ( fileInfo . fullPath )
387+ . done ( function ( doc ) {
388+ var matches = _getSearchMatches ( doc . getText ( ) , queryExpr ) ;
389+
390+ if ( matches && matches . length ) {
391+ searchResults . push ( {
392+ fullPath : fileInfo . fullPath ,
393+ matches : matches
394+ } ) ;
395+ }
396+ result . resolve ( ) ;
397+ } )
398+ . fail ( function ( error ) {
399+ // Error reading this file. This is most likely because the file isn't a text file.
400+ // Resolve here so we move on to the next file.
401+ result . resolve ( ) ;
402+ } ) ;
403+ }
362404 return result . promise ( ) ;
363405 } )
364406 . done ( function ( ) {
365- _showSearchResults ( searchResults , query ) ;
407+ // Done searching all files: show results
408+ _showSearchResults ( searchResults , query , scope ) ;
366409 StatusBar . hideBusyIndicator ( ) ;
367410 } )
368411 . fail ( function ( ) {
@@ -374,11 +417,23 @@ define(function (require, exports, module) {
374417 } ) ;
375418 }
376419
420+ /** Search within the file/subtree defined by the sidebar selection */
421+ function doFindInSubtree ( ) {
422+ // Prefer project tree selection, else use working set selection
423+ var selectedEntry = ProjectManager . getSelectedItem ( ) ;
424+ if ( ! selectedEntry ) {
425+ var doc = DocumentManager . getCurrentDocument ( ) ;
426+ selectedEntry = ( doc && doc . file ) ;
427+ }
428+
429+ doFindInFiles ( selectedEntry ) ;
430+ }
431+
432+
377433 // Initialize items dependent on HTML DOM
378434 AppInit . htmlReady ( function ( ) {
379435 var $searchResults = $ ( "#search-results" ) ,
380436 $searchContent = $ ( "#search-results .table-container" ) ;
381-
382437 } ) ;
383438
384439 function _fileNameChangeHandler ( event , oldName , newName ) {
@@ -392,5 +447,7 @@ define(function (require, exports, module) {
392447 }
393448
394449 $ ( DocumentManager ) . on ( "fileNameChange" , _fileNameChangeHandler ) ;
395- CommandManager . register ( Strings . CMD_FIND_IN_FILES , Commands . EDIT_FIND_IN_FILES , doFindInFiles ) ;
450+
451+ CommandManager . register ( Strings . CMD_FIND_IN_FILES , Commands . EDIT_FIND_IN_FILES , doFindInFiles ) ;
452+ CommandManager . register ( Strings . CMD_FIND_IN_SUBTREE , Commands . EDIT_FIND_IN_SUBTREE , doFindInSubtree ) ;
396453} ) ;
0 commit comments