diff --git a/package.json b/package.json index af93f8847..657ae4bda 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,8 @@ "react": "~16.9.0", "react-dom": "~16.9.0", "react-textarea-autosize": "^7.1.2", + "react-virtualized-auto-sizer": "^1.0.2", + "react-window": "^1.8.5", "typestyle": "^2.0.1" }, "devDependencies": { @@ -86,6 +88,8 @@ "@types/react": "~16.8.13", "@types/react-dom": "~16.0.5", "@types/react-textarea-autosize": "^4.3.5", + "@types/react-virtualized-auto-sizer": "^1.0.0", + "@types/react-window": "^1.8.2", "@typescript-eslint/eslint-plugin": "^2.25.0", "@typescript-eslint/parser": "^2.25.0", "all-contributors-cli": "^6.14.0", diff --git a/src/components/FileItem.tsx b/src/components/FileItem.tsx index 13188f8c5..40d85f8f7 100644 --- a/src/components/FileItem.tsx +++ b/src/components/FileItem.tsx @@ -34,6 +34,7 @@ export interface IFileItemProps { onDoubleClick: () => void; selected?: boolean; selectFile?: (file: Git.IStatusFile | null) => void; + style: React.CSSProperties; } export interface IGitMarkBoxProps { @@ -92,6 +93,7 @@ export class FileItem extends React.Component { }) } onDoubleClick={this.props.onDoubleClick} + style={this.props.style} title={`${this.props.file.to} ● ${status}`} > {this.props.markBox && ( diff --git a/src/components/FileList.tsx b/src/components/FileList.tsx index 84c275863..83e5df423 100644 --- a/src/components/FileList.tsx +++ b/src/components/FileList.tsx @@ -3,6 +3,7 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { CommandRegistry } from '@lumino/commands'; import { Menu } from '@lumino/widgets'; import * as React from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; import { CommandIDs } from '../commandsAndMenu'; import { GitExtension } from '../model'; import { hiddenButtonStyle } from '../style/ActionButtonStyle'; @@ -227,7 +228,9 @@ export class FileList extends React.Component { if (this.props.settings.composite['simpleStaging']) { return (
- {this._renderSimpleStage(this.props.files)} + + {({ height }) => this._renderSimpleStage(this.props.files, height)} +
); } else { @@ -267,9 +270,15 @@ export class FileList extends React.Component { className={fileListWrapperClass} onContextMenu={event => event.preventDefault()} > - {this._renderStaged(stagedFiles)} - {this._renderChanged(unstagedFiles)} - {this._renderUntracked(untrackedFiles)} + + {({ height }) => ( + <> + {this._renderStaged(stagedFiles, height)} + {this._renderChanged(unstagedFiles, height)} + {this._renderUntracked(untrackedFiles, height)} + + )} + ); } @@ -289,9 +298,57 @@ export class FileList extends React.Component { ); } - private _renderStaged(files: Git.IStatusFile[]) { + private _renderStaged(files: Git.IStatusFile[], height: number) { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; + + const renderStagedRow = ( + file: Git.IStatusFile, + style: React.CSSProperties + ) => { + const openFile = () => { + this.props.commands.execute(CommandIDs.gitFileOpen, file as any); + }; + const diffButton = this._createDiffButton(file); + return ( + + + {diffButton} + { + this.resetStagedFile(file.to); + }} + /> + + } + file={file} + contextMenu={this.openContextMenu} + model={this.props.model} + selected={this._isSelectedFile(file)} + selectFile={this.updateSelectedFile} + onDoubleClick={ + doubleClickDiff + ? diffButton + ? () => this._openDiffView(file) + : () => undefined + : openFile + } + style={style} + /> + ); + }; + return ( { /> } collapsible + files={files} heading={'Staged'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - const openFile = () => { - this.props.commands.execute(CommandIDs.gitFileOpen, file as any); - }; - const diffButton = this._createDiffButton(file); - return ( - - - {diffButton} - { - this.resetStagedFile(file.to); - }} - /> - - } - file={file} - contextMenu={this.openContextMenu} - model={this.props.model} - selected={this._isSelectedFile(file)} - selectFile={this.updateSelectedFile} - onDoubleClick={ - doubleClickDiff - ? diffButton - ? () => this._openDiffView(file) - : () => undefined - : openFile - } - /> - ); - })} - + height={height} + rowRenderer={renderStagedRow} + /> ); } - private _renderChanged(files: Git.IStatusFile[]) { + private _renderChanged(files: Git.IStatusFile[], height: number) { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; const disabled = files.length === 0; + + const renderChangedRow = ( + file: Git.IStatusFile, + style: React.CSSProperties + ) => { + const openFile = () => { + this.props.commands.execute(CommandIDs.gitFileOpen, file as any); + }; + const diffButton = this._createDiffButton(file); + return ( + + + {diffButton} + { + this.discardChanges(file); + }} + /> + { + this.addFile(file.to); + }} + /> + + } + file={file} + contextMenu={this.openContextMenu} + model={this.props.model} + selected={this._isSelectedFile(file)} + selectFile={this.updateSelectedFile} + onDoubleClick={ + doubleClickDiff + ? diffButton + ? () => this._openDiffView(file) + : () => undefined + : openFile + } + style={style} + /> + ); + }; return ( { } collapsible heading={'Changed'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - const openFile = () => { - this.props.commands.execute(CommandIDs.gitFileOpen, file as any); - }; - const diffButton = this._createDiffButton(file); - return ( - - - {diffButton} - { - this.discardChanges(file); - }} - /> - { - this.addFile(file.to); - }} - /> - - } - file={file} - contextMenu={this.openContextMenu} - model={this.props.model} - selected={this._isSelectedFile(file)} - selectFile={this.updateSelectedFile} - onDoubleClick={ - doubleClickDiff - ? diffButton - ? () => this._openDiffView(file) - : () => undefined - : openFile - } - /> - ); - })} - + height={height} + files={files} + rowRenderer={renderChangedRow} + /> ); } - private _renderUntracked(files: Git.IStatusFile[]) { + private _renderUntracked(files: Git.IStatusFile[], height: number) { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; + + const renderUntrackedRow = ( + file: Git.IStatusFile, + style: React.CSSProperties + ) => { + return ( + + { + this.props.commands.execute( + CommandIDs.gitFileOpen, + file as any + ); + }} + /> + { + this.addFile(file.to); + }} + /> + + } + file={file} + contextMenu={this.openContextMenu} + model={this.props.model} + onDoubleClick={() => { + if (!doubleClickDiff) { + this.props.commands.execute(CommandIDs.gitFileOpen, file as any); + } + }} + selected={this._isSelectedFile(file)} + selectFile={this.updateSelectedFile} + style={style} + /> + ); + }; + return ( { } collapsible heading={'Untracked'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - return ( - - { - this.props.commands.execute( - CommandIDs.gitFileOpen, - file as any - ); - }} - /> - { - this.addFile(file.to); - }} - /> - - } - file={file} - contextMenu={this.openContextMenu} - model={this.props.model} - onDoubleClick={() => { - if (!doubleClickDiff) { - this.props.commands.execute( - CommandIDs.gitFileOpen, - file as any - ); - } - }} - selected={this._isSelectedFile(file)} - selectFile={this.updateSelectedFile} - /> - ); - })} - + height={height} + files={files} + rowRenderer={renderUntrackedRow} + /> ); } - private _renderSimpleStage(files: Git.IStatusFile[]) { + private _renderSimpleStage(files: Git.IStatusFile[], height: number) { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; + + const renderSimpleStageRow = ( + file: Git.IStatusFile, + style: React.CSSProperties + ) => { + const openFile = () => { + this.props.commands.execute(CommandIDs.gitFileOpen, file as any); + }; + + // Default value for actions and double click + let actions: JSX.Element = ( + + ); + let onDoubleClick = doubleClickDiff ? (): void => undefined : openFile; + + if (file.status === 'unstaged' || file.status === 'partially-staged') { + const diffButton = this._createDiffButton(file); + actions = ( + + + {diffButton} + { + this.discardChanges(file); + }} + /> + + ); + onDoubleClick = doubleClickDiff + ? diffButton + ? () => this._openDiffView(file) + : () => undefined + : openFile; + } else if (file.status === 'staged') { + const diffButton = this._createDiffButton(file); + actions = ( + + + {diffButton} + { + this.discardChanges(file); + }} + /> + + ); + onDoubleClick = doubleClickDiff + ? diffButton + ? () => this._openDiffView(file) + : () => undefined + : openFile; + } + + return ( + + ); + }; + return ( { /> } heading={'Changed'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - const openFile = () => { - this.props.commands.execute(CommandIDs.gitFileOpen, file as any); - }; - - // Default value for actions and double click - let actions: JSX.Element = ( - - ); - let onDoubleClick = doubleClickDiff - ? (): void => undefined - : openFile; - - if ( - file.status === 'unstaged' || - file.status === 'partially-staged' - ) { - const diffButton = this._createDiffButton(file); - actions = ( - - - {diffButton} - { - this.discardChanges(file); - }} - /> - - ); - onDoubleClick = doubleClickDiff - ? diffButton - ? () => this._openDiffView(file) - : () => undefined - : openFile; - } else if (file.status === 'staged') { - const diffButton = this._createDiffButton(file); - actions = ( - - - {diffButton} - { - this.discardChanges(file); - }} - /> - - ); - onDoubleClick = doubleClickDiff - ? diffButton - ? () => this._openDiffView(file) - : () => undefined - : openFile; - } - - return ( - - ); - })} - + height={height} + files={files} + rowRenderer={renderSimpleStageRow} + /> ); } diff --git a/src/components/GitStage.tsx b/src/components/GitStage.tsx index 2daaff627..67e38a18f 100644 --- a/src/components/GitStage.tsx +++ b/src/components/GitStage.tsx @@ -1,5 +1,6 @@ import { caretDownIcon, caretRightIcon } from '@jupyterlab/ui-components'; import * as React from 'react'; +import { FixedSizeList } from 'react-window'; import { changeStageButtonStyle, sectionAreaStyle, @@ -7,6 +8,10 @@ import { sectionHeaderLabelStyle, sectionHeaderSizeStyle } from '../style/GitStageStyle'; +import { Git } from '../tokens'; + +const HEADER_HEIGHT = 34; +const ITEM_HEIGHT = 25; /** * Git stage component properties @@ -20,14 +25,22 @@ export interface IGitStageProps { * Is this group collapsible */ collapsible?: boolean; + /** + * Files in the group + */ + files: Git.IStatusFile[]; /** * Group title */ heading: string; + height: number; /** - * Number of files in the group + * Row renderer */ - nFiles: number; + rowRenderer: ( + file: Git.IStatusFile, + style: React.CSSProperties + ) => JSX.Element; } /** @@ -38,9 +51,21 @@ export interface IGitStageState { } export const GitStage: React.FunctionComponent = ( - props: React.PropsWithChildren + props: IGitStageProps ) => { const [showFiles, setShowFiles] = React.useState(true); + const nFiles = props.files.length; + + const _renderList = ({ + index, + style + }: { + index: number; + style: React.CSSProperties; + }) => { + const file = props.files[index]; + return props.rowRenderer(file, style); + }; return (
@@ -49,12 +74,12 @@ export const GitStage: React.FunctionComponent = (
- {showFiles && ( -
    {props.children}
+ {showFiles && nFiles > 0 && ( + data[index].to} + itemSize={ITEM_HEIGHT} + width={'100%'} + style={{ overflowX: 'visible' }} + > + {_renderList} + )} ); diff --git a/src/style/FileItemStyle.ts b/src/style/FileItemStyle.ts index 38c05f058..ecd697e95 100644 --- a/src/style/FileItemStyle.ts +++ b/src/style/FileItemStyle.ts @@ -6,6 +6,7 @@ export const fileStyle = style( display: 'flex', flexDirection: 'row', alignItems: 'center', + boxSizing: 'border-box', color: 'var(--jp-ui-font-color1)', lineHeight: 'var(--jp-private-running-item-height)', padding: '0px 4px', diff --git a/src/style/FileListStyle.ts b/src/style/FileListStyle.ts index 5155617ac..e80df39b8 100644 --- a/src/style/FileListStyle.ts +++ b/src/style/FileListStyle.ts @@ -1,7 +1,7 @@ import { style } from 'typestyle'; export const fileListWrapperClass = style({ - height: 'auto', + height: 'inherit', minHeight: '150px', overflow: 'hidden', diff --git a/yarn.lock b/yarn.lock index 8a1c722b1..baa93f9c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1822,6 +1822,20 @@ dependencies: "@types/react" "*" +"@types/react-virtualized-auto-sizer@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.0.tgz#fc32f30a8dab527b5816f3a757e1e1d040c8f272" + integrity sha512-NMErdIdSnm2j/7IqMteRiRvRulpjoELnXWUwdbucYCz84xG9PHcoOrr7QfXwB/ku7wd6egiKFrzt/+QK4Imeeg== + dependencies: + "@types/react" "*" + +"@types/react-window@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.2.tgz#a5a6b2762ce73ffaab7911ee1397cf645f2459fe" + integrity sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@~16.8.13", "@types/react@~16.8.4", "@types/react@~16.9.16": version "16.8.25" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.25.tgz#0247613ab58b1b11ba10fed662e1947c5f2bb89c" @@ -5150,6 +5164,11 @@ matcher@^1.0.0: dependencies: escape-string-regexp "^1.0.4" +"memoize-one@>=3.1.1 <6": + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -6040,6 +6059,19 @@ react-transition-group@^4.4.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-virtualized-auto-sizer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd" + integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg== + +react-window@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1" + integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@~16.9.0: version "16.9.0" resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"