Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 76 additions & 84 deletions src/plugins/sdcard/src/android/WorkspaceIndex.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ class WorkspaceIndex {
private static final int EXPLICIT_INCLUDE_READ_LIMIT_BYTES = 128 * 1024 * 1024;
private static final int SAMPLE_BYTES = 8192;
private static final int MAX_MATCHES_PER_FILE = 5000;
private static final int SEARCH_RESULT_BATCH_SIZE = 12;
private static final int SEARCH_RESULT_BATCH_MATCHES = 600;
private static final int SEARCH_RESULT_BATCH_MATCHES = 200;

private static final Set<String> BINARY_EXTENSIONS = new HashSet<>();
private static final Set<String> TEXT_EXTENSIONS = new HashSet<>();
Expand Down Expand Up @@ -965,8 +964,6 @@ private void runSearch(Job job, JSONObject options, CallbackContext callback)
boolean batchResults = options.optBoolean("batchResults", false);

Pattern pattern = compileSearchPattern(search, searchOptions);
JSONArray searchResultBatch = new JSONArray();
int batchedMatches = 0;
int total = files.length();
int processed = 0;
int lastProgress = -1;
Expand Down Expand Up @@ -1013,38 +1010,18 @@ private void runSearch(Job job, JSONObject options, CallbackContext callback)

if ("replace".equals(mode)) {
String replacement = Matcher.quoteReplacement(replace == null ? "" : replace);
String text = pattern.matcher(content).replaceAll(replacement);
String text = pattern.matcher(new SearchInput(content, job)).replaceAll(replacement);
JSONObject result = baseEvent(job.id, "replace-result");
result.put("file", file);
result.put("text", text);
send(callback, result, true);
} else {
JSONObject result = searchInContent(file, content, pattern);
if (result != null) {
if (batchResults) {
searchResultBatch.put(result);
batchedMatches += result.getJSONArray("matches").length();
if (
searchResultBatch.length() >= SEARCH_RESULT_BATCH_SIZE ||
batchedMatches >= SEARCH_RESULT_BATCH_MATCHES
) {
flushSearchResultBatch(callback, job.id, searchResultBatch);
batchedMatches = 0;
}
} else {
JSONObject event = baseEvent(job.id, "search-result");
event.put("data", result);
send(callback, event, true);
}
}
searchInContent(file, content, pattern, job, callback, batchResults);
}

processed += 1;
}

if (batchResults) {
flushSearchResultBatch(callback, job.id, searchResultBatch);
}
sendProgress(callback, job.id, 100);
send(callback, baseEvent(job.id, "replace".equals(mode) ? "done-replacing" : "done-searching"), false);
}
Expand All @@ -1063,32 +1040,30 @@ private Pattern compileSearchPattern(String search, JSONObject options)
return Pattern.compile(pattern, flags);
}

private JSONObject searchInContent(
private void searchInContent(
JSONObject file,
String content,
Pattern pattern
Pattern pattern,
Job job,
CallbackContext callback,
boolean batchResults
) throws JSONException {
Matcher matcher = pattern.matcher(content);
SearchInput input = new SearchInput(content, job);
Matcher matcher = pattern.matcher(input);
JSONArray matches = new JSONArray();
StringBuilder text = new StringBuilder(file.optString("name"));
if (text.length() > 30) {
text = new StringBuilder("..." + text.substring(text.length() - 30));
}

boolean limited = false;
int matchCount = 0;
int cursor = 0;
int row = 0;
int column = 0;
while (matcher.find()) {
if (matches.length() >= MAX_MATCHES_PER_FILE) {
limited = true;
break;
}
String word = matcher.group();
while (true) {
input.check();
if (!matcher.find()) break;
String word = content.substring(matcher.start(), Math.min(matcher.end(), matcher.start() + 160));
int start = matcher.start();
int end = matcher.end();
String[] surrounding = getSurrounding(content, word, start, end);
while (cursor < start) {
if ((cursor & 1023) == 0) input.check();
if (content.charAt(cursor) == '\n') {
row += 1;
column = 0;
Expand All @@ -1099,6 +1074,7 @@ private JSONObject searchInContent(
}
JSONObject startPosition = lineColumn(row, column);
while (cursor < end) {
if ((cursor & 1023) == 0) input.check();
if (content.charAt(cursor) == '\n') {
row += 1;
column = 0;
Expand All @@ -1114,34 +1090,31 @@ private JSONObject searchInContent(
match.put("line", surrounding[0].trim());
match.put("position", position(startPosition, endPosition));
matches.put(match);
text.append("\n\t").append(surrounding[0].trim());
matchCount++;
if (matchCount == MAX_MATCHES_PER_FILE) {
sendSearchMatches(callback, job.id, file, matches, batchResults, matcher.find());
return;
}
if (matches.length() >= SEARCH_RESULT_BATCH_MATCHES) {
sendSearchMatches(callback, job.id, file, matches, batchResults, false);
matches = new JSONArray();
input.renewDeadline();
}
}

if (matches.length() == 0) return null;
sendSearchMatches(callback, job.id, file, matches, batchResults, false);
}

private void sendSearchMatches(CallbackContext callback, String id, JSONObject file,
JSONArray matches, boolean batchResults, boolean limited) throws JSONException {
if (matches.length() == 0) return;
JSONObject data = new JSONObject();
data.put("file", file);
data.put("matches", matches);
if (limited) {
text
.append("\n\t")
.append("... result limit reached for this file");
}
data.put("limited", limited);
data.put("text", text.toString());
return data;
}

private void flushSearchResultBatch(
CallbackContext callback,
String id,
JSONArray batch
) throws JSONException {
if (batch.length() == 0) return;
JSONObject event = baseEvent(id, "search-results");
event.put("data", new JSONArray(batch.toString()));
JSONObject event = baseEvent(id, batchResults ? "search-results" : "search-result");
event.put("data", batchResults ? new JSONArray().put(data) : data);
send(callback, event, true);
while (batch.length() > 0) batch.remove(0);
}

private String getFileContent(
Expand Down Expand Up @@ -1590,37 +1563,56 @@ private JSONObject lineColumn(int row, int column) throws JSONException {
return result;
}

private String[] getSurrounding(String content, String word, int start, int end) {
int max = 160;
int lineStart = start;
while (lineStart > 0) {
char previous = content.charAt(lineStart - 1);
if (previous == '\n' || previous == '\r') break;
lineStart--;
// Java's Matcher does not honor thread interruption. Check its input access
// so backtracking is subject to the same cancellation and deadline as results.
private static final class SearchInput implements CharSequence {
final String content;
final Job job;
long deadline = System.nanoTime() + 2_000_000_000L;
int accesses;

SearchInput(String content, Job job) {
this.content = content;
this.job = job;
}

int lineEnd = end;
while (lineEnd < content.length()) {
char current = content.charAt(lineEnd);
if (current == '\n' || current == '\r') break;
lineEnd++;
void renewDeadline() { deadline = System.nanoTime() + 2_000_000_000L; }

void check() {
if (job.cancelled) throw new IllegalStateException("Search cancelled");
if (System.nanoTime() > deadline) throw new IllegalStateException("Search timed out; simplify the expression");
}

int snippetStart = lineStart;
int snippetEnd = lineEnd;
if (lineEnd - lineStart > max) {
int matchLength = Math.max(1, end - start);
int remaining = Math.max(0, max - matchLength);
int left = remaining / 2;
int right = remaining - left;
snippetStart = Math.max(lineStart, start - left);
snippetEnd = Math.min(lineEnd, end + right);
public int length() { return content.length(); }
public char charAt(int index) {
if ((accesses++ & 1023) == 0) check();
return content.charAt(index);
}
public CharSequence subSequence(int start, int end) { return content.subSequence(start, end); }
public String toString() { return content; }
}

private String[] getSurrounding(String content, String word, int start, int end) {
int max = 160;
int remaining = Math.max(0, max - (end - start));
int snippetStart = start;
int leftLimit = Math.max(0, start - remaining / 2);
while (snippetStart > leftLimit) {
char c = content.charAt(snippetStart - 1);
if (c == '\n' || c == '\r') break;
snippetStart--;
}
int snippetEnd = Math.min(end, start + max);
int rightLimit = Math.min(content.length(), snippetStart + max);
while (snippetEnd < rightLimit) {
char c = content.charAt(snippetEnd);
if (c == '\n' || c == '\r') break;
snippetEnd++;
}
StringBuilder line = new StringBuilder();
if (snippetStart > lineStart) line.append("...");
if (snippetStart > 0 && content.charAt(snippetStart - 1) != '\n' && content.charAt(snippetStart - 1) != '\r') line.append("...");
line.append(content.substring(snippetStart, snippetEnd).trim());
if (snippetEnd < lineEnd) line.append("...");
if (snippetEnd < content.length() && content.charAt(snippetEnd) != '\n' && content.charAt(snippetEnd) != '\r') line.append("...");
String renderText = word;

return new String[] {
Expand Down
15 changes: 4 additions & 11 deletions src/sidebarApps/searchInFiles/cmResultView.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import helpers from "utils/helpers";
* @param {object} opts
* @param {(lineIndex:number)=>void} opts.onLineClick
* @param {()=>string[]} opts.getWords - returns list of words to highlight
* @param {()=>string[]} opts.getFileNames - returns list of filenames (used to style header lines)
* @param {(lineIndex:number)=>object} opts.getFileInfo - file metadata for a result row
*/
export function createSearchResultView(
container,
{ onLineClick, getWords, getFileNames, getRegex },
{ onLineClick, getWords, getFileInfo, getRegex },
) {
let view;
let isGhostText = false;
Expand Down Expand Up @@ -145,15 +145,11 @@ export function createSearchResultView(
function buildGroupDecos(state) {
const doc = state.doc;
const folded = state.field(foldState, false) || new Set();
// No removed groups
const fns =
(typeof getFileNames === "function" ? getFileNames() : []) || [];
if (isGhostText || !fns.length || doc.length === 0 || doc.lines === 0)
if (isGhostText || doc.length === 0 || doc.lines === 0)
return Decoration.none;

const builder = [];
// Build header chevrons and collapses per group
let groupIndex = 0;
eachGroup(doc, ({ start, end }) => {
const header = doc.line(start);
const key = start - 1;
Expand All @@ -163,9 +159,7 @@ export function createSearchResultView(
Decoration.line({ class: "cm-fileName" }).range(header.from),
);
// File icon
const fileNames =
(typeof getFileNames === "function" ? getFileNames() : []) || [];
const fileInfo = fileNames[groupIndex] || {};
const fileInfo = getFileInfo?.(key) || {};
const fname =
typeof fileInfo === "string" ? fileInfo : fileInfo.name || "";
const iconClass = helpers.getIconForFile(fname);
Expand Down Expand Up @@ -210,7 +204,6 @@ export function createSearchResultView(
}).range(first.from),
);
}
groupIndex++;
});

return Decoration.set(builder, true);
Expand Down
Loading