Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Search results as text #933

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 10 additions & 2 deletions lib/find.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ FindView = require './find-view'
ProjectFindView = require './project-find-view'
ResultsModel = require './project/results-model'
ResultsPaneView = require './project/results-pane'
ResultsTextViewManager = require './project/results-text-view-manager'

module.exports =
activate: ({findOptions, findHistory, replaceHistory, pathsHistory}={}) ->
Expand All @@ -17,8 +18,12 @@ module.exports =
atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'right')
atom.config.unset('find-and-replace.openProjectFindResultsInRightPane')

atom.workspace.addOpener (filePath) ->
new ResultsPaneView() if filePath is ResultsPaneView.URI
atom.workspace.addOpener (filePath) =>
if filePath is ResultsPaneView.URI
if atom.config.get('find-and-replace.findResultsAsText')
return @resultsTextViewManager.getResultsTextEditor()
else
return new ResultsPaneView()

@subscriptions = new CompositeDisposable
@findHistory = new History(findHistory)
Expand All @@ -29,6 +34,8 @@ module.exports =
@findModel = new BufferSearch(@findOptions)
@resultsModel = new ResultsModel(@findOptions)

@resultsTextViewManager = new ResultsTextViewManager(@resultsModel)

@subscriptions.add atom.workspace.getCenter().observeActivePaneItem (paneItem) =>
if paneItem?.getBuffer?()
@findModel.setEditor(paneItem)
Expand Down Expand Up @@ -174,6 +181,7 @@ module.exports =
@projectFindView = null

ResultsPaneView.model = null
ResultsTextViewManager.model = null
@resultsModel = null

@subscriptions?.dispose()
Expand Down
186 changes: 186 additions & 0 deletions lib/project/results-text-view-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
const { Range, CompositeDisposable } = require('atom');
const {sanitizePattern} = require('./util');

const findGrammar = atom.grammars.createGrammar('find', {
'scopeName': 'find.results',
'name': 'Find Results',
'fileTypes': [
'results'
],
'patterns': [
{
'begin': '\x1B',
'end': '\x1B',
'name': 'find-results-main-line-number'
},
{
'begin': '\x1D',
'end': '\x1D',
'name': 'find-results-context-line-number'
},
{
'begin': '\x1C',
'end': '\x1C',
'name': 'find-results-path'
}
]
});

const LEFT_PAD = ' ';
const pad = (str) => {
str = str.toString();
return LEFT_PAD.substring(0, LEFT_PAD.length - str.length) + str;
}

module.exports =
class ResultsTextViewManager {
constructor(model) {
this.model = model;
this.model.setActive(true);
this.editor = null;
this.cursorLine = null;
this.lineToFilesMap = {};
this.isLoading = false;
this.searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
this.searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
}

onDoubleClick() {
if (this.cursorLine && this.lineToFilesMap[this.cursorLine]) {
atom.workspace
.open(this.lineToFilesMap[this.cursorLine].filePath)
// , {
// pending,
// split: reverseDirections[atom.config.get('find-and-replace.projectSearchResultsPaneSplitDirection')]
// })
.then(editor => {
editor.setSelectedBufferRange(this.lineToFilesMap[this.cursorLine].range, {autoscroll: true})
});
}
}

onCursorPositionChanged(e) {
this.cursorLine = e.newBufferPosition && e.newBufferPosition.row;
};

onDestroyEditor() {
this.model.setActive(false);
this.subscriptions.dispose();
this.editor = null;
}

onSearch() {
this.editor.setText('Searching...');
}

onFinishedSearching(results) {
if (this.model.getPaths().length === 0) {
this.editor.setText(`No results found for ${sanitizePattern(results.findPattern)}.`);
return;
}
this.editor.setGrammar(findGrammar);
const searchContextLineCountTotal = this.searchContextLineCountBefore + this.searchContextLineCountAfter;
let resultsLines = [];
this.model.getPaths().forEach((filePath) => {
const result = this.model.results[filePath];
resultsLines.push('\x1C' + filePath + ':\x1C');
let lastLineNumber = null;
for (let i = 0; i < result.matches.length; i++) {
const match = result.matches[i];
const mainLineNumber = Range.fromObject(match.range).start.row + 1;

// Add leading lines
const linesToPrevMatch = mainLineNumber - lastLineNumber - 1;
const leadingLines = linesToPrevMatch < match.leadingContextLines.length ?
match.leadingContextLines.slice(
match.leadingContextLines.length - linesToPrevMatch,
match.leadingContextLines.length
)
: match.leadingContextLines;
for (let i = 0; i < leadingLines.length; i++) {
const lineNumber = mainLineNumber - leadingLines.length + i;
resultsLines.push('\x1D' + pad(lineNumber) + '\x1D ' + leadingLines[i]);
};

// Avoid adding the same line multiple times
if (mainLineNumber !== lastLineNumber) {
// Add main line
resultsLines.push('\x1B' + pad(mainLineNumber) + ':\x1B ' + match.lineText);
}

// Store the file path and range info for retrieving it on double click
this.lineToFilesMap[resultsLines.length - 1] = {
range: match.range,
filePath: filePath
}

// Check if there is overlap with the next match
// If there is, adjust the number of trailing lines to be added
let linesOverlap = false;
let numberOfTrailingLines = match.trailingContextLines.length;
if (i < result.matches.length - 1) {
const nextMatch = result.matches[i + 1];
const nextLineNumber = Range.fromObject(nextMatch.range).start.row + 1;
const linesToNextMatch = nextLineNumber - mainLineNumber - 1;
if (linesToNextMatch <= searchContextLineCountTotal) {
linesOverlap = true;
if (linesToNextMatch - this.searchContextLineCountBefore < this.searchContextLineCountAfter) {
numberOfTrailingLines = Math.max(
linesToNextMatch - this.searchContextLineCountBefore,
0
);
}
}
}
// Add trailing lines
for (let j = 0; j < numberOfTrailingLines; j++) {
const lineNumber = mainLineNumber + j + 1;
resultsLines.push('\x1D' + pad(lineNumber) + '\x1D ' + match.trailingContextLines[j]);
};

// Separator
if (!linesOverlap) {
resultsLines.push(pad('.'.repeat((mainLineNumber + numberOfTrailingLines).toString().length)));
}

lastLineNumber = mainLineNumber;
};
// Pop last separator
resultsLines.pop();
resultsLines.push('');
});
this.editor.setText(resultsLines.join('\n'));
this.editor.setCursorBufferPosition([0, 0]);
}

onSearchContextLineCountChanged() {
this.searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
this.searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
}

getResultsTextEditor() {
if (!this.editor) {
this.cursorLine = null;
this.lineToFilesMap = {};
const textEditorRegistry = atom.workspace.textEditorRegistry;
const editor = textEditorRegistry.build(({autoHeight: false}));
editor.getTitle = () => 'Project Find Results';
editor.getIconName = () => 'search';
editor.shouldPromptToSave = () => false;
editor.element.addEventListener('dblclick', this.onDoubleClick.bind(this));
this.subscriptions = new CompositeDisposable(
editor.onDidChangeCursorPosition(this.onCursorPositionChanged.bind(this)),
editor.onDidDestroy(this.onDestroyEditor.bind(this)),
this.model.onDidStartSearching(this.onSearch.bind(this)),
this.model.onDidFinishSearching(this.onFinishedSearching.bind(this)),
atom.config.observe('find-and-replace.searchContextLineCountBefore', this.onSearchContextLineCountChanged.bind(this)),
atom.config.observe('find-and-replace.searchContextLineCountAfter', this.onSearchContextLineCountChanged.bind(this))
);

this.editor = editor;
}
// Update the editor in case there are already results available
this.onFinishedSearching(this.model.getResultsSummary());
return this.editor;
}
}
55 changes: 55 additions & 0 deletions spec/results-text-view-manager-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/** @babel */
const path = require('path');
const ResultsPaneView = require('../lib/project/results-pane');
const {beforeEach, it, fit, ffit, fffit} = require('./async-spec-helpers')

global.beforeEach(function() {
this.addMatchers({
toBeWithin(value, delta) {
this.message = `Expected ${this.actual} to be within ${delta} of ${value}`
return Math.abs(this.actual - value) < delta;
}
});
});

describe('ResultsTextViewManager', () => {
let projectFindView, resultsView, searchPromise, workspaceElement;

function getResultsEditor() {
return atom.workspace.getActiveTextEditor();
}

beforeEach(async () => {
workspaceElement = atom.views.getView(atom.workspace);
jasmine.attachToDOM(workspaceElement);

atom.config.set('core.excludeVcsIgnoredPaths', false);
atom.config.set('find-and-replace.findResultsAsText', true);
atom.project.setPaths([path.join(__dirname, 'fixtures')]);

let activationPromise = atom.packages.activatePackage("find-and-replace").then(function({mainModule}) {
mainModule.createViews();
({projectFindView} = mainModule);
const spy = spyOn(projectFindView, 'confirm').andCallFake(() => {
return searchPromise = spy.originalValue.call(projectFindView)
});
});

atom.commands.dispatch(workspaceElement, 'project-find:show');

await activationPromise;
});

describe("when the result is for a long line", () => {
it("renders just one line", async () => {
projectFindView.findEditor.setText('ghijkl');
atom.commands.dispatch(projectFindView.element, 'core:confirm');
await searchPromise;

resultsEditor = getResultsEditor();
resultsEditor.update({autoHeight: false})
const lines = resultsEditor.getText().split('\n');
expect(lines[1]).toBe("\x1B 1:\x1B test test test test test test test test test test test a b c d e f g h i j k l abcdefghijklmnopqrstuvwxyz");
})
});
});
18 changes: 18 additions & 0 deletions styles/find-and-replace.atom-text-editor.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Note: This file is specially named!
// Less files with the name `*.atom-text-editor.less` will be loaded into the text-editor!
//
// Since these styles applies to the editor, this file needs to be loaded in the
// context of the editor.
// See example in decoration-example Atom package (not published?)

// Highlighting ranges of text
// Remember: no default is provided for highlight decorations.
.syntax--find-results-main-line-number {
color: @syntax-color-variable;
}
.syntax--find-results-context-line-number {
color: fadeout(@syntax-color-variable, 30%);
}
.syntax--find-results-path {
color: @syntax-color-import;
}