Skip to content

Commit

Permalink
Add literal search option (#397)
Browse files Browse the repository at this point in the history
* Add literal search option

* Pull EscapeRegExp into common.js

* Add db/ to Jest ignore patterns

* Test EscapeRegExp() matches its input

* Test vacuous EscapeRegExp
  • Loading branch information
jrarmstro authored Jul 6, 2021
1 parent c9a7667 commit ca5c7c8
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 35 deletions.
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func Setup(m *http.ServeMux, idx map[string]*searcher.Searcher) {
opt.FileRegexp = r.FormValue("files")
opt.ExcludeFileRegexp = r.FormValue("excludeFiles")
opt.IgnoreCase = parseAsBool(r.FormValue("i"))
opt.LiteralSearch = parseAsBool(r.FormValue("literal"))
opt.LinesOfContext = parseAsUintValue(
r.FormValue("ctx"),
0,
Expand Down
11 changes: 10 additions & 1 deletion codesearch/regexp/regexp.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
// use in grep-like programs.
package regexp

import "regexp/syntax"
import (
"regexp"
"regexp/syntax"
)

func bug() {
panic("codesearch/regexp: internal error")
Expand Down Expand Up @@ -57,3 +60,9 @@ func (r *Regexp) Match(b []byte, beginText, endText bool) (end int) {
func (r *Regexp) MatchString(s string, beginText, endText bool) (end int) {
return r.m.matchString(s, beginText, endText)
}

// QuoteMeta returns a string that escapes all regular expression
// metacharacters inside the argument text.
func QuoteMeta(s string) string {
return regexp.QuoteMeta(s)
}
8 changes: 7 additions & 1 deletion index/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type IndexOptions struct {

type SearchOptions struct {
IgnoreCase bool
LiteralSearch bool
LinesOfContext uint
FileRegexp string
ExcludeFileRegexp string
Expand Down Expand Up @@ -146,7 +147,12 @@ func (n *Index) Search(pat string, opt *SearchOptions) (*SearchResponse, error)
n.lck.RLock()
defer n.lck.RUnlock()

re, err := regexp.Compile(GetRegexpPattern(pat, opt.IgnoreCase))
patForRe := pat
if opt.LiteralSearch {
patForRe = regexp.QuoteMeta(pat)
}

re, err := regexp.Compile(GetRegexpPattern(patForRe, opt.IgnoreCase))
if err != nil {
return nil, err
}
Expand Down
4 changes: 3 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ module.exports = {
// moduleNameMapper: {},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: [
"db"
],

// Activates notifications for test results
// notify: false,
Expand Down
4 changes: 4 additions & 0 deletions ui/assets/js/common.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export function EscapeRegExp(regexp) {
return regexp.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&');
}

export function ExpandVars(template, values) {
for (var name in values) {
template = template.replace('{' + name + '}', values[name]);
Expand Down
29 changes: 28 additions & 1 deletion ui/assets/js/common.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
import { ExpandVars, UrlParts, UrlToRepo } from "./common";
import { EscapeRegExp, ExpandVars, UrlToRepo } from "./common";

describe("EscapeRegExp", () => {
const testRegs = [
["Some test regexes", ["Some patterns that should not match"]],
["ab+c", ["abc"]],
["^\d+$", ["1", "123", "abc"]],
["./...", ["a/abc"]],
["\w+", []],
["\r\n|\r|\n", []],
["^[a-z]+\[[0-9]+\]$", []],
["/[-[\]{}()*+!<=:?.\/\\^$|#\s,]", ["/[-[\]{}()*!<=:?.\/\\^$|#\s,]"]],
["^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})*$", []],
["(H..).(o..)", []],
["^[a-zA-Z0-9 ]*$", []]
];

test.each(testRegs)(
"EscapeRegExp(%s) returns the RegExp matching the input",
(regexp, shouldFail) => {
const re = new RegExp(EscapeRegExp(regexp))
expect(re.test(regexp)).toBe(true);
shouldFail.forEach((failCase) => {
expect(re.test(failCase)).toBe(false);
});
},
);
});

describe("ExpandVars", () => {
test("Replaces template variables with their values", () => {
Expand Down
29 changes: 25 additions & 4 deletions ui/assets/js/hound.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {UrlParts, UrlToRepo} from './common';
import {EscapeRegExp, UrlParts, UrlToRepo} from './common';

var Signal = function() {
};
Expand Down Expand Up @@ -75,6 +75,7 @@ var ParamsFromUrl = function(params) {
params = params || {
q: '',
i: 'nope',
literal: 'nope',
files: '',
excludeFiles: '',
repos: '*'
Expand Down Expand Up @@ -399,8 +400,12 @@ var SearchBar = React.createClass({
this.props.onSearchRequested(this.getParams());
},
getRegExp : function() {
var regexp = this.refs.q.getDOMNode().value.trim()
if (this.refs.lsearch.getDOMNode().checked) {
regexp = EscapeRegExp(regexp)
}
return new RegExp(
this.refs.q.getDOMNode().value.trim(),
regexp,
this.refs.icase.getDOMNode().checked ? 'ig' : 'g');
},
getParams: function() {
Expand All @@ -416,22 +421,29 @@ var SearchBar = React.createClass({
files : this.refs.files.getDOMNode().value.trim(),
excludeFiles : this.refs.excludeFiles.getDOMNode().value.trim(),
repos : repos.join(','),
i: this.refs.icase.getDOMNode().checked ? 'fosho' : 'nope'
i: this.refs.icase.getDOMNode().checked ? 'fosho' : 'nope',
literal: this.refs.lsearch.getDOMNode().checked ? 'fosho' : 'nope'
};
},
setParams: function(params) {
var q = this.refs.q.getDOMNode(),
i = this.refs.icase.getDOMNode(),
literal = this.refs.lsearch.getDOMNode(),
files = this.refs.files.getDOMNode(),
excludeFiles = this.refs.excludeFiles.getDOMNode();

q.value = params.q;
i.checked = ParamValueToBool(params.i);
literal.checked = ParamValueToBool(params.literal)
files.value = params.files;
excludeFiles.value = params.excludeFiles;
},
hasAdvancedValues: function() {
return this.refs.files.getDOMNode().value.trim() !== '' || this.refs.excludeFiles.getDOMNode().value.trim() !== '' || this.refs.icase.getDOMNode().checked || this.refs.repos.getDOMNode().value !== '';
return this.refs.files.getDOMNode().value.trim() !== ''
|| this.refs.excludeFiles.getDOMNode().value.trim() !== ''
|| this.refs.icase.getDOMNode().checked
|| this.refs.lsearch.getDOMNode().checked
|| this.refs.repos.getDOMNode().value !== '';
},
isAdvancedEmpty: function() {
return this.refs.files.getDOMNode().value.trim() === '' && this.refs.excludeFiles.getDOMNode().value.trim() === '';
Expand Down Expand Up @@ -542,6 +554,12 @@ var SearchBar = React.createClass({
<input id="ignore-case" type="checkbox" ref="icase" />
</div>
</div>
<div className="field">
<label htmlFor="literal-search">Use string literally</label>
<div className="field-input">
<input id="literal-search" type="checkbox" ref="lsearch" />
</div>
</div>
<div className="field">
<label className="multiselect_label" htmlFor="repos">Select Repo</label>
<div className="field-input">
Expand Down Expand Up @@ -801,6 +819,7 @@ var App = React.createClass({
this.setState({
q: params.q,
i: params.i,
literal: params.literal,
files: params.files,
excludeFiles: params.excludeFiles,
repos: repos
Expand Down Expand Up @@ -856,6 +875,7 @@ var App = React.createClass({
var path = location.pathname +
'?q=' + encodeURIComponent(params.q) +
'&i=' + encodeURIComponent(params.i) +
'&literal=' + encodeURIComponent(params.literal) +
'&files=' + encodeURIComponent(params.files) +
'&excludeFiles=' + encodeURIComponent(params.excludeFiles) +
'&repos=' + params.repos;
Expand All @@ -867,6 +887,7 @@ var App = React.createClass({
<SearchBar ref="searchBar"
q={this.state.q}
i={this.state.i}
literal={this.state.literal}
files={this.state.files}
excludeFiles={this.state.excludeFiles}
repos={this.state.repos}
Expand Down
Loading

0 comments on commit ca5c7c8

Please sign in to comment.