Skip to content

Commit 162f3c5

Browse files
committed
Merge pull request #318 from FriendCode/feature/codesearch
Feature/codesearch
2 parents 1feb58f + 165075c commit 162f3c5

File tree

9 files changed

+468
-45
lines changed

9 files changed

+468
-45
lines changed

addons/cb.files.editor/editor/view.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,6 @@ define([
508508
updateDebugLine: function() {
509509
var position = debugManager.getPosition();
510510

511-
console.log("update debug position: ", position);
512-
513511
// Clear previous marker
514512
if (this.debugMarker != null) {
515513
this.editor.session.removeMarker(this.debugMarker);

client/core/app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ define([
2323
'core/search/commands',
2424
'core/search/files',
2525
'core/search/tags',
26-
'core/search/addons'
26+
'core/search/addons',
27+
'core/search/code'
2728
], function (hr, url, dialogs, alerts, loading, GridView, templateFile,
2829
box, session, addons, box, files, commands, menu, statusbar, palette, tabs, panels, operations, localfs, themes) {
2930

client/core/search/code.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
define([
2+
'hr/promise',
3+
'hr/utils',
4+
'hr/hr',
5+
'models/command',
6+
'core/commands/menu',
7+
'core/backends/rpc',
8+
'core/search',
9+
'core/files',
10+
'utils/dialogs'
11+
], function(Q, _, hr, Command, menu, rpc, search, files, dialogs) {
12+
var OPTIONS = [
13+
'query', 'casesensitive', 'replacement', 'pattern', 'maxresults',
14+
'wholeword', 'regexp', 'replaceAll'
15+
];
16+
17+
18+
// Normalize results as a buffer
19+
var normResults = function(results) {
20+
// Header
21+
var buffer = 'Searching 1 file for "'+results.options.query+'"';
22+
if (results.options.casesensitive) buffer += " (case sensitive)"
23+
buffer += '\n\n';
24+
25+
_.each(results.files, function(lines, path) {
26+
buffer += path+"\n";
27+
_.each(lines, function(line) {
28+
buffer += line.line+" "+line.content+"\n";
29+
});
30+
buffer += '\n\n';
31+
});
32+
33+
// Footer
34+
buffer += results.matches+" matches across "+_.size(results.files)+" files";
35+
36+
return buffer;
37+
};
38+
39+
// Do a basic search
40+
var searchCode = function(options) {
41+
options = _.extend({}, options || {});
42+
return rpc.execute("search/code", _.pick(options, OPTIONS));
43+
};
44+
45+
46+
47+
var searchCommandHandler = function(title, fields, forceOptions) {
48+
return function(args) {
49+
if (_.isString(args)) args = {'query': args};
50+
args = _.defaults(args || {}, {});
51+
52+
var doSearch = function(_args) {
53+
return searchCode(_.extend(_args, forceOptions || {}))
54+
.then(function(results) {
55+
return normResults(results);
56+
})
57+
.then(function(buffer) {
58+
return files.openNew("Find Results", buffer);
59+
})
60+
.fail(function(err) {
61+
console.error("error", err);
62+
});
63+
};
64+
65+
if (!args.query) {
66+
return dialogs.fields(title, fields, args)
67+
.then(doSearch);
68+
}
69+
70+
return doSearch(args);
71+
}
72+
};
73+
74+
75+
// Command search code
76+
var commandSearch = Command.register("code.search", {
77+
title: "Find in Files",
78+
category: "Find",
79+
shortcuts: [
80+
"mod+shift+f"
81+
],
82+
action: searchCommandHandler("Find in Files", {
83+
'query': {
84+
'label': "Find",
85+
'type': "text"
86+
},
87+
'regexp': {
88+
'label': "Regular expression",
89+
'type': "checkbox"
90+
},
91+
'casesensitive': {
92+
'label': "Case sensitive",
93+
'type': "checkbox"
94+
},
95+
'wholeword': {
96+
'label': "Whole word",
97+
'type': "checkbox"
98+
}
99+
})
100+
});
101+
102+
// Command replace code
103+
var commandReplace = Command.register("code.replace", {
104+
title: "Replace in Files",
105+
category: "Find",
106+
shortcuts: [],
107+
action: searchCommandHandler("Find and Replace in Files", {
108+
'query': {
109+
'label': "Find",
110+
'type': "text"
111+
},
112+
'replacement': {
113+
'label': "Replace",
114+
'type': "text"
115+
},
116+
'regexp': {
117+
'label': "Regular expression",
118+
'type': "checkbox"
119+
},
120+
'casesensitive': {
121+
'label': "Case Sensitive",
122+
'type': "checkbox"
123+
},
124+
'wholeword': {
125+
'label': "Whole word",
126+
'type': "checkbox"
127+
}
128+
}, {
129+
replaceAll: true
130+
})
131+
})
132+
133+
134+
// Create find menu
135+
menu.register("find", {
136+
title: "Find",
137+
position: 5
138+
}).menuSection([
139+
commandSearch,
140+
commandReplace
141+
]);
142+
143+
return {
144+
search: searchCode
145+
};
146+
});

client/views/dialogs/base.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ define([
8181

8282
// Unbind dowument keydown
8383
$(document).unbind("keydown", this.keydownHandler);
84-
85-
this.trigger("close", this.value, e);
86-
84+
8785
// Hide modal
8886
this.$el.modal('hide');
8987
},
@@ -92,6 +90,7 @@ define([
9290
* (event) Modal is hidden
9391
*/
9492
hidden: function(e) {
93+
this.trigger("close", this.value, e);
9594
this.remove();
9695
DialogView.current = null;
9796
},

core/cb.rpc.search/service.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@ SearchRPCService.prototype.files = function(args) {
1515
return this.search.files(args);
1616
};
1717

18+
SearchRPCService.prototype.code = function(args) {
19+
_.defaults(args, {});
20+
return this.search.code(args);
21+
};
22+
1823
// Exports
1924
exports.SearchRPCService = SearchRPCService;

core/cb.search/code.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
var os = require("os");
2+
var path = require("path");
3+
var _ = require("lodash");
4+
var Q = require("q");
5+
var spawn = require('child_process').spawn;
6+
7+
var types = require("./types");
8+
9+
var config = {
10+
grepCmd: "grep",
11+
perlCmd: "perl",
12+
platform: os.platform()
13+
};
14+
15+
var assembleCommand = function(options) {
16+
var include = "";
17+
var cmd = config.grepCmd + " -s -r --color=never --binary-files=without-match -n " +
18+
(!options.casesensitive ? "-i " : "") +
19+
(process.platform != "darwin" ? "-P " : "");
20+
21+
if (options.pattern) { // handles grep peculiarities with --include
22+
if (options.pattern.split(",").length > 1)
23+
include = "{" + options.pattern + "}";
24+
else
25+
include = options.pattern;
26+
}
27+
else {
28+
include = (process.platform != "darwin" ? "\\" : "") + "*{" + types.PATTERN_EXT + "}";
29+
}
30+
31+
if (options.maxresults)
32+
cmd += "-m " + parseInt(options.maxresults, 10);
33+
if (options.wholeword)
34+
cmd += " -w";
35+
36+
var query = options.query;
37+
if (!query)
38+
return;
39+
40+
// grep has a funny way of handling new lines (that is to say, it's non-existent)
41+
// if we're not doing a regex search, then we must split everything between the
42+
// new lines, escape the content, and then smush it back together; due to
43+
// new lines, this is also why we're now passing -P as default to grep
44+
if (!options.replaceAll && !options.regexp) {
45+
var splitQuery = query.split("\\n");
46+
47+
for (var q in splitQuery) {
48+
splitQuery[q] = types.grepEscapeRegExp(splitQuery[q]);
49+
}
50+
query = splitQuery.join("\\n");
51+
}
52+
53+
query = query.replace(new RegExp("\\\'", "g"), "'\\''"); // ticks must be double escaped for BSD grep
54+
55+
cmd += " " + types.PATTERN_EDIR + " " +
56+
" --include=" + include +
57+
" '" + query.replace(/-/g, "\\-") + "'" +
58+
" \"" + types.escapeShell(options.path) + "\"";
59+
60+
if (options.replaceAll) {
61+
if (!options.replacement)
62+
options.replacement = "";
63+
64+
if (options.regexp)
65+
query = types.escapeRegExp(query);
66+
67+
// pipe the grep results into perl
68+
cmd += " -l | xargs " + config.perlCmd +
69+
// print the grep result to STDOUT (to arrange in parseSearchResult())
70+
" -pi -e 'print STDOUT \"$ARGV:$.:$_\"" +
71+
// do the actual replace
72+
" if s/" + query + "/" + options.replacement + "/mg" + ( options.casesensitive ? "" : "i" ) + ";'";
73+
}
74+
75+
var args = ["-c", cmd];
76+
args.command = "bash";
77+
return args;
78+
};
79+
80+
81+
var search = function(root, args) {
82+
var d = Q.defer(), prevfile = null;
83+
var results = {};
84+
var nMatches = 0;
85+
86+
args = _.defaults(args || {}, {
87+
pattern: null,
88+
casesensitive: false,
89+
maxresults: null,
90+
wholeword: false,
91+
regexp: null,
92+
93+
// replace
94+
replaceAll: false,
95+
replacement: null
96+
});
97+
98+
if (!args.query) return Q.reject(new Error("Need a query to search for code"));
99+
100+
var command = assembleCommand(_.extend({}, args, {
101+
path: root
102+
}));
103+
104+
var proc = spawn(command.command, command);
105+
proc.stdout.on('data', function(data) {
106+
data = data.toString();
107+
var lines = data.toString().split(/([\n\r]+)/g);
108+
109+
for (var i = 0, l = lines.length; i < l; ++i) {
110+
var parts = lines[i].split(":");
111+
if (parts.length < 3) continue;
112+
113+
var _path = path.normalize(parts.shift().replace(root, "").trimRight());
114+
var _line = parseInt(parts.shift());
115+
if (!_line) {
116+
if (prevfile) {
117+
results[prevfile][results[prevfile].length - 1].content += "\n\r"+lines[i];
118+
}
119+
continue;
120+
}
121+
122+
prevfile = _path;
123+
results[_path] = results[_path] || [];
124+
results[_path].push({
125+
'line': _line,
126+
'content': parts.join(":")
127+
});
128+
nMatches = nMatches + 1;
129+
}
130+
});
131+
132+
proc.on('error', function(err) {
133+
d.reject(err)
134+
});
135+
proc.on('exit', function(code) {
136+
if (code !== 0) {
137+
d.reject(new Error("ack exited with code "+code));
138+
} else {
139+
d.resolve({
140+
'options': args,
141+
'files': results,
142+
'matches': nMatches
143+
});
144+
}
145+
});
146+
147+
return d.promise;
148+
};
149+
150+
module.exports = search;

core/cb.search/files.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
var _ = require('lodash');
2+
var glob = require("glob");
3+
var Q = require('q');
4+
5+
var search = function(root, args) {
6+
var d = Q.defer();
7+
8+
args = _.defaults({}, args || {}, {
9+
'start': 0,
10+
'limit': 30
11+
});
12+
13+
glob("**/*"+args.query+"*", {
14+
'cwd': root,
15+
'mark': true
16+
}, function (err, files) {
17+
if (err) {
18+
d.reject(err);
19+
} else {
20+
var results = _.chain(files)
21+
.filter(function(path) {
22+
return !(!path.length || path[path.length-1] == "/");
23+
})
24+
.map(function(path) {
25+
return "/"+path;
26+
})
27+
.value();
28+
29+
d.resolve({
30+
'files': results.slice(args.start, args.start+args.limit),
31+
'n': _.size(results)
32+
});
33+
}
34+
});
35+
36+
return d.promise;
37+
};
38+
39+
module.exports = search;

0 commit comments

Comments
 (0)