Skip to content

Feature/codesearch #318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 24, 2014
2 changes: 0 additions & 2 deletions addons/cb.files.editor/editor/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion client/core/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
146 changes: 146 additions & 0 deletions client/core/search/code.js
Original file line number Diff line number Diff line change
@@ -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
};
});
5 changes: 2 additions & 3 deletions client/views/dialogs/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand All @@ -92,6 +90,7 @@ define([
* (event) Modal is hidden
*/
hidden: function(e) {
this.trigger("close", this.value, e);
this.remove();
DialogView.current = null;
},
Expand Down
5 changes: 5 additions & 0 deletions core/cb.rpc.search/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
150 changes: 150 additions & 0 deletions core/cb.search/code.js
Original file line number Diff line number Diff line change
@@ -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;
39 changes: 39 additions & 0 deletions core/cb.search/files.js
Original file line number Diff line number Diff line change
@@ -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;
Loading