Start work on partial rendering
ADD
lib/project/list-view.js
+const etch = require('etch');
+const $ = require('../dom-helpers');
+const resizeDetector = require('element-resize-detector')({strategy: 'scroll'});
+
+module.exports = class ListView {
+ constructor({items, heightForItem, itemComponent, className}) {
+ this.items = items;
+ this.heightForItem = heightForItem;
+ this.itemComponent = itemComponent;
+ this.className = className;
+ etch.initialize(this);
+
+ resizeDetector.listenTo(this.element, () => etch.update(this));
+ this.element.addEventListener('scroll', () => etch.update(this));
+ }
+
+ update({items, heightForItem, itemComponent, className} = {}) {
+ if (items) this.items = items;
+ if (heightForItem) this.heightForItem = heightForItem;
+ if (itemComponent) this.itemComponent = itemComponent;
+ if (className) this.className = className;
+ return etch.update(this)
+ }
+
+ render() {
+ const children = [];
+ let itemTopPosition = 0;
+
+ if (this.element) {
+ const {scrollTop, clientHeight} = this.element;
+ const scrollBottom = scrollTop + clientHeight;
+
+ let i = 0;
+
+ for (; i < this.items.length; i++) {
+ let itemBottomPosition = itemTopPosition + this.heightForItem(this.items[i], i);
+ if (itemBottomPosition > scrollTop) break;
+ itemTopPosition = itemBottomPosition;
+ }
+
+ for (; i < this.items.length; i++) {
+ const item = this.items[i];
+ const itemHeight = this.heightForItem(this.items[i], i);
+ children.push(
+ $.div(
+ {
+ style: {
+ position: 'absolute',
+ height: itemHeight + 'px',
+ width: '100%',
+ top: itemTopPosition + 'px'
+ },
+ key: i
+ },
+ $(this.itemComponent, {
+ item: item,
+ top: Math.max(0, scrollTop - itemTopPosition),
+ bottom: Math.min(itemHeight, scrollBottom - itemTopPosition)
+ })
+ )
+ );
+
+ itemTopPosition += itemHeight;
+ if (itemTopPosition >= scrollBottom) {
+ i++
+ break;
+ }
+ }
+
+ for (; i < this.items.length; i++) {
+ itemTopPosition += this.heightForItem(this.items[i], i);
+ }
+ }
+
+ return $.div(
+ {
+ style: {
+ position: 'relative',
+ height: '100%',
+ overflow: 'auto',
+ }
+ },
+ $.ol(
+ {
+ ref: 'list',
+ className: this.className,
+ style: {height: itemTopPosition + 'px'}
+ },
+ ...children
+ )
+ );
+ }
+};

MODIFY
lib/project/result-view.js
lib/project/result-view.js
@@ -6,11 +6,16 @@ const $ = require('../dom-helpers');
module.exports =
class ResultView {
- constructor(params) {
+ constructor({item, top, bottom} = {}) {
const {
filePath, matches, isSelected, selectedMatchIndex, isExpanded,
- regex, replacePattern, previewStyle
- } = params;
+ regex, replacePattern, previewStyle, pathDetailsHeight, matchHeight
+ } = item;
+
+ this.top = top;
+ this.bottom = bottom;
+ this.pathDetailsHeight = pathDetailsHeight;
+ this.matchHeight = matchHeight;
this.filePath = filePath
this.matches = matches
@@ -23,11 +28,11 @@ class ResultView {
etch.initialize(this);
}
- update(params) {
+ update({item, top, bottom} = {}) {
const {
filePath, matches, isSelected, selectedMatchIndex, isExpanded,
- regex, replacePattern, previewStyle
- } = params;
+ regex, replacePattern, previewStyle, pathDetailsHeight, matchHeight
+ } = item;
const changed =
matches !== this.matches ||
@@ -36,7 +41,11 @@ class ResultView {
selectedMatchIndex !== this.selectedMatchIndex ||
regex !== this.regex ||
replacePattern !== this.replacePattern ||
- previewStyle !== this.previewStyle;
+ previewStyle !== this.previewStyle ||
+ top !== this.top ||
+ bottom !== this.bottom ||
+ pathDetailsHeight !== this.pathDetailsHeight ||
+ matchHeight !== this.matchHeight;
if (changed) {
this.filePath = filePath;
@@ -47,6 +56,10 @@ class ResultView {
this.regex = regex;
this.replacePattern = replacePattern;
this.previewStyle = previewStyle;
+ this.top = top;
+ this.bottom = bottom;
+ this.pathDetailsHeight = pathDetailsHeight;
+ this.matchHeight = matchHeight;
etch.update(this);
}
}
@@ -92,10 +105,35 @@ class ResultView {
)
),
- $.ul({ref: 'matches', className: 'matches list-tree'},
- ...(
- (this.matches && this.matches.length > 0) ?
- this.matches.map((match, i) =>
+ this.renderList()
+ )
+ );
+ }
+
+ renderList() {
+ const children = [];
+ let itemTopPosition = this.pathDetailsHeight;
+ let i = 0;
+
+ for (; i < this.matches.length; i++) {
+ let itemBottomPosition = itemTopPosition + this.matchHeight;
+ if (itemBottomPosition > this.top) break;
+ itemTopPosition = itemBottomPosition;
+ }
+
+ for (; i < this.matches.length; i++) {
+ const match = this.matches[i];
+ children.push(
+ $.div(
+ {
+ style: {
+ position: 'absolute',
+ height: this.matchHeight + 'px',
+ minWidth: '100%',
+ top: itemTopPosition - this.pathDetailsHeight + 'px'
+ },
+ key: i
+ },
$(MatchView, {
match,
regex: this.regex,
@@ -103,11 +141,20 @@ class ResultView {
isSelected: (i === this.selectedMatchIndex),
previewStyle: this.previewStyle
})
- ) :
- ['none']
- )
- )
)
);
+
+ itemTopPosition += this.matchHeight;
+ if (itemTopPosition >= this.bottom) break;
+ }
+
+ return $.ol(
+ {
+ ref: 'list',
+ className: 'matches list-tree',
+ style: {height: itemTopPosition + 'px', position: 'relative'}
+ },
+ ...children
+ )
}
};

MODIFY
lib/project/results-model.coffee
lib/project/results-model.coffee
@@ -206,6 +206,9 @@ class ResultsModel
getResult: (filePath) ->
@results[filePath]
+ getResultAt: (index) ->
+ @results[@paths[index]]
+
setResult: (filePath, result) ->
if result
@addResult(filePath, result)

MODIFY
lib/project/results-view.js
lib/project/results-view.js
-const {Range, CompositeDisposable} = require('atom');
+const {Range, CompositeDisposable, Disposable} = require('atom');
const ResultView = require('./result-view');
-const etch = require('etch')
-const $ = require('../dom-helpers')
+const ListView = require('./list-view');
+const etch = require('etch');
+const $ = require('../dom-helpers');
+const resizeDetector = require('element-resize-detector')({strategy: 'scroll'});
const reverseDirections = {
left: 'right',
@@ -15,16 +17,15 @@ class ResultsView {
constructor({model}) {
this.model = model;
this.pixelOverdraw = 100;
- this.searchResults = [];
this.selectedResultIndex = 0;
this.selectedMatchIndex = -1;
this.collapsedResultIndices = [];
+ this.heightForSearchResult = this.heightForSearchResult.bind(this);
etch.initialize(this);
+ resizeDetector.listenTo(this.element, this.invalidateItemHeights.bind(this));
this.element.addEventListener('mousedown', this.handleClick.bind(this));
- this.element.addEventListener('scroll', () => this.renderResults())
- this.element.addEventListener('resize', () => this.renderResults())
this.subscriptions = new CompositeDisposable(
atom.config.observe('editor.fontFamily', this.fontFamilyChanged.bind(this)),
@@ -47,8 +48,6 @@ class ResultsView {
'find-and-replace:copy-path': this.copyPath.bind(this)
})
);
-
- this.renderResults();
}
update() {}
@@ -58,7 +57,6 @@ class ResultsView {
}
clear() {
- this.searchResults = [];
this.selectedResultIndex = -1;
this.selectedMatchIndex = -1;
etch.update(this);
@@ -71,83 +69,85 @@ class ResultsView {
replacePattern = this.model.getFindOptions().replacePattern;
}
- return (
+ return $.div(
+ {className: 'results-view'},
+
$.ol(
{
- className: 'results-view list-tree focusable-panel has-collapsable-children',
- tabIndex: -1
+ className: 'list-tree',
+ style: {visibility: 'hidden', position: 'absolute'}
},
-
- ...this.searchResults.map(({filePath, matches}, i) => {
+ $(ResultView, {
+ ref: 'dummyResultView',
+ top: 0,
+ bottom: Infinity,
+ item: {
+ filePath: 'fake-file-path',
+ matches: [{
+ range: [[0, 1], [0, 2]],
+ lineTextOffset: 1,
+ lineText: 'fake-line-text',
+ matchText: 'fake-match-text',
+ isSelected: false,
+ }],
+ matchHeight: 1,
+ pathDetailsHeight: 1,
+ isSelected: false,
+ isExpanded: true,
+ previewStyle: this.previewStyle
+ }
+ })
+ ),
+
+ this.pathDetailsHeight != null ?
+ $(ListView, {
+ className: 'list-tree',
+ itemComponent: ResultView,
+ heightForItem: this.heightForSearchResult,
+ items: this.model.getPaths().map((filePath, i) => {
const isSelected = (i === this.selectedResultIndex);
const isExpanded = !this.collapsedResultIndices[i];
const selectedMatchIndex = isSelected && this.selectedMatchIndex;
-
- return $(ResultView, {
- filePath, matches, isSelected, isExpanded, selectedMatchIndex,
- regex, replacePattern, previewStyle: this.previewStyle
- });
- })
- )
+ return Object.assign({
+ isSelected,
+ isExpanded,
+ selectedMatchIndex,
+ previewStyle: this.previewStyle,
+ pathDetailsHeight: this.pathDetailsHeight,
+ matchHeight: this.matchHeight
+ }, this.model.results[filePath]);
+ }),
+ }) :
+ $.div()
);
}
- addResult({filePath, result, filePathInsertedIndex}) {
- result.filePath = filePath;
-
- const existingIndex = this.searchResults.findIndex(existingResult =>
- existingResult.filePath === filePath
- );
-
- if (existingIndex !== -1) {
- this.searchResults[existingIndex] = result;
+ heightForSearchResult(searchResult, i) {
+ if (this.collapsedResultIndices[i]) {
+ return this.pathDetailsHeight;
} else {
- if (filePathInsertedIndex < this.searchResults.length || this.shouldRenderMoreResults()) {
- this.searchResults.splice(filePathInsertedIndex, 0, result);
+ return this.pathDetailsHeight + searchResult.matches.length * this.matchHeight;
+ }
}
- if (!this.userMovedSelection || this.model.getPathCount() === 1) {
- this.selectFirstResult();
+ invalidateItemHeights() {
+ const pathDetailsHeight = this.refs.dummyResultView.refs.pathDetails.offsetHeight;
+ const matchHeight = this.refs.dummyResultView.element.getElementsByClassName('search-result')[0].offsetHeight;
+ if (pathDetailsHeight !== this.pathDetailsHeight || matchHeight !== this.matchHeight) {
+ console.log('NEW MEASUREMENTS', pathDetailsHeight, matchHeight);
+ this.pathDetailsHeight = pathDetailsHeight;
+ this.matchHeight = matchHeight;
+ etch.update(this);
}
}
+ addResult() {
etch.update(this);
}
removeResult({filePath}) {
- const index = this.searchResults.findIndex(result => result.filePath === filePath);
- if (index !== -1) {
- this.searchResults.splice(index, 1);
etch.update(this);
}
- }
-
- renderResults(scrollTop) {
- for (const filePath of this.model.getPaths().slice(this.searchResults.length)) {
- if (!this.shouldRenderMoreResults(scrollTop)) break;
- this.searchResults.push(this.model.getResult(filePath));
- }
-
- return etch.update(this);
- }
-
- shouldRenderMoreResults(scrollTop) {
- if (scrollTop == null) scrollTop = this.element.scrollTop;
- const pathDetailsElement = this.element.querySelector('.path-details');
- const matchElement = this.element.querySelector('.path:not(.collapsed) .search-result');
- const pathDetailsHeight = pathDetailsElement ? pathDetailsElement.offsetHeight : 0;
- const matchHeight = matchElement ? matchElement.offsetHeight : 0;
-
- let renderedHeight = 0;
- for (let i = 0; i < this.searchResults.length; i++) {
- renderedHeight += pathDetailsHeight;
- if (!this.collapsedResultIndices[i]) {
- renderedHeight += this.searchResults[i].matches.length * matchHeight;
- }
- }
-
- return renderedHeight < scrollTop + this.element.offsetHeight + this.pixelOverdraw;
- }
handleClick(event) {
const clickedResultElement = event.target.closest('.path');
@@ -155,10 +155,8 @@ class ResultsView {
if (event.ctrlKey || !clickedResultElement) return;
const clickedFilePath = clickedResultElement.dataset.path;
- this.selectedResultIndex = this.searchResults.findIndex(searchResult =>
- searchResult.filePath === clickedFilePath
- );
- const clickedResult = this.searchResults[this.selectedResultIndex];
+ this.selectedResultIndex = this.model.getPaths().indexOf(clickedFilePath);
+ const clickedResult = this.model.getResult(clickedFilePath)
if (clickedMatchElement) {
const clickedRange = clickedMatchElement.dataset.range;
@@ -171,7 +169,7 @@ class ResultsView {
this.confirmResult();
}
- this.renderResults();
+ etch.update(this);
}
selectFirstResult() {
@@ -190,16 +188,14 @@ class ResultsView {
}
moveToBottom() {
- return this.renderResults(Infinity).then(() => {
- this.selectedResultIndex = this.searchResults.length - 1;
+ this.selectedResultIndex = this.model.getPathCount() - 1;
if (this.collapsedResultIndices[this.selectedResultIndex]) {
this.selectedMatchIndex = -1;
} else {
- this.selectedMatchIndex = this.searchResults[this.selectedResultIndex].matches.length - 1;
+ this.selectedMatchIndex = this.model.getResultAt(this.selectedResultIndex).matches.length - 1;
}
this.element.scrollTop = this.element.scrollHeight - this.element.offsetHeight;
etch.update(this);
- })
}
pageUp() {
@@ -225,14 +221,12 @@ class ResultsView {
const position = this.positionOfSelectedResult(pathDetailsHeight, matchHeight);
const newScrollTop = this.element.scrollTop + this.element.offsetHeight;
- return this.renderResults(newScrollTop).then(() => {
this.element.scrollTop = newScrollTop;
this.selectResultAtPosition(
position + this.element.offsetHeight,
pathDetailsHeight,
matchHeight
);
- });
}
positionOfSelectedResult(pathDetailsHeight, matchHeight) {
@@ -240,7 +234,7 @@ class ResultsView {
for (let i = 0; i < this.selectedResultIndex; i++) {
result += pathDetailsHeight;
if (!this.collapsedResultIndices[i]) {
- result += this.searchResults[i].matches.length * matchHeight
+ result += this.model.getResultAt(i).matches.length * matchHeight
}
}
@@ -252,12 +246,12 @@ class ResultsView {
}
selectResultAtPosition(position, pathDetailsHeight, matchHeight) {
- this.selectedResultIndex = this.searchResults.length - 1;
+ this.selectedResultIndex = this.model.getPathCount() - 1;
this.selectedMatchIndex = this.collapsedResultIndices[this.selectedResultIndex] ?
- -1 : this.searchResults[this.selectedResultIndex].matches.length - 1;
+ -1 : this.getResultAt(this.selectedResultIndex).matches.length - 1;
let top = 0, bottom = 0;
- for (let i = 0; i < this.searchResults.length; i++) {
+ for (let i = 0; i < this.model.getPathCount(); i++) {
bottom = top + pathDetailsHeight;
if (bottom > position) {
this.selectedResultIndex = i;
@@ -268,7 +262,7 @@ class ResultsView {
top = bottom;
if (!this.collapsedResultIndices[i]) {
- const matchesBottom = top + this.searchResults[i].matches.length * matchHeight;
+ const matchesBottom = top + this.model.getResultAt(i).matches.length * matchHeight;
if (matchesBottom > position) {
this.selectedResultIndex = i;
this.selectedMatchIndex = Math.round((position - top) / matchHeight);
@@ -299,16 +293,16 @@ class ResultsView {
}
this.userMovedSelection = true;
- const selectedResult = this.searchResults[this.selectedResultIndex];
+ const selectedResult = this.model.getResultAt(this.selectedResultIndex);
if (this.selectedMatchIndex < selectedResult.matches.length - 1 &&
!this.collapsedResultIndices[this.selectedResultIndex]) {
this.selectedMatchIndex++;
- } else if (this.selectedResultIndex < this.searchResults.length - 1) {
+ } else if (this.selectedResultIndex < this.model.getPathCount() - 1) {
this.selectedResultIndex++;
this.selectedMatchIndex = -1;
}
- this.renderResults().then(this.scrollToSelectedMatch.bind(this));
+ this.scrollToSelectedMatch();
}
moveUp() {
@@ -322,7 +316,7 @@ class ResultsView {
this.selectedMatchIndex--;
} else if (this.selectedResultIndex > 0) {
this.selectedResultIndex--;
- const selectedResult = this.searchResults[this.selectedResultIndex];
+ const selectedResult = this.model.getResultAt(this.selectedResultIndex);
if (this.collapsedResultIndices[this.selectedResultIndex]) {
this.selectedMatchIndex = -1;
} else {
@@ -337,7 +331,6 @@ class ResultsView {
if (this.selectedResultIndex !== -1 && this.selectedMatchIndex == -1) {
this.collapsedResultIndices[this.selectedResultIndex] = false;
this.selectedMatchIndex = 0;
- this.renderResults();
}
}
@@ -345,14 +338,13 @@ class ResultsView {
if (this.selectedResultIndex !== -1) {
this.collapsedResultIndices[this.selectedResultIndex] = true;
this.selectedMatchIndex = -1;
- this.renderResults();
}
}
confirmResult({pending} = {}) {
if (this.selectedResultIndex !== -1) {
if (this.selectedMatchIndex !== -1) {
- const result = this.searchResults[this.selectedResultIndex];
+ const result = this.model.getResultAt(this.selectedResultIndex);
const match = result.matches[this.selectedMatchIndex];
atom.workspace
.open(result.filePath, {
@@ -364,7 +356,6 @@ class ResultsView {
});
} else {
this.collapsedResultIndices[this.selectedResultIndex] = !this.collapsedResultIndices[this.selectedResultIndex];
- this.renderResults();
}
}
}
@@ -372,7 +363,7 @@ class ResultsView {
copyResult() {
if (this.selectedResultIndex !== -1) {
if (this.selectedMatchIndex !== -1) {
- const result = this.searchResults[this.selectedResultIndex];
+ const result = this.model.getResultAt(this.selectedResultIndex);
const match = result.matches[this.selectedMatchIndex];
atom.clipboard.write(match.lineText);
}
@@ -380,7 +371,7 @@ class ResultsView {
}
copyPath() {
- const {filePath} = this.searchResults[this.selectedResultIndex];
+ const {filePath} = this.model.getResultAt(this.selectedResultIndex);
let [rootPath, relativePath] = atom.project.relativizePath(filePath);
if (rootPath && atom.project.getDirectories().length > 1) {
relativePath = path.join(path.basename(rootPath), relativePath);
@@ -389,15 +380,17 @@ class ResultsView {
}
expandAllResults() {
- this.collapsedResultIndices = new Array(this.searchResults.length);
+ this.collapsedResultIndices = new Array(this.model.getPathCount());
this.collapsedResultIndices.fill(false);
- this.renderResults(Infinity);
+ this.element.scrollTop = 0
+ etch.update(this)
}
collapseAllResults() {
this.collapsedResultIndices = new Array(this.model.getPaths().length);
this.collapsedResultIndices.fill(true);
- this.renderResults(Infinity);
+ this.element.scrollTop = 0
+ etch.update(this)
}
scrollToSelectedMatch() {
@@ -424,7 +417,6 @@ class ResultsView {
}
scrollToBottom() {
- this.renderResults(Infinity);
this.element.scrollTop = this.element.scrollHeight;
}

MODIFY
package.json
package.json
"atom": "*"
},
"dependencies": {
+ "element-resize-detector": "^1.1.10",
"etch": "^0.8.0",
"fs-plus": "2.x",
"temp": "^0.8.3",

Start work on partial rendering

Signed-off-by: Max Brunsfeld <maxbrunsfeld@github.com>