├── .coffeelintignore
├── .github
├── no-response.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmignore
├── .pairs
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── LICENSE.md
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── coffeelint.json
├── docs
└── events.md
├── keymaps
└── find-and-replace.cson
├── lib
├── buffer-search.js
├── default-file-icons.js
├── escape-helper.coffee
├── find-options.coffee
├── find-view.js
├── find.coffee
├── get-icon-services.js
├── history.coffee
├── project-find-view.js
├── project
│ ├── list-view.js
│ ├── result-row-view.js
│ ├── result-row.js
│ ├── results-model.js
│ ├── results-pane.js
│ ├── results-view.js
│ └── util.coffee
├── reporter-proxy.js
└── select-next.coffee
├── menus
└── find-and-replace.cson
├── package-lock.json
├── package.json
├── spec
├── async-spec-helpers.js
├── buffer-search-spec.js
├── find-spec.js
├── find-view-spec.js
├── fixtures
│ ├── one-long-line.coffee
│ ├── project
│ │ ├── long-match.js
│ │ ├── one-long-line.coffee
│ │ ├── sample.coffee
│ │ └── sample.js
│ ├── sample.coffee
│ └── sample.js
├── item
│ ├── deferred-editor-item.js
│ ├── embedded-editor-item.js
│ └── unrecognized-item.js
├── project-find-view-spec.js
├── result-row-spec.js
├── results-model-spec.js
├── results-view-spec.js
├── select-next-spec.js
└── setup-spec.js
└── styles
└── find-and-replace.less
/.coffeelintignore:
--------------------------------------------------------------------------------
1 | spec/fixtures
2 |
--------------------------------------------------------------------------------
/.github/no-response.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-no-response - https://github.com/probot/no-response
2 |
3 | # Number of days of inactivity before an issue is closed for lack of response
4 | daysUntilClose: 28
5 |
6 | # Label requiring a response
7 | responseRequiredLabel: more-information-needed
8 |
9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable.
10 | closeComment: >
11 | This issue has been automatically closed because there has been no response
12 | to our request for more information from the original author. With only the
13 | information that is currently in the issue, we don't have enough information
14 | to take action. Please reach out if you have or find the answers we need so
15 | that we can investigate further.
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | env:
6 | CI: true
7 |
8 | jobs:
9 | Test:
10 | strategy:
11 | matrix:
12 | os: [macos-latest]
13 | channel: [stable, beta]
14 | runs-on: ${{ matrix.os }}
15 | steps:
16 | - uses: actions/checkout@v1
17 | - uses: UziTech/action-setup-atom@v2
18 | with:
19 | version: ${{ matrix.channel }}
20 | - name: Install dependencies
21 | run: apm install
22 | - name: Run tests
23 | run: atom --test spec
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .pairs
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/.pairs:
--------------------------------------------------------------------------------
1 | pairs:
2 | ns: Nathan Sobo; nathan
3 | cj: Corey Johnson; cj
4 | dg: David Graham; dgraham
5 | ks: Kevin Sawicki; kevin
6 | jc: Jerry Cheung; jerry
7 | bl: Brian Lopez; brian
8 | jp: Justin Palmer; justin
9 | gt: Garen Torikian; garen
10 | mc: Matt Colyer; mcolyer
11 | bo: Ben Ogle; benogle
12 | jr: Jason Rudolph; jasonrudolph
13 | jl: Jessica Lord; jlord
14 | email:
15 | domain: github.com
16 | #global: true
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md)
2 |
3 | To run tests on core atom packages like this one, see: https://flight-manual.atom.io/hacking-atom/sections/writing-specs/#running-specs
4 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ### Prerequisites
10 |
11 | * [ ] Put an X between the brackets on this line if you have done all of the following:
12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/
14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq
15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom
16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages
17 |
18 | ### Description
19 |
20 | [Description of the issue]
21 |
22 | ### Steps to Reproduce
23 |
24 | 1. [First Step]
25 | 2. [Second Step]
26 | 3. [and so on...]
27 |
28 | **Expected behavior:** [What you expect to happen]
29 |
30 | **Actual behavior:** [What actually happens]
31 |
32 | **Reproduces how often:** [What percentage of the time does it reproduce?]
33 |
34 | ### Versions
35 |
36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running.
37 |
38 | ### Additional Information
39 |
40 | Any additional information, configuration or data that might be necessary to reproduce the issue.
41 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 GitHub Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Requirements
2 |
3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.
4 | * All new code requires tests to ensure against regressions
5 |
6 | ### Description of the Change
7 |
8 |
13 |
14 | ### Alternate Designs
15 |
16 |
17 |
18 | ### Benefits
19 |
20 |
21 |
22 | ### Possible Drawbacks
23 |
24 |
25 |
26 | ### Applicable Issues
27 |
28 |
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/)
2 | # Find and Replace package
3 | [](https://github.com/atom/find-and-replace/actions/workflows/ci.yml)
4 |
5 | Find and replace in the current buffer or across the entire project in Atom.
6 |
7 | ## Find in buffer
8 |
9 | Using the shortcut cmd-f (Mac) or ctrl-f (Windows and Linux).
10 | 
11 |
12 | ## Find in project
13 |
14 | Using the shortcut cmd-shift-f (Mac) or ctrl-shift-f (Windows and Linux).
15 | 
16 |
17 | ## Provided Service
18 |
19 | If you need access the marker layer containing result markers for a given editor, use the `find-and-replace@0.0.1` service. The service exposes one method, `resultsMarkerLayerForTextEditor`, which takes a `TextEditor` and returns a `TextEditorMarkerLayer` that you can interact with. Keep in mind that any work you do in synchronous event handlers on this layer will impact the performance of find and replace.
20 |
--------------------------------------------------------------------------------
/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "max_line_length": {
3 | "level": "ignore"
4 | },
5 | "no_empty_param_list": {
6 | "level": "error"
7 | },
8 | "arrow_spacing": {
9 | "level": "error"
10 | },
11 | "no_interpolation_in_single_quotes": {
12 | "level": "error"
13 | },
14 | "no_debugger": {
15 | "level": "error"
16 | },
17 | "prefer_english_operator": {
18 | "level": "error"
19 | },
20 | "colon_assignment_spacing": {
21 | "spacing": {
22 | "left": 0,
23 | "right": 1
24 | },
25 | "level": "error"
26 | },
27 | "braces_spacing": {
28 | "spaces": 0,
29 | "level": "error"
30 | },
31 | "spacing_after_comma": {
32 | "level": "error"
33 | },
34 | "no_stand_alone_at": {
35 | "level": "error"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/docs/events.md:
--------------------------------------------------------------------------------
1 | # Events specification
2 |
3 | This document specifies all the data (along with the format) which gets sent from the Find and Replace package to the GitHub analytics pipeline. This document follows the same format and nomenclature as the [Atom Core Events spec](https://github.com/atom/metrics/blob/master/docs/events.md).
4 |
5 | ## Counters
6 |
7 | Currently Find and Replace does not log any counter events.
8 |
9 | ## Timing events
10 |
11 | #### Time to search on a project
12 |
13 | * **eventType**: `find-and-replace-v1`
14 | * **metadata**
15 |
16 | | field | value |
17 | |-------|-------|
18 | | `ec` | `time-to-search`
19 | | `ev` | Number of found results
20 | | `el` | Search system in use (`ripgrep` or `standard`)
21 |
22 | ## Standard events
23 |
24 | Currently Find and Replace does not log any standard events.
--------------------------------------------------------------------------------
/keymaps/find-and-replace.cson:
--------------------------------------------------------------------------------
1 | '.platform-darwin':
2 | 'cmd-F': 'project-find:show'
3 | 'cmd-f': 'find-and-replace:show'
4 | 'cmd-alt-f': 'find-and-replace:show-replace'
5 |
6 | '.platform-win32, .platform-linux':
7 | 'ctrl-F': 'project-find:show'
8 | 'ctrl-f': 'find-and-replace:show'
9 |
10 | '.platform-darwin atom-text-editor':
11 | 'cmd-g': 'find-and-replace:find-next'
12 | 'cmd-G': 'find-and-replace:find-previous'
13 | 'cmd-f3': 'find-and-replace:find-next-selected'
14 | 'cmd-shift-f3': 'find-and-replace:find-previous-selected'
15 | 'cmd-ctrl-g': 'find-and-replace:select-all'
16 | 'cmd-d': 'find-and-replace:select-next'
17 | 'cmd-alt-e': 'find-and-replace:replace-next'
18 | 'cmd-e': 'find-and-replace:use-selection-as-find-pattern'
19 | 'cmd-shift-e': 'find-and-replace:use-selection-as-replace-pattern'
20 | 'cmd-u': 'find-and-replace:select-undo'
21 | 'cmd-k cmd-d': 'find-and-replace:select-skip'
22 |
23 | '.platform-win32 atom-text-editor, .platform-linux atom-text-editor':
24 | 'f3': 'find-and-replace:find-next'
25 | 'shift-f3': 'find-and-replace:find-previous'
26 | 'ctrl-f3': 'find-and-replace:find-next-selected'
27 | 'ctrl-shift-f3': 'find-and-replace:find-previous-selected'
28 | 'alt-f3': 'find-and-replace:select-all'
29 | 'ctrl-d': 'find-and-replace:select-next'
30 | 'ctrl-e': 'find-and-replace:use-selection-as-find-pattern'
31 | 'ctrl-shift-e': 'find-and-replace:use-selection-as-replace-pattern'
32 | 'ctrl-u': 'find-and-replace:select-undo'
33 | 'ctrl-k ctrl-d': 'find-and-replace:select-skip'
34 |
35 | '.platform-darwin .find-and-replace':
36 | 'shift-enter': 'find-and-replace:show-previous'
37 | 'cmd-enter': 'find-and-replace:confirm'
38 | 'alt-enter': 'find-and-replace:find-all'
39 | 'cmd-alt-/': 'find-and-replace:toggle-regex-option'
40 | 'cmd-alt-c': 'find-and-replace:toggle-case-option'
41 | 'cmd-alt-s': 'find-and-replace:toggle-selection-option'
42 | 'cmd-alt-w': 'find-and-replace:toggle-whole-word-option'
43 |
44 | '.platform-win32 .find-and-replace, .platform-linux .find-and-replace':
45 | 'shift-enter': 'find-and-replace:show-previous'
46 | 'ctrl-enter': 'find-and-replace:confirm'
47 | 'alt-enter': 'find-and-replace:find-all'
48 | 'ctrl-alt-/': 'find-and-replace:toggle-regex-option'
49 | 'ctrl-shift-c': 'find-and-replace:toggle-case-option'
50 |
51 | '.platform-darwin .project-find':
52 | 'cmd-enter': 'project-find:confirm'
53 | 'cmd-alt-/': 'project-find:toggle-regex-option'
54 | 'cmd-alt-c': 'project-find:toggle-case-option'
55 | 'cmd-alt-w': 'project-find:toggle-whole-word-option'
56 |
57 | '.platform-win32 .project-find, .platform-linux .project-find':
58 | 'ctrl-enter': 'project-find:confirm'
59 | 'ctrl-alt-/': 'project-find:toggle-regex-option'
60 | 'ctrl-shift-c': 'project-find:toggle-case-option'
61 |
62 | '.find-and-replace, .project-find, .project-find .results-view':
63 | 'tab': 'find-and-replace:focus-next'
64 | 'shift-tab': 'find-and-replace:focus-previous'
65 |
66 | '.platform-darwin .find-and-replace .replace-container atom-text-editor':
67 | 'cmd-enter': 'find-and-replace:replace-all'
68 | '.platform-darwin .project-find .replace-container atom-text-editor':
69 | 'cmd-enter': 'project-find:replace-all'
70 |
71 | '.platform-win32 .find-and-replace .replace-container atom-text-editor':
72 | 'ctrl-enter': 'find-and-replace:replace-all'
73 | '.platform-win32 .project-find .replace-container atom-text-editor':
74 | 'ctrl-enter': 'project-find:replace-all'
75 |
76 | '.platform-linux .find-and-replace .replace-container atom-text-editor':
77 | 'ctrl-enter': 'find-and-replace:replace-all'
78 | '.platform-linux .project-find .replace-container atom-text-editor':
79 | 'ctrl-enter': 'project-find:replace-all'
80 |
81 | '.results-view':
82 | 'home': 'core:move-to-top'
83 | 'ctrl-home': 'core:move-to-top'
84 | 'end': 'core:move-to-bottom'
85 | 'ctrl-end': 'core:move-to-bottom'
86 |
--------------------------------------------------------------------------------
/lib/buffer-search.js:
--------------------------------------------------------------------------------
1 | const { Point, Range, Emitter, CompositeDisposable, TextBuffer } = require('atom');
2 | const FindOptions = require('./find-options');
3 | const escapeHelper = require('./escape-helper');
4 | const Util = require('./project/util');
5 |
6 | const ResultsMarkerLayersByEditor = new WeakMap;
7 |
8 | module.exports =
9 | class BufferSearch {
10 | constructor(findOptions) {
11 | this.findOptions = findOptions;
12 | this.emitter = new Emitter;
13 | this.subscriptions = null;
14 | this.markers = [];
15 | this.editor = null;
16 | }
17 |
18 | onDidUpdate(callback) {
19 | return this.emitter.on('did-update', callback);
20 | }
21 |
22 | onDidError(callback) {
23 | return this.emitter.on('did-error', callback);
24 | }
25 |
26 | onDidChangeCurrentResult(callback) {
27 | return this.emitter.on('did-change-current-result', callback);
28 | }
29 |
30 | setEditor(editor) {
31 | this.editor = editor;
32 | if (this.subscriptions) this.subscriptions.dispose();
33 | if (this.editor) {
34 | this.subscriptions = new CompositeDisposable;
35 | this.subscriptions.add(this.editor.onDidStopChanging(this.bufferStoppedChanging.bind(this)));
36 | this.subscriptions.add(this.editor.onDidAddSelection(this.setCurrentMarkerFromSelection.bind(this)));
37 | this.subscriptions.add(this.editor.onDidChangeSelectionRange(this.setCurrentMarkerFromSelection.bind(this)));
38 | this.resultsMarkerLayer = this.resultsMarkerLayerForTextEditor(this.editor);
39 | if (this.resultsLayerDecoration) this.resultsLayerDecoration.destroy();
40 | this.resultsLayerDecoration = this.editor.decorateMarkerLayer(this.resultsMarkerLayer, {type: 'highlight', class: 'find-result'});
41 | }
42 | this.recreateMarkers();
43 | }
44 |
45 | getEditor() { return this.editor; }
46 |
47 | setFindOptions(newParams) { return this.findOptions.set(newParams); }
48 |
49 | getFindOptions() { return this.findOptions; }
50 |
51 | resultsMarkerLayerForTextEditor(editor) {
52 | let layer = ResultsMarkerLayersByEditor.get(editor)
53 | if (!layer) {
54 | layer = editor.addMarkerLayer({maintainHistory: false});
55 | ResultsMarkerLayersByEditor.set(editor, layer);
56 | }
57 | return layer;
58 | }
59 |
60 | patternMatchesEmptyString(findPattern) {
61 | const findOptions = new FindOptions(this.findOptions.serialize());
62 | findOptions.set({findPattern});
63 | try {
64 | return findOptions.getFindPatternRegex().test('');
65 | } catch (e) {
66 | this.emitter.emit('did-error', e);
67 | return false;
68 | }
69 | }
70 |
71 | search(findPattern, otherOptions) {
72 | let options = {findPattern};
73 | Object.assign(options, otherOptions);
74 |
75 | const changedParams = this.findOptions.set(options);
76 | if (!this.editor ||
77 | changedParams.findPattern != null ||
78 | changedParams.useRegex != null ||
79 | changedParams.wholeWord != null ||
80 | changedParams.caseSensitive != null ||
81 | changedParams.inCurrentSelection != null ||
82 | (this.findOptions.inCurrentSelection === true
83 | && !selectionsEqual(this.editor.getSelectedBufferRanges(), this.selectedRanges))) {
84 | this.recreateMarkers();
85 | }
86 | }
87 |
88 | replace(markers, replacePattern) {
89 | if (!markers || markers.length === 0) return;
90 |
91 | this.findOptions.set({replacePattern});
92 | const preserveCaseOnReplace = atom.config.get('find-and-replace.preserveCaseOnReplace')
93 |
94 | this.editor.transact(() => {
95 | let findRegex = null
96 |
97 | if (this.findOptions.useRegex) {
98 | findRegex = this.getFindPatternRegex();
99 | replacePattern = escapeHelper.unescapeEscapeSequence(replacePattern);
100 | }
101 |
102 | for (let i = 0, n = markers.length; i < n; i++) {
103 | const marker = markers[i]
104 | const bufferRange = marker.getBufferRange();
105 | const replacedText = this.editor.getTextInBufferRange(bufferRange)
106 | let replacementText = findRegex ? replacedText.replace(findRegex, replacePattern) : replacePattern;
107 | replacementText = preserveCaseOnReplace ? Util.preserveCase(replacementText, replacedText): replacementText
108 | this.editor.setTextInBufferRange(bufferRange, replacementText);
109 |
110 | marker.destroy();
111 | this.markers.splice(this.markers.indexOf(marker), 1);
112 | }
113 | });
114 |
115 | return this.emitter.emit('did-update', this.markers.slice());
116 | }
117 |
118 | destroy() {
119 | if (this.subscriptions) this.subscriptions.dispose();
120 | }
121 |
122 | /*
123 | Section: Private
124 | */
125 |
126 | recreateMarkers() {
127 | if (this.resultsMarkerLayer) {
128 | this.resultsMarkerLayer.clear()
129 | }
130 |
131 | this.markers.length = 0;
132 | const markers = this.createMarkers(Point.ZERO, Point.INFINITY);
133 | if (markers) {
134 | this.markers = markers;
135 | return this.emitter.emit("did-update", this.markers.slice());
136 | }
137 | }
138 |
139 | createMarkers(start, end) {
140 | let newMarkers = [];
141 | if (this.findOptions.findPattern && this.editor) {
142 | this.selectedRanges = this.editor.getSelectedBufferRanges()
143 |
144 | let searchRanges = []
145 | if (this.findOptions.inCurrentSelection) {
146 | searchRanges.push(...this.selectedRanges.filter(range => !range.isEmpty()))
147 | }
148 | if (searchRanges.length === 0) {
149 | searchRanges.push(Range(start, end))
150 | }
151 |
152 | const buffer = this.editor.getBuffer()
153 | const regex = this.getFindPatternRegex(buffer.hasAstral && buffer.hasAstral())
154 | if (regex) {
155 | try {
156 | for (const range of searchRanges) {
157 | const bufferMarkers = this.editor.getBuffer().findAndMarkAllInRangeSync(
158 | this.resultsMarkerLayer.bufferMarkerLayer,
159 | regex,
160 | range,
161 | {invalidate: 'inside'}
162 | );
163 | for (const bufferMarker of bufferMarkers) {
164 | newMarkers.push(this.resultsMarkerLayer.getMarker(bufferMarker.id))
165 | }
166 | }
167 | } catch (error) {
168 | this.emitter.emit('did-error', error);
169 | return false;
170 | }
171 | } else {
172 | return false;
173 | }
174 | }
175 | return newMarkers;
176 | }
177 |
178 | bufferStoppedChanging({changes}) {
179 | let marker;
180 | let scanEnd = Point.ZERO;
181 | let markerIndex = 0;
182 |
183 | for (let change of changes) {
184 | const changeStart = change.start;
185 | const changeEnd = change.start.traverse(change.newExtent);
186 | if (changeEnd.isLessThan(scanEnd)) continue;
187 |
188 | let precedingMarkerIndex = -1;
189 | while (marker = this.markers[markerIndex]) {
190 | if (marker.isValid()) {
191 | if (marker.getBufferRange().end.isGreaterThan(changeStart)) { break; }
192 | precedingMarkerIndex = markerIndex;
193 | } else {
194 | this.markers[markerIndex] = this.recreateMarker(marker);
195 | }
196 | markerIndex++;
197 | }
198 |
199 | let followingMarkerIndex = -1;
200 | while (marker = this.markers[markerIndex]) {
201 | if (marker.isValid()) {
202 | followingMarkerIndex = markerIndex;
203 | if (marker.getBufferRange().start.isGreaterThanOrEqual(changeEnd)) { break; }
204 | } else {
205 | this.markers[markerIndex] = this.recreateMarker(marker);
206 | }
207 | markerIndex++;
208 | }
209 |
210 | let spliceStart, scanStart
211 | if (precedingMarkerIndex >= 0) {
212 | spliceStart = precedingMarkerIndex;
213 | scanStart = this.markers[precedingMarkerIndex].getBufferRange().start;
214 | } else {
215 | spliceStart = 0;
216 | scanStart = Point.ZERO;
217 | }
218 |
219 | let spliceEnd
220 | if (followingMarkerIndex >= 0) {
221 | spliceEnd = followingMarkerIndex;
222 | scanEnd = this.markers[followingMarkerIndex].getBufferRange().end;
223 | } else {
224 | spliceEnd = Infinity;
225 | scanEnd = Point.INFINITY;
226 | }
227 |
228 | const newMarkers = this.createMarkers(scanStart, scanEnd) || [];
229 | const oldMarkers = this.markers.splice(spliceStart, (spliceEnd - spliceStart) + 1, ...newMarkers);
230 | for (let oldMarker of oldMarkers) {
231 | oldMarker.destroy();
232 | }
233 | markerIndex += newMarkers.length - oldMarkers.length;
234 | }
235 |
236 | while (marker = this.markers[++markerIndex]) {
237 | if (!marker.isValid()) {
238 | this.markers[markerIndex] = this.recreateMarker(marker);
239 | }
240 | }
241 |
242 | this.emitter.emit('did-update', this.markers.slice());
243 | this.currentResultMarker = null;
244 | this.setCurrentMarkerFromSelection();
245 | }
246 |
247 | setCurrentMarkerFromSelection() {
248 | const marker = this.findMarker(this.editor.getSelectedBufferRange());
249 |
250 | if (marker === this.currentResultMarker) return;
251 |
252 | if (this.currentResultMarker) {
253 | this.resultsLayerDecoration.setPropertiesForMarker(this.currentResultMarker, null);
254 | this.currentResultMarker = null;
255 | }
256 |
257 | if (marker && !marker.isDestroyed()) {
258 | this.resultsLayerDecoration.setPropertiesForMarker(marker, {type: 'highlight', class: 'current-result'});
259 | this.currentResultMarker = marker;
260 | }
261 |
262 | this.emitter.emit('did-change-current-result', this.currentResultMarker);
263 | }
264 |
265 | findMarker(range) {
266 | if (this.resultsMarkerLayer) {
267 | return this.resultsMarkerLayer.findMarkers({
268 | startBufferPosition: range.start,
269 | endBufferPosition: range.end
270 | })[0];
271 | }
272 | }
273 |
274 | recreateMarker(marker) {
275 | const range = marker.getBufferRange()
276 | marker.destroy();
277 | return this.createMarker(range);
278 | }
279 |
280 | createMarker(range) {
281 | return this.resultsMarkerLayer.markBufferRange(range, {invalidate: 'inside'});
282 | }
283 |
284 | getFindPatternRegex(forceUnicode) {
285 | try {
286 | return this.findOptions.getFindPatternRegex(forceUnicode);
287 | } catch (e) {
288 | this.emitter.emit('did-error', e);
289 | return null;
290 | }
291 | }
292 | };
293 |
294 | function selectionsEqual(selectionsA, selectionsB) {
295 | if (selectionsA.length === selectionsB.length) {
296 | for (let i = 0; i < selectionsA.length; i++) {
297 | if (!selectionsA[i].isEqual(selectionsB[i])) {
298 | return false
299 | }
300 | }
301 | return true
302 | } else {
303 | return false
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/lib/default-file-icons.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-plus')
2 | const path = require('path')
3 |
4 | class DefaultFileIcons {
5 | iconClassForPath (filePath) {
6 | const extension = path.extname(filePath)
7 |
8 | if (fs.isSymbolicLinkSync(filePath)) {
9 | return 'icon-file-symlink-file'
10 | } else if (fs.isReadmePath(filePath)) {
11 | return 'icon-book'
12 | } else if (fs.isCompressedExtension(extension)) {
13 | return 'icon-file-zip'
14 | } else if (fs.isImageExtension(extension)) {
15 | return 'icon-file-media'
16 | } else if (fs.isPdfExtension(extension)) {
17 | return 'icon-file-pdf'
18 | } else if (fs.isBinaryExtension(extension)) {
19 | return 'icon-file-binary'
20 | } else {
21 | return 'icon-file-text'
22 | }
23 | }
24 | }
25 |
26 | module.exports = new DefaultFileIcons()
27 |
--------------------------------------------------------------------------------
/lib/escape-helper.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 | unescapeEscapeSequence: (string) ->
3 | string.replace /\\(.)/gm, (match, char) ->
4 | if char is 't'
5 | '\t'
6 | else if char is 'n'
7 | '\n'
8 | else if char is 'r'
9 | '\r'
10 | else if char is '\\'
11 | '\\'
12 | else
13 | match
14 |
--------------------------------------------------------------------------------
/lib/find-options.coffee:
--------------------------------------------------------------------------------
1 | _ = require 'underscore-plus'
2 | {Emitter} = require 'atom'
3 |
4 | Params = [
5 | 'findPattern'
6 | 'replacePattern'
7 | 'pathsPattern'
8 | 'useRegex'
9 | 'wholeWord'
10 | 'caseSensitive'
11 | 'inCurrentSelection'
12 | 'leadingContextLineCount'
13 | 'trailingContextLineCount'
14 | ]
15 |
16 | module.exports =
17 | class FindOptions
18 | constructor: (state={}) ->
19 | @emitter = new Emitter
20 |
21 | @findPattern = ''
22 | @replacePattern = state.replacePattern ? ''
23 | @pathsPattern = state.pathsPattern ? ''
24 | @useRegex = state.useRegex ? atom.config.get('find-and-replace.useRegex') ? false
25 | @caseSensitive = state.caseSensitive ? atom.config.get('find-and-replace.caseSensitive') ? false
26 | @wholeWord = state.wholeWord ? atom.config.get('find-and-replace.wholeWord') ? false
27 | @inCurrentSelection = state.inCurrentSelection ? atom.config.get('find-and-replace.inCurrentSelection') ? false
28 | @leadingContextLineCount = state.leadingContextLineCount ? atom.config.get('find-and-replace.leadingContextLineCount') ? 0
29 | @trailingContextLineCount = state.trailingContextLineCount ? atom.config.get('find-and-replace.trailingContextLineCount') ? 0
30 |
31 | onDidChange: (callback) ->
32 | @emitter.on('did-change', callback)
33 |
34 | onDidChangeUseRegex: (callback) ->
35 | @emitter.on('did-change-useRegex', callback)
36 |
37 | onDidChangeReplacePattern: (callback) ->
38 | @emitter.on('did-change-replacePattern', callback)
39 |
40 | serialize: ->
41 | result = {}
42 | for param in Params
43 | result[param] = this[param]
44 | result
45 |
46 | set: (newParams={}) ->
47 | changedParams = {}
48 | for key in Params
49 | if newParams[key]? and newParams[key] isnt this[key]
50 | changedParams ?= {}
51 | this[key] = changedParams[key] = newParams[key]
52 |
53 | if Object.keys(changedParams).length
54 | for param, val of changedParams
55 | @emitter.emit("did-change-#{param}")
56 | @emitter.emit('did-change', changedParams)
57 | return changedParams
58 |
59 | getFindPatternRegex: (forceUnicode = false) ->
60 | for i in [0..@findPattern.length]
61 | if @findPattern.charCodeAt(i) > 128
62 | forceUnicode = true
63 | break
64 |
65 | flags = 'gm'
66 | flags += 'i' unless @caseSensitive
67 | flags += 'u' if forceUnicode
68 |
69 | if @useRegex
70 | expression = @findPattern
71 | else
72 | expression = escapeRegExp(@findPattern)
73 |
74 | expression = "\\b#{expression}\\b" if @wholeWord
75 |
76 | new RegExp(expression, flags)
77 |
78 | # This is different from _.escapeRegExp, which escapes dashes. Escaped dashes
79 | # are not allowed outside of character classes in RegExps with the `u` flag.
80 | #
81 | # See atom/find-and-replace#1022
82 | escapeRegExp = (string) ->
83 | string.replace(/[\/\\^$*+?.()|[\]{}]/g, '\\$&')
84 |
--------------------------------------------------------------------------------
/lib/find.coffee:
--------------------------------------------------------------------------------
1 | {CompositeDisposable, Disposable, TextBuffer} = require 'atom'
2 |
3 | SelectNext = require './select-next'
4 | {History, HistoryCycler} = require './history'
5 | FindOptions = require './find-options'
6 | BufferSearch = require './buffer-search'
7 | getIconServices = require './get-icon-services'
8 | FindView = require './find-view'
9 | ProjectFindView = require './project-find-view'
10 | ResultsModel = require './project/results-model'
11 | ResultsPaneView = require './project/results-pane'
12 | ReporterProxy = require './reporter-proxy'
13 |
14 | metricsReporter = new ReporterProxy()
15 |
16 | module.exports =
17 | activate: ({findOptions, findHistory, replaceHistory, pathsHistory}={}) ->
18 | # Convert old config setting for backward compatibility.
19 | if atom.config.get('find-and-replace.openProjectFindResultsInRightPane')
20 | atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'right')
21 | atom.config.unset('find-and-replace.openProjectFindResultsInRightPane')
22 |
23 | atom.workspace.addOpener (filePath) ->
24 | new ResultsPaneView() if filePath.indexOf(ResultsPaneView.URI) isnt -1
25 |
26 | @subscriptions = new CompositeDisposable
27 | @currentItemSub = new Disposable
28 | @findHistory = new History(findHistory)
29 | @replaceHistory = new History(replaceHistory)
30 | @pathsHistory = new History(pathsHistory)
31 |
32 | @findOptions = new FindOptions(findOptions)
33 | @findModel = new BufferSearch(@findOptions)
34 | @resultsModel = new ResultsModel(@findOptions, metricsReporter)
35 |
36 | @subscriptions.add atom.workspace.getCenter().observeActivePaneItem (paneItem) =>
37 | @subscriptions.delete @currentItemSub
38 | @currentItemSub.dispose()
39 |
40 | if atom.workspace.isTextEditor(paneItem)
41 | @findModel.setEditor(paneItem)
42 | else if paneItem?.observeEmbeddedTextEditor?
43 | @currentItemSub = paneItem.observeEmbeddedTextEditor (editor) =>
44 | if atom.workspace.getCenter().getActivePaneItem() is paneItem
45 | @findModel.setEditor(editor)
46 | @subscriptions.add @currentItemSub
47 | else if paneItem?.getEmbeddedTextEditor?
48 | @findModel.setEditor(paneItem.getEmbeddedTextEditor())
49 | else
50 | @findModel.setEditor(null)
51 |
52 | @subscriptions.add atom.commands.add '.find-and-replace, .project-find', 'window:focus-next-pane', ->
53 | atom.views.getView(atom.workspace).focus()
54 |
55 | @subscriptions.add atom.commands.add 'atom-workspace', 'project-find:show', =>
56 | @createViews()
57 | showPanel @projectFindPanel, @findPanel, => @projectFindView.focusFindElement()
58 |
59 | @subscriptions.add atom.commands.add 'atom-workspace', 'project-find:toggle', =>
60 | @createViews()
61 | togglePanel @projectFindPanel, @findPanel, => @projectFindView.focusFindElement()
62 |
63 | @subscriptions.add atom.commands.add 'atom-workspace', 'project-find:show-in-current-directory', ({target}) =>
64 | @createViews()
65 | @findPanel.hide()
66 | @projectFindPanel.show()
67 | @projectFindView.focusFindElement()
68 | @projectFindView.findInCurrentlySelectedDirectory(target)
69 |
70 | @subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:use-selection-as-find-pattern', =>
71 | return if @projectFindPanel?.isVisible() or @findPanel?.isVisible()
72 | @createViews()
73 |
74 | @subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:use-selection-as-replace-pattern', =>
75 | return if @projectFindPanel?.isVisible() or @findPanel?.isVisible()
76 | @createViews()
77 |
78 | @subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:toggle', =>
79 | @createViews()
80 | togglePanel @findPanel, @projectFindPanel, => @findView.focusFindEditor()
81 |
82 | @subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:show', =>
83 | @createViews()
84 | showPanel @findPanel, @projectFindPanel, => @findView.focusFindEditor()
85 |
86 | @subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:show-replace', =>
87 | @createViews()
88 | showPanel @findPanel, @projectFindPanel, => @findView.focusReplaceEditor()
89 |
90 | @subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:clear-history', =>
91 | @findHistory.clear()
92 | @replaceHistory.clear()
93 |
94 | # Handling cancel in the workspace + code editors
95 | handleEditorCancel = ({target}) =>
96 | isMiniEditor = target.tagName is 'ATOM-TEXT-EDITOR' and target.hasAttribute('mini')
97 | unless isMiniEditor
98 | @findPanel?.hide()
99 | @projectFindPanel?.hide()
100 |
101 | @subscriptions.add atom.commands.add 'atom-workspace',
102 | 'core:cancel': handleEditorCancel
103 | 'core:close': handleEditorCancel
104 |
105 | selectNextObjectForEditorElement = (editorElement) =>
106 | @selectNextObjects ?= new WeakMap()
107 | editor = editorElement.getModel()
108 | selectNext = @selectNextObjects.get(editor)
109 | unless selectNext?
110 | selectNext = new SelectNext(editor)
111 | @selectNextObjects.set(editor, selectNext)
112 | selectNext
113 |
114 | showPanel = (panelToShow, panelToHide, postShowAction) ->
115 | panelToHide.hide()
116 | panelToShow.show()
117 | postShowAction?()
118 |
119 | togglePanel = (panelToToggle, panelToHide, postToggleAction) ->
120 | panelToHide.hide()
121 |
122 | if panelToToggle.isVisible()
123 | panelToToggle.hide()
124 | else
125 | panelToToggle.show()
126 | postToggleAction?()
127 |
128 | @subscriptions.add atom.commands.add '.editor:not(.mini)',
129 | 'find-and-replace:select-next': (event) ->
130 | selectNextObjectForEditorElement(this).findAndSelectNext()
131 | 'find-and-replace:select-all': (event) ->
132 | selectNextObjectForEditorElement(this).findAndSelectAll()
133 | 'find-and-replace:select-undo': (event) ->
134 | selectNextObjectForEditorElement(this).undoLastSelection()
135 | 'find-and-replace:select-skip': (event) ->
136 | selectNextObjectForEditorElement(this).skipCurrentSelection()
137 |
138 | consumeMetricsReporter: (service) ->
139 | metricsReporter.setReporter(service)
140 | new Disposable ->
141 | metricsReporter.unsetReporter()
142 |
143 | consumeElementIcons: (service) ->
144 | getIconServices().setElementIcons service
145 | new Disposable ->
146 | getIconServices().resetElementIcons()
147 |
148 | consumeFileIcons: (service) ->
149 | getIconServices().setFileIcons service
150 | new Disposable ->
151 | getIconServices().resetFileIcons()
152 |
153 | toggleAutocompletions: (value) ->
154 | if not @findView?
155 | return
156 | if value
157 | @autocompleteSubscriptions = new CompositeDisposable
158 | disposable = @autocompleteWatchEditor?(@findView.findEditor, ['default'])
159 | if disposable?
160 | @autocompleteSubscriptions.add(disposable)
161 | else
162 | @autocompleteSubscriptions?.dispose()
163 |
164 | consumeAutocompleteWatchEditor: (watchEditor) ->
165 | @autocompleteWatchEditor = watchEditor
166 | atom.config.observe(
167 | 'find-and-replace.autocompleteSearches',
168 | (value) => @toggleAutocompletions(value))
169 | new Disposable =>
170 | @autocompleteSubscriptions?.dispose()
171 | @autocompleteWatchEditor = null
172 |
173 | provideService: ->
174 | resultsMarkerLayerForTextEditor: @findModel.resultsMarkerLayerForTextEditor.bind(@findModel)
175 |
176 | createViews: ->
177 | return if @findView?
178 |
179 | findBuffer = new TextBuffer
180 | replaceBuffer = new TextBuffer
181 | pathsBuffer = new TextBuffer
182 |
183 | findHistoryCycler = new HistoryCycler(findBuffer, @findHistory)
184 | replaceHistoryCycler = new HistoryCycler(replaceBuffer, @replaceHistory)
185 | pathsHistoryCycler = new HistoryCycler(pathsBuffer, @pathsHistory)
186 |
187 | options = {findBuffer, replaceBuffer, pathsBuffer, findHistoryCycler, replaceHistoryCycler, pathsHistoryCycler}
188 |
189 | @findView = new FindView(@findModel, options)
190 |
191 | @projectFindView = new ProjectFindView(@resultsModel, options)
192 |
193 | @findPanel = atom.workspace.addBottomPanel(item: @findView, visible: false, className: 'tool-panel panel-bottom')
194 | @projectFindPanel = atom.workspace.addBottomPanel(item: @projectFindView, visible: false, className: 'tool-panel panel-bottom')
195 |
196 | @findView.setPanel(@findPanel)
197 | @projectFindView.setPanel(@projectFindPanel)
198 |
199 | # HACK: Soooo, we need to get the model to the pane view whenever it is
200 | # created. Creation could come from the opener below, or, more problematic,
201 | # from a deserialize call when splitting panes. For now, all pane views will
202 | # use this same model. This needs to be improved! I dont know the best way
203 | # to deal with this:
204 | # 1. How should serialization work in the case of a shared model.
205 | # 2. Or maybe we create the model each time a new pane is created? Then
206 | # ProjectFindView needs to know about each model so it can invoke a search.
207 | # And on each new model, it will run the search again.
208 | #
209 | # See https://github.com/atom/find-and-replace/issues/63
210 | #ResultsPaneView.model = @resultsModel
211 | # This makes projectFindView accesible in ResultsPaneView so that resultsModel
212 | # can be properly set for ResultsPaneView instances and ProjectFindView instance
213 | # as different pane views don't necessarily use same models anymore
214 | # but most recent pane view and projectFindView do
215 | ResultsPaneView.projectFindView = @projectFindView
216 |
217 | @toggleAutocompletions atom.config.get('find-and-replace.autocompleteSearches')
218 |
219 | deactivate: ->
220 | @findPanel?.destroy()
221 | @findPanel = null
222 | @findView?.destroy()
223 | @findView = null
224 | @findModel?.destroy()
225 | @findModel = null
226 |
227 | @projectFindPanel?.destroy()
228 | @projectFindPanel = null
229 | @projectFindView?.destroy()
230 | @projectFindView = null
231 |
232 | ResultsPaneView.model = null
233 |
234 | @autocompleteSubscriptions?.dispose()
235 | @autocompleteManagerService = null
236 | @subscriptions?.dispose()
237 | @subscriptions = null
238 |
239 | serialize: ->
240 | findOptions: @findOptions.serialize()
241 | findHistory: @findHistory.serialize()
242 | replaceHistory: @replaceHistory.serialize()
243 | pathsHistory: @pathsHistory.serialize()
244 |
--------------------------------------------------------------------------------
/lib/get-icon-services.js:
--------------------------------------------------------------------------------
1 | const DefaultFileIcons = require('./default-file-icons')
2 | const {Emitter, Disposable, CompositeDisposable} = require('atom')
3 |
4 | let iconServices
5 | module.exports = function () {
6 | if (!iconServices) iconServices = new IconServices()
7 | return iconServices
8 | }
9 |
10 | class IconServices {
11 | constructor () {
12 | this.emitter = new Emitter()
13 | this.elementIcons = null
14 | this.elementIconDisposables = new CompositeDisposable()
15 | this.fileIcons = DefaultFileIcons
16 | }
17 |
18 | onDidChange (callback) {
19 | return this.emitter.on('did-change', callback)
20 | }
21 |
22 | resetElementIcons () {
23 | this.setElementIcons(null)
24 | }
25 |
26 | resetFileIcons () {
27 | this.setFileIcons(DefaultFileIcons)
28 | }
29 |
30 | setElementIcons (service) {
31 | if (service !== this.elementIcons) {
32 | if (this.elementIconDisposables != null) {
33 | this.elementIconDisposables.dispose()
34 | }
35 | if (service) { this.elementIconDisposables = new CompositeDisposable() }
36 | this.elementIcons = service
37 | return this.emitter.emit('did-change')
38 | }
39 | }
40 |
41 | setFileIcons (service) {
42 | if (service !== this.fileIcons) {
43 | this.fileIcons = service
44 | return this.emitter.emit('did-change')
45 | }
46 | }
47 |
48 | updateIcon (view, filePath) {
49 | if (this.elementIcons) {
50 | if (view.refs && view.refs.icon instanceof Element) {
51 | if (view.iconDisposable) {
52 | view.iconDisposable.dispose()
53 | this.elementIconDisposables.remove(view.iconDisposable)
54 | }
55 | view.iconDisposable = this.elementIcons(view.refs.icon, filePath)
56 | this.elementIconDisposables.add(view.iconDisposable)
57 | }
58 | } else {
59 | let iconClass = this.fileIcons.iconClassForPath(filePath, 'find-and-replace') || ''
60 | if (Array.isArray(iconClass)) {
61 | iconClass = iconClass.join(' ')
62 | }
63 | view.refs.icon.className = iconClass + ' icon'
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/history.coffee:
--------------------------------------------------------------------------------
1 | _ = require 'underscore-plus'
2 | {Emitter} = require 'atom'
3 |
4 | HISTORY_MAX = 25
5 |
6 | class History
7 | constructor: (@items=[]) ->
8 | @emitter = new Emitter
9 | @length = @items.length
10 |
11 | onDidAddItem: (callback) ->
12 | @emitter.on 'did-add-item', callback
13 |
14 | serialize: ->
15 | @items[-HISTORY_MAX..]
16 |
17 | getLast: ->
18 | _.last(@items)
19 |
20 | getAtIndex: (index) ->
21 | @items[index]
22 |
23 | add: (text) ->
24 | @items.push(text)
25 | @length = @items.length
26 | @emitter.emit 'did-add-item', text
27 |
28 | clear: ->
29 | @items = []
30 | @length = 0
31 |
32 | # Adds the ability to cycle through history
33 | class HistoryCycler
34 |
35 | # * `buffer` an {Editor} instance to attach the cycler to
36 | # * `history` a {History} object
37 | constructor: (@buffer, @history) ->
38 | @index = @history.length
39 | @history.onDidAddItem (text) =>
40 | @buffer.setText(text) if text isnt @buffer.getText()
41 |
42 | addEditorElement: (editorElement) ->
43 | atom.commands.add editorElement,
44 | 'core:move-up': => @previous()
45 | 'core:move-down': => @next()
46 |
47 | previous: ->
48 | if @history.length is 0 or (@atLastItem() and @buffer.getText() isnt @history.getLast())
49 | @scratch = @buffer.getText()
50 | else if @index > 0
51 | @index--
52 |
53 | @buffer.setText @history.getAtIndex(@index) ? ''
54 |
55 | next: ->
56 | if @index < @history.length - 1
57 | @index++
58 | item = @history.getAtIndex(@index)
59 | else if @scratch
60 | item = @scratch
61 | else
62 | item = ''
63 |
64 | @buffer.setText item
65 |
66 | atLastItem: ->
67 | @index is @history.length - 1
68 |
69 | store: ->
70 | text = @buffer.getText()
71 | return if not text or text is @history.getLast()
72 | @scratch = null
73 | @history.add(text)
74 | @index = @history.length - 1
75 |
76 | module.exports = {History, HistoryCycler}
77 |
--------------------------------------------------------------------------------
/lib/project-find-view.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-plus');
2 | const path = require('path');
3 | const _ = require('underscore-plus');
4 | const { TextEditor, Disposable, CompositeDisposable } = require('atom');
5 | const etch = require('etch');
6 | const Util = require('./project/util');
7 | const ResultsModel = require('./project/results-model');
8 | const ResultsPaneView = require('./project/results-pane');
9 | const $ = etch.dom;
10 |
11 | module.exports =
12 | class ProjectFindView {
13 | constructor(model, {findBuffer, replaceBuffer, pathsBuffer, findHistoryCycler, replaceHistoryCycler, pathsHistoryCycler}) {
14 | this.model = model
15 | this.findBuffer = findBuffer
16 | this.replaceBuffer = replaceBuffer
17 | this.pathsBuffer = pathsBuffer
18 | this.findHistoryCycler = findHistoryCycler;
19 | this.replaceHistoryCycler = replaceHistoryCycler;
20 | this.pathsHistoryCycler = pathsHistoryCycler;
21 | this.subscriptions = new CompositeDisposable()
22 | this.modelSupbscriptions = new CompositeDisposable()
23 |
24 | etch.initialize(this)
25 |
26 | this.handleEvents();
27 |
28 | this.findHistoryCycler.addEditorElement(this.findEditor.element);
29 | this.replaceHistoryCycler.addEditorElement(this.replaceEditor.element);
30 | this.pathsHistoryCycler.addEditorElement(this.pathsEditor.element);
31 |
32 | this.onlyRunIfChanged = true;
33 |
34 | this.clearMessages();
35 | this.updateOptionViews();
36 | this.updateSyntaxHighlighting();
37 | }
38 |
39 | update() {}
40 |
41 | render() {
42 | return (
43 | $.div({tabIndex: -1, className: 'project-find padded'},
44 | $.header({className: 'header'},
45 | $.span({ref: 'closeButton', className: 'header-item close-button pull-right'},
46 | $.i({className: "icon icon-x clickable"})
47 | ),
48 | $.span({ref: 'descriptionLabel', className: 'header-item description'}),
49 | $.span({className: 'header-item options-label pull-right'},
50 | $.span({}, 'Finding with Options: '),
51 | $.span({ref: 'optionsLabel', className: 'options'}),
52 | $.span({className: 'btn-group btn-toggle btn-group-options'},
53 | $.button({ref: 'regexOptionButton', className: 'btn option-regex'},
54 | $.svg({className: "icon", innerHTML: ``})
55 | ),
56 | $.button({ref: 'caseOptionButton', className: 'btn option-case-sensitive'},
57 | $.svg({className: "icon", innerHTML: ``})
58 | ),
59 | $.button({ref: 'wholeWordOptionButton', className: 'btn option-whole-word'},
60 | $.svg({className: "icon", innerHTML:``})
61 | )
62 | )
63 | )
64 | ),
65 |
66 | $.section({ref: 'replacmentInfoBlock', className: 'input-block'},
67 | $.progress({ref: 'replacementProgress', className: 'inline-block'}),
68 | $.span({ref: 'replacmentInfo', className: 'inline-block'}, 'Replaced 2 files of 10 files')
69 | ),
70 |
71 | $.section({className: 'input-block find-container'},
72 | $.div({className: 'input-block-item input-block-item--flex editor-container'},
73 | etch.dom(TextEditor, {
74 | ref: 'findEditor',
75 | mini: true,
76 | placeholderText: 'Find in project',
77 | buffer: this.findBuffer
78 | })
79 | ),
80 | $.div({className: 'input-block-item'},
81 | $.div({className: 'btn-group btn-group-find'},
82 | $.button({ref: 'findAllButton', className: 'btn'}, 'Find All')
83 | )
84 | )
85 | ),
86 |
87 | $.section({className: 'input-block replace-container'},
88 | $.div({className: 'input-block-item input-block-item--flex editor-container'},
89 | etch.dom(TextEditor, {
90 | ref: 'replaceEditor',
91 | mini: true,
92 | placeholderText: 'Replace in project',
93 | buffer: this.replaceBuffer
94 | })
95 | ),
96 | $.div({className: 'input-block-item'},
97 | $.div({className: 'btn-group btn-group-replace-all'},
98 | $.button({ref: 'replaceAllButton', className: 'btn disabled'}, 'Replace All')
99 | )
100 | )
101 | ),
102 |
103 | $.section({className: 'input-block paths-container'},
104 | $.div({className: 'input-block-item editor-container'},
105 | etch.dom(TextEditor, {
106 | ref: 'pathsEditor',
107 | mini: true,
108 | placeholderText: 'File/directory pattern: For example `src` to search in the "src" directory; `*.js` to search all JavaScript files; `!src` to exclude the "src" directory; `!*.json` to exclude all JSON files',
109 | buffer: this.pathsBuffer
110 | })
111 | )
112 | )
113 | )
114 | );
115 | }
116 |
117 | get findEditor() { return this.refs.findEditor }
118 | get replaceEditor() { return this.refs.replaceEditor }
119 | get pathsEditor() { return this.refs.pathsEditor }
120 |
121 | destroy() {
122 | if (this.subscriptions) this.subscriptions.dispose();
123 | if (this.tooltipSubscriptions) this.tooltipSubscriptions.dispose();
124 | if (this.modelSupbscriptions) this.modelSupbscriptions.dispose();
125 | this.model = null;
126 | }
127 |
128 | setPanel(panel) {
129 | this.panel = panel;
130 | this.subscriptions.add(this.panel.onDidChangeVisible(visible => {
131 | if (visible) {
132 | this.didShow();
133 | } else {
134 | this.didHide();
135 | }
136 | }));
137 | }
138 |
139 | didShow() {
140 | atom.views.getView(atom.workspace).classList.add('find-visible');
141 | if (this.tooltipSubscriptions != null) { return; }
142 |
143 | this.updateReplaceAllButtonEnablement();
144 | this.tooltipSubscriptions = new CompositeDisposable(
145 | atom.tooltips.add(this.refs.closeButton, {
146 | title: 'Close Panel Esc',
147 | html: true
148 | }),
149 |
150 | atom.tooltips.add(this.refs.regexOptionButton, {
151 | title: "Use Regex",
152 | keyBindingCommand: 'project-find:toggle-regex-option',
153 | keyBindingTarget: this.findEditor.element
154 | }),
155 |
156 | atom.tooltips.add(this.refs.caseOptionButton, {
157 | title: "Match Case",
158 | keyBindingCommand: 'project-find:toggle-case-option',
159 | keyBindingTarget: this.findEditor.element
160 | }),
161 |
162 | atom.tooltips.add(this.refs.wholeWordOptionButton, {
163 | title: "Whole Word",
164 | keyBindingCommand: 'project-find:toggle-whole-word-option',
165 | keyBindingTarget: this.findEditor.element
166 | }),
167 |
168 | atom.tooltips.add(this.refs.findAllButton, {
169 | title: "Find All",
170 | keyBindingCommand: 'find-and-replace:search',
171 | keyBindingTarget: this.findEditor.element
172 | })
173 | );
174 | }
175 |
176 | didHide() {
177 | this.hideAllTooltips();
178 | let workspaceElement = atom.views.getView(atom.workspace);
179 | workspaceElement.focus();
180 | workspaceElement.classList.remove('find-visible');
181 | }
182 |
183 | hideAllTooltips() {
184 | this.tooltipSubscriptions.dispose();
185 | this.tooltipSubscriptions = null;
186 | }
187 |
188 | handleEvents() {
189 | this.subscriptions.add(atom.commands.add('atom-workspace', {
190 | 'find-and-replace:use-selection-as-find-pattern': () => this.setSelectionAsFindPattern(),
191 | 'find-and-replace:use-selection-as-replace-pattern': () => this.setSelectionAsReplacePattern()
192 | }));
193 |
194 | this.subscriptions.add(atom.commands.add(this.element, {
195 | 'find-and-replace:focus-next': () => this.focusNextElement(1),
196 | 'find-and-replace:focus-previous': () => this.focusNextElement(-1),
197 | 'core:confirm': () => this.confirm(),
198 | 'core:close': () => this.panel && this.panel.hide(),
199 | 'core:cancel': () => this.panel && this.panel.hide(),
200 | 'project-find:confirm': () => this.confirm(),
201 | 'project-find:toggle-regex-option': () => this.toggleRegexOption(),
202 | 'project-find:toggle-case-option': () => this.toggleCaseOption(),
203 | 'project-find:toggle-whole-word-option': () => this.toggleWholeWordOption(),
204 | 'project-find:replace-all': () => this.replaceAll()
205 | }));
206 |
207 | let updateInterfaceForSearching = () => {
208 | this.setInfoMessage('Searching...');
209 | };
210 |
211 | let updateInterfaceForResults = results => {
212 | if (results.matchCount === 0 && results.findPattern === '') {
213 | this.clearMessages();
214 | } else {
215 | this.generateResultsMessage(results);
216 | }
217 | this.updateReplaceAllButtonEnablement(results);
218 | };
219 |
220 | const resetInterface = () => {
221 | this.clearMessages();
222 | this.updateReplaceAllButtonEnablement(null);
223 | };
224 | this.handleEvents.resetInterface = resetInterface;
225 |
226 | let afterSearch = () => {
227 | if (atom.config.get('find-and-replace.closeFindPanelAfterSearch')) {
228 | this.panel && this.panel.hide();
229 | }
230 | }
231 |
232 | let searchFinished = results => {
233 | afterSearch();
234 | updateInterfaceForResults(results);
235 | };
236 |
237 | const addModelHandlers = () => {
238 | this.modelSupbscriptions.add(this.model.onDidClear(resetInterface));
239 | this.modelSupbscriptions.add(this.model.onDidClearReplacementState(updateInterfaceForResults));
240 | this.modelSupbscriptions.add(this.model.onDidStartSearching(updateInterfaceForSearching));
241 | this.modelSupbscriptions.add(this.model.onDidNoopSearch(afterSearch));
242 | this.modelSupbscriptions.add(this.model.onDidFinishSearching(searchFinished));
243 | this.modelSupbscriptions.add(this.model.getFindOptions().onDidChange(this.updateOptionViews.bind(this)));
244 | this.modelSupbscriptions.add(this.model.getFindOptions().onDidChangeUseRegex(this.updateSyntaxHighlighting.bind(this)));
245 | }
246 | this.handleEvents.addModelHandlers=addModelHandlers;
247 | addModelHandlers();
248 |
249 | this.element.addEventListener('focus', () => this.findEditor.element.focus());
250 | this.refs.closeButton.addEventListener('click', () => this.panel && this.panel.hide());
251 | this.refs.regexOptionButton.addEventListener('click', () => this.toggleRegexOption());
252 | this.refs.caseOptionButton.addEventListener('click', () => this.toggleCaseOption());
253 | this.refs.wholeWordOptionButton.addEventListener('click', () => this.toggleWholeWordOption());
254 | this.refs.replaceAllButton.addEventListener('click', () => this.replaceAll());
255 | this.refs.findAllButton.addEventListener('click', () => this.search());
256 |
257 | const focusCallback = () => this.onlyRunIfChanged = false;
258 | window.addEventListener('focus', focusCallback);
259 | this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', focusCallback)))
260 |
261 | this.findEditor.getBuffer().onDidChange(() => {
262 | this.updateReplaceAllButtonEnablement(this.model.getResultsSummary());
263 | });
264 | this.handleEventsForReplace();
265 | }
266 |
267 | handleEventsForReplace() {
268 | this.replaceEditor.getBuffer().onDidChange(() => this.model.clearReplacementState());
269 | this.replaceEditor.onDidStopChanging(() => this.model.getFindOptions().set({replacePattern: this.replaceEditor.getText()}));
270 | this.replacementsMade = 0;
271 | const addReplaceModelHandlers = () => {
272 | this.modelSupbscriptions.add(this.model.onDidStartReplacing(promise => {
273 | this.replacementsMade = 0;
274 | this.refs.replacmentInfoBlock.style.display = '';
275 | this.refs.replacementProgress.removeAttribute('value');
276 | }));
277 |
278 | this.modelSupbscriptions.add(this.model.onDidReplacePath(result => {
279 | this.replacementsMade++;
280 | this.refs.replacementProgress.value = this.replacementsMade / this.model.getPathCount();
281 | this.refs.replacmentInfo.textContent = `Replaced ${this.replacementsMade} of ${_.pluralize(this.model.getPathCount(), 'file')}`;
282 | }));
283 |
284 | this.modelSupbscriptions.add(this.model.onDidFinishReplacing(result => this.onFinishedReplacing(result)));
285 | }
286 | this.handleEventsForReplace.addReplaceModelHandlers=addReplaceModelHandlers;
287 | addReplaceModelHandlers();
288 | }
289 |
290 | focusNextElement(direction) {
291 | const elements = [
292 | this.findEditor.element,
293 | this.replaceEditor.element,
294 | this.pathsEditor.element
295 | ];
296 |
297 | let focusedIndex = elements.findIndex(el => el.hasFocus()) + direction;
298 | if (focusedIndex >= elements.length) focusedIndex = 0;
299 | if (focusedIndex < 0) focusedIndex = elements.length - 1;
300 |
301 | elements[focusedIndex].focus();
302 | elements[focusedIndex].getModel().selectAll();
303 | }
304 |
305 | focusFindElement() {
306 | const activeEditor = atom.workspace.getCenter().getActiveTextEditor();
307 | let selectedText = activeEditor && activeEditor.getSelectedText()
308 | if (selectedText && selectedText.indexOf('\n') < 0) {
309 | if (this.model.getFindOptions().useRegex) {
310 | selectedText = Util.escapeRegex(selectedText);
311 | }
312 | this.findEditor.setText(selectedText);
313 | }
314 | this.findEditor.getElement().focus();
315 | this.findEditor.selectAll();
316 | }
317 |
318 | confirm() {
319 | if (this.findEditor.getText().length === 0) {
320 | this.model.clear();
321 | return;
322 | }
323 |
324 | this.findHistoryCycler.store();
325 | this.replaceHistoryCycler.store();
326 | this.pathsHistoryCycler.store();
327 |
328 | let searchPromise = this.search({onlyRunIfChanged: this.onlyRunIfChanged});
329 | this.onlyRunIfChanged = true;
330 | return searchPromise;
331 | }
332 |
333 | async search(options) {
334 | // We always want to set the options passed in, even if we dont end up doing the search
335 | if (options == null) { options = {}; }
336 | this.model.getFindOptions().set(options);
337 |
338 | let findPattern = this.findEditor.getText();
339 | let pathsPattern = this.pathsEditor.getText();
340 | let replacePattern = this.replaceEditor.getText();
341 |
342 | let {onlyRunIfActive, onlyRunIfChanged} = options;
343 | if ((onlyRunIfActive && !this.model.active) || !findPattern) return Promise.resolve();
344 |
345 | await this.showResultPane()
346 |
347 | try {
348 | return await this.model.search(findPattern, pathsPattern, replacePattern, options);
349 | } catch (e) {
350 | this.setErrorMessage(e.message);
351 | }
352 | }
353 |
354 | replaceAll() {
355 | if (!this.model.matchCount) {
356 | atom.beep();
357 | return;
358 | }
359 |
360 | const findPattern = this.model.getLastFindPattern();
361 | const currentPattern = this.findEditor.getText();
362 | if (findPattern && findPattern !== currentPattern) {
363 | atom.confirm({
364 | message: `The searched pattern '${findPattern}' was changed to '${currentPattern}'`,
365 | detailedMessage: `Please run the search with the new pattern '${currentPattern}' before running a replace-all`,
366 | buttons: ['OK']
367 | });
368 | return;
369 | }
370 |
371 | return this.showResultPane().then(() => {
372 | const pathsPattern = this.pathsEditor.getText();
373 | const replacePattern = this.replaceEditor.getText();
374 |
375 | const message = `This will replace '${findPattern}' with '${replacePattern}' ${_.pluralize(this.model.matchCount, 'time')} in ${_.pluralize(this.model.pathCount, 'file')}`;
376 | const buttonChosen = atom.confirm({
377 | message: 'Are you sure you want to replace all?',
378 | detailedMessage: message,
379 | buttons: ['OK', 'Cancel']
380 | });
381 |
382 | if (buttonChosen === 0) {
383 | this.clearMessages();
384 | return this.model.replace(pathsPattern, replacePattern, this.model.getPaths());
385 | }
386 | });
387 | }
388 |
389 | directoryPathForElement(element) {
390 | const directoryElement = element.closest('.directory');
391 | if (directoryElement) {
392 | const pathElement = directoryElement.querySelector('[data-path]')
393 | return pathElement && pathElement.dataset.path;
394 | } else {
395 | const activeEditor = atom.workspace.getCenter().getActiveTextEditor();
396 | if (activeEditor) {
397 | const editorPath = activeEditor.getPath()
398 | if (editorPath) {
399 | return path.dirname(editorPath);
400 | }
401 | }
402 | }
403 | }
404 |
405 | findInCurrentlySelectedDirectory(selectedElement) {
406 | const absolutePath = this.directoryPathForElement(selectedElement);
407 | if (absolutePath) {
408 | let [rootPath, relativePath] = atom.project.relativizePath(absolutePath);
409 | if (rootPath && atom.project.getDirectories().length > 1) {
410 | relativePath = path.join(path.basename(rootPath), relativePath);
411 | }
412 | this.pathsEditor.setText(relativePath);
413 | this.findEditor.getElement().focus();
414 | this.findEditor.selectAll();
415 | }
416 | }
417 |
418 | showResultPane() {
419 | let options = {searchAllPanes: true};
420 | let openDirection = atom.config.get('find-and-replace.projectSearchResultsPaneSplitDirection');
421 | if (openDirection !== 'none') { options.split = openDirection; }
422 | return atom.workspace.open(ResultsPaneView.URI, options);
423 | }
424 |
425 | onFinishedReplacing(results) {
426 | if (!results.replacedPathCount) atom.beep();
427 | this.refs.replacmentInfoBlock.style.display = 'none';
428 | }
429 |
430 | generateResultsMessage(results) {
431 | let message = Util.getSearchResultsMessage(results);
432 | if (results.replacedPathCount != null) { message = Util.getReplacementResultsMessage(results); }
433 | this.setInfoMessage(message);
434 | }
435 |
436 | clearMessages() {
437 | this.element.classList.remove('has-results', 'has-no-results');
438 | this.setInfoMessage('Find in Project');
439 | this.refs.replacmentInfoBlock.style.display = 'none';
440 | }
441 |
442 | setInfoMessage(infoMessage) {
443 | this.refs.descriptionLabel.innerHTML = infoMessage;
444 | this.refs.descriptionLabel.classList.remove('text-error');
445 | }
446 |
447 | setErrorMessage(errorMessage) {
448 | this.refs.descriptionLabel.innerHTML = errorMessage;
449 | this.refs.descriptionLabel.classList.add('text-error');
450 | }
451 |
452 | updateReplaceAllButtonEnablement(results) {
453 | const canReplace = results &&
454 | results.matchCount &&
455 | results.findPattern == this.findEditor.getText();
456 | if (canReplace && !this.refs.replaceAllButton.classList.contains('disabled')) return;
457 |
458 | if (this.replaceTooltipSubscriptions) this.replaceTooltipSubscriptions.dispose();
459 | this.replaceTooltipSubscriptions = new CompositeDisposable;
460 |
461 | if (canReplace) {
462 | this.refs.replaceAllButton.classList.remove('disabled');
463 | this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
464 | title: "Replace All",
465 | keyBindingCommand: 'project-find:replace-all',
466 | keyBindingTarget: this.replaceEditor.element
467 | }));
468 | } else {
469 | this.refs.replaceAllButton.classList.add('disabled');
470 | this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
471 | title: "Replace All [run a search to enable]"}
472 | ));
473 | }
474 | }
475 |
476 | setSelectionAsFindPattern() {
477 | const editor = atom.workspace.getCenter().getActivePaneItem();
478 | if (editor && editor.getSelectedText) {
479 | let pattern = editor.getSelectedText() || editor.getWordUnderCursor();
480 | if (this.model.getFindOptions().useRegex) {
481 | pattern = Util.escapeRegex(pattern);
482 | }
483 | if (pattern) {
484 | this.findEditor.setText(pattern);
485 | this.findEditor.getElement().focus();
486 | this.findEditor.selectAll();
487 | }
488 | }
489 | }
490 |
491 | setSelectionAsReplacePattern() {
492 | const editor = atom.workspace.getCenter().getActivePaneItem();
493 | if (editor && editor.getSelectedText) {
494 | let pattern = editor.getSelectedText() || editor.getWordUnderCursor();
495 | if (this.model.getFindOptions().useRegex) {
496 | pattern = Util.escapeRegex(pattern);
497 | }
498 | if (pattern) {
499 | this.replaceEditor.setText(pattern);
500 | this.replaceEditor.getElement().focus();
501 | this.replaceEditor.selectAll();
502 | }
503 | }
504 | }
505 |
506 | updateOptionViews() {
507 | this.updateOptionButtons();
508 | this.updateOptionsLabel();
509 | }
510 |
511 | updateSyntaxHighlighting() {
512 | if (this.model.getFindOptions().useRegex) {
513 | this.findEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp'));
514 | return this.replaceEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp.replacement'));
515 | } else {
516 | this.findEditor.setGrammar(atom.grammars.nullGrammar);
517 | return this.replaceEditor.setGrammar(atom.grammars.nullGrammar);
518 | }
519 | }
520 |
521 | updateOptionsLabel() {
522 | const label = [];
523 |
524 | if (this.model.getFindOptions().useRegex) {
525 | label.push('Regex');
526 | }
527 |
528 | if (this.model.getFindOptions().caseSensitive) {
529 | label.push('Case Sensitive');
530 | } else {
531 | label.push('Case Insensitive');
532 | }
533 |
534 | if (this.model.getFindOptions().wholeWord) {
535 | label.push('Whole Word');
536 | }
537 |
538 | this.refs.optionsLabel.textContent = label.join(', ');
539 | }
540 |
541 | updateOptionButtons() {
542 | this.setOptionButtonState(this.refs.regexOptionButton, this.model.getFindOptions().useRegex);
543 | this.setOptionButtonState(this.refs.caseOptionButton, this.model.getFindOptions().caseSensitive);
544 | this.setOptionButtonState(this.refs.wholeWordOptionButton, this.model.getFindOptions().wholeWord);
545 | }
546 |
547 | setOptionButtonState(optionButton, selected) {
548 | if (selected) {
549 | optionButton.classList.add('selected');
550 | } else {
551 | optionButton.classList.remove('selected');
552 | }
553 | }
554 |
555 | toggleRegexOption() {
556 | this.search({onlyRunIfActive: true, useRegex: !this.model.getFindOptions().useRegex});
557 | }
558 |
559 | toggleCaseOption() {
560 | this.search({onlyRunIfActive: true, caseSensitive: !this.model.getFindOptions().caseSensitive});
561 | }
562 |
563 | toggleWholeWordOption() {
564 | this.search({onlyRunIfActive: true, wholeWord: !this.model.getFindOptions().wholeWord});
565 | }
566 | };
567 |
--------------------------------------------------------------------------------
/lib/project/list-view.js:
--------------------------------------------------------------------------------
1 | const etch = require('etch');
2 | const $ = etch.dom;
3 |
4 | module.exports = class ListView {
5 | constructor({items, heightForItem, itemComponent, className}) {
6 | this.items = items;
7 | this.heightForItem = heightForItem;
8 | this.itemComponent = itemComponent;
9 | this.className = className;
10 | this.previousScrollTop = 0
11 | this.previousClientHeight = 0
12 | etch.initialize(this);
13 |
14 | const resizeObserver = new ResizeObserver(() => etch.update(this));
15 | resizeObserver.observe(this.element);
16 | this.element.addEventListener('scroll', () => etch.update(this));
17 | }
18 |
19 | update({items, heightForItem, itemComponent, className} = {}) {
20 | if (items) this.items = items;
21 | if (heightForItem) this.heightForItem = heightForItem;
22 | if (itemComponent) this.itemComponent = itemComponent;
23 | if (className) this.className = className;
24 | return etch.update(this)
25 | }
26 |
27 | render() {
28 | const children = [];
29 | let itemTopPosition = 0;
30 |
31 | if (this.element) {
32 | let {scrollTop, clientHeight} = this.element;
33 | if (clientHeight > 0) {
34 | this.previousScrollTop = scrollTop
35 | this.previousClientHeight = clientHeight
36 | } else {
37 | scrollTop = this.previousScrollTop
38 | clientHeight = this.previousClientHeight
39 | }
40 |
41 | const scrollBottom = scrollTop + clientHeight;
42 |
43 | let i = 0;
44 |
45 | for (; i < this.items.length; i++) {
46 | let itemBottomPosition = itemTopPosition + this.heightForItem(this.items[i], i);
47 | if (itemBottomPosition > scrollTop) break;
48 | itemTopPosition = itemBottomPosition;
49 | }
50 |
51 | for (; i < this.items.length; i++) {
52 | const item = this.items[i];
53 | const itemHeight = this.heightForItem(this.items[i], i);
54 | children.push(
55 | $.div(
56 | {
57 | style: {
58 | position: 'absolute',
59 | height: `${itemHeight}px`,
60 | width: '100%',
61 | top: `${itemTopPosition}px`
62 | },
63 | key: i
64 | },
65 | etch.dom(this.itemComponent, {
66 | item: item,
67 | top: Math.max(0, scrollTop - itemTopPosition),
68 | bottom: Math.min(itemHeight, scrollBottom - itemTopPosition)
69 | })
70 | )
71 | );
72 |
73 | itemTopPosition += itemHeight;
74 | if (itemTopPosition >= scrollBottom) {
75 | i++
76 | break;
77 | }
78 | }
79 | for (; i < this.items.length; i++) {
80 | itemTopPosition += this.heightForItem(this.items[i], i);
81 | }
82 | }
83 |
84 | return $.div(
85 | {
86 | className: 'results-view-container',
87 | style: {
88 | position: 'relative',
89 | height: '100%',
90 | overflow: 'auto',
91 | }
92 | },
93 | $.ol(
94 | {
95 | ref: 'list',
96 | className: this.className,
97 | style: {height: `${itemTopPosition}px`}
98 | },
99 | ...children
100 | )
101 | );
102 | }
103 | };
104 |
--------------------------------------------------------------------------------
/lib/project/result-row-view.js:
--------------------------------------------------------------------------------
1 | const getIconServices = require('../get-icon-services');
2 | const { Range } = require('atom');
3 | const {
4 | LeadingContextRow,
5 | TrailingContextRow,
6 | ResultPathRow,
7 | MatchRow,
8 | ResultRowGroup
9 | } = require('./result-row');
10 | const {showIf} = require('./util');
11 |
12 | const _ = require('underscore-plus');
13 | const path = require('path');
14 | const assert = require('assert');
15 | const etch = require('etch');
16 | const $ = etch.dom;
17 |
18 | class ResultPathRowView {
19 | constructor({groupData, isSelected}) {
20 | const props = {groupData, isSelected};
21 | this.props = Object.assign({}, props);
22 |
23 | etch.initialize(this);
24 | getIconServices().updateIcon(this, groupData.filePath);
25 | }
26 |
27 | destroy() {
28 | return etch.destroy(this)
29 | }
30 |
31 | update({groupData, isSelected}) {
32 | const props = {groupData, isSelected};
33 |
34 | if (!_.isEqual(props, this.props)) {
35 | this.props = Object.assign({}, props);
36 | etch.update(this);
37 | }
38 | }
39 |
40 | writeAfterUpdate() {
41 | getIconServices().updateIcon(this, this.props.groupData.filePath);
42 | }
43 |
44 | render() {
45 | let relativePath = this.props.groupData.filePath;
46 | if (atom.project) {
47 | let rootPath;
48 | [rootPath, relativePath] = atom.project.relativizePath(this.props.groupData.filePath);
49 | if (rootPath && atom.project.getDirectories().length > 1) {
50 | relativePath = path.join(path.basename(rootPath), relativePath);
51 | }
52 | }
53 | const groupData = this.props.groupData;
54 | return (
55 | $.li(
56 | {
57 | className: [
58 | // This triggers the CSS displaying the "expand / collapse" arrows
59 | // See `styles/lists.less` in the atom-ui repository for details
60 | 'list-nested-item',
61 | groupData.isCollapsed ? 'collapsed' : '',
62 | this.props.isSelected ? 'selected' : ''
63 | ].join(' ').trim(),
64 | key: groupData.filePath
65 | },
66 | $.div(
67 | {
68 | className: 'list-item path-row',
69 | dataset: { filePath: groupData.filePath }
70 | },
71 | $.span({
72 | dataset: {name: path.basename(groupData.filePath)},
73 | ref: 'icon',
74 | className: 'icon'
75 | }),
76 | $.span({className: 'path-name bright'}, relativePath),
77 | $.span(
78 | {ref: 'description', className: 'path-match-number'},
79 | `(${groupData.matchCount} match${groupData.matchCount === 1 ? '' : 'es'})`
80 | )
81 | )
82 | )
83 | )
84 | }
85 | };
86 |
87 | class MatchRowView {
88 | constructor({rowData, groupData, isSelected, replacePattern, regex}) {
89 | const props = {rowData, groupData, isSelected, replacePattern, regex};
90 | const previewData = {matches: rowData.matches, replacePattern, regex};
91 |
92 | this.props = Object.assign({}, props);
93 | this.previewData = previewData;
94 | this.previewNode = this.generatePreviewNode(previewData);
95 |
96 | etch.initialize(this);
97 | }
98 |
99 | update({rowData, groupData, isSelected, replacePattern, regex}) {
100 | const props = {rowData, groupData, isSelected, replacePattern, regex};
101 | const previewData = {matches: rowData.matches, replacePattern, regex};
102 |
103 | if (!_.isEqual(props, this.props)) {
104 | if (!_.isEqual(previewData, this.previewData)) {
105 | this.previewData = previewData;
106 | this.previewNode = this.generatePreviewNode(previewData);
107 | }
108 | this.props = Object.assign({}, props);
109 | etch.update(this);
110 | }
111 | }
112 |
113 | generatePreviewNode({matches, replacePattern, regex}) {
114 | const subnodes = [];
115 |
116 | let prevMatchEnd = matches[0].lineTextOffset;
117 | 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;
121 |
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
125 |
126 | const prefix = match.lineText.slice(prefixStart, matchStart);
127 |
128 | let replacementText = ''
129 | if (replacePattern && regex) {
130 | replacementText = match.matchText.replace(regex, replacePattern);
131 | } else if (replacePattern) {
132 | replacementText = replacePattern;
133 | }
134 |
135 | subnodes.push(
136 | $.span({}, prefix),
137 | $.span(
138 | {
139 | className:
140 | `match ${replacementText ? 'highlight-error' : 'highlight-info'}`
141 | },
142 | match.matchText
143 | ),
144 | $.span(
145 | {
146 | className: 'replacement highlight-success',
147 | style: showIf(replacementText)
148 | },
149 | replacementText
150 | )
151 | );
152 | prevMatchEnd = range.end.column;
153 | }
154 |
155 | const lastMatch = matches[matches.length - 1];
156 | const suffix = lastMatch.lineText.slice(
157 | prevMatchEnd - lastMatch.lineTextOffset
158 | );
159 |
160 | return $.span(
161 | {className: 'preview'},
162 | ...subnodes,
163 | $.span({}, suffix)
164 | );
165 | }
166 |
167 | render() {
168 | return (
169 | $.li(
170 | {
171 | className: [
172 | 'list-item',
173 | 'match-row',
174 | this.props.isSelected ? 'selected' : '',
175 | this.props.rowData.separator ? 'separator' : ''
176 | ].join(' ').trim(),
177 | dataset: {
178 | filePath: this.props.groupData.filePath,
179 | matchLineNumber: this.props.rowData.lineNumber,
180 | }
181 | },
182 | $.span(
183 | {className: 'line-number text-subtle'},
184 | this.props.rowData.lineNumber + 1
185 | ),
186 | this.previewNode
187 | )
188 | );
189 | }
190 | };
191 |
192 | class ContextRowView {
193 | constructor({rowData, groupData, isSelected}) {
194 | const props = {rowData, groupData, isSelected};
195 | this.props = Object.assign({}, props);
196 |
197 | etch.initialize(this);
198 | }
199 |
200 | destroy() {
201 | return etch.destroy(this)
202 | }
203 |
204 | update({rowData, groupData, isSelected}) {
205 | const props = {rowData, groupData, isSelected};
206 |
207 | if (!_.isEqual(props, this.props)) {
208 | this.props = Object.assign({}, props);
209 | etch.update(this);
210 | }
211 | }
212 |
213 | render() {
214 | return (
215 | $.li(
216 | {
217 | className: [
218 | 'list-item',
219 | 'context-row',
220 | this.props.rowData.separator ? 'separator' : ''
221 | ].join(' ').trim(),
222 | dataset: {
223 | filePath: this.props.groupData.filePath,
224 | matchLineNumber: this.props.rowData.matchLineNumber
225 | },
226 | },
227 | $.span({className: 'line-number text-subtle'}, this.props.rowData.lineNumber + 1),
228 | $.span({className: 'preview'}, $.span({}, this.props.rowData.line))
229 | )
230 | )
231 | }
232 | }
233 |
234 | function getRowViewType(row) {
235 | if (row instanceof ResultPathRow) {
236 | return ResultPathRowView;
237 | }
238 | if (row instanceof MatchRow) {
239 | return MatchRowView;
240 | }
241 | if (row instanceof LeadingContextRow) {
242 | return ContextRowView;
243 | }
244 | if (row instanceof TrailingContextRow) {
245 | return ContextRowView;
246 | }
247 | assert(false);
248 | }
249 |
250 | module.exports =
251 | class ResultRowView {
252 | constructor({item}) {
253 | const props = {
254 | rowData: Object.assign({}, item.row.data),
255 | groupData: Object.assign({}, item.row.group.data),
256 | isSelected: item.isSelected,
257 | replacePattern: item.replacePattern,
258 | regex: item.regex
259 | };
260 | this.props = props;
261 | this.rowViewType = getRowViewType(item.row);
262 |
263 | etch.initialize(this);
264 | }
265 |
266 | destroy() {
267 | return etch.destroy(this);
268 | }
269 |
270 | update({item}) {
271 | const props = {
272 | rowData: Object.assign({}, item.row.data),
273 | groupData: Object.assign({}, item.row.group.data),
274 | isSelected: item.isSelected,
275 | replacePattern: item.replacePattern,
276 | regex: item.regex
277 | }
278 | this.props = props;
279 | this.rowViewType = getRowViewType(item.row);
280 | etch.update(this);
281 | }
282 |
283 | render() {
284 | return $(this.rowViewType, this.props);
285 | }
286 | };
287 |
--------------------------------------------------------------------------------
/lib/project/result-row.js:
--------------------------------------------------------------------------------
1 | const {Range} = require('atom');
2 |
3 | class LeadingContextRow {
4 | constructor(rowGroup, line, separator, matchLineNumber, rowOffset) {
5 | this.group = rowGroup
6 | this.rowOffset = rowOffset
7 | // props
8 | this.data = {
9 | separator,
10 | line,
11 | lineNumber: matchLineNumber - rowOffset,
12 | matchLineNumber,
13 | }
14 | }
15 | }
16 |
17 | class TrailingContextRow {
18 | constructor(rowGroup, line, separator, matchLineNumber, rowOffset) {
19 | this.group = rowGroup
20 | this.rowOffset = rowOffset
21 | // props
22 | this.data = {
23 | separator,
24 | line,
25 | lineNumber: matchLineNumber + rowOffset,
26 | matchLineNumber,
27 | }
28 | }
29 | }
30 |
31 | class ResultPathRow {
32 | constructor(rowGroup) {
33 | this.group = rowGroup
34 | // props
35 | this.data = {
36 | separator: false,
37 | }
38 | }
39 | }
40 |
41 | class MatchRow {
42 | constructor(rowGroup, separator, lineNumber, matches) {
43 | this.group = rowGroup
44 | // props
45 | this.data = {
46 | separator,
47 | lineNumber,
48 | matchLineNumber: lineNumber,
49 | matches,
50 | }
51 | }
52 | }
53 |
54 | class ResultRowGroup {
55 | constructor(result, findOptions) {
56 | this.data = { isCollapsed: false }
57 | this.setResult(result)
58 |
59 | this.rows = []
60 | this.collapsedRows = []
61 | this.generateRows(findOptions)
62 | this.previousRowCount = this.rows.length
63 | }
64 |
65 | setResult(result) {
66 | this.result = result
67 | this.data = {
68 | filePath: result.filePath,
69 | matchCount: result.matches.length,
70 | isCollapsed: this.data.isCollapsed,
71 | }
72 | }
73 |
74 | generateRows(findOptions) {
75 | const { leadingContextLineCount, trailingContextLineCount } = findOptions
76 | this.leadingContextLineCount = leadingContextLineCount
77 | this.trailingContextLineCount = trailingContextLineCount
78 | let rowArrays = [ [new ResultPathRow(this)] ]
79 |
80 | // This loop accumulates the match lines and the context lines of the
81 | // result; the added complexity comes from the fact that there musn't be
82 | // context lines between adjacent match lines
83 | let prevMatch = null
84 | let prevMatchRow = null
85 | let prevLineNumber
86 | for (const match of this.result.matches) {
87 | const { leadingContextLines } = match
88 | const lineNumber = Range.fromObject(match.range).start.row
89 |
90 | let leadCount
91 | if (prevMatch) {
92 | const interval = Math.max(lineNumber - prevLineNumber - 1, 0)
93 |
94 | const trailCount = Math.min(trailingContextLineCount, interval)
95 | const { trailingContextLines } = prevMatch
96 | rowArrays.push(
97 | trailingContextLines.slice(0, trailCount).map((line, i) => (
98 | new TrailingContextRow(this, line, false, prevLineNumber, i + 1)
99 | ))
100 | )
101 | leadCount = Math.min(leadingContextLineCount, interval - trailCount)
102 | } else {
103 | leadCount = Math.min(leadingContextLineCount, leadingContextLines.length)
104 | }
105 |
106 | rowArrays.push(
107 | leadingContextLines.slice(leadingContextLines.length - leadCount).map((line, i) => (
108 | new LeadingContextRow(this, line, false, lineNumber, leadCount - i)
109 | ))
110 | )
111 |
112 | if (prevMatchRow && lineNumber === prevLineNumber) {
113 | prevMatchRow.data.matches.push(match)
114 | } else {
115 | prevMatchRow = new MatchRow(this, false, lineNumber, [match])
116 | rowArrays.push([ prevMatchRow ])
117 | }
118 |
119 | prevMatch = match
120 | prevLineNumber = lineNumber
121 | }
122 |
123 | const { trailingContextLines } = prevMatch
124 | rowArrays.push(
125 | trailingContextLines.slice(0, trailingContextLineCount).map((line, i) => (
126 | new TrailingContextRow(this, line, false, prevLineNumber, i + 1)
127 | ))
128 | )
129 | this.rows = [].concat(...rowArrays)
130 | this.collapsedRows = [ this.rows[0] ]
131 |
132 | let prevRow = null
133 | for (const row of this.rows) {
134 | row.data.separator = (
135 | prevRow &&
136 | row.data.lineNumber != null && prevRow.data.lineNumber != null &&
137 | row.data.lineNumber > prevRow.data.lineNumber + 1
138 | ) ? true : false
139 | prevRow = row
140 | }
141 | }
142 |
143 | displayedRows() {
144 | return this.data.isCollapsed ? this.collapsedRows : this.rows
145 | }
146 | }
147 |
148 | module.exports = {
149 | LeadingContextRow,
150 | TrailingContextRow,
151 | ResultPathRow,
152 | MatchRow,
153 | ResultRowGroup
154 | }
155 |
--------------------------------------------------------------------------------
/lib/project/results-model.js:
--------------------------------------------------------------------------------
1 | const _ = require('underscore-plus')
2 | const {Emitter, TextEditor, Range} = require('atom')
3 | const escapeHelper = require('../escape-helper')
4 |
5 | class Result {
6 | static create (result) {
7 | if (result && result.matches && result.matches.length) {
8 | const matches = []
9 |
10 | for (const m of result.matches) {
11 | const range = Range.fromObject(m.range)
12 | const matchSplit = m.matchText.split('\n')
13 | const linesSplit = m.lineText.split('\n')
14 |
15 | // If the result spans across multiple lines, process each of
16 | // them separately by creating separate `matches` objects for
17 | // each line on the match.
18 | for (let row = range.start.row; row <= range.end.row; row++) {
19 | const lineText = linesSplit[row - range.start.row]
20 | const matchText = matchSplit[row - range.start.row]
21 |
22 | // When receiving multiline results from opened buffers, only
23 | // the first result line is provided on the `lineText` property.
24 | // This makes it impossible to properly render the part of the result
25 | // that's part of other lines.
26 | // In order to prevent an error we just need to ignore these parts.
27 | if (lineText === undefined || matchText === undefined) {
28 | continue
29 | }
30 |
31 | // Adapt the range column number based on which line we're at:
32 | // - the first line of a multiline result will always start at the range start
33 | // and will end at the end of the line.
34 | // - middle lines will start at 0 and end at the end of the line
35 | // - last line will start at 0 and end at the range end.
36 | const startColumn = row === range.start.row ? range.start.column : 0
37 | const endColumn = row === range.end.row ? range.end.column : lineText.length
38 |
39 | matches.push({
40 | matchText,
41 | lineText,
42 | lineTextOffset: m.lineTextOffset,
43 | range: {
44 | start: {
45 | row,
46 | column: startColumn
47 | },
48 | end: {
49 | row,
50 | column: endColumn
51 | }
52 | },
53 | leadingContextLines: m.leadingContextLines,
54 | trailingContextLines: m.trailingContextLines
55 | })
56 | }
57 | }
58 |
59 | return new Result({filePath: result.filePath, matches})
60 | } else {
61 | return null
62 | }
63 | }
64 |
65 | constructor (result) {
66 | _.extend(this, result)
67 | }
68 | }
69 |
70 | module.exports = class ResultsModel {
71 | constructor (findOptions, metricsReporter) {
72 | this.metricsReporter = metricsReporter
73 | this.onContentsModified = this.onContentsModified.bind(this)
74 | this.findOptions = findOptions
75 | this.emitter = new Emitter()
76 |
77 | atom.workspace.getCenter().observeActivePaneItem(item => {
78 | if (item instanceof TextEditor && atom.project.contains(item.getPath())) {
79 | item.onDidStopChanging(() => this.onContentsModified(item))
80 | }
81 | })
82 |
83 | this.clear()
84 | }
85 |
86 | onDidClear (callback) {
87 | return this.emitter.on('did-clear', callback)
88 | }
89 |
90 | onDidClearSearchState (callback) {
91 | return this.emitter.on('did-clear-search-state', callback)
92 | }
93 |
94 | onDidClearReplacementState (callback) {
95 | return this.emitter.on('did-clear-replacement-state', callback)
96 | }
97 |
98 | onDidSearchPaths (callback) {
99 | return this.emitter.on('did-search-paths', callback)
100 | }
101 |
102 | onDidErrorForPath (callback) {
103 | return this.emitter.on('did-error-for-path', callback)
104 | }
105 |
106 | onDidNoopSearch (callback) {
107 | return this.emitter.on('did-noop-search', callback)
108 | }
109 |
110 | onDidStartSearching (callback) {
111 | return this.emitter.on('did-start-searching', callback)
112 | }
113 |
114 | onDidCancelSearching (callback) {
115 | return this.emitter.on('did-cancel-searching', callback)
116 | }
117 |
118 | onDidFinishSearching (callback) {
119 | return this.emitter.on('did-finish-searching', callback)
120 | }
121 |
122 | onDidStartReplacing (callback) {
123 | return this.emitter.on('did-start-replacing', callback)
124 | }
125 |
126 | onDidFinishReplacing (callback) {
127 | return this.emitter.on('did-finish-replacing', callback)
128 | }
129 |
130 | onDidSearchPath (callback) {
131 | return this.emitter.on('did-search-path', callback)
132 | }
133 |
134 | onDidReplacePath (callback) {
135 | return this.emitter.on('did-replace-path', callback)
136 | }
137 |
138 | onDidAddResult (callback) {
139 | return this.emitter.on('did-add-result', callback)
140 | }
141 |
142 | onDidSetResult (callback) {
143 | return this.emitter.on('did-set-result', callback)
144 | }
145 |
146 | onDidRemoveResult (callback) {
147 | return this.emitter.on('did-remove-result', callback)
148 | }
149 |
150 | clear () {
151 | this.clearSearchState()
152 | this.clearReplacementState()
153 | this.emitter.emit('did-clear', this.getResultsSummary())
154 | }
155 |
156 | clearSearchState () {
157 | this.pathCount = 0
158 | this.matchCount = 0
159 | this.regex = null
160 | this.results = {}
161 | this.active = false
162 | this.searchErrors = null
163 |
164 | if (this.inProgressSearchPromise != null) {
165 | this.inProgressSearchPromise.cancel()
166 | this.inProgressSearchPromise = null
167 | }
168 |
169 | this.emitter.emit('did-clear-search-state', this.getResultsSummary())
170 | }
171 |
172 | clearReplacementState () {
173 | this.replacePattern = null
174 | this.replacedPathCount = null
175 | this.replacementCount = null
176 | this.replacementErrors = null
177 | this.emitter.emit('did-clear-replacement-state', this.getResultsSummary())
178 | }
179 |
180 | shouldRerunSearch (findPattern, pathsPattern, options = {}) {
181 | return (
182 | !options.onlyRunIfChanged ||
183 | findPattern == null ||
184 | findPattern !== this.lastFindPattern ||
185 | pathsPattern == null ||
186 | pathsPattern !== this.lastPathsPattern
187 | )
188 | }
189 |
190 | async search (findPattern, pathsPattern, replacePattern, options = {}) {
191 | if (!this.shouldRerunSearch(findPattern, pathsPattern, options)) {
192 | this.emitter.emit('did-noop-search')
193 | return Promise.resolve()
194 | }
195 |
196 | const {keepReplacementState} = options
197 | if (keepReplacementState) {
198 | this.clearSearchState()
199 | } else {
200 | this.clear()
201 | }
202 |
203 | this.lastFindPattern = findPattern
204 | this.lastPathsPattern = pathsPattern
205 | this.findOptions.set(_.extend({findPattern, replacePattern, pathsPattern}, options))
206 | this.regex = this.findOptions.getFindPatternRegex()
207 |
208 | this.active = true
209 | const searchPaths = this.pathsArrayFromPathsPattern(pathsPattern)
210 |
211 | const onPathsSearched = numberOfPathsSearched => {
212 | this.emitter.emit('did-search-paths', numberOfPathsSearched)
213 | }
214 |
215 | const leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore')
216 | const trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter')
217 |
218 | const startTime = Date.now()
219 | const useRipgrep = atom.config.get('find-and-replace.useRipgrep')
220 | const enablePCRE2 = atom.config.get('find-and-replace.enablePCRE2')
221 |
222 | this.inProgressSearchPromise = atom.workspace.scan(
223 | this.regex,
224 | {
225 | paths: searchPaths,
226 | onPathsSearched,
227 | leadingContextLineCount,
228 | ripgrep: useRipgrep,
229 | PCRE2: enablePCRE2,
230 | trailingContextLineCount
231 | },
232 | (result, error) => {
233 | if (result) {
234 | this.setResult(result.filePath, Result.create(result))
235 | } else {
236 | if (this.searchErrors == null) { this.searchErrors = [] }
237 | this.searchErrors.push(error)
238 | this.emitter.emit('did-error-for-path', error)
239 | }
240 | })
241 |
242 | this.emitter.emit('did-start-searching', this.inProgressSearchPromise)
243 |
244 | const message = await this.inProgressSearchPromise
245 |
246 | if (message === 'cancelled') {
247 | this.emitter.emit('did-cancel-searching')
248 | } else {
249 | const resultsSummary = this.getResultsSummary()
250 |
251 | this.metricsReporter.sendSearchEvent(
252 | Date.now() - startTime,
253 | resultsSummary.matchCount,
254 | useRipgrep ? 'ripgrep' : 'standard'
255 | )
256 | this.inProgressSearchPromise = null
257 | this.emitter.emit('did-finish-searching', resultsSummary)
258 | }
259 | }
260 |
261 | replace (pathsPattern, replacePattern, replacementPaths) {
262 | if (!this.findOptions.findPattern || (this.regex == null)) { return }
263 |
264 | this.findOptions.set({replacePattern, pathsPattern})
265 |
266 | if (this.findOptions.useRegex) { replacePattern = escapeHelper.unescapeEscapeSequence(replacePattern) }
267 |
268 | this.active = false // not active until the search is finished
269 | this.replacedPathCount = 0
270 | this.replacementCount = 0
271 |
272 | const promise = atom.workspace.replace(this.regex, replacePattern, replacementPaths, (result, error) => {
273 | if (result) {
274 | if (result.replacements) {
275 | this.replacedPathCount++
276 | this.replacementCount += result.replacements
277 | }
278 | this.emitter.emit('did-replace-path', result)
279 | } else {
280 | if (this.replacementErrors == null) { this.replacementErrors = [] }
281 | this.replacementErrors.push(error)
282 | this.emitter.emit('did-error-for-path', error)
283 | }
284 | })
285 |
286 | this.emitter.emit('did-start-replacing', promise)
287 | return promise.then(() => {
288 | this.emitter.emit('did-finish-replacing', this.getResultsSummary())
289 | return this.search(this.findOptions.findPattern, this.findOptions.pathsPattern,
290 | this.findOptions.replacePattern, {keepReplacementState: true})
291 | }).catch(e => console.error(e.stack))
292 | }
293 |
294 | setActive (isActive) {
295 | if ((isActive && this.findOptions.findPattern) || !isActive) {
296 | this.active = isActive
297 | }
298 | }
299 |
300 | getActive () { return this.active }
301 |
302 | getFindOptions () { return this.findOptions }
303 |
304 | getLastFindPattern () { return this.lastFindPattern }
305 |
306 | getResultsSummary () {
307 | const findPattern = this.lastFindPattern != null ? this.lastFindPattern : this.findOptions.findPattern
308 | const { replacePattern } = this.findOptions
309 | return {
310 | findPattern,
311 | replacePattern,
312 | pathCount: this.pathCount,
313 | matchCount: this.matchCount,
314 | searchErrors: this.searchErrors,
315 | replacedPathCount: this.replacedPathCount,
316 | replacementCount: this.replacementCount,
317 | replacementErrors: this.replacementErrors
318 | }
319 | }
320 |
321 | getPathCount () {
322 | return this.pathCount
323 | }
324 |
325 | getMatchCount () {
326 | return this.matchCount
327 | }
328 |
329 | getPaths () {
330 | return Object.keys(this.results)
331 | }
332 |
333 | getResult (filePath) {
334 | return this.results[filePath]
335 | }
336 |
337 | setResult (filePath, result) {
338 | if (result == null) {
339 | return this.removeResult(filePath)
340 | }
341 | if (!this.results[filePath]) {
342 | return this.addResult(filePath, result)
343 | }
344 |
345 | this.matchCount += result.matches.length - this.results[filePath].matches.length
346 |
347 | this.results[filePath] = result
348 | this.emitter.emit('did-set-result', {filePath, result})
349 | }
350 |
351 | addResult (filePath, result) {
352 | this.pathCount++
353 | this.matchCount += result.matches.length
354 |
355 | this.results[filePath] = result
356 | this.emitter.emit('did-add-result', {filePath, result})
357 | }
358 |
359 | removeResult (filePath) {
360 | if (!this.results[filePath]) {
361 | return
362 | }
363 |
364 | this.pathCount--
365 | this.matchCount -= this.results[filePath].matches.length
366 |
367 | const result = this.results[filePath]
368 | delete this.results[filePath]
369 | this.emitter.emit('did-remove-result', {filePath, result})
370 | }
371 |
372 | onContentsModified (editor) {
373 | if (!this.active || !this.regex || !editor.getPath()) { return }
374 |
375 | const matches = []
376 | const leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore')
377 | const trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter')
378 | editor.scan(this.regex,
379 | {leadingContextLineCount, trailingContextLineCount},
380 | (match) => matches.push(match)
381 | )
382 |
383 | const result = Result.create({filePath: editor.getPath(), matches})
384 | this.setResult(editor.getPath(), result)
385 | this.emitter.emit('did-finish-searching', this.getResultsSummary())
386 | }
387 |
388 | pathsArrayFromPathsPattern (pathsPattern) {
389 | return pathsPattern.trim().split(',').map((inputPath) => inputPath.trim())
390 | }
391 | }
392 |
393 | // Exported for tests
394 | module.exports.Result = Result
395 |
--------------------------------------------------------------------------------
/lib/project/results-pane.js:
--------------------------------------------------------------------------------
1 | const _ = require('underscore-plus');
2 | const {CompositeDisposable} = require('atom');
3 | const ResultsView = require('./results-view');
4 | const ResultsModel = require('./results-model');
5 | const {showIf, getSearchResultsMessage, escapeHtml} = require('./util');
6 | const etch = require('etch');
7 | const $ = etch.dom;
8 |
9 |
10 | module.exports =
11 | class ResultsPaneView {
12 | constructor() {
13 | this.model = ResultsPaneView.projectFindView.model;
14 | this.model.setActive(true);
15 | this.isLoading = false;
16 | this.searchErrors = [];
17 | this.searchResults = null;
18 | this.searchingIsSlow = false;
19 | this.numberOfPathsSearched = 0;
20 | this.searchContextLineCountBefore = 0;
21 | this.searchContextLineCountAfter = 0;
22 | this.uri = ResultsPaneView.URI;
23 |
24 | etch.initialize(this);
25 |
26 | this.onFinishedSearching(this.model.getResultsSummary());
27 | this.element.addEventListener('focus', this.focused.bind(this));
28 | this.element.addEventListener('click', event => {
29 | switch (event.target) {
30 | case this.refs.collapseAll:
31 | this.collapseAllResults();
32 | break;
33 | case this.refs.expandAll:
34 | this.expandAllResults();
35 | break;
36 | case this.refs.decrementLeadingContextLines:
37 | this.decrementLeadingContextLines();
38 | break;
39 | case this.refs.toggleLeadingContextLines:
40 | this.toggleLeadingContextLines();
41 | break;
42 | case this.refs.incrementLeadingContextLines:
43 | this.incrementLeadingContextLines();
44 | break;
45 | case this.refs.decrementTrailingContextLines:
46 | this.decrementTrailingContextLines();
47 | break;
48 | case this.refs.toggleTrailingContextLines:
49 | this.toggleTrailingContextLines();
50 | break;
51 | case this.refs.incrementTrailingContextLines:
52 | this.incrementTrailingContextLines();
53 | case this.refs.dontOverrideTab:
54 | this.dontOverrideTab();
55 | break;
56 | }
57 | })
58 |
59 | this.subscriptions = new CompositeDisposable(
60 | this.model.onDidStartSearching(this.onSearch.bind(this)),
61 | this.model.onDidFinishSearching(this.onFinishedSearching.bind(this)),
62 | this.model.onDidClear(this.onCleared.bind(this)),
63 | this.model.onDidClearReplacementState(this.onReplacementStateCleared.bind(this)),
64 | this.model.onDidSearchPaths(this.onPathsSearched.bind(this)),
65 | this.model.onDidErrorForPath(error => this.appendError(error.message)),
66 | atom.config.observe('find-and-replace.searchContextLineCountBefore', this.searchContextLineCountChanged.bind(this)),
67 | atom.config.observe('find-and-replace.searchContextLineCountAfter', this.searchContextLineCountChanged.bind(this))
68 | );
69 | }
70 |
71 | update() {}
72 |
73 | destroy() {
74 | this.model.setActive(false);
75 | this.subscriptions.dispose();
76 | if(this.separatePane)
77 | this.model = null;
78 | }
79 |
80 | render() {
81 | const matchCount = this.searchResults && this.searchResults.matchCount;
82 |
83 | return (
84 | $.div(
85 | {
86 | tabIndex: -1,
87 | className: `preview-pane pane-item ${matchCount === 0 ? 'no-results' : ''}`,
88 | },
89 |
90 | $.div({className: 'preview-header'},
91 | $.span({
92 | ref: 'previewCount',
93 | className: 'preview-count inline-block',
94 | innerHTML: this.isLoading
95 | ? 'Searching...'
96 | : (getSearchResultsMessage(this.searchResults) || 'Project search results')
97 | }),
98 |
99 | $.button(
100 | {
101 | ref: 'dontOverrideTab',
102 | style: {display: matchCount == 0 || this.isLoading ? 'none' : ''},
103 | className: 'btn'
104 | }, "Don't override this tab"),
105 |
106 | $.div(
107 | {
108 | ref: 'previewControls',
109 | className: 'preview-controls',
110 | style: {display: matchCount > 0 ? '' : 'none'}
111 | },
112 |
113 | this.searchContextLineCountBefore > 0 ?
114 | $.div({className: 'btn-group'},
115 | $.button(
116 | {
117 | ref: 'decrementLeadingContextLines',
118 | className: 'btn' + (this.model.getFindOptions().leadingContextLineCount === 0 ? ' disabled' : '')
119 | }, '-'),
120 | $.button(
121 | {
122 | ref: 'toggleLeadingContextLines',
123 | className: 'btn'
124 | },
125 | $.svg(
126 | {
127 | className: 'icon',
128 | innerHTML: ''
129 | }
130 | )
131 | ),
132 | $.button(
133 | {
134 | ref: 'incrementLeadingContextLines',
135 | className: 'btn' + (this.model.getFindOptions().leadingContextLineCount >= this.searchContextLineCountBefore ? ' disabled' : '')
136 | }, '+')
137 | ) : null,
138 |
139 | this.searchContextLineCountAfter > 0 ?
140 | $.div({className: 'btn-group'},
141 | $.button(
142 | {
143 | ref: 'decrementTrailingContextLines',
144 | className: 'btn' + (this.model.getFindOptions().trailingContextLineCount === 0 ? ' disabled' : '')
145 | }, '-'),
146 | $.button(
147 | {
148 | ref: 'toggleTrailingContextLines',
149 | className: 'btn'
150 | },
151 | $.svg(
152 | {
153 | className: 'icon',
154 | innerHTML: ''
155 | }
156 | )
157 | ),
158 | $.button(
159 | {
160 | ref: 'incrementTrailingContextLines',
161 | className: 'btn' + (this.model.getFindOptions().trailingContextLineCount >= this.searchContextLineCountAfter ? ' disabled' : '')
162 | }, '+')
163 | ) : null,
164 |
165 | $.div({className: 'btn-group'},
166 | $.button({ref: 'collapseAll', className: 'btn'}, 'Collapse All'),
167 | $.button({ref: 'expandAll', className: 'btn'}, 'Expand All')
168 | )
169 | ),
170 |
171 | $.div({className: 'inline-block', style: showIf(this.isLoading)},
172 | $.div({className: 'loading loading-spinner-tiny inline-block'}),
173 |
174 | $.div(
175 | {
176 | className: 'inline-block',
177 | style: showIf(this.isLoading && this.searchingIsSlow)
178 | },
179 |
180 | $.span({ref: 'searchedCount', className: 'searched-count'},
181 | this.numberOfPathsSearched.toString()
182 | ),
183 | $.span({}, ' paths searched')
184 | )
185 | )
186 | ),
187 |
188 | $.ul(
189 | {
190 | ref: 'errorList',
191 | className: 'error-list list-group padded',
192 | style: showIf(this.searchErrors.length > 0)
193 | },
194 |
195 | ...this.searchErrors.map(message =>
196 | $.li({className: 'text-error'}, escapeHtml(message))
197 | )
198 | ),
199 |
200 | etch.dom(ResultsView, {ref: 'resultsView', model: this.model}),
201 |
202 | $.ul(
203 | {
204 | className: 'centered background-message no-results-overlay',
205 | style: showIf(matchCount === 0)
206 | },
207 | $.li({}, 'No Results')
208 | )
209 | )
210 | );
211 | }
212 |
213 | copy() {
214 | return new ResultsPaneView();
215 | }
216 |
217 | getTitle() {
218 | return 'Project Find Results';
219 | }
220 |
221 | getIconName() {
222 | return 'search';
223 | }
224 |
225 | getURI() {
226 | return this.uri;
227 | }
228 |
229 | focused() {
230 | this.refs.resultsView.element.focus();
231 | }
232 |
233 | appendError(message) {
234 | this.searchErrors.push(message)
235 | etch.update(this);
236 | }
237 |
238 | onSearch(searchPromise) {
239 | this.isLoading = true;
240 | this.searchingIsSlow = false;
241 | this.numberOfPathsSearched = 0;
242 |
243 | setTimeout(() => {
244 | this.searchingIsSlow = true;
245 | etch.update(this);
246 | }, 500);
247 |
248 | etch.update(this);
249 |
250 | let stopLoading = () => {
251 | this.isLoading = false;
252 | etch.update(this);
253 | };
254 | return searchPromise.then(stopLoading, stopLoading);
255 | }
256 |
257 | onPathsSearched(numberOfPathsSearched) {
258 | this.numberOfPathsSearched = numberOfPathsSearched;
259 | etch.update(this);
260 | }
261 |
262 | onFinishedSearching(results) {
263 | this.searchResults = results;
264 | if (results.searchErrors || results.replacementErrors) {
265 | this.searchErrors =
266 | _.pluck(results.replacementErrors, 'message')
267 | .concat(_.pluck(results.searchErrors, 'message'));
268 | } else {
269 | this.searchErrors = [];
270 | }
271 | etch.update(this);
272 | }
273 |
274 | onReplacementStateCleared(results) {
275 | this.searchResults = results;
276 | this.searchErrors = [];
277 | etch.update(this);
278 | }
279 |
280 | onCleared() {
281 | this.isLoading = false;
282 | this.searchErrors = [];
283 | this.searchResults = {};
284 | this.searchingIsSlow = false;
285 | this.numberOfPathsSearched = 0;
286 | etch.update(this);
287 | }
288 |
289 | collapseAllResults() {
290 | this.refs.resultsView.collapseAllResults();
291 | this.refs.resultsView.element.focus();
292 | }
293 |
294 | expandAllResults() {
295 | this.refs.resultsView.expandAllResults();
296 | this.refs.resultsView.element.focus();
297 | }
298 |
299 | decrementLeadingContextLines() {
300 | this.refs.resultsView.decrementLeadingContextLines();
301 | etch.update(this);
302 | }
303 |
304 | toggleLeadingContextLines() {
305 | this.refs.resultsView.toggleLeadingContextLines();
306 | etch.update(this);
307 | }
308 |
309 | incrementLeadingContextLines() {
310 | this.refs.resultsView.incrementLeadingContextLines();
311 | etch.update(this);
312 | }
313 |
314 | decrementTrailingContextLines() {
315 | this.refs.resultsView.decrementTrailingContextLines();
316 | etch.update(this);
317 | }
318 |
319 | toggleTrailingContextLines() {
320 | this.refs.resultsView.toggleTrailingContextLines();
321 | etch.update(this);
322 | }
323 |
324 | incrementTrailingContextLines() {
325 | this.refs.resultsView.incrementTrailingContextLines();
326 | etch.update(this);
327 | }
328 |
329 | searchContextLineCountChanged() {
330 | this.searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
331 | this.searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
332 | // update the visible line count in the find options to not exceed the maximum available lines
333 | let findOptionsChanged = false;
334 | if (this.searchContextLineCountBefore < this.model.getFindOptions().leadingContextLineCount) {
335 | this.model.getFindOptions().leadingContextLineCount = this.searchContextLineCountBefore;
336 | findOptionsChanged = true;
337 | }
338 | if (this.searchContextLineCountAfter < this.model.getFindOptions().trailingContextLineCount) {
339 | this.model.getFindOptions().trailingContextLineCount = this.searchContextLineCountAfter;
340 | findOptionsChanged = true;
341 | }
342 | etch.update(this);
343 | if (findOptionsChanged) {
344 | etch.update(this.refs.resultsView);
345 | }
346 | }
347 |
348 | dontOverrideTab(){
349 | let view = ResultsPaneView.projectFindView;
350 | view.handleEvents.resetInterface();
351 | view.model = new ResultsModel(view.model.findOptions,view.model.metricsReporter);
352 | this.uri = ResultsPaneView.URI + "/" + this.model.getLastFindPattern();
353 | this.refs.dontOverrideTab.classList.add('disabled');
354 |
355 | view.modelSupbscriptions.dispose();
356 | view.handleEvents.addModelHandlers();
357 | view.handleEventsForReplace.addReplaceModelHandlers();
358 | this.separatePane=true;
359 | }
360 | }
361 |
362 | module.exports.URI = "atom://find-and-replace/project-results";
363 |
--------------------------------------------------------------------------------
/lib/project/results-view.js:
--------------------------------------------------------------------------------
1 | const { Range, CompositeDisposable, Disposable } = require('atom');
2 | const ResultRowView = require('./result-row-view');
3 | const {
4 | LeadingContextRow,
5 | TrailingContextRow,
6 | ResultPathRow,
7 | MatchRow,
8 | ResultRowGroup
9 | } = require('./result-row');
10 |
11 | const ListView = require('./list-view');
12 | const etch = require('etch');
13 | const binarySearch = require('binary-search')
14 |
15 | const path = require('path');
16 | const _ = require('underscore-plus');
17 | const $ = etch.dom;
18 |
19 | const reverseDirections = {
20 | left: 'right',
21 | right: 'left',
22 | up: 'down',
23 | down: 'up'
24 | };
25 |
26 | const filepathComp = (path1, path2) => path1.localeCompare(path2)
27 |
28 | module.exports =
29 | class ResultsView {
30 | constructor({model}) {
31 | this.model = model;
32 | this.pixelOverdraw = 100;
33 |
34 | this.resultRowGroups = Object.values(model.results).map(result =>
35 | new ResultRowGroup(result, this.model.getFindOptions())
36 | )
37 | this.resultRowGroups.sort((group1, group2) => filepathComp(
38 | group1.result.filePath, group2.result.filePath
39 | ))
40 | this.rowGroupLengths = this.resultRowGroups.map(group => group.rows.length)
41 |
42 | this.resultRows = [].concat(...this.resultRowGroups.map(group => group.rows))
43 | this.selectedRowIndex = this.resultRows.length ? 0 : -1
44 |
45 | this.fakeGroup = new ResultRowGroup({
46 | filePath: 'fake-file-path',
47 | matches: [{
48 | range: [[0, 1], [0, 2]],
49 | leadingContextLines: ['test-line-before'],
50 | trailingContextLines: ['test-line-after'],
51 | lineTextOffset: 1,
52 | lineText: 'fake-line-text',
53 | matchText: 'fake-match-text',
54 | }],
55 | },
56 | {
57 | leadingContextLineCount: 1,
58 | trailingContextLineCount: 0
59 | })
60 |
61 | etch.initialize(this);
62 |
63 | const resizeObserver = new ResizeObserver(this.invalidateItemHeights.bind(this));
64 | resizeObserver.observe(this.element);
65 | this.element.addEventListener('mousedown', this.handleClick.bind(this));
66 |
67 | this.subscriptions = new CompositeDisposable(
68 | atom.config.observe('editor.fontFamily', this.fontFamilyChanged.bind(this)),
69 | this.model.onDidAddResult(this.didAddResult.bind(this)),
70 | this.model.onDidSetResult(this.didSetResult.bind(this)),
71 | this.model.onDidRemoveResult(this.didRemoveResult.bind(this)),
72 | this.model.onDidClearSearchState(this.didClearSearchState.bind(this)),
73 | this.model.getFindOptions().onDidChangeReplacePattern(() => etch.update(this)),
74 |
75 | atom.commands.add(this.element, {
76 | 'core:move-up': this.moveUp.bind(this),
77 | 'core:move-down': this.moveDown.bind(this),
78 | 'core:move-left': this.collapseResult.bind(this),
79 | 'core:move-right': this.expandResult.bind(this),
80 | 'core:page-up': this.pageUp.bind(this),
81 | 'core:page-down': this.pageDown.bind(this),
82 | 'core:move-to-top': this.moveToTop.bind(this),
83 | 'core:move-to-bottom': this.moveToBottom.bind(this),
84 | 'core:confirm': this.confirmResult.bind(this),
85 | 'core:copy': this.copyResult.bind(this),
86 | 'find-and-replace:copy-path': this.copyPath.bind(this),
87 | 'find-and-replace:open-in-new-tab': this.openInNewTab.bind(this),
88 | })
89 | );
90 | }
91 |
92 | update() {}
93 |
94 | destroy() {
95 | this.subscriptions.dispose();
96 | }
97 |
98 | getRowHeight(resultRow) {
99 | if (resultRow instanceof LeadingContextRow) {
100 | return this.contextRowHeight
101 | } else if (resultRow instanceof TrailingContextRow) {
102 | return this.contextRowHeight
103 | } else if (resultRow instanceof ResultPathRow) {
104 | return this.pathRowHeight
105 | } else if (resultRow instanceof MatchRow) {
106 | return this.matchRowHeight
107 | }
108 | }
109 |
110 | render () {
111 | this.maintainPreviousScrollPosition();
112 |
113 | let regex = null, replacePattern = null;
114 | if (this.model.replacedPathCount == null) {
115 | regex = this.model.regex;
116 | replacePattern = this.model.getFindOptions().replacePattern;
117 | }
118 |
119 | return $.div(
120 | {
121 | className: 'results-view focusable-panel',
122 | tabIndex: '-1',
123 | style: this.previewStyle
124 | },
125 |
126 | $.ol(
127 | {
128 | className: 'list-tree has-collapsable-children',
129 | style: {visibility: 'hidden', position: 'absolute', overflow: 'hidden', left: 0, top: 0, right: 0}
130 | },
131 | $(ResultRowView, {
132 | ref: 'dummyResultPathRowView',
133 | item: {
134 | row: this.fakeGroup.rows[0],
135 | regex, replacePattern
136 | }
137 | }),
138 | $(ResultRowView, {
139 | ref: 'dummyContextRowView',
140 | item: {
141 | row: this.fakeGroup.rows[1],
142 | regex, replacePattern
143 | }
144 | }),
145 | $(ResultRowView, {
146 | ref: 'dummyMatchRowView',
147 | item: {
148 | row: this.fakeGroup.rows[2],
149 | regex, replacePattern
150 | }
151 | })
152 | ),
153 |
154 | $(ListView, {
155 | ref: 'listView',
156 | className: 'list-tree has-collapsable-children',
157 | itemComponent: ResultRowView,
158 | heightForItem: item => this.getRowHeight(item.row),
159 | items: this.resultRows.map((row, i) => ({
160 | row,
161 | isSelected: i === this.selectedRowIndex,
162 | regex,
163 | replacePattern
164 | }))
165 | })
166 | );
167 | }
168 |
169 | async invalidateItemHeights() {
170 | const {
171 | dummyResultPathRowView,
172 | dummyMatchRowView,
173 | dummyContextRowView,
174 | } = this.refs;
175 |
176 | const pathRowHeight = dummyResultPathRowView.element.offsetHeight
177 | const matchRowHeight = dummyMatchRowView.element.offsetHeight
178 | const contextRowHeight = dummyContextRowView.element.offsetHeight
179 |
180 | const clientHeight = this.refs.listView && this.refs.listView.element.clientHeight;
181 |
182 | if (matchRowHeight !== this.matchRowHeight ||
183 | pathRowHeight !== this.pathRowHeight ||
184 | contextRowHeight !== this.contextRowHeight ||
185 | clientHeight !== this.clientHeight) {
186 | this.matchRowHeight = matchRowHeight;
187 | this.pathRowHeight = pathRowHeight;
188 | this.contextRowHeight = contextRowHeight;
189 | this.clientHeight = clientHeight;
190 | await etch.update(this);
191 | }
192 |
193 | etch.update(this);
194 | }
195 |
196 | // This method should be the only one allowed to modify this.resultRows
197 | spliceRows(start, deleteCount, rows) {
198 | this.resultRows.splice(start, deleteCount, ...rows)
199 |
200 | if (this.selectedRowIndex >= start + deleteCount) {
201 | this.selectedRowIndex += rows.length - deleteCount
202 | this.scrollToSelectedMatch()
203 | } else if (this.selectedRowIndex >= start + rows.length) {
204 | this.selectRow(start + rows.length - 1)
205 | }
206 | }
207 |
208 | invalidateRowGroup(firstRowIndex, groupIndex) {
209 | const { leadingContextLineCount, trailingContextLineCount } = this.model.getFindOptions()
210 | const rowGroup = this.resultRowGroups[groupIndex]
211 |
212 | if (!rowGroup.data.isCollapsed) {
213 | rowGroup.generateRows(this.model.getFindOptions())
214 | }
215 | this.spliceRows(
216 | firstRowIndex, this.rowGroupLengths[groupIndex],
217 | rowGroup.displayedRows()
218 | )
219 | this.rowGroupLengths[groupIndex] = rowGroup.displayedRows().length
220 | }
221 |
222 | getGroupCountBefore(filePath) {
223 | const res = binarySearch(
224 | this.resultRowGroups, filePath,
225 | (rowGroup, needle) => filepathComp(rowGroup.result.filePath, needle)
226 | )
227 | return res < 0 ? -res - 1 : res
228 | }
229 |
230 | getRowCountBefore(groupIndex) {
231 | let rowCount = 0
232 | for (let i = 0; i < groupIndex; ++i) {
233 | rowCount += this.resultRowGroups[i].displayedRows().length
234 | }
235 | return rowCount
236 | }
237 |
238 | // These four methods are the only ones allowed to modify this.resultRowGroups
239 | didAddResult({result, filePath}) {
240 | const groupIndex = this.getGroupCountBefore(filePath)
241 | const rowGroup = new ResultRowGroup(result, this.model.getFindOptions())
242 |
243 | this.resultRowGroups.splice(groupIndex, 0, rowGroup)
244 | this.rowGroupLengths.splice(groupIndex, 0, rowGroup.rows.length)
245 |
246 | const rowIndex = this.getRowCountBefore(groupIndex)
247 | this.spliceRows(rowIndex, 0, rowGroup.displayedRows())
248 |
249 | if (this.selectedRowIndex === -1) {
250 | this.selectRow(0)
251 | }
252 |
253 | etch.update(this);
254 | }
255 |
256 | didSetResult({result, filePath}) {
257 | const groupIndex = this.getGroupCountBefore(filePath)
258 | const rowGroup = this.resultRowGroups[groupIndex]
259 | const rowIndex = this.getRowCountBefore(groupIndex)
260 |
261 | rowGroup.result = result
262 | this.invalidateRowGroup(rowIndex, groupIndex)
263 |
264 | etch.update(this);
265 | }
266 |
267 | didRemoveResult({filePath}) {
268 | const groupIndex = this.getGroupCountBefore(filePath)
269 | const rowGroup = this.resultRowGroups[groupIndex]
270 | const rowIndex = this.getRowCountBefore(groupIndex)
271 |
272 | this.spliceRows(rowIndex, rowGroup.displayedRows().length, [])
273 | this.resultRowGroups.splice(groupIndex, 1)
274 | this.rowGroupLengths.splice(groupIndex, 1)
275 |
276 | etch.update(this);
277 | }
278 |
279 | didClearSearchState() {
280 | this.selectedRowIndex = -1
281 | this.resultRowGroups = []
282 | this.resultRows = []
283 | etch.update(this);
284 | }
285 |
286 | handleClick(event) {
287 | const clickedItem = event.target.closest('.list-item');
288 |
289 | if (!clickedItem) return;
290 |
291 | const groupIndex = this.getGroupCountBefore(clickedItem.dataset.filePath)
292 | const group = this.resultRowGroups[groupIndex]
293 |
294 | if (clickedItem.matches('.context-row, .match-row')) {
295 | // The third argument restricts the range to omit the path row
296 | const rowIndex = binarySearch(
297 | group.rows, clickedItem.dataset.matchLineNumber,
298 | ((row, lineNb) => row.data.lineNumber - lineNb),
299 | 1
300 | )
301 | this.selectRow(this.getRowCountBefore(groupIndex) + rowIndex)
302 | } else {
303 | // If the user clicks on the left of a match, the match group is collapsed
304 | this.selectRow(this.getRowCountBefore(groupIndex))
305 | }
306 |
307 | // Only apply confirmResult (open editor, collapse group) on left click
308 | if (!event.ctrlKey && event.button === 0 && event.which !== 3) {
309 | this.confirmResult({pending: event.detail === 1});
310 | event.preventDefault();
311 | }
312 | etch.update(this);
313 | }
314 |
315 | // This method should be the only one allowed to modify this.selectedRowIndex
316 | selectRow(i) {
317 | if (this.resultRows.length === 0) {
318 | this.selectedRowIndex = -1
319 | return etch.update(this)
320 | }
321 |
322 | if (i < 0) {
323 | this.selectedRowIndex = 0
324 | } else if (i >= this.resultRows.length) {
325 | this.selectedRowIndex = this.resultRows.length - 1
326 | } else {
327 | this.selectedRowIndex = i
328 | }
329 |
330 | const resultRow = this.resultRows[this.selectedRowIndex]
331 |
332 | if (resultRow instanceof LeadingContextRow) {
333 | this.selectedRowIndex += resultRow.rowOffset
334 | } else if (resultRow instanceof TrailingContextRow) {
335 | this.selectedRowIndex -= resultRow.rowOffset
336 | }
337 |
338 | if (i >= this.resultRows.length) {
339 | this.scrollToBottom()
340 | } else {
341 | this.scrollToSelectedMatch()
342 | }
343 |
344 | return etch.update(this)
345 | }
346 |
347 | selectFirstResult() {
348 | return this.selectRow(0)
349 | }
350 |
351 | moveToTop() {
352 | return this.selectRow(0)
353 | }
354 |
355 | moveToBottom() {
356 | return this.selectRow(this.resultRows.length)
357 | }
358 |
359 | pageUp() {
360 | if (this.refs.listView) {
361 | const {clientHeight} = this.refs.listView.element
362 | const position = this.positionOfSelectedResult()
363 | return this.selectResultAtPosition(position - clientHeight)
364 | }
365 | }
366 |
367 | pageDown() {
368 | if (this.refs.listView) {
369 | const {clientHeight} = this.refs.listView.element
370 | const position = this.positionOfSelectedResult()
371 | return this.selectResultAtPosition(position + clientHeight)
372 | }
373 | }
374 |
375 | positionOfSelectedResult() {
376 | let y = 0;
377 |
378 | for (let i = 0; i < this.selectedRowIndex; i++) {
379 | y += this.getRowHeight(this.resultRows[i])
380 | }
381 | return y
382 | }
383 |
384 | selectResultAtPosition(position) {
385 | if (this.refs.listView && this.model.getPathCount() > 0) {
386 | const {clientHeight} = this.refs.listView.element
387 |
388 | let top = 0
389 | for (let i = 0; i < this.resultRows.length; i++) {
390 | const bottom = top + this.getRowHeight(this.resultRows[i])
391 | if (bottom > position) {
392 | return this.selectRow(i)
393 | }
394 | top = bottom
395 | }
396 | }
397 | return this.selectRow(this.resultRows.length)
398 | }
399 |
400 | moveDown() {
401 | if (this.selectedRowIndex === -1) {
402 | return this.selectRow(0)
403 | }
404 |
405 | for (let i = this.selectedRowIndex + 1; i < this.resultRows.length; i++) {
406 | const row = this.resultRows[i]
407 |
408 | if (row instanceof ResultPathRow || row instanceof MatchRow) {
409 | return this.selectRow(i)
410 | }
411 | }
412 | return this.selectRow(this.resultRows.length)
413 | }
414 |
415 | moveUp() {
416 | if (this.selectedRowIndex === -1) {
417 | return this.selectRow(0)
418 | }
419 |
420 | for (let i = this.selectedRowIndex - 1; i >= 0; i--) {
421 | const row = this.resultRows[i]
422 |
423 | if (row instanceof ResultPathRow || row instanceof MatchRow) {
424 | return this.selectRow(i)
425 | }
426 | }
427 | return this.selectRow(0)
428 | }
429 |
430 | selectedRow() {
431 | return this.resultRows[this.selectedRowIndex]
432 | }
433 |
434 | expandResult() {
435 | if (this.selectedRowIndex === -1) {
436 | return
437 | }
438 |
439 | const rowGroup = this.selectedRow().group
440 | const groupIndex = this.resultRowGroups.indexOf(rowGroup)
441 | const rowIndex = this.getRowCountBefore(groupIndex)
442 |
443 | if (!rowGroup.data.isCollapsed) {
444 | if (this.selectedRowIndex === rowIndex) {
445 | this.selectRow(rowIndex + 1)
446 | }
447 | return
448 | }
449 |
450 | rowGroup.data.isCollapsed = false
451 | this.invalidateRowGroup(rowIndex, groupIndex)
452 | this.selectRow(rowIndex + 1)
453 | return etch.update(this);
454 | }
455 |
456 | collapseResult() {
457 | if (this.selectedRowIndex === -1) {
458 | return
459 | }
460 |
461 | const rowGroup = this.selectedRow().group
462 |
463 | if (rowGroup.data.isCollapsed) {
464 | return
465 | }
466 |
467 | const groupIndex = this.resultRowGroups.indexOf(rowGroup)
468 | const rowIndex = this.getRowCountBefore(groupIndex)
469 |
470 | rowGroup.data.isCollapsed = true
471 | this.invalidateRowGroup(rowIndex, groupIndex)
472 | return etch.update(this);
473 | }
474 |
475 | // This is the method called when clicking a result or pressing enter
476 | async confirmResult({pending} = {}) {
477 | if (this.selectedRowIndex === -1) {
478 | return
479 | }
480 |
481 | const selectedRow = this.selectedRow()
482 |
483 | if (selectedRow instanceof MatchRow) {
484 | this.currentScrollTop = this.getScrollTop();
485 | const match = selectedRow.data.matches[0]
486 | const editor = await atom.workspace.open(selectedRow.group.result.filePath, {
487 | pending,
488 | searchAllPanes: true,
489 | split: reverseDirections[atom.config.get('find-and-replace.projectSearchResultsPaneSplitDirection')]
490 | })
491 | editor.unfoldBufferRow(match.range.start.selectedRow)
492 | editor.setSelectedBufferRange(match.range, {flash: true})
493 | editor.scrollToCursorPosition()
494 | } else if (selectedRow.group.data.isCollapsed) {
495 | this.expandResult()
496 | } else {
497 | this.collapseResult()
498 | }
499 | }
500 |
501 | copyResult() {
502 | if (this.selectedRowIndex === -1) {
503 | return
504 | }
505 |
506 | const selectedRow = this.selectedRow()
507 | if (selectedRow.data.matches) {
508 | // TODO - If row has multiple matches, copy them all, using the same
509 | // algorithm as `Selection.copy`; ideally, that algorithm should be
510 | // isolated for D.R.Y. purposes
511 | atom.clipboard.write(selectedRow.data.matches[0].lineText);
512 | }
513 | }
514 |
515 | copyPath() {
516 | if (this.selectedRowIndex === -1) {
517 | return
518 | }
519 |
520 | const {filePath} = this.selectedRow().group.result
521 | let [projectPath, relativePath] = atom.project.relativizePath(filePath);
522 | if (projectPath && atom.project.getDirectories().length > 1) {
523 | relativePath = path.join(path.basename(projectPath), relativePath);
524 | }
525 | atom.clipboard.write(relativePath);
526 | }
527 |
528 | async openInNewTab() {
529 | if (this.selectedRowIndex !== -1) {
530 | const result = this.selectedRow();
531 |
532 | if (result) {
533 | let editor;
534 | const editors = atom.workspace.getTextEditors();
535 | const filepath = result.group.result.filePath;
536 | const exists = _.some(editors, (editor) => editor.getPath() == filepath);
537 |
538 | if (!exists) {
539 | editor = await atom.workspace.open(filepath, {
540 | activatePane: false,
541 | activateItem: false
542 | });
543 | }
544 | else {
545 | editor = await atom.workspace.open(filepath);
546 | }
547 | if (result.data.matches) {
548 | const match = result.data.matches[0];
549 |
550 | if (match && editor) {
551 | editor.unfoldBufferRow(match.range.start.selectedRow);
552 | editor.setSelectedBufferRange(match.range, {flash: true});
553 | editor.scrollToCursorPosition();
554 | }
555 | }
556 | }
557 | }
558 | }
559 |
560 | expandAllResults() {
561 | let rowIndex = 0
562 | // Since the whole array is re-generated, this makes splices cheaper
563 | this.resultRows = []
564 | for (let i = 0; i < this.resultRowGroups.length; i++) {
565 | const group = this.resultRowGroups[i]
566 |
567 | group.data.isCollapsed = false
568 | this.invalidateRowGroup(rowIndex, i)
569 | rowIndex += group.displayedRows().length
570 | }
571 | this.scrollToSelectedMatch();
572 | return etch.update(this);
573 | }
574 |
575 | collapseAllResults() {
576 | let rowIndex = 0
577 | // Since the whole array is re-generated, this makes splices cheaper
578 | this.resultRows = []
579 | for (let i = 0; i < this.resultRowGroups.length; i++) {
580 | const group = this.resultRowGroups[i]
581 |
582 | group.data.isCollapsed = true
583 | this.invalidateRowGroup(rowIndex, i)
584 | rowIndex += group.displayedRows().length
585 | }
586 | this.scrollToSelectedMatch();
587 | return etch.update(this);
588 | }
589 |
590 | decrementLeadingContextLines() {
591 | if (this.model.getFindOptions().leadingContextLineCount > 0) {
592 | this.model.getFindOptions().leadingContextLineCount--;
593 | return this.contextLinesChanged();
594 | }
595 | }
596 |
597 | toggleLeadingContextLines() {
598 | if (this.model.getFindOptions().leadingContextLineCount > 0) {
599 | this.model.getFindOptions().leadingContextLineCount = 0;
600 | return this.contextLinesChanged();
601 | } else {
602 | const searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
603 | if (this.model.getFindOptions().leadingContextLineCount < searchContextLineCountBefore) {
604 | this.model.getFindOptions().leadingContextLineCount = searchContextLineCountBefore;
605 | return this.contextLinesChanged();
606 | }
607 | }
608 | }
609 |
610 | incrementLeadingContextLines() {
611 | const searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
612 | if (this.model.getFindOptions().leadingContextLineCount < searchContextLineCountBefore) {
613 | this.model.getFindOptions().leadingContextLineCount++;
614 | return this.contextLinesChanged();
615 | }
616 | }
617 |
618 | decrementTrailingContextLines() {
619 | if (this.model.getFindOptions().trailingContextLineCount > 0) {
620 | this.model.getFindOptions().trailingContextLineCount--;
621 | return this.contextLinesChanged();
622 | }
623 | }
624 |
625 | toggleTrailingContextLines() {
626 | if (this.model.getFindOptions().trailingContextLineCount > 0) {
627 | this.model.getFindOptions().trailingContextLineCount = 0;
628 | return this.contextLinesChanged();
629 | } else {
630 | const searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
631 | if (this.model.getFindOptions().trailingContextLineCount < searchContextLineCountAfter) {
632 | this.model.getFindOptions().trailingContextLineCount = searchContextLineCountAfter;
633 | return this.contextLinesChanged();
634 | }
635 | }
636 | }
637 |
638 | incrementTrailingContextLines() {
639 | const searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
640 | if (this.model.getFindOptions().trailingContextLineCount < searchContextLineCountAfter) {
641 | this.model.getFindOptions().trailingContextLineCount++;
642 | return this.contextLinesChanged();
643 | }
644 | }
645 |
646 | async contextLinesChanged() {
647 | let rowIndex = 0
648 | // Since the whole array is re-generated, this makes splices cheaper
649 | this.resultRows = []
650 | for (let i = 0; i < this.resultRowGroups.length; i++) {
651 | const group = this.resultRowGroups[i]
652 |
653 | this.invalidateRowGroup(rowIndex, i)
654 | rowIndex += group.displayedRows().length
655 | }
656 | await etch.update(this);
657 | this.scrollToSelectedMatch();
658 | }
659 |
660 | scrollToSelectedMatch() {
661 | if (this.selectedRowIndex === -1) {
662 | return
663 | }
664 | if (this.refs.listView) {
665 | const top = this.positionOfSelectedResult();
666 | const bottom = top + this.getRowHeight(this.selectedRow());
667 |
668 | if (bottom > this.getScrollTop() + this.refs.listView.element.clientHeight) {
669 | this.setScrollTop(bottom - this.refs.listView.element.clientHeight);
670 | } else if (top < this.getScrollTop()) {
671 | this.setScrollTop(top);
672 | }
673 | }
674 | }
675 |
676 | scrollToBottom() {
677 | this.setScrollTop(this.getScrollHeight());
678 | }
679 |
680 | scrollToTop() {
681 | this.setScrollTop(0);
682 | }
683 |
684 | setScrollTop (scrollTop) {
685 | if (this.refs.listView) {
686 | this.refs.listView.element.scrollTop = scrollTop;
687 | this.refs.listView.element.dispatchEvent(new UIEvent('scroll'))
688 | }
689 | }
690 |
691 | getScrollTop () {
692 | return this.refs.listView ? this.refs.listView.element.scrollTop : 0;
693 | }
694 |
695 | getScrollHeight () {
696 | return this.refs.listView ? this.refs.listView.element.scrollHeight : 0;
697 | }
698 |
699 | maintainPreviousScrollPosition() {
700 | if(this.selectedRowIndex === -1 || !this.currentScrollTop) {
701 | return;
702 | }
703 |
704 | this.setScrollTop(this.currentScrollTop);
705 | }
706 |
707 | fontFamilyChanged(fontFamily) {
708 | this.previewStyle = {fontFamily};
709 | etch.update(this);
710 | }
711 | };
712 |
--------------------------------------------------------------------------------
/lib/project/util.coffee:
--------------------------------------------------------------------------------
1 | _ = require 'underscore-plus'
2 |
3 | escapeNode = null
4 |
5 | escapeHtml = (str) ->
6 | escapeNode ?= document.createElement('div')
7 | escapeNode.innerText = str
8 | escapeNode.innerHTML
9 |
10 | escapeRegex = (str) ->
11 | str.replace /[.?*+^$[\]\\(){}|-]/g, (match) -> "\\" + match
12 |
13 | sanitizePattern = (pattern) ->
14 | pattern = escapeHtml(pattern)
15 | pattern.replace(/\n/g, '\\n').replace(/\t/g, '\\t')
16 |
17 | getReplacementResultsMessage = ({findPattern, replacePattern, replacedPathCount, replacementCount}) ->
18 | if replacedPathCount
19 | "Replaced #{sanitizePattern(findPattern)} with #{sanitizePattern(replacePattern)} #{_.pluralize(replacementCount, 'time')} in #{_.pluralize(replacedPathCount, 'file')}"
20 | else
21 | "Nothing replaced"
22 |
23 | getSearchResultsMessage = (results) ->
24 | if results?.findPattern?
25 | {findPattern, matchCount, pathCount, replacedPathCount} = results
26 | if matchCount
27 | "#{_.pluralize(matchCount, 'result')} found in #{_.pluralize(pathCount, 'file')} for #{sanitizePattern(findPattern)}"
28 | else
29 | "No #{if replacedPathCount? then 'more' else ''} results found for '#{sanitizePattern(findPattern)}'"
30 | else
31 | ''
32 |
33 | showIf = (condition) ->
34 | if condition
35 | null
36 | else
37 | {display: 'none'}
38 |
39 | capitalize = (str) -> str[0].toUpperCase() + str.toLowerCase().slice(1)
40 | titleize = (str) -> str.toLowerCase().replace(/(?:^|\s)\S/g, (capital) -> capital.toUpperCase())
41 |
42 | preserveCase = (text, reference) ->
43 | # If replaced text is capitalized (strict) like a sentence, capitalize replacement
44 | if reference is capitalize(reference.toLowerCase())
45 | capitalize(text)
46 |
47 | # If replaced text is titleized (i.e., each word start with an uppercase), titleize replacement
48 | else if reference is titleize(reference.toLowerCase())
49 | titleize(text)
50 |
51 | # If replaced text is uppercase, uppercase replacement
52 | else if reference is reference.toUpperCase()
53 | text.toUpperCase()
54 |
55 | # If replaced text is lowercase, lowercase replacement
56 | else if reference is reference.toLowerCase()
57 | text.toLowerCase()
58 | else
59 | text
60 |
61 |
62 | module.exports = {
63 | escapeHtml, escapeRegex, sanitizePattern, getReplacementResultsMessage,
64 | getSearchResultsMessage, showIf, preserveCase
65 | }
66 |
--------------------------------------------------------------------------------
/lib/reporter-proxy.js:
--------------------------------------------------------------------------------
1 | module.exports = class ReporterProxy {
2 | constructor () {
3 | this.reporter = null
4 | this.timingsQueue = []
5 |
6 | this.eventType = 'find-and-replace-v1'
7 | }
8 |
9 | setReporter (reporter) {
10 | this.reporter = reporter
11 | let timingsEvent
12 |
13 | while ((timingsEvent = this.timingsQueue.shift())) {
14 | this.reporter.addTiming(this.eventType, timingsEvent.duration, timingsEvent.metadata)
15 | }
16 | }
17 |
18 | unsetReporter () {
19 | delete this.reporter
20 | }
21 |
22 | sendSearchEvent (duration, numResults, crawler) {
23 | const metadata = {
24 | ec: 'time-to-search',
25 | ev: numResults,
26 | el: crawler
27 | }
28 |
29 | this._addTiming(duration, metadata)
30 | }
31 |
32 | _addTiming (duration, metadata) {
33 | if (this.reporter) {
34 | this.reporter.addTiming(this.eventType, duration, metadata)
35 | } else {
36 | this.timingsQueue.push({duration, metadata})
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/select-next.coffee:
--------------------------------------------------------------------------------
1 | _ = require 'underscore-plus'
2 | {CompositeDisposable, Range} = require 'atom'
3 |
4 | # Find and select the next occurrence of the currently selected text.
5 | #
6 | # The word under the cursor will be selected if the selection is empty.
7 | module.exports =
8 | class SelectNext
9 | selectionRanges: null
10 |
11 | constructor: (@editor) ->
12 | @selectionRanges = []
13 |
14 | findAndSelectNext: ->
15 | if @editor.getLastSelection().isEmpty()
16 | @selectWord()
17 | else
18 | @selectNextOccurrence()
19 |
20 | findAndSelectAll: ->
21 | @selectWord() if @editor.getLastSelection().isEmpty()
22 | @selectAllOccurrences()
23 |
24 | undoLastSelection: ->
25 | @updateSavedSelections()
26 |
27 | return if @selectionRanges.length < 1
28 |
29 | if @selectionRanges.length > 1
30 | @selectionRanges.pop()
31 | @editor.setSelectedBufferRanges @selectionRanges
32 | else
33 | @editor.clearSelections()
34 |
35 | @editor.scrollToCursorPosition()
36 |
37 | skipCurrentSelection: ->
38 | @updateSavedSelections()
39 |
40 | return if @selectionRanges.length < 1
41 |
42 | if @selectionRanges.length > 1
43 | lastSelection = @selectionRanges.pop()
44 | @editor.setSelectedBufferRanges @selectionRanges
45 | @selectNextOccurrence(start: lastSelection.end)
46 | else
47 | @selectNextOccurrence()
48 | @selectionRanges.shift()
49 | return if @selectionRanges.length < 1
50 | @editor.setSelectedBufferRanges @selectionRanges
51 |
52 | selectWord: ->
53 | @editor.selectWordsContainingCursors()
54 | lastSelection = @editor.getLastSelection()
55 | if @wordSelected = @isWordSelected(lastSelection)
56 | disposables = new CompositeDisposable
57 | clearWordSelected = =>
58 | @wordSelected = null
59 | disposables.dispose()
60 | disposables.add lastSelection.onDidChangeRange clearWordSelected
61 | disposables.add lastSelection.onDidDestroy clearWordSelected
62 |
63 | selectAllOccurrences: ->
64 | range = [[0, 0], @editor.getEofBufferPosition()]
65 | @scanForNextOccurrence range, ({range, stop}) =>
66 | @addSelection(range)
67 |
68 | selectNextOccurrence: (options={}) ->
69 | startingRange = options.start ? @editor.getSelectedBufferRange().end
70 | range = @findNextOccurrence([startingRange, @editor.getEofBufferPosition()])
71 | range ?= @findNextOccurrence([[0, 0], @editor.getSelections()[0].getBufferRange().start])
72 | @addSelection(range) if range?
73 |
74 | findNextOccurrence: (scanRange) ->
75 | foundRange = null
76 | @scanForNextOccurrence scanRange, ({range, stop}) ->
77 | foundRange = range
78 | stop()
79 | foundRange
80 |
81 | addSelection: (range) ->
82 | reversed = @editor.getLastSelection().isReversed()
83 | selection = @editor.addSelectionForBufferRange(range, {reversed})
84 | @updateSavedSelections selection
85 |
86 | scanForNextOccurrence: (range, callback) ->
87 | selection = @editor.getLastSelection()
88 | text = _.escapeRegExp(selection.getText())
89 |
90 | if @wordSelected
91 | nonWordCharacters = atom.config.get('editor.nonWordCharacters')
92 | text = "(^|[ \t#{_.escapeRegExp(nonWordCharacters)}]+)#{text}(?=$|[\\s#{_.escapeRegExp(nonWordCharacters)}]+)"
93 |
94 | @editor.scanInBufferRange new RegExp(text, 'g'), range, (result) ->
95 | if prefix = result.match[1]
96 | result.range = result.range.translate([0, prefix.length], [0, 0])
97 | callback(result)
98 |
99 | updateSavedSelections: (selection=null) ->
100 | selections = @editor.getSelections()
101 | @selectionRanges = [] if selections.length < 3
102 | if @selectionRanges.length is 0
103 | @selectionRanges.push s.getBufferRange() for s in selections
104 | else if selection
105 | selectionRange = selection.getBufferRange()
106 | return if @selectionRanges.some (existingRange) -> existingRange.isEqual(selectionRange)
107 | @selectionRanges.push selectionRange
108 |
109 | isNonWordCharacter: (character) ->
110 | nonWordCharacters = atom.config.get('editor.nonWordCharacters')
111 | new RegExp("[ \t#{_.escapeRegExp(nonWordCharacters)}]").test(character)
112 |
113 | isNonWordCharacterToTheLeft: (selection) ->
114 | selectionStart = selection.getBufferRange().start
115 | range = Range.fromPointWithDelta(selectionStart, 0, -1)
116 | @isNonWordCharacter(@editor.getTextInBufferRange(range))
117 |
118 | isNonWordCharacterToTheRight: (selection) ->
119 | selectionEnd = selection.getBufferRange().end
120 | range = Range.fromPointWithDelta(selectionEnd, 0, 1)
121 | @isNonWordCharacter(@editor.getTextInBufferRange(range))
122 |
123 | isWordSelected: (selection) ->
124 | if selection.getBufferRange().isSingleLine()
125 | selectionRange = selection.getBufferRange()
126 | lineRange = @editor.bufferRangeForBufferRow(selectionRange.start.row)
127 | nonWordCharacterToTheLeft = _.isEqual(selectionRange.start, lineRange.start) or
128 | @isNonWordCharacterToTheLeft(selection)
129 | nonWordCharacterToTheRight = _.isEqual(selectionRange.end, lineRange.end) or
130 | @isNonWordCharacterToTheRight(selection)
131 | containsOnlyWordCharacters = not @isNonWordCharacter(selection.getText())
132 |
133 | nonWordCharacterToTheLeft and nonWordCharacterToTheRight and containsOnlyWordCharacters
134 | else
135 | false
136 |
--------------------------------------------------------------------------------
/menus/find-and-replace.cson:
--------------------------------------------------------------------------------
1 | 'menu': [
2 | 'label': 'Find'
3 | 'submenu': [
4 | { 'label': 'Find in Buffer', 'command': 'find-and-replace:show'}
5 | { 'label': 'Replace in Buffer', 'command': 'find-and-replace:show-replace'}
6 | { 'label': 'Select Next', 'command': 'find-and-replace:select-next'}
7 | { 'label': 'Select All', 'command': 'find-and-replace:select-all'}
8 | { 'label': 'Toggle Find in Buffer', 'command': 'find-and-replace:toggle'}
9 | { 'type': 'separator' }
10 | { 'label': 'Find in Project', 'command': 'project-find:show'}
11 | { 'label': 'Toggle Find in Project', 'command': 'project-find:toggle'}
12 | { 'type': 'separator' }
13 | { 'label': 'Find All', 'command': 'find-and-replace:find-all'}
14 | { 'label': 'Find Next', 'command': 'find-and-replace:find-next'}
15 | { 'label': 'Find Previous', 'command': 'find-and-replace:find-previous'}
16 | { 'label': 'Replace Next', 'command': 'find-and-replace:replace-next'}
17 | { 'label': 'Replace All', 'command': 'find-and-replace:replace-all'}
18 | { 'type': 'separator' }
19 | { 'label': 'Clear History', 'command': 'find-and-replace:clear-history'}
20 | { 'type': 'separator' }
21 | ]
22 | ]
23 |
24 | 'context-menu':
25 | '.tree-view li.directory': [
26 | { 'label': 'Search in Folder', 'command': 'project-find:show-in-current-directory' }
27 | ]
28 | '.list-item.match-row': [
29 | { 'label': 'Open in New Tab', 'command': 'find-and-replace:open-in-new-tab' }
30 | { 'label': 'Copy', 'command': 'core:copy' }
31 | { 'label': 'Copy Path', 'command': 'find-and-replace:copy-path' }
32 | ]
33 | '.list-item.path-row': [
34 | { 'label': 'Open in New Tab', 'command': 'find-and-replace:open-in-new-tab' }
35 | { 'label': 'Copy Path', 'command': 'find-and-replace:copy-path' }
36 | ]
37 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "find-and-replace",
3 | "version": "0.219.8",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "async": {
8 | "version": "1.5.2",
9 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
10 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
11 | },
12 | "balanced-match": {
13 | "version": "1.0.0",
14 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
15 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
16 | },
17 | "binary-search": {
18 | "version": "1.3.4",
19 | "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.4.tgz",
20 | "integrity": "sha512-dPxU/vZLnH0tEVjVPgi015oSwqu6oLfCeHywuFRhBE0yM0mYocvleTl8qsdM1YFhRzTRhM1+VzS8XLDVrHPopg=="
21 | },
22 | "brace-expansion": {
23 | "version": "1.1.8",
24 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
25 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
26 | "requires": {
27 | "balanced-match": "^1.0.0",
28 | "concat-map": "0.0.1"
29 | }
30 | },
31 | "coffee-script": {
32 | "version": "1.11.1",
33 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.11.1.tgz",
34 | "integrity": "sha1-vxxHrWREOg2V0S3ysUfMCk2q1uk=",
35 | "dev": true
36 | },
37 | "coffeelint": {
38 | "version": "1.16.0",
39 | "resolved": "https://registry.npmjs.org/coffeelint/-/coffeelint-1.16.0.tgz",
40 | "integrity": "sha1-g9jtHa/eOmd95E57ihi+YHdh5tg=",
41 | "dev": true,
42 | "requires": {
43 | "coffee-script": "~1.11.0",
44 | "glob": "^7.0.6",
45 | "ignore": "^3.0.9",
46 | "optimist": "^0.6.1",
47 | "resolve": "^0.6.3",
48 | "strip-json-comments": "^1.0.2"
49 | }
50 | },
51 | "concat-map": {
52 | "version": "0.0.1",
53 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
54 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
55 | },
56 | "dedent": {
57 | "version": "0.6.0",
58 | "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.6.0.tgz",
59 | "integrity": "sha1-Dm2o8M5Sg471zsXI+TlrDBtko8s=",
60 | "dev": true
61 | },
62 | "etch": {
63 | "version": "0.9.3",
64 | "resolved": "https://registry.npmjs.org/etch/-/etch-0.9.3.tgz",
65 | "integrity": "sha1-2uxSmVv2E1A9a5K0H1Si6qEuMis="
66 | },
67 | "fs-plus": {
68 | "version": "3.1.1",
69 | "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz",
70 | "integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==",
71 | "requires": {
72 | "async": "^1.5.2",
73 | "mkdirp": "^0.5.1",
74 | "rimraf": "^2.5.2",
75 | "underscore-plus": "1.x"
76 | }
77 | },
78 | "fs.realpath": {
79 | "version": "1.0.0",
80 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
81 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
82 | },
83 | "glob": {
84 | "version": "7.1.2",
85 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
86 | "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
87 | "requires": {
88 | "fs.realpath": "^1.0.0",
89 | "inflight": "^1.0.4",
90 | "inherits": "2",
91 | "minimatch": "^3.0.4",
92 | "once": "^1.3.0",
93 | "path-is-absolute": "^1.0.0"
94 | }
95 | },
96 | "ignore": {
97 | "version": "3.3.3",
98 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz",
99 | "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=",
100 | "dev": true
101 | },
102 | "inflight": {
103 | "version": "1.0.6",
104 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
105 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
106 | "requires": {
107 | "once": "^1.3.0",
108 | "wrappy": "1"
109 | }
110 | },
111 | "inherits": {
112 | "version": "2.0.3",
113 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
114 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
115 | },
116 | "minimatch": {
117 | "version": "3.0.4",
118 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
119 | "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
120 | "requires": {
121 | "brace-expansion": "^1.1.7"
122 | }
123 | },
124 | "minimist": {
125 | "version": "0.0.8",
126 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
127 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
128 | },
129 | "mkdirp": {
130 | "version": "0.5.1",
131 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
132 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
133 | "requires": {
134 | "minimist": "0.0.8"
135 | }
136 | },
137 | "once": {
138 | "version": "1.4.0",
139 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
140 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
141 | "requires": {
142 | "wrappy": "1"
143 | }
144 | },
145 | "optimist": {
146 | "version": "0.6.1",
147 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
148 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
149 | "dev": true,
150 | "requires": {
151 | "minimist": "~0.0.1",
152 | "wordwrap": "~0.0.2"
153 | }
154 | },
155 | "os-tmpdir": {
156 | "version": "1.0.2",
157 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
158 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
159 | },
160 | "path-is-absolute": {
161 | "version": "1.0.1",
162 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
163 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
164 | },
165 | "resolve": {
166 | "version": "0.6.3",
167 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz",
168 | "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=",
169 | "dev": true
170 | },
171 | "rimraf": {
172 | "version": "2.6.1",
173 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz",
174 | "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=",
175 | "requires": {
176 | "glob": "^7.0.5"
177 | }
178 | },
179 | "strip-json-comments": {
180 | "version": "1.0.4",
181 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
182 | "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=",
183 | "dev": true
184 | },
185 | "temp": {
186 | "version": "0.8.3",
187 | "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz",
188 | "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=",
189 | "requires": {
190 | "os-tmpdir": "^1.0.0",
191 | "rimraf": "~2.2.6"
192 | },
193 | "dependencies": {
194 | "rimraf": {
195 | "version": "2.2.8",
196 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
197 | "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI="
198 | }
199 | }
200 | },
201 | "underscore": {
202 | "version": "1.6.0",
203 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
204 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag="
205 | },
206 | "underscore-plus": {
207 | "version": "1.6.6",
208 | "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.6.6.tgz",
209 | "integrity": "sha1-ZezeG9xEGjXYnmUP1w3PE65Dmn0=",
210 | "requires": {
211 | "underscore": "~1.6.0"
212 | }
213 | },
214 | "wordwrap": {
215 | "version": "0.0.3",
216 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
217 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
218 | "dev": true
219 | },
220 | "wrappy": {
221 | "version": "1.0.2",
222 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
223 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "find-and-replace",
3 | "main": "./lib/find",
4 | "description": "Find and replace within buffers and across the project.",
5 | "version": "0.219.8",
6 | "license": "MIT",
7 | "activationCommands": {
8 | "atom-workspace": [
9 | "project-find:show",
10 | "project-find:toggle",
11 | "project-find:show-in-current-directory",
12 | "find-and-replace:show",
13 | "find-and-replace:toggle",
14 | "find-and-replace:find-all",
15 | "find-and-replace:find-next",
16 | "find-and-replace:find-previous",
17 | "find-and-replace:find-next-selected",
18 | "find-and-replace:find-previous-selected",
19 | "find-and-replace:use-selection-as-find-pattern",
20 | "find-and-replace:use-selection-as-replace-pattern",
21 | "find-and-replace:show-replace",
22 | "find-and-replace:replace-next",
23 | "find-and-replace:replace-all",
24 | "find-and-replace:select-next",
25 | "find-and-replace:select-all",
26 | "find-and-replace:clear-history"
27 | ]
28 | },
29 | "repository": "https://github.com/atom/find-and-replace",
30 | "engines": {
31 | "atom": "*"
32 | },
33 | "dependencies": {
34 | "binary-search": "^1.3.3",
35 | "etch": "0.9.3",
36 | "fs-plus": "^3.0.0",
37 | "temp": "^0.8.3",
38 | "underscore-plus": "1.x"
39 | },
40 | "devDependencies": {
41 | "coffeelint": "^1.9.7",
42 | "dedent": "^0.6.0"
43 | },
44 | "consumedServices": {
45 | "atom.file-icons": {
46 | "versions": {
47 | "1.0.0": "consumeFileIcons"
48 | }
49 | },
50 | "autocomplete.watchEditor": {
51 | "versions": {
52 | "1.0.0": "consumeAutocompleteWatchEditor"
53 | }
54 | },
55 | "file-icons.element-icons": {
56 | "versions": {
57 | "1.0.0": "consumeElementIcons"
58 | }
59 | },
60 | "metrics-reporter": {
61 | "versions": {
62 | "^1.1.0": "consumeMetricsReporter"
63 | }
64 | }
65 | },
66 | "providedServices": {
67 | "find-and-replace": {
68 | "description": "Atom's bundled find-and-replace package",
69 | "versions": {
70 | "0.0.1": "provideService"
71 | }
72 | }
73 | },
74 | "configSchema": {
75 | "focusEditorAfterSearch": {
76 | "type": "boolean",
77 | "default": false,
78 | "description": "Focus the editor and select the next match when a file search is executed. If no matches are found, the editor will not be focused."
79 | },
80 | "projectSearchResultsPaneSplitDirection": {
81 | "type": "string",
82 | "default": "none",
83 | "enum": [
84 | "none",
85 | "right",
86 | "down"
87 | ],
88 | "title": "Direction to open results pane",
89 | "description": "Direction to split the active pane when showing project search results. If 'none', the results will be shown in the active pane."
90 | },
91 | "closeFindPanelAfterSearch": {
92 | "type": "boolean",
93 | "default": false,
94 | "title": "Close Project Find Panel After Search",
95 | "description": "Close the find panel after executing a project-wide search."
96 | },
97 | "scrollToResultOnLiveSearch": {
98 | "type": "boolean",
99 | "default": false,
100 | "title": "Scroll To Result On Live-Search (incremental find in buffer)",
101 | "description": "Scroll to and select the closest match while typing in the buffer find box."
102 | },
103 | "liveSearchMinimumCharacters": {
104 | "type": "integer",
105 | "default": 3,
106 | "minimum": 0,
107 | "description": "The minimum number of characters which need to be typed into the buffer find box before search starts matching and highlighting matches as you type."
108 | },
109 | "searchContextLineCountBefore": {
110 | "type": "integer",
111 | "default": 3,
112 | "minimum": 0,
113 | "description": "The number of extra lines of context to query before the match for project results"
114 | },
115 | "searchContextLineCountAfter": {
116 | "type": "integer",
117 | "default": 3,
118 | "minimum": 0,
119 | "description": "The number of extra lines of context to query after the match for project results"
120 | },
121 | "showSearchWrapIcon": {
122 | "type": "boolean",
123 | "default": true,
124 | "title": "Show Search Wrap Icon",
125 | "description": "Display a visual cue over the editor when looping through search results."
126 | },
127 | "useRipgrep": {
128 | "type": "boolean",
129 | "default": false,
130 | "title": "Use ripgrep",
131 | "description": "Use the experimental `ripgrep` search crawler. This will make searches substantially faster on large projects."
132 | },
133 | "enablePCRE2": {
134 | "type": "boolean",
135 | "default": false,
136 | "title": "Enable PCRE2 regex engine",
137 | "description": "Enable PCRE2 regex engine (applies only to `ripgrep` search). This will enable additional regex features such as lookbehind, but may make searches slower."
138 | },
139 | "autocompleteSearches": {
140 | "type": "boolean",
141 | "default": false,
142 | "title": "Autocomplete Search",
143 | "description": "Autocompletes entries in the find search field."
144 | },
145 | "preserveCaseOnReplace": {
146 | "type": "boolean",
147 | "default": false,
148 | "title": "Preserve case during replace.",
149 | "description": "Keep the replaced text case during replace: replacing 'user' with 'person' will replace 'User' with 'Person' and 'USER' with 'PERSON'."
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/spec/async-spec-helpers.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | export async function conditionPromise (condition) {
4 | const startTime = Date.now()
5 |
6 | while (true) {
7 | await timeoutPromise(100)
8 |
9 | if (await condition()) {
10 | return
11 | }
12 |
13 | if (Date.now() - startTime > 5000) {
14 | throw new Error("Timed out waiting on condition")
15 | }
16 | }
17 | }
18 |
19 | function timeoutPromise (timeout) {
20 | return new Promise(function (resolve) {
21 | global.setTimeout(resolve, timeout)
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/spec/find-spec.js:
--------------------------------------------------------------------------------
1 | const BufferSearch = require('../lib/buffer-search')
2 | const EmbeddedEditorItem = require('./item/embedded-editor-item')
3 | const DeferredEditorItem = require('./item/deferred-editor-item');
4 | const UnrecognizedItem = require('./item/unrecognized-item');
5 |
6 | describe('Find', () => {
7 | describe('updating the find model', () => {
8 | beforeEach(async () => {
9 | atom.workspace.addOpener(EmbeddedEditorItem.opener)
10 | atom.workspace.addOpener(UnrecognizedItem.opener)
11 | atom.workspace.addOpener(DeferredEditorItem.opener)
12 |
13 | const activationPromise = atom.packages.activatePackage('find-and-replace')
14 | atom.commands.dispatch(atom.views.getView(atom.workspace), 'find-and-replace:show')
15 | await activationPromise
16 |
17 | spyOn(BufferSearch.prototype, 'setEditor')
18 | })
19 |
20 | it("sets the find model's editor whenever an editor is focused", async () => {
21 | let editor = await atom.workspace.open()
22 | expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(editor)
23 |
24 | editor = await atom.workspace.open('sample.js')
25 | expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(editor)
26 | })
27 |
28 | it("sets the find model's editor to an embedded text editor", async () => {
29 | const embedded = await atom.workspace.open(EmbeddedEditorItem.uri)
30 | expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(embedded.refs.theEditor)
31 | })
32 |
33 | it("sets the find model's editor to an embedded text editor after activation", async () => {
34 | const deferred = await atom.workspace.open(DeferredEditorItem.uri)
35 | expect(BufferSearch.prototype.setEditor).not.toHaveBeenCalled()
36 |
37 | await deferred.showEditor()
38 | expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(deferred.refs.theEditor)
39 |
40 | await deferred.hideEditor()
41 | expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(null)
42 | })
43 |
44 | it("sets the find model's editor to null if a non-editor is focused", async () => {
45 | await atom.workspace.open(UnrecognizedItem.uri)
46 | expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(null)
47 | })
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/spec/fixtures/one-long-line.coffee:
--------------------------------------------------------------------------------
1 | test test test test test test test test test test test test test test test test test test test test test test a b c d e f g h i j k l abcdefghijklmnopqrstuvwxyz
2 |
--------------------------------------------------------------------------------
/spec/fixtures/project/long-match.js:
--------------------------------------------------------------------------------
1 | 1
2 | 1
3 | 1
4 | 1
5 | 1
6 | 1
7 | 1
8 | 1
9 | 1
10 | 1
11 | 1
12 | 1
13 | 1
14 | 1
15 | 1
16 | 1
17 |
18 | 1
19 | 1
20 | 1
21 | 1
22 | 1
23 | 1
24 | 1
25 | 1
26 | 1
27 | 1
28 | 1
29 | 1
30 | 1
31 | 1
32 |
33 | 1
34 | 1
35 | 1
36 | 1
37 | 1
38 | 1
39 | 1
40 | 1
41 | 1
42 |
43 | 1
44 | 1
45 | 1
46 | 1
47 | 1
48 | 1
49 | 1
50 | 1
51 | 1
52 | 1
53 | 1
54 | 1
55 | 1
56 | 1
57 | 1
58 | 1
59 |
60 | 1
61 | 1
62 | 1
63 | 1
64 | 1
65 | 1
66 | 1
67 | 1
68 | 1
69 | 1
70 | 1
71 | 1
72 | 1
73 | 1
74 | 1
75 | 1
76 | 1
77 |
78 | 1
79 | 11
80 |
81 | 11
82 | 1
83 |
84 | 11
85 |
86 | 1
87 | 1
88 | 1
89 | 1
90 | 1
91 |
--------------------------------------------------------------------------------
/spec/fixtures/project/one-long-line.coffee:
--------------------------------------------------------------------------------
1 | test test test test test test test test test test test test test test test test test test test test test test a b c d e f g h i j k l abcdefghijklmnopqrstuvwxyz
2 |
--------------------------------------------------------------------------------
/spec/fixtures/project/sample.coffee:
--------------------------------------------------------------------------------
1 | class quicksort
2 | sort: (items) ->
3 | return items if items.length <= 1
4 |
5 | pivot = items.shift()
6 | left = []
7 | right = []
8 |
9 | # Comment in the middle (and add the word 'items' again)
10 |
11 | while items.length > 0
12 | current = items.shift()
13 | if current < pivot
14 | left.push(current)
15 | else
16 | right.push(current);
17 |
18 | sort(left).concat(pivot).concat(sort(right))
19 |
20 | noop: ->
21 | # just a noop
22 |
23 | exports.modules = quicksort
24 |
--------------------------------------------------------------------------------
/spec/fixtures/project/sample.js:
--------------------------------------------------------------------------------
1 | var quicksort = function () {
2 | var sort = function(items) {
3 | if (items.length <= 1) return items;
4 | var pivot = items.shift(), current, left = [], right = [];
5 | while(items.length > 0) {
6 | current = items.shift();
7 | current < pivot ? left.push(current) : right.push(current);
8 | }
9 | return sort(left).concat(pivot).concat(sort(right));
10 | };
11 |
12 | return sort(Array.apply(this, arguments));
13 | };
--------------------------------------------------------------------------------
/spec/fixtures/sample.coffee:
--------------------------------------------------------------------------------
1 | class quicksort
2 | sort: (items) ->
3 | return items if items.length <= 1
4 |
5 | pivot = items.shift()
6 | left = []
7 | right = []
8 |
9 | # Comment in the middle (and add the word 'items' again)
10 |
11 | while items.length > 0
12 | current = items.shift()
13 | if current < pivot
14 | left.push(current)
15 | else
16 | right.push(current);
17 |
18 | sort(left).concat(pivot).concat(sort(right))
19 |
20 | noop: ->
21 | # just a noop
22 |
23 | exports.modules = quicksort
24 |
--------------------------------------------------------------------------------
/spec/fixtures/sample.js:
--------------------------------------------------------------------------------
1 | var quicksort = function () {
2 | var sort = function(items) {
3 | if (items.length <= 1) return items;
4 | var pivot = items.shift(), current, left = [], right = [];
5 | while(items.length > 0) {
6 | current = items.shift();
7 | current < pivot ? left.push(current) : right.push(current);
8 | }
9 | return sort(left).concat(pivot).concat(sort(right));
10 | };
11 |
12 | return sort(Array.apply(this, arguments));
13 | };
--------------------------------------------------------------------------------
/spec/item/deferred-editor-item.js:
--------------------------------------------------------------------------------
1 | // An active workspace item that embeds an AtomTextEditor we wish to expose to Find and Replace that does not become
2 | // available until some time after the item is activated.
3 |
4 | const etch = require('etch');
5 | const $ = etch.dom;
6 |
7 | const { TextEditor, Emitter } = require('atom');
8 |
9 | class DeferredEditorItem {
10 | static opener(u) {
11 | if (u === DeferredEditorItem.uri) {
12 | return new DeferredEditorItem();
13 | } else {
14 | return undefined;
15 | }
16 | }
17 |
18 | constructor() {
19 | this.editorShown = false;
20 | this.emitter = new Emitter();
21 |
22 | etch.initialize(this);
23 | }
24 |
25 | render() {
26 | if (this.editorShown) {
27 | return (
28 | $.div({className: 'wrapper'},
29 | etch.dom(TextEditor, {ref: 'theEditor'})
30 | )
31 | )
32 | } else {
33 | return (
34 | $.div({className: 'wrapper'}, 'Empty')
35 | )
36 | }
37 | }
38 |
39 | update() {
40 | return etch.update(this)
41 | }
42 |
43 | observeEmbeddedTextEditor(cb) {
44 | if (this.editorShown) {
45 | cb(this.refs.theEditor)
46 | }
47 | return this.emitter.on('did-change-embedded-text-editor', cb)
48 | }
49 |
50 | async showEditor() {
51 | const wasShown = this.editorShown
52 | this.editorShown = true
53 | await this.update()
54 | if (!wasShown) {
55 | this.emitter.emit('did-change-embedded-text-editor', this.refs.theEditor)
56 | }
57 | }
58 |
59 | async hideEditor() {
60 | const wasShown = this.editorShown
61 | this.editorShown = false
62 | await this.update()
63 | if (wasShown) {
64 | this.emitter.emit('did-change-embedded-text-editor', null)
65 | }
66 | }
67 | }
68 |
69 | DeferredEditorItem.uri = 'atom://find-and-replace/spec/deferred-editor'
70 |
71 | module.exports = DeferredEditorItem
72 |
--------------------------------------------------------------------------------
/spec/item/embedded-editor-item.js:
--------------------------------------------------------------------------------
1 | // An active workspace item that embeds an AtomTextEditor we wish to expose to Find and Replace.
2 |
3 | const etch = require('etch');
4 | const $ = etch.dom;
5 |
6 | const { TextEditor } = require('atom');
7 |
8 | class EmbeddedEditorItem {
9 | static opener(u) {
10 | if (u === EmbeddedEditorItem.uri) {
11 | return new EmbeddedEditorItem();
12 | } else {
13 | return undefined;
14 | }
15 | }
16 |
17 | constructor() {
18 | etch.initialize(this);
19 | }
20 |
21 | render() {
22 | return (
23 | $.div({className: 'wrapper'},
24 | etch.dom(TextEditor, {ref: 'theEditor'})
25 | )
26 | )
27 | }
28 |
29 | update() {}
30 |
31 | getEmbeddedTextEditor() {
32 | return this.refs.theEditor
33 | }
34 | }
35 |
36 | EmbeddedEditorItem.uri = 'atom://find-and-replace/spec/embedded-editor'
37 |
38 | module.exports = EmbeddedEditorItem
39 |
--------------------------------------------------------------------------------
/spec/item/unrecognized-item.js:
--------------------------------------------------------------------------------
1 | // An active workspace item that doesn't contain a TextEditor.
2 |
3 | const etch = require('etch');
4 | const $ = etch.dom;
5 |
6 | class UnrecognizedItem {
7 | static opener(u) {
8 | if (u === UnrecognizedItem.uri) {
9 | return new UnrecognizedItem();
10 | } else {
11 | return undefined;
12 | }
13 | }
14 |
15 | constructor() {
16 | etch.initialize(this);
17 | }
18 |
19 | render() {
20 | return (
21 | $.div({className: 'wrapper'}, 'Some text')
22 | )
23 | }
24 |
25 | update() {}
26 | }
27 |
28 | UnrecognizedItem.uri = 'atom://find-and-replace/spec/unrecognized'
29 |
30 | module.exports = UnrecognizedItem
31 |
--------------------------------------------------------------------------------
/spec/result-row-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | const {
4 | LeadingContextRow,
5 | TrailingContextRow,
6 | ResultPathRow,
7 | MatchRow,
8 | ResultRowGroup
9 | } = require("../lib/project/result-row");
10 |
11 | describe("ResultRowGroup", () => {
12 | const lines = (new Array(18)).fill().map((x, i) => `line-${i}`)
13 | const rg = (i) => [[i, 0], [i, lines[i].length]]
14 | const testedRowIndices = [0, 7, 13, 16, 17]
15 |
16 | const result = {
17 | filePath: 'fake-file-path',
18 | matches: testedRowIndices.map(lineNb => ({
19 | range: rg(lineNb),
20 | leadingContextLines: lines.slice(Math.max(lineNb - 3, 0), lineNb),
21 | trailingContextLines: lines.slice(lineNb + 1, lineNb + 4),
22 | lineTextOffset: 0,
23 | lineText: lines[lineNb],
24 | matchText: 'fake-match-text'
25 | }))
26 | }
27 |
28 | describe("generateRows", () => {
29 | it("generates a path row and several match rows", () => {
30 | const rowGroup = new ResultRowGroup(
31 | result,
32 | { leadingContextLineCount: 0, trailingContextLineCount: 0 }
33 | )
34 |
35 | const expectedRows = [
36 | new ResultPathRow(rowGroup),
37 | new MatchRow(rowGroup, false, 0, [ result.matches[0] ]),
38 | new MatchRow(rowGroup, true, 7, [ result.matches[1] ]),
39 | new MatchRow(rowGroup, true, 13, [ result.matches[2] ]),
40 | new MatchRow(rowGroup, true, 16, [ result.matches[3] ]),
41 | new MatchRow(rowGroup, false, 17, [ result.matches[4] ])
42 | ]
43 |
44 | for (let i = 0; i < rowGroup.rows.length; ++i) {
45 | expect(rowGroup.rows[i].data).toEqual(expectedRows[i].data)
46 | }
47 | })
48 |
49 | it("generates context rows between matches", () => {
50 | const rowGroup = new ResultRowGroup(
51 | result,
52 | { leadingContextLineCount: 3, trailingContextLineCount: 2 }
53 | )
54 |
55 | const expectedRows = [
56 | new ResultPathRow(rowGroup),
57 |
58 | new MatchRow(rowGroup, false, 0, [ result.matches[0] ]),
59 | new TrailingContextRow(rowGroup, lines[1], false, 0, 1),
60 | new TrailingContextRow(rowGroup, lines[2], false, 0, 2),
61 |
62 | new LeadingContextRow(rowGroup, lines[4], true, 7, 3),
63 | new LeadingContextRow(rowGroup, lines[5], false, 7, 2),
64 | new LeadingContextRow(rowGroup, lines[6], false, 7, 1),
65 | new MatchRow(rowGroup, false, 7, [ result.matches[1] ]),
66 | new TrailingContextRow(rowGroup, lines[8], false, 7, 1),
67 | new TrailingContextRow(rowGroup, lines[9], false, 7, 2),
68 |
69 | new LeadingContextRow(rowGroup, lines[10], false, 13, 3),
70 | new LeadingContextRow(rowGroup, lines[11], false, 13, 2),
71 | new LeadingContextRow(rowGroup, lines[12], false, 13, 1),
72 | new MatchRow(rowGroup, false, 13, [ result.matches[2] ]),
73 | new TrailingContextRow(rowGroup, lines[14], false, 13, 1),
74 | new TrailingContextRow(rowGroup, lines[15], false, 13, 2),
75 |
76 | new MatchRow(rowGroup, false, 16, [ result.matches[3] ]),
77 | new MatchRow(rowGroup, false, 17, [ result.matches[4] ])
78 | ]
79 |
80 | for (let i = 0; i < rowGroup.rows.length; ++i) {
81 | expect(rowGroup.rows[i].data).toEqual(expectedRows[i].data)
82 | }
83 | })
84 | })
85 |
86 | describe("getLineNumber", () => {
87 | it("generates correct line numbers", () => {
88 | const rowGroup = new ResultRowGroup(
89 | result,
90 | { leadingContextLineCount: 1, trailingContextLineCount: 1 }
91 | )
92 |
93 | expect(rowGroup.rows.slice(1).map(row => row.data.lineNumber)).toEqual(
94 | [0, 1, 6, 7, 8, 12, 13, 14, 15, 16, 17]
95 | )
96 | })
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/spec/results-model-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | const path = require("path");
4 | const ResultsModel = require("../lib/project/results-model");
5 | const FindOptions = require("../lib/find-options");
6 |
7 | describe("ResultsModel", () => {
8 | let editor, resultsModel, reporterSpy;
9 |
10 | beforeEach(async () => {
11 | atom.config.set("core.excludeVcsIgnoredPaths", false);
12 | atom.config.set("find-and-replace.searchContextLineCountBefore", 2);
13 | atom.config.set("find-and-replace.searchContextLineCountAfter", 3);
14 | atom.project.setPaths([path.join(__dirname, "fixtures/project")]);
15 |
16 | editor = await atom.workspace.open("sample.js");
17 | reporterSpy = {
18 | sendSearchEvent: jasmine.createSpy()
19 | }
20 | resultsModel = new ResultsModel(new FindOptions(), reporterSpy);
21 | });
22 |
23 | describe("searching for a pattern", () => {
24 | it("populates the model with all the results, and updates in response to changes in the buffer", async () => {
25 | const resultAddedSpy = jasmine.createSpy();
26 | const resultSetSpy = jasmine.createSpy();
27 | const resultRemovedSpy = jasmine.createSpy();
28 |
29 | resultsModel.onDidAddResult(resultAddedSpy);
30 | resultsModel.onDidSetResult(resultSetSpy);
31 | resultsModel.onDidRemoveResult(resultRemovedSpy);
32 | await resultsModel.search("items", "*.js", "");
33 |
34 | expect(resultAddedSpy).toHaveBeenCalled();
35 | expect(resultAddedSpy.callCount).toBe(1);
36 |
37 | let result = resultsModel.getResult(editor.getPath());
38 | expect(result.matches.length).toBe(6);
39 | expect(resultsModel.getPathCount()).toBe(1);
40 | expect(resultsModel.getMatchCount()).toBe(6);
41 | expect(resultsModel.getPaths()).toEqual([editor.getPath()]);
42 | expect(result.matches[0].leadingContextLines.length).toBe(1);
43 | expect(result.matches[0].leadingContextLines[0]).toBe("var quicksort = function () {");
44 | expect(result.matches[0].trailingContextLines.length).toBe(3);
45 | expect(result.matches[0].trailingContextLines[0]).toBe(" if (items.length <= 1) return items;");
46 | expect(result.matches[0].trailingContextLines[1]).toBe(" var pivot = items.shift(), current, left = [], right = [];");
47 | expect(result.matches[0].trailingContextLines[2]).toBe(" while(items.length > 0) {");
48 | expect(result.matches[5].leadingContextLines.length).toBe(2);
49 | expect(result.matches[5].trailingContextLines.length).toBe(3);
50 |
51 | editor.setText("there are some items in here");
52 | advanceClock(editor.buffer.stoppedChangingDelay);
53 | expect(resultAddedSpy.callCount).toBe(1);
54 | expect(resultSetSpy.callCount).toBe(1);
55 |
56 | result = resultsModel.getResult(editor.getPath());
57 | expect(result.matches.length).toBe(1);
58 | expect(resultsModel.getPathCount()).toBe(1);
59 | expect(resultsModel.getMatchCount()).toBe(1);
60 | expect(resultsModel.getPaths()).toEqual([editor.getPath()]);
61 | expect(result.matches[0].lineText).toBe("there are some items in here");
62 | expect(result.matches[0].leadingContextLines.length).toBe(0);
63 | expect(result.matches[0].trailingContextLines.length).toBe(0);
64 |
65 | editor.setText("no matches in here");
66 | advanceClock(editor.buffer.stoppedChangingDelay);
67 | expect(resultAddedSpy.callCount).toBe(1);
68 | expect(resultSetSpy.callCount).toBe(1);
69 | expect(resultRemovedSpy.callCount).toBe(1);
70 |
71 | result = resultsModel.getResult(editor.getPath());
72 | expect(result).not.toBeDefined();
73 | expect(resultsModel.getPathCount()).toBe(0);
74 | expect(resultsModel.getMatchCount()).toBe(0);
75 |
76 | resultsModel.clear();
77 | spyOn(editor, "scan").andCallThrough();
78 | editor.setText("no matches in here");
79 | advanceClock(editor.buffer.stoppedChangingDelay);
80 | expect(editor.scan).not.toHaveBeenCalled();
81 | expect(resultsModel.getPathCount()).toBe(0);
82 | expect(resultsModel.getMatchCount()).toBe(0);
83 | });
84 |
85 | it("ignores changes in untitled buffers", async () => {
86 | await atom.workspace.open();
87 | await resultsModel.search("items", "*.js", "");
88 |
89 | editor = atom.workspace.getCenter().getActiveTextEditor();
90 | editor.setText("items\nitems");
91 | spyOn(editor, "scan").andCallThrough();
92 | advanceClock(editor.buffer.stoppedChangingDelay);
93 | expect(editor.scan).not.toHaveBeenCalled();
94 | });
95 |
96 | it("contains valid match objects after destroying a buffer (regression)", async () => {
97 | await resultsModel.search('items', '*.js', '');
98 |
99 | advanceClock(editor.buffer.stoppedChangingDelay)
100 | editor.getBuffer().destroy()
101 | result = resultsModel.getResult(editor.getPath())
102 | expect(result.matches[0].lineText).toBe(" var sort = function(items) {")
103 | });
104 | });
105 |
106 | describe("cancelling a search", () => {
107 | let cancelledSpy;
108 |
109 | beforeEach(() => {
110 | cancelledSpy = jasmine.createSpy();
111 | resultsModel.onDidCancelSearching(cancelledSpy);
112 | });
113 |
114 | it("populates the model with all the results, and updates in response to changes in the buffer", async () => {
115 | const searchPromise = resultsModel.search("items", "*.js", "");
116 | expect(resultsModel.inProgressSearchPromise).toBeTruthy();
117 | resultsModel.clear();
118 | expect(resultsModel.inProgressSearchPromise).toBeFalsy();
119 |
120 | await searchPromise;
121 | expect(cancelledSpy).toHaveBeenCalled();
122 | });
123 |
124 | it("populates the model with all the results, and updates in response to changes in the buffer", async () => {
125 | resultsModel.search("items", "*.js", "");
126 | await resultsModel.search("sort", "*.js", "");
127 |
128 | expect(cancelledSpy).toHaveBeenCalled();
129 | expect(resultsModel.getPathCount()).toBe(1);
130 | expect(resultsModel.getMatchCount()).toBe(5);
131 | });
132 | });
133 |
134 | describe("logging metrics", () => {
135 | it("logs the elapsed time and the number of results", async () => {
136 | await resultsModel.search('items', '*.js', '');
137 |
138 | advanceClock(editor.buffer.stoppedChangingDelay)
139 | editor.getBuffer().destroy()
140 | result = resultsModel.getResult(editor.getPath())
141 |
142 | expect(Number.isInteger(reporterSpy.sendSearchEvent.calls[0].args[0])).toBeTruthy()
143 | expect(reporterSpy.sendSearchEvent.calls[0].args[1]).toBe(6)
144 | });
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/spec/select-next-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | const path = require('path');
4 | const SelectNext = require('../lib/select-next');
5 | const dedent = require('dedent');
6 |
7 | describe("SelectNext", () => {
8 | let workspaceElement, editorElement, editor;
9 |
10 | beforeEach(async () => {
11 | workspaceElement = atom.views.getView(atom.workspace);
12 | atom.project.setPaths([path.join(__dirname, 'fixtures')]);
13 |
14 | editor = await atom.workspace.open('sample.js');
15 | editorElement = atom.views.getView(editor);
16 |
17 | jasmine.attachToDOM(workspaceElement);
18 | const activationPromise = atom.packages.activatePackage("find-and-replace");
19 | atom.commands.dispatch(editorElement, 'find-and-replace:show');
20 | await activationPromise;
21 | });
22 |
23 | describe("find-and-replace:select-next", () => {
24 | describe("when nothing is selected", () => {
25 | it("selects the word under the cursor", () => {
26 | editor.setCursorBufferPosition([1, 3]);
27 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
28 | expect(editor.getSelectedBufferRanges()).toEqual([[[1, 2], [1, 5]]]);
29 | });
30 | });
31 |
32 | describe("when a word is selected", () => {
33 | describe("when the selection was created using select-next", () => {
34 | beforeEach(() => {});
35 |
36 | it("selects the next occurrence of the selected word skipping any non-word matches", () => {
37 | editor.setText(dedent`
38 | for
39 | information
40 | format
41 | another for
42 | fork
43 | a 3rd for is here
44 | `);
45 |
46 | editor.setCursorBufferPosition([0, 0]);
47 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
48 | expect(editor.getSelectedBufferRanges()).toEqual([
49 | [[0, 0], [0, 3]]
50 | ]);
51 |
52 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
53 | expect(editor.getSelectedBufferRanges()).toEqual([
54 | [[0, 0], [0, 3]],
55 | [[3, 8], [3, 11]]
56 | ]);
57 |
58 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
59 | expect(editor.getSelectedBufferRanges()).toEqual([
60 | [[0, 0], [0, 3]],
61 | [[3, 8], [3, 11]],
62 | [[5, 6], [5, 9]]
63 | ]);
64 |
65 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
66 | expect(editor.getSelectedBufferRanges()).toEqual([
67 | [[0, 0], [0, 3]],
68 | [[3, 8], [3, 11]],
69 | [[5, 6], [5, 9]]
70 | ]);
71 |
72 | editor.setText("Testing reallyTesting");
73 | editor.setCursorBufferPosition([0, 0]);
74 |
75 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
76 | expect(editor.getSelectedBufferRanges()).toEqual([
77 | [[0, 0], [0, 7]]
78 | ]);
79 |
80 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
81 | expect(editor.getSelectedBufferRanges()).toEqual([
82 | [[0, 0], [0, 7]]
83 | ]);});});
84 |
85 | describe("when the selection was not created using select-next", () => {
86 | it("selects the next occurrence of the selected characters including non-word matches", () => {
87 | editor.setText(dedent`
88 | for
89 | information
90 | format
91 | another for
92 | fork
93 | a 3rd for is here
94 | `);
95 |
96 | editor.setSelectedBufferRange([[0, 0], [0, 3]]);
97 |
98 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
99 | expect(editor.getSelectedBufferRanges()).toEqual([
100 | [[0, 0], [0, 3]],
101 | [[1, 2], [1, 5]]
102 | ]);
103 |
104 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
105 | expect(editor.getSelectedBufferRanges()).toEqual([
106 | [[0, 0], [0, 3]],
107 | [[1, 2], [1, 5]],
108 | [[2, 0], [2, 3]]
109 | ]);
110 |
111 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
112 | expect(editor.getSelectedBufferRanges()).toEqual([
113 | [[0, 0], [0, 3]],
114 | [[1, 2], [1, 5]],
115 | [[2, 0], [2, 3]],
116 | [[3, 8], [3, 11]]
117 | ]);
118 |
119 | editor.setText("Testing reallyTesting");
120 | editor.setSelectedBufferRange([[0, 0], [0, 7]]);
121 |
122 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
123 | expect(editor.getSelectedBufferRanges()).toEqual([
124 | [[0, 0], [0, 7]],
125 | [[0, 14], [0, 21]]
126 | ]);
127 | });
128 | });
129 | });
130 |
131 | describe("when part of a word is selected", () => {
132 | it("selects the next occurrence of the selected text", () => {
133 | editor.setText(dedent`
134 | for
135 | information
136 | format
137 | another for
138 | fork
139 | a 3rd for is here
140 | `);
141 |
142 | editor.setSelectedBufferRange([[1, 2], [1, 5]]);
143 |
144 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
145 | expect(editor.getSelectedBufferRanges()).toEqual([
146 | [[1, 2], [1, 5]],
147 | [[2, 0], [2, 3]]
148 | ]);
149 |
150 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
151 | expect(editor.getSelectedBufferRanges()).toEqual([
152 | [[1, 2], [1, 5]],
153 | [[2, 0], [2, 3]],
154 | [[3, 8], [3, 11]]
155 | ]);
156 |
157 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
158 | expect(editor.getSelectedBufferRanges()).toEqual([
159 | [[1, 2], [1, 5]],
160 | [[2, 0], [2, 3]],
161 | [[3, 8], [3, 11]],
162 | [[4, 0], [4, 3]]
163 | ]);
164 |
165 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
166 | expect(editor.getSelectedBufferRanges()).toEqual([
167 | [[1, 2], [1, 5]],
168 | [[2, 0], [2, 3]],
169 | [[3, 8], [3, 11]],
170 | [[4, 0], [4, 3]],
171 | [[5, 6], [5, 9]]
172 | ]);
173 |
174 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
175 | expect(editor.getSelectedBufferRanges()).toEqual([
176 | [[1, 2], [1, 5]],
177 | [[2, 0], [2, 3]],
178 | [[3, 8], [3, 11]],
179 | [[4, 0], [4, 3]],
180 | [[5, 6], [5, 9]],
181 | [[0, 0], [0, 3]]
182 | ]);
183 | });
184 | });
185 |
186 | describe("when a non-word is selected", () => {
187 | it("selects the next occurrence of the selected text", () => {
188 | editor.setText(dedent`
189 | {
202 | it("does not select the newlines", () => {
203 | editor.setText(dedent`
204 | a
205 |
206 | a
207 | `);
208 |
209 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
210 | expect(editor.getSelectedBufferRanges()).toEqual([
211 | [[0, 0], [0, 1]]
212 | ]);
213 |
214 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
215 | expect(editor.getSelectedBufferRanges()).toEqual([
216 | [[0, 0], [0, 1]],
217 | [[2, 0], [2, 1]]
218 | ]);
219 |
220 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
221 | expect(editor.getSelectedBufferRanges()).toEqual([
222 | [[0, 0], [0, 1]],
223 | [[2, 0], [2, 1]]
224 | ]);
225 | });
226 | });
227 |
228 | it('honors the reversed orientation of previous selections', () => {
229 | editor.setText('ab ab ab ab')
230 | editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: true})
231 |
232 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
233 | expect(editor.getSelections().length).toBe(2)
234 | expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
235 |
236 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
237 | expect(editor.getSelections().length).toBe(3)
238 | expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
239 |
240 | editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: false})
241 |
242 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
243 | expect(editor.getSelections().length).toBe(2)
244 | expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
245 |
246 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
247 | expect(editor.getSelections().length).toBe(3)
248 | expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
249 | })
250 | });
251 |
252 | describe("find-and-replace:select-all", () => {
253 | describe("when there is no selection", () => {
254 | it("find and selects all occurrences of the word under the cursor", () => {
255 | editor.setText(dedent`
256 | for
257 | information
258 | format
259 | another for
260 | fork
261 | a 3rd for is here
262 | `);
263 |
264 | atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
265 | expect(editor.getSelectedBufferRanges()).toEqual([
266 | [[0, 0], [0, 3]],
267 | [[3, 8], [3, 11]],
268 | [[5, 6], [5, 9]]
269 | ]);
270 |
271 | atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
272 | expect(editor.getSelectedBufferRanges()).toEqual([
273 | [[0, 0], [0, 3]],
274 | [[3, 8], [3, 11]],
275 | [[5, 6], [5, 9]]
276 | ]);
277 | })
278 | });
279 |
280 | describe("when a word is selected", () => {
281 | describe("when the word was selected using select-next", () => {
282 | it("find and selects all occurrences of the word", () => {
283 | editor.setText(dedent`
284 | for
285 | information
286 | format
287 | another for
288 | fork
289 | a 3rd for is here
290 | `);
291 |
292 | atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
293 | expect(editor.getSelectedBufferRanges()).toEqual([
294 | [[0, 0], [0, 3]],
295 | [[3, 8], [3, 11]],
296 | [[5, 6], [5, 9]]
297 | ]);
298 |
299 | atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
300 | expect(editor.getSelectedBufferRanges()).toEqual([
301 | [[0, 0], [0, 3]],
302 | [[3, 8], [3, 11]],
303 | [[5, 6], [5, 9]]
304 | ]);
305 | });
306 | });
307 |
308 | describe("when the word was not selected using select-next", () => {
309 | it("find and selects all occurrences including non-words", () => {
310 | editor.setText(dedent`
311 | for
312 | information
313 | format
314 | another for
315 | fork
316 | a 3rd for is here
317 | `);
318 |
319 | editor.setSelectedBufferRange([[3, 8], [3, 11]]);
320 |
321 | atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
322 | expect(editor.getSelectedBufferRanges()).toEqual([
323 | [[3, 8], [3, 11]],
324 | [[0, 0], [0, 3]],
325 | [[1, 2], [1, 5]],
326 | [[2, 0], [2, 3]],
327 | [[4, 0], [4, 3]],
328 | [[5, 6], [5, 9]]
329 | ]);
330 | });
331 | });
332 | });
333 |
334 | describe("when a non-word is selected", () => {
335 | it("selects the next occurrence of the selected text", () => {
336 | editor.setText(dedent`
337 | {
350 | editor.setText('ab ab ab ab')
351 | editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: true})
352 |
353 | atom.commands.dispatch(editorElement, 'find-and-replace:select-all')
354 | expect(editor.getSelections().length).toBe(4)
355 | expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
356 |
357 | editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: false})
358 |
359 | atom.commands.dispatch(editorElement, 'find-and-replace:select-all')
360 | expect(editor.getSelections().length).toBe(4)
361 | expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
362 | })
363 | });
364 |
365 | describe("find-and-replace:select-undo", () => {
366 | describe("when there is no selection", () => {
367 | it("does nothing", () => {
368 | editor.setText(dedent`
369 | for
370 | information
371 | format
372 | another for
373 | fork
374 | a 3rd for is here
375 | `);
376 |
377 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
378 | expect(editor.getSelectedBufferRanges()).toEqual([
379 | [[0, 0], [0, 0]]
380 | ]);
381 | })
382 | });
383 |
384 | describe("when a word is selected", () => {
385 | it("unselects current word", () => {
386 | editor.setText(dedent`
387 | for
388 | information
389 | format
390 | another for
391 | fork
392 | a 3rd for is here
393 | `);
394 |
395 | editor.setSelectedBufferRange([[3, 8], [3, 11]]);
396 |
397 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
398 | expect(editor.getSelectedBufferRanges()).toEqual([
399 | [[3, 11], [3, 11]]
400 | ]);
401 | })
402 | });
403 |
404 | describe("when two words are selected", () => {
405 | it("unselects words in order", () => {
406 | editor.setText(dedent`
407 | for
408 | information
409 | format
410 | another for
411 | fork
412 | a 3rd for is here
413 | `);
414 |
415 | editor.setSelectedBufferRange([[3, 8], [3, 11]]);
416 |
417 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
418 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
419 | expect(editor.getSelectedBufferRanges()).toEqual([
420 | [[3, 8], [3, 11]]
421 | ]);
422 |
423 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
424 | expect(editor.getSelectedBufferRanges()).toEqual([
425 | [[3, 11], [3, 11]]
426 | ]);
427 | })
428 | });
429 |
430 | describe("when three words are selected", () => {
431 | it("unselects words in order", () => {
432 | editor.setText(dedent`
433 | for
434 | information
435 | format
436 | another for
437 | fork
438 | a 3rd for is here
439 | `);
440 |
441 | editor.setCursorBufferPosition([0, 0]);
442 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
443 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
444 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
445 |
446 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
447 | expect(editor.getSelectedBufferRanges()).toEqual([
448 | [[0, 0], [0, 3]],
449 | [[3, 8], [3, 11]]
450 | ]);
451 |
452 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
453 | expect(editor.getSelectedBufferRanges()).toEqual([
454 | [[0, 0], [0, 3]]
455 | ]);
456 | })
457 | });
458 |
459 | describe("when starting at the bottom word", () => {
460 | it("unselects words in order", () => {
461 | editor.setText(dedent`
462 | for
463 | information
464 | format
465 | another for
466 | fork
467 | a 3rd for is here
468 | `);
469 |
470 | editor.setCursorBufferPosition([5, 7]);
471 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
472 | expect(editor.getSelectedBufferRanges()).toEqual([
473 | [[5, 6], [5, 9]]
474 | ]);
475 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
476 | expect(editor.getSelectedBufferRanges()).toEqual([
477 | [[5, 6], [5, 9]],
478 | [[0, 0], [0, 3]]
479 | ]);
480 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
481 | expect(editor.getSelectedBufferRanges()).toEqual([
482 | [[5, 6], [5, 9]]
483 | ]);});
484 |
485 | it("doesn't stack previously selected", () => {
486 | editor.setText(dedent`
487 | for
488 | information
489 | format
490 | another for
491 | fork
492 | a 3rd for is here
493 | `);
494 |
495 | editor.setCursorBufferPosition([5, 7]);
496 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
497 | expect(editor.getSelectedBufferRanges()).toEqual([
498 | [[5, 6], [5, 9]]
499 | ]);
500 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
501 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
502 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
503 | atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
504 | expect(editor.getSelectedBufferRanges()).toEqual([
505 | [[5, 6], [5, 9]],
506 | [[0, 0], [0, 3]]
507 | ]);
508 | });
509 | });
510 | });
511 |
512 | describe("find-and-replace:select-skip", () => {
513 | describe("when there is no selection", () => {
514 | it("does nothing", () => {
515 | editor.setText(dedent`
516 | for
517 | information
518 | format
519 | another for
520 | fork
521 | a 3rd for is here
522 | `);
523 |
524 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
525 | expect(editor.getSelectedBufferRanges()).toEqual([
526 | [[0, 0], [0, 0]]
527 | ]);
528 | })
529 | });
530 |
531 | describe("when a word is selected", () => {
532 | it("unselects current word and selects next match", () => {
533 | editor.setText(dedent`
534 | for
535 | information
536 | format
537 | another for
538 | fork
539 | a 3rd for is here
540 | `);
541 |
542 | editor.setCursorBufferPosition([3, 8]);
543 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
544 | expect(editor.getSelectedBufferRanges()).toEqual([
545 | [[3, 8], [3, 11]]
546 | ]);
547 |
548 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
549 | expect(editor.getSelectedBufferRanges()).toEqual([
550 | [[5, 6], [5, 9]]
551 | ]);
552 | })
553 | });
554 |
555 | describe("when two words are selected", () => {
556 | it("unselects second word and selects next match", () => {
557 | editor.setText(dedent`
558 | for
559 | information
560 | format
561 | another for
562 | fork
563 | a 3rd for is here
564 | `);
565 |
566 | editor.setCursorBufferPosition([0, 0]);
567 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
568 | expect(editor.getSelectedBufferRanges()).toEqual([
569 | [[0, 0], [0, 3]]
570 | ]);
571 |
572 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
573 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
574 | expect(editor.getSelectedBufferRanges()).toEqual([
575 | [[0, 0], [0, 3]],
576 | [[5, 6], [5, 9]]
577 | ]);
578 |
579 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
580 | expect(editor.getSelectedBufferRanges()).toEqual([
581 | [[0, 0], [0, 3]]
582 | ]);
583 | });
584 | });
585 |
586 | describe("when starting at the bottom word", () => {
587 | it("unselects second word and selects next match", () => {
588 | editor.setText(dedent`
589 | for
590 | information
591 | format
592 | another for
593 | fork
594 | a 3rd for is here
595 | `);
596 |
597 | editor.setCursorBufferPosition([5, 7]);
598 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
599 | expect(editor.getSelectedBufferRanges()).toEqual([
600 | [[5, 6], [5, 9]]
601 | ]);
602 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
603 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
604 | expect(editor.getSelectedBufferRanges()).toEqual([
605 | [[5, 6], [5, 9]],
606 | [[3, 8], [3, 11]]
607 | ]);
608 | });
609 | });
610 |
611 | it('honors the reversed orientation of previous selections', () => {
612 | editor.setText('ab ab ab ab')
613 | editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: true})
614 |
615 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
616 | expect(editor.getSelections().length).toBe(1)
617 | expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
618 |
619 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
620 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
621 | expect(editor.getSelections().length).toBe(2)
622 | expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
623 |
624 | editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: false})
625 |
626 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
627 | expect(editor.getSelections().length).toBe(1)
628 | expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
629 |
630 | atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
631 | atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
632 | expect(editor.getSelections().length).toBe(2)
633 | expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
634 | })
635 | });
636 | });
637 |
--------------------------------------------------------------------------------
/spec/setup-spec.js:
--------------------------------------------------------------------------------
1 | require('etch').setScheduler({
2 | updateDocument(callback) { callback(); },
3 | getNextUpdatePromise() { return Promise.resolve(); }
4 | });
5 |
6 | // The CI on Atom has been failing with this package. Experiments
7 | // indicate the dev tools were being opened, which caused test
8 | // failures. Dev tools are meant to be triggered on an uncaught
9 | // exception.
10 | //
11 | // These failures are flaky though, so an exact cause
12 | // has not yet been found. For now the following is added
13 | // to reduce the number of flaky failures, which have been
14 | // causing false failures on unrelated Atom PRs.
15 | //
16 | // See more in https://github.com/atom/atom/pull/21335
17 | global.beforeEach(() => {
18 | spyOn(atom, 'openDevTools').andReturn((console.error("ERROR: Dev tools attempted to open"), Promise.resolve()));
19 | });
20 |
--------------------------------------------------------------------------------
/styles/find-and-replace.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 | @import "syntax-variables";
3 |
4 | // result markers
5 | atom-text-editor {
6 | .find-result .region {
7 | background-color: transparent;
8 | border-radius: @component-border-radius;
9 | border: 1px solid @syntax-result-marker-color;
10 | box-sizing: border-box;
11 | z-index: 0;
12 | }
13 |
14 | .current-result .region {
15 | border-radius: @component-border-radius;
16 | border: 1px solid @syntax-result-marker-color-selected;
17 | box-sizing: border-box;
18 | z-index: 0;
19 | }
20 |
21 | .find-result,
22 | .current-result {
23 | display: none;
24 | }
25 | }
26 |
27 | atom-workspace.find-visible {
28 | atom-text-editor {
29 | .find-result,
30 | .current-result {
31 | display: block;
32 | }
33 | }
34 | }
35 |
36 | // Both project and buffer FNR styles
37 | .find-and-replace,
38 | .preview-pane,
39 | .project-find {
40 | @min-width: 200px; // min width before it starts scrolling
41 |
42 | -webkit-user-select: none;
43 | padding: @component-padding/2;
44 | overflow-x: auto;
45 |
46 | .header {
47 | padding: @component-padding/4 @component-padding/2;
48 | min-width: @min-width;
49 | line-height: 1.75;
50 | }
51 | .header-item {
52 | margin: @component-padding/4 0;
53 | }
54 |
55 | .input-block {
56 | display: flex;
57 | flex-wrap: wrap;
58 | width: 100%;
59 | min-width: @min-width;
60 | }
61 | .input-block-item {
62 | display: flex;
63 | flex: 1;
64 | padding: @component-padding / 2;
65 | }
66 |
67 | .btn-group {
68 | display: flex;
69 | flex: 1;
70 | .btn {
71 | flex: 1;
72 | }
73 | & + .btn-group {
74 | margin-left: @component-padding;
75 | }
76 | }
77 |
78 | .btn > .icon {
79 | width: 20px;
80 | height: 16px;
81 | vertical-align: middle;
82 | fill: currentColor;
83 | stroke: currentColor;
84 | pointer-events: none;
85 | }
86 |
87 | .close-button {
88 | margin-left: @component-padding;
89 | cursor: pointer;
90 | color: @text-color-subtle;
91 | &:hover {
92 | color: @text-color-highlight;
93 | }
94 | .icon::before {
95 | margin-right: 0;
96 | text-align: center;
97 | vertical-align: middle;
98 | }
99 | }
100 |
101 | .description {
102 | display: inline-block;
103 | .subtle-info-message {
104 | padding-left: 5px;
105 | color: @text-color-subtle;
106 | .highlight {
107 | color: @text-color;
108 | font-weight: normal;
109 | }
110 | }
111 | }
112 |
113 | .options-label {
114 | color: @text-color-subtle;
115 | position: relative;
116 | .options {
117 | margin-right: .5em;
118 | color: @text-color;
119 | }
120 | }
121 |
122 | .btn-group-options {
123 | display: inline-flex;
124 | margin-top: -.1em;
125 |
126 | .btn {
127 | width: 36px;
128 | padding: 0;
129 | line-height: 1.75;
130 | }
131 | }
132 |
133 | .editor-container {
134 | position: relative;
135 | atom-text-editor {
136 | width: 100%;
137 | }
138 | }
139 |
140 | }
141 |
142 | // Buffer find and replace
143 | .find-and-replace {
144 | @input-width: 260px;
145 | @block-width: 260px;
146 |
147 | .input-block-item {
148 | flex: 1 1 @block-width;
149 | }
150 | .input-block-item--flex {
151 | flex: 100 1 @input-width;
152 | }
153 |
154 | .btn-group-find,
155 | .btn-group-replace {
156 | flex: 1;
157 | }
158 |
159 | .btn-group-find-all,
160 | .btn-group-replace-all {
161 | flex: 2;
162 | }
163 |
164 | .find-container atom-text-editor {
165 | padding-right: 64px; // leave some room for the results count
166 | }
167 |
168 | // results count
169 | .find-meta-container {
170 | position: absolute;
171 | top: 1px;
172 | right: 0;
173 | margin: @component-padding/2 @component-padding/2 0 0;
174 | z-index: 2;
175 | font-size: .9em;
176 | line-height: @component-line-height;
177 | pointer-events: none;
178 | .result-counter {
179 | margin-right: @component-padding;
180 | }
181 | }
182 | }
183 |
184 | .find-wrap-icon {
185 | @wrap-size: @font-size * 10;
186 |
187 | opacity: 0;
188 | transition: opacity 0.5s;
189 | &.visible { opacity: 1; }
190 |
191 | position: absolute;
192 |
193 | // These are getting placed in the DOM as a pane item, so override the pane
194 | // item positioning styles. :/
195 | top: 50% !important;
196 | left: 50% !important;
197 | right: initial !important;
198 | bottom: initial !important;
199 |
200 | margin-top: @wrap-size * -0.5;
201 | margin-left: @wrap-size * -0.5;
202 |
203 | background: fadeout(darken(@syntax-background-color, 4%), 55%);
204 | border-radius: @component-border-radius * 2;
205 | text-align: center;
206 | pointer-events: none;
207 | &:before {
208 | // Octicons look best in sizes that are multiples of 16px
209 | font-size: @wrap-size - mod(@wrap-size, 16px) - 32px;
210 | line-height: @wrap-size;
211 | height: @wrap-size;
212 | width: @wrap-size;
213 | color: @syntax-text-color;
214 | opacity: .5;
215 | }
216 | }
217 |
218 | // Project find and replace
219 | .project-find {
220 | @project-input-width: 260px;
221 | @project-block-width: 160px;
222 |
223 | .input-block-item {
224 | flex: 1 1 @project-block-width;
225 | }
226 | .input-block-item--flex {
227 | flex: 100 1 @project-input-width;
228 | }
229 |
230 | .loading,
231 | .preview-block,
232 | .error-messages,
233 | .filter-container {
234 | display: none;
235 | }
236 | }
237 |
238 | .preview-pane {
239 | position: relative;
240 | display: flex;
241 | flex-direction: column;
242 | padding: 0;
243 |
244 | .preview-header {
245 | display: flex;
246 | flex-wrap: wrap;
247 | padding: @component-padding/2;
248 | align-items: center;
249 | justify-content: space-between;
250 | overflow: hidden;
251 | font-weight: normal;
252 | border-bottom: 1px solid @panel-heading-border-color;
253 | background-color: @panel-heading-background-color;
254 | }
255 |
256 | .preview-count {
257 | margin: @component-padding/2;
258 | }
259 |
260 | .preview-controls {
261 | display: flex;
262 | flex-wrap: wrap;
263 | .btn-group {
264 | margin: @component-padding/2;
265 | }
266 | }
267 |
268 | .loading-spinner-tiny,
269 | .loading-spinner-tiny + .inline-block {
270 | vertical-align: middle;
271 | }
272 |
273 | .no-results-overlay {
274 | visibility: hidden;
275 | }
276 |
277 | &.no-results .no-results-overlay {
278 | visibility: visible;
279 | }
280 |
281 | .results-view {
282 | overflow: auto;
283 | position: relative;
284 | flex: 1;
285 |
286 | &-container {
287 | // adds some padding
288 | // so the last item can be clicked
289 | // when there is a horizontal scrollbar -> #943
290 | padding-bottom: @component-padding;
291 | }
292 |
293 | .list-item {
294 | padding: 0 0 0 @component-padding;
295 | }
296 | .context-row, .match-row {
297 | padding: 0 0 0 @component-padding;
298 | margin-left: 8px;
299 |
300 | box-shadow: inset 0 1px 0 mix(@base-border-color, @base-background-color);
301 |
302 | // box-shadow over a border is used to not affect height calculation
303 | &.separator {
304 | box-shadow: inset 0 1px 0 @base-border-color;
305 | }
306 | }
307 |
308 | .line-number {
309 | margin-right: 1ex;
310 | text-align: right;
311 | display: inline-block;
312 | }
313 | .match-row.selected .line-number {
314 | color: @text-color-selected;
315 | }
316 |
317 | .path-match-number {
318 | padding-left: @component-padding;
319 | color: @text-color-subtle;
320 | }
321 |
322 | .preview {
323 | word-break: break-all;
324 | white-space: pre;
325 | color: @text-color-subtle;
326 | }
327 |
328 | .match-row .preview {
329 | color: @text-color-highlight;
330 | }
331 | .match-row.selected .preview {
332 | color: @text-color-selected;
333 | }
334 |
335 |
336 | .selected {
337 | .highlight-info {
338 | box-shadow: inset 0 0 1px lighten(@background-color-info, 50%);
339 | }
340 |
341 | .highlight-error {
342 | box-shadow: inset 0 0 1px lighten(@background-color-error, 25%);
343 | }
344 |
345 | .highlight-success {
346 | box-shadow: inset 0 0 1px lighten(@background-color-success, 25%);
347 | }
348 | }
349 | }
350 | }
351 |
352 | .find-container atom-text-editor, .replace-container atom-text-editor {
353 | // Styles for regular expression highlighting
354 | .syntax--regexp {
355 | .syntax--escape {
356 | color: @text-color-info;
357 | }
358 | .syntax--range, .syntax--character-class, .syntax--wildcard {
359 | color: @text-color-success;
360 | }
361 | .syntax--wildcard {
362 | font-weight: bold;
363 | }
364 | .syntax--set {
365 | color: inherit;
366 | }
367 | .syntax--keyword, .syntax--punctuation {
368 | color: @text-color-error;
369 | font-weight: normal;
370 | }
371 |
372 | .syntax--replacement.syntax--variable {
373 | color: @text-color-warning;
374 | }
375 | }
376 | }
377 |
--------------------------------------------------------------------------------