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

Commit 0a57ef1

Browse files
committed
Stitch together multiple matches from single line
1 parent 6028925 commit 0a57ef1

File tree

4 files changed

+144
-49
lines changed

4 files changed

+144
-49
lines changed

lib/project/result-row-view.js

Lines changed: 88 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const getIconServices = require('../get-icon-services');
2-
const { Range } = require('atom');
2+
const { Point, Range } = require('atom');
33
const {
44
LeadingContextRow,
55
TrailingContextRow,
@@ -111,57 +111,104 @@ class MatchRowView {
111111
}
112112

113113
generatePreviewNode({matches, replacePattern, regex}) {
114-
const subnodes = [];
114+
const TYPE = {
115+
MATCH: 1,
116+
TEXT: 2,
117+
ELLIPSIS: 3
118+
};
119+
120+
const segments = [];
115121

116-
let prevMatchEnd = matches[0].lineTextOffset;
117122
for (const match of matches) {
118-
const range = Range.fromObject(match.range);
119-
const prefixStart = Math.max(0, prevMatchEnd - match.lineTextOffset);
120-
const matchStart = range.start.column - match.lineTextOffset;
123+
if (segments.length) {
124+
const previousRange = segments[segments.length - 1].range;
125+
const currentRange = new Range(
126+
new Point(0, match.lineTextOffset),
127+
new Point(0, match.lineTextOffset + match.lineText.length)
128+
);
129+
130+
if (previousRange.intersectsWith(currentRange)) {
131+
const segment = segments.pop();
132+
// current range starts before previous range, therefore it should contain everything from previous range
133+
if (previousRange.start.isGreaterThanOrEqual(currentRange.start)) {
134+
// Delete everything up to previous match from the beginning of current range
135+
match.lineText = match.lineText.substring(previousRange.start.column - currentRange.start.column);
136+
match.lineTextOffset = previousRange.start.column;
137+
} else { // current range starts midway through previous range
138+
// Prepend non-overlapping part of previous range to current range
139+
const preprefix = segment.content.substring(0, currentRange.start.column - previousRange.start.column);
140+
match.lineText = preprefix + match.lineText;
141+
match.lineTextOffset -= preprefix.length;
142+
}
143+
} else { // current range does not intersect and comes after previous range
144+
segments.push({ type: TYPE.ELLIPSIS, range: new Range(previousRange.end, currentRange.start) });
145+
}
146+
}
121147

122-
// TODO - Handle case where (prevMatchEnd < match.lineTextOffset)
123-
// The solution probably needs Workspace.scan to be reworked to account
124-
// for multiple matches lines first
148+
const prefix = match.lineText.substring(0, match.range.start.column - match.lineTextOffset);
149+
const suffix = match.lineText.substring(match.range.end.column - match.lineTextOffset);
125150

126-
const prefix = match.lineText.slice(prefixStart, matchStart);
151+
if (prefix) {
152+
segments.push({
153+
type: TYPE.TEXT,
154+
content: prefix,
155+
range: new Range(
156+
new Point(0, match.lineTextOffset),
157+
new Point(0, match.range.start.column)
158+
)
159+
});
160+
}
127161

128-
let replacementText = ''
129-
if (replacePattern && regex) {
130-
replacementText = match.matchText.replace(regex, replacePattern);
131-
} else if (replacePattern) {
132-
replacementText = replacePattern;
162+
let replacementText
163+
if (replacePattern) {
164+
replacementText = regex ? match.matchText.replace(regex, replacePattern) : replacePattern;
133165
}
134166

135-
subnodes.push(
136-
$.span({}, prefix),
137-
$.span(
138-
{
139-
className:
140-
`match ${replacementText ? 'highlight-error' : 'highlight-info'}`
141-
},
142-
match.matchText
167+
segments.push({
168+
type: TYPE.MATCH,
169+
content: match.matchText,
170+
range: new Range(
171+
new Point(0, match.range.start.column),
172+
new Point(0, match.range.end.column)
143173
),
144-
$.span(
145-
{
146-
className: 'replacement highlight-success',
147-
style: showIf(replacementText)
148-
},
149-
replacementText
150-
)
151-
);
152-
prevMatchEnd = range.end.column;
174+
replacement: replacementText
175+
});
176+
177+
if (suffix) {
178+
segments.push({
179+
type: TYPE.TEXT,
180+
content: suffix,
181+
range: new Range(
182+
new Point(0, match.range.end.column),
183+
new Point(0, match.lineTextOffset + match.lineText.length)
184+
)
185+
});
186+
}
153187
}
154188

155-
const lastMatch = matches[matches.length - 1];
156-
const suffix = lastMatch.lineText.slice(
157-
prevMatchEnd - lastMatch.lineTextOffset
158-
);
189+
const subnodes = [];
159190

160-
return $.span(
161-
{className: 'preview'},
162-
...subnodes,
163-
$.span({}, suffix)
164-
);
191+
for (const segment of segments) {
192+
if (segment.type === TYPE.TEXT) {
193+
subnodes.push($.span({}, segment.content));
194+
}
195+
196+
if (segment.type === TYPE.MATCH) {
197+
subnodes.push(
198+
$.span({ className: `match ${segment.replacement ? 'highlight-error' : 'highlight-info'}` }, segment.content)
199+
);
200+
201+
if (segment.replacement) {
202+
subnodes.push($.span({ className: 'replacement highlight-success' }, segment.replacement));
203+
}
204+
}
205+
206+
if (segment.type === TYPE.ELLIPSIS) {
207+
subnodes.push($.span({}, '…'));
208+
}
209+
}
210+
211+
return $.span({ className: 'preview' }, ...subnodes);
165212
}
166213

167214
render() {

lib/project/results-view.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { Range, CompositeDisposable, Disposable } = require('atom');
1+
const { Point, Range, CompositeDisposable, Disposable } = require('atom');
22
const ResultRowView = require('./result-row-view');
33
const {
44
LeadingContextRow,
@@ -48,7 +48,7 @@ class ResultsView {
4848
this.fakeGroup = new ResultRowGroup({
4949
filePath: 'fake-file-path',
5050
matches: [{
51-
range: [[0, 1], [0, 2]],
51+
range: new Range(new Point(0, 1), new Point(0, 2)),
5252
leadingContextLines: ['test-line-before'],
5353
trailingContextLines: ['test-line-after'],
5454
lineTextOffset: 1,

spec/fixtures/project/combined.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
1234 pxdding0 pxdding1 pxdding2 pxdding3 pxdding4 pxdding5 pxdding6 pxdding7 pxdding8 pxdding9 pxdding10 pxdding11 pxdding12 pxdding13 pxdding14 pxdding15 pxdding16 1234 pxdding17 pxdding18 pxdding19 pxdding20 pxdding21 pxdding22 pxdding23 pxdding24 pxdding25 pxdding26 pxdding27 pxdding28 pxdding29 pxdding30 pxdding31 pxdding32 pxdding33 pxdding34 1234
2+
3+
1234 pxdding0 pxdding1 pxdding2 1234 pxdding3 pxdding4 1234 pxdding5 1234 pxdding6 pxdding7 pxdding8 1234
4+
5+
1234 pxdding0 pxdding1 pxdding2 pxdding3 pxdding4 pxdding5 pxdding6 pxdding7 pxdding8 pxdding9 pxdding10 pxdding11 pxdding12 pxdding13 pxdding14 1234 pxdding15 pxdding16 pxdding17 pxdding18 pxdding19 pxdding20 pxdding21 pxdding22 pxdding23 pxdding24 pxdding25 pxdding26 pxdding27 1234
6+
7+
1234 pxdding0 pxdding1 pxdding2 pxdding3 pxdding4 pxdding5 pxdding6 pxdding7 pxdding8 pxdding9 pxdding10 pxdding11 pxdding12 pxdding13 pxdding14 pxdding15 pxdding16 1234 short text 1234 pxdding17 pxdding18 pxdding19 pxdding20 pxdding21 pxdding22 pxdding23 pxdding24 pxdding25 pxdding26 pxdding27 pxdding28 pxdding29 1234

spec/results-view-spec.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,48 @@ describe('ResultsView', () => {
6969
expect(resultsView.refs.listView.element.querySelectorAll('.preview').length).toBe(1);
7070
expect(resultsView.refs.listView.element.querySelector('.preview').textContent).toBe('test test test test test test test test test test test a b c d e f g h i j k l abcdefghijklmnopqrstuvwxyz');
7171
expect(resultsView.refs.listView.element.querySelector('.match').textContent).toBe('ghijkl');
72-
})
72+
});
73+
});
74+
75+
describe("when the result has lines that contain multiple matches", () => {
76+
let previews;
77+
78+
beforeEach(async () => {
79+
projectFindView.findEditor.setText('1234');
80+
atom.commands.dispatch(projectFindView.element, 'core:confirm');
81+
await searchPromise;
82+
83+
resultsView = getResultsView();
84+
await resultsView.heightInvalidationPromise;
85+
86+
previews = resultsView.refs.listView.element.querySelectorAll('.preview');
87+
});
88+
89+
it('returns search results for combined test', () => {
90+
expect(resultsView.refs.listView.element.querySelector('.path-name').textContent).toBe("combined.txt");
91+
expect(previews.length).toBe(4);
92+
});
93+
94+
it('concatenates noncontiguous matches on same line with ellipsis', () => {
95+
expect(previews[0].textContent).toBe('1234 pxdding0 pxdding1 pxdding2 pxdding3 pxdding4 pxdding5 pxdding6 pxdding7 pxdding8 pxdding9 pxdding10…pxdding12 pxdding13 pxdding14 pxdding15 pxdding16 1234 pxdding17 pxdding18 pxdding19 pxdding20 pxdding21…pxdding25 pxdding26 pxdding27 pxdding28 pxdding29 pxdding30 pxdding31 pxdding32 pxdding33 pxdding34 1234');
96+
expect(previews[0].querySelectorAll('.match')[0].textContent).toBe('1234');
97+
expect(previews[0].querySelectorAll('.match').length).toBe(3);
98+
});
99+
100+
it('concatenates contiguous matches with touching matches seamlessly', () => {
101+
expect(previews[1].textContent).toBe('1234 pxdding0 pxdding1 pxdding2 1234 pxdding3 pxdding4 1234 pxdding5 1234 pxdding6 pxdding7 pxdding8 1234');
102+
expect(previews[1].querySelectorAll('.match').length).toBe(5);
103+
});
104+
105+
it('concatenates contigous matches with touching contexts seamlessly', () => {
106+
expect(previews[2].textContent).toBe('1234 pxdding0 pxdding1 pxdding2 pxdding3 pxdding4 pxdding5 pxdding6 pxdding7 pxdding8 pxdding9 pxdding10 pxdding11 pxdding12 pxdding13 pxdding14 1234 pxdding15 pxdding16 pxdding17 pxdding18 pxdding19 pxdding20 pxdding21 pxdding22 pxdding23 pxdding24 pxdding25 pxdding26 pxdding27 1234');
107+
expect(previews[2].querySelectorAll('.match').length).toBe(3);
108+
});
109+
110+
it('concatenates different types of (non)contigous matches on same line accordingly', () => {
111+
expect(previews[3].textContent).toBe('1234 pxdding0 pxdding1 pxdding2 pxdding3 pxdding4 pxdding5 pxdding6 pxdding7 pxdding8 pxdding9 pxdding10…pxdding12 pxdding13 pxdding14 pxdding15 pxdding16 1234 short text 1234 pxdding17 pxdding18 pxdding19 pxdding20 pxdding21 pxdding22 pxdding23 pxdding24 pxdding25 pxdding26 pxdding27 pxdding28 pxdding29 1234');
112+
expect(previews[3].querySelectorAll('.match').length).toBe(4);
113+
});
73114
});
74115

75116
describe("when there are multiple project paths", () => {
@@ -117,8 +158,7 @@ describe('ResultsView', () => {
117158
await resultsView.heightInvalidationPromise;
118159
expect(resultsView.refs.listView.element.querySelector('.match').textContent).toBe('ghijkl');
119160
expect(resultsView.refs.listView.element.querySelector('.match')).toHaveClass('highlight-info');
120-
expect(resultsView.refs.listView.element.querySelector('.replacement').textContent).toBe('');
121-
expect(resultsView.refs.listView.element.querySelector('.replacement')).toBeHidden();
161+
expect(resultsView.refs.listView.element.querySelector('.replacement')).toBeNull();
122162

123163
projectFindView.replaceEditor.setText('cats');
124164
advanceClock(modifiedDelay);
@@ -135,7 +175,7 @@ describe('ResultsView', () => {
135175

136176
expect(resultsView.refs.listView.element.querySelector('.match').textContent).toBe('ghijkl');
137177
expect(resultsView.refs.listView.element.querySelector('.match')).toHaveClass('highlight-info');
138-
expect(resultsView.refs.listView.element.querySelector('.replacement')).toBeHidden();
178+
expect(resultsView.refs.listView.element.querySelector('.replacement')).toBeNull();
139179
});
140180

141181
it('renders the captured text when the replace pattern uses captures', async () => {
@@ -816,8 +856,9 @@ describe('ResultsView', () => {
816856
runs(() => {
817857
resultsView = getResultsView()
818858
const iconElements = resultsView.element.querySelectorAll(iconSelector)
819-
expect(iconElements[0].className.trim()).toBe('icon foo bar')
820-
expect(iconElements[1].className.trim()).toBe('icon baz qlux')
859+
expect(iconElements[0].className.trim()).toBe('icon baz qlux')
860+
expect(iconElements[1].className.trim()).toBe('icon foo bar')
861+
expect(iconElements[2].className.trim()).toBe('icon baz qlux')
821862
expect(resultsView.element.querySelector('.icon-file-text')).toBe(null)
822863

823864
disposable.dispose()

0 commit comments

Comments
 (0)