diff --git a/addons/cb.files.editor/editor/view.js b/addons/cb.files.editor/editor/view.js index 39fe720c..95394a50 100644 --- a/addons/cb.files.editor/editor/view.js +++ b/addons/cb.files.editor/editor/view.js @@ -508,8 +508,6 @@ define([ updateDebugLine: function() { var position = debugManager.getPosition(); - console.log("update debug position: ", position); - // Clear previous marker if (this.debugMarker != null) { this.editor.session.removeMarker(this.debugMarker); diff --git a/client/core/app.js b/client/core/app.js index fcd95be7..9a5237f7 100644 --- a/client/core/app.js +++ b/client/core/app.js @@ -23,7 +23,8 @@ define([ 'core/search/commands', 'core/search/files', 'core/search/tags', - 'core/search/addons' + 'core/search/addons', + 'core/search/code' ], function (hr, url, dialogs, alerts, loading, GridView, templateFile, box, session, addons, box, files, commands, menu, statusbar, palette, tabs, panels, operations, localfs, themes) { diff --git a/client/core/search/code.js b/client/core/search/code.js new file mode 100644 index 00000000..4b1e1ed0 --- /dev/null +++ b/client/core/search/code.js @@ -0,0 +1,146 @@ +define([ + 'hr/promise', + 'hr/utils', + 'hr/hr', + 'models/command', + 'core/commands/menu', + 'core/backends/rpc', + 'core/search', + 'core/files', + 'utils/dialogs' +], function(Q, _, hr, Command, menu, rpc, search, files, dialogs) { + var OPTIONS = [ + 'query', 'casesensitive', 'replacement', 'pattern', 'maxresults', + 'wholeword', 'regexp', 'replaceAll' + ]; + + + // Normalize results as a buffer + var normResults = function(results) { + // Header + var buffer = 'Searching 1 file for "'+results.options.query+'"'; + if (results.options.casesensitive) buffer += " (case sensitive)" + buffer += '\n\n'; + + _.each(results.files, function(lines, path) { + buffer += path+"\n"; + _.each(lines, function(line) { + buffer += line.line+" "+line.content+"\n"; + }); + buffer += '\n\n'; + }); + + // Footer + buffer += results.matches+" matches across "+_.size(results.files)+" files"; + + return buffer; + }; + + // Do a basic search + var searchCode = function(options) { + options = _.extend({}, options || {}); + return rpc.execute("search/code", _.pick(options, OPTIONS)); + }; + + + + var searchCommandHandler = function(title, fields, forceOptions) { + return function(args) { + if (_.isString(args)) args = {'query': args}; + args = _.defaults(args || {}, {}); + + var doSearch = function(_args) { + return searchCode(_.extend(_args, forceOptions || {})) + .then(function(results) { + return normResults(results); + }) + .then(function(buffer) { + return files.openNew("Find Results", buffer); + }) + .fail(function(err) { + console.error("error", err); + }); + }; + + if (!args.query) { + return dialogs.fields(title, fields, args) + .then(doSearch); + } + + return doSearch(args); + } + }; + + + // Command search code + var commandSearch = Command.register("code.search", { + title: "Find in Files", + category: "Find", + shortcuts: [ + "mod+shift+f" + ], + action: searchCommandHandler("Find in Files", { + 'query': { + 'label': "Find", + 'type': "text" + }, + 'regexp': { + 'label': "Regular expression", + 'type': "checkbox" + }, + 'casesensitive': { + 'label': "Case sensitive", + 'type': "checkbox" + }, + 'wholeword': { + 'label': "Whole word", + 'type': "checkbox" + } + }) + }); + + // Command replace code + var commandReplace = Command.register("code.replace", { + title: "Replace in Files", + category: "Find", + shortcuts: [], + action: searchCommandHandler("Find and Replace in Files", { + 'query': { + 'label': "Find", + 'type': "text" + }, + 'replacement': { + 'label': "Replace", + 'type': "text" + }, + 'regexp': { + 'label': "Regular expression", + 'type': "checkbox" + }, + 'casesensitive': { + 'label': "Case Sensitive", + 'type': "checkbox" + }, + 'wholeword': { + 'label': "Whole word", + 'type': "checkbox" + } + }, { + replaceAll: true + }) + }) + + + // Create find menu + menu.register("find", { + title: "Find", + position: 5 + }).menuSection([ + commandSearch, + commandReplace + ]); + + return { + search: searchCode + }; +}); \ No newline at end of file diff --git a/client/views/dialogs/base.js b/client/views/dialogs/base.js index 81c593a0..67a29afc 100644 --- a/client/views/dialogs/base.js +++ b/client/views/dialogs/base.js @@ -81,9 +81,7 @@ define([ // Unbind dowument keydown $(document).unbind("keydown", this.keydownHandler); - - this.trigger("close", this.value, e); - + // Hide modal this.$el.modal('hide'); }, @@ -92,6 +90,7 @@ define([ * (event) Modal is hidden */ hidden: function(e) { + this.trigger("close", this.value, e); this.remove(); DialogView.current = null; }, diff --git a/core/cb.rpc.search/service.js b/core/cb.rpc.search/service.js index a1b0de41..3b79b034 100644 --- a/core/cb.rpc.search/service.js +++ b/core/cb.rpc.search/service.js @@ -15,5 +15,10 @@ SearchRPCService.prototype.files = function(args) { return this.search.files(args); }; +SearchRPCService.prototype.code = function(args) { + _.defaults(args, {}); + return this.search.code(args); +}; + // Exports exports.SearchRPCService = SearchRPCService; diff --git a/core/cb.search/code.js b/core/cb.search/code.js new file mode 100644 index 00000000..b5acb181 --- /dev/null +++ b/core/cb.search/code.js @@ -0,0 +1,150 @@ +var os = require("os"); +var path = require("path"); +var _ = require("lodash"); +var Q = require("q"); +var spawn = require('child_process').spawn; + +var types = require("./types"); + +var config = { + grepCmd: "grep", + perlCmd: "perl", + platform: os.platform() +}; + +var assembleCommand = function(options) { + var include = ""; + var cmd = config.grepCmd + " -s -r --color=never --binary-files=without-match -n " + + (!options.casesensitive ? "-i " : "") + + (process.platform != "darwin" ? "-P " : ""); + + if (options.pattern) { // handles grep peculiarities with --include + if (options.pattern.split(",").length > 1) + include = "{" + options.pattern + "}"; + else + include = options.pattern; + } + else { + include = (process.platform != "darwin" ? "\\" : "") + "*{" + types.PATTERN_EXT + "}"; + } + + if (options.maxresults) + cmd += "-m " + parseInt(options.maxresults, 10); + if (options.wholeword) + cmd += " -w"; + + var query = options.query; + if (!query) + return; + + // grep has a funny way of handling new lines (that is to say, it's non-existent) + // if we're not doing a regex search, then we must split everything between the + // new lines, escape the content, and then smush it back together; due to + // new lines, this is also why we're now passing -P as default to grep + if (!options.replaceAll && !options.regexp) { + var splitQuery = query.split("\\n"); + + for (var q in splitQuery) { + splitQuery[q] = types.grepEscapeRegExp(splitQuery[q]); + } + query = splitQuery.join("\\n"); + } + + query = query.replace(new RegExp("\\\'", "g"), "'\\''"); // ticks must be double escaped for BSD grep + + cmd += " " + types.PATTERN_EDIR + " " + + " --include=" + include + + " '" + query.replace(/-/g, "\\-") + "'" + + " \"" + types.escapeShell(options.path) + "\""; + + if (options.replaceAll) { + if (!options.replacement) + options.replacement = ""; + + if (options.regexp) + query = types.escapeRegExp(query); + + // pipe the grep results into perl + cmd += " -l | xargs " + config.perlCmd + + // print the grep result to STDOUT (to arrange in parseSearchResult()) + " -pi -e 'print STDOUT \"$ARGV:$.:$_\"" + + // do the actual replace + " if s/" + query + "/" + options.replacement + "/mg" + ( options.casesensitive ? "" : "i" ) + ";'"; + } + + var args = ["-c", cmd]; + args.command = "bash"; + return args; +}; + + +var search = function(root, args) { + var d = Q.defer(), prevfile = null; + var results = {}; + var nMatches = 0; + + args = _.defaults(args || {}, { + pattern: null, + casesensitive: false, + maxresults: null, + wholeword: false, + regexp: null, + + // replace + replaceAll: false, + replacement: null + }); + + if (!args.query) return Q.reject(new Error("Need a query to search for code")); + + var command = assembleCommand(_.extend({}, args, { + path: root + })); + + var proc = spawn(command.command, command); + proc.stdout.on('data', function(data) { + data = data.toString(); + var lines = data.toString().split(/([\n\r]+)/g); + + for (var i = 0, l = lines.length; i < l; ++i) { + var parts = lines[i].split(":"); + if (parts.length < 3) continue; + + var _path = path.normalize(parts.shift().replace(root, "").trimRight()); + var _line = parseInt(parts.shift()); + if (!_line) { + if (prevfile) { + results[prevfile][results[prevfile].length - 1].content += "\n\r"+lines[i]; + } + continue; + } + + prevfile = _path; + results[_path] = results[_path] || []; + results[_path].push({ + 'line': _line, + 'content': parts.join(":") + }); + nMatches = nMatches + 1; + } + }); + + proc.on('error', function(err) { + d.reject(err) + }); + proc.on('exit', function(code) { + if (code !== 0) { + d.reject(new Error("ack exited with code "+code)); + } else { + d.resolve({ + 'options': args, + 'files': results, + 'matches': nMatches + }); + } + }); + + return d.promise; +}; + +module.exports = search; \ No newline at end of file diff --git a/core/cb.search/files.js b/core/cb.search/files.js new file mode 100644 index 00000000..ed60506e --- /dev/null +++ b/core/cb.search/files.js @@ -0,0 +1,39 @@ +var _ = require('lodash'); +var glob = require("glob"); +var Q = require('q'); + +var search = function(root, args) { + var d = Q.defer(); + + args = _.defaults({}, args || {}, { + 'start': 0, + 'limit': 30 + }); + + glob("**/*"+args.query+"*", { + 'cwd': root, + 'mark': true + }, function (err, files) { + if (err) { + d.reject(err); + } else { + var results = _.chain(files) + .filter(function(path) { + return !(!path.length || path[path.length-1] == "/"); + }) + .map(function(path) { + return "/"+path; + }) + .value(); + + d.resolve({ + 'files': results.slice(args.start, args.start+args.limit), + 'n': _.size(results) + }); + } + }); + + return d.promise; +}; + +module.exports = search; \ No newline at end of file diff --git a/core/cb.search/main.js b/core/cb.search/main.js index 3ea5c097..52779378 100644 --- a/core/cb.search/main.js +++ b/core/cb.search/main.js @@ -1,53 +1,18 @@ -// Requires var _ = require('lodash'); -var glob = require("glob"); -var Q = require('q'); +var filesSearch = require("./files"); +var codeSearch = require("./code"); function setup(options, imports, register) { // Import var vfs = imports.vfs; var workspace = imports.workspace; - // Construct - var filesSearch = function(args) { - var d = Q.defer(); - - args = _.defaults({}, args || {}, { - 'start': 0, - 'limit': 30 - }); - - glob("**/*"+args.query+"*", { - 'cwd': workspace.root, - 'mark': true - }, function (err, files) { - if (err) { - d.reject(err); - } else { - var results = _.chain(files) - .filter(function(path) { - return !(!path.length || path[path.length-1] == "/"); - }) - .map(function(path) { - return "/"+path; - }) - .value(); - - d.resolve({ - 'files': results.slice(args.start, args.start+args.limit), - 'n': _.size(results) - }); - } - }); - - return d.promise; - }; - // Register register(null, { "search": { - files: filesSearch + files: _.partial(filesSearch, workspace.root), + code: _.partial(codeSearch, workspace.root) } }); } diff --git a/core/cb.search/types.js b/core/cb.search/types.js new file mode 100644 index 00000000..ea66b2ad --- /dev/null +++ b/core/cb.search/types.js @@ -0,0 +1,120 @@ +var _ = require("lodash"); + +var escapeRegExp = function(str) { + return str.replace(/([.*+?\^${}()|\[\]\/\\])/g, "\\$1"); +}; + +// taken from http://xregexp.com/ +var grepEscapeRegExp = function(str) { + return str.replace(/[[\]{}()*+?.,\\^$|#\s"']/g, "\\$&"); +}; + +var escapeShell = function(str) { + return str.replace(/([\\"'`$\s\(\)<>])/g, "\\$1"); +}; + + +// file types +var IGNORE_DIRS = { + ".bzr" : "Bazaar", + ".cdv" : "Codeville", + "~.dep" : "Interface Builder", + "~.dot" : "Interface Builder", + "~.nib" : "Interface Builder", + "~.plst" : "Interface Builder", + ".git" : "Git", + ".hg" : "Mercurial", + ".pc" : "quilt", + ".svn" : "Subversion", + "_MTN" : "Monotone", + "blib" : "Perl module building", + "CVS" : "CVS", + "RCS" : "RCS", + "SCCS" : "SCCS", + "_darcs" : "darcs", + "_sgbak" : "Vault/Fortress", + "autom4te.cache" : "autoconf", + "cover_db" : "Devel::Cover", + "_build" : "Module::Build", + "node_modules" : "Node", +}; + +var MAPPINGS = { + "actionscript": ["as", "mxml"], + "ada" : ["ada", "adb", "ads"], + "asm" : ["asm", "s"], + "batch" : ["bat", "cmd"], + //"binary" : q{Binary files, as defined by Perl's -B op (default: off)}, + "cc" : ["c", "h", "xs"], + "cfmx" : ["cfc", "cfm", "cfml"], + "clojure" : ["clj"], + "cpp" : ["cpp", "cc", "cxx", "m", "hpp", "hh", "h", "hxx"], + "csharp" : ["cs"], + "css" : ["css", "less", "scss", "sass"], + "coffee" : ["coffee"], + "elisp" : ["el"], + "erlang" : ["erl", "hrl"], + "fortran" : ["f", "f77", "f90", "f95", "f03", "for", "ftn", "fpp"], + "haskell" : ["hs", "lhs"], + "hh" : ["h"], + "html" : ["htm", "html", "shtml", "xhtml"], + "jade" : ["jade"], + "java" : ["java", "properties"], + "groovy" : ["groovy"], + "js" : ["js"], + "json" : ["json"], + "latex" : ["latex", "ltx"], + "jsp" : ["jsp", "jspx", "jhtm", "jhtml"], + "lisp" : ["lisp", "lsp"], + "logiql" : ["logic", "lql"], + "lua" : ["lua"], + "make" : ["makefile", "Makefile"], + "mason" : ["mas", "mhtml", "mpl", "mtxt"], + "markdown" : ["md", "markdown"], + "objc" : ["m", "h"], + "objcpp" : ["mm", "h"], + "ocaml" : ["ml", "mli"], + "parrot" : ["pir", "pasm", "pmc", "ops", "pod", "pg", "tg"], + "perl" : ["pl", "pm", "pod", "t"], + "php" : ["php", "phpt", "php3", "php4", "php5", "phtml"], + "plone" : ["pt", "cpt", "metadata", "cpy", "py"], + "powershell" : ["ps1"], + "python" : ["py"], + "rake" : ["rakefile"], + "ruby" : ["rb", "ru", "rhtml", "rjs", "rxml", "erb", "rake", "gemspec"], + "scala" : ["scala"], + "scheme" : ["scm", "ss"], + "shell" : ["sh", "bash", "csh", "tcsh", "ksh", "zsh"], + //"skipped" : "q"{"Files but not directories normally skipped by ack ("default": "off")}, + "smalltalk" : ["st"], + "sql" : ["sql", "ctl"], + "tcl" : ["tcl", "itcl", "itk"], + "tex" : ["tex", "cls", "sty"], + "text" : ["txt"], + "textile" : ["textile"], + "tt" : ["tt", "tt2", "ttml"], + "vb" : ["bas", "cls", "frm", "ctl", "vb", "resx"], + "vim" : ["vim"], + "yaml" : ["yaml", "yml"], + "xml" : ["xml", "dtd", "xslt", "ent", "rdf", "rss", "svg", "wsdl", "atom", "mathml", "mml"] +}; +var exts = []; +for (var type in MAPPINGS) { + exts = exts.concat(MAPPINGS[type]); +} +// grep pattern matching for extensions +var PATTERN_EXT = _.unique(exts).join(","); + +var dirs = _.keys(IGNORE_DIRS); +var PATTERN_DIR = escapeRegExp(dirs.join("|")); +var PATTERN_EDIR = " --exclude-dir="+dirs.join(" --exclude-dir="); + +module.exports = { + 'grepEscapeRegExp': grepEscapeRegExp, + 'escapeRegExp': escapeRegExp, + 'escapeShell': escapeShell, + + 'PATTERN_EXT': PATTERN_EXT, + 'PATTERN_DIR': PATTERN_DIR, + 'PATTERN_EDIR': PATTERN_EDIR +}; \ No newline at end of file