'use strict' ; import * as path from 'path' ; import * as fs from 'fs' ; import { TreeDataProvider , TreeItem , TreeItemCollapsibleState , Uri , window , Event , EventEmitter , Disposable , commands } from 'vscode' ; import { GitService , GitCommittedFile } from './gitService' ; import { Model , FilesViewContext } from './model' ; import { Icons , getIconUri } from './icons' ; const rootFolderIcon = { dark : getIconUri ( 'structure' , 'dark' ) , light : getIconUri ( 'structure' , 'light' ) } ; class InfoItem extends TreeItem { constructor ( content , label ) { super ( label ) ; this . command = { title : '' , command : 'githd.openCommitInfo' , arguments : [ content ] } ; this . iconPath = getIconUri ( 'info' , '' ) ; } ; readonly parent = null ; } class CommittedFileItem extends TreeItem { constructor ( readonly parent , readonly file , label ) { super ( label ) ; this . command = { title : '' , command : 'githd.openCommittedFile' , arguments : [ this . file ] } ; if ( this . file . status ) { this . iconPath = { light : this . _getIconPath ( 'light' ) , dark : this . _getIconPath ( 'dark' ) } ; } } ; private _getIconPath ( theme ) { switch ( this . file . status [ 0 ] . toUpperCase ( ) ) { case 'M' : return Icons [ theme ] . Modified ; case 'A' : return Icons [ theme ] . Added ; case 'D' : return Icons [ theme ] . Deleted ; case 'R' : return Icons [ theme ] . Renamed ; case 'C' : return Icons [ theme ] . Copied ; default : return void 0 ; } } } class FolderItem extends TreeItem { private _subFolders : FolderItem [ ] = [ ] ; private _files : CommittedFileItem [ ] = [ ] ; private _infoItem : InfoItem ; constructor ( private _parent , private _gitRelativePath , label , iconPath ? ) { super ( label ) ; this . contextValue = 'folder' ; this . iconPath = iconPath ; this . collapsibleState = TreeItemCollapsibleState . Expanded ; } readonly parent = this . _parent ; readonly gitRelativePath : string = this . _gitRelativePath ; get subFolders ( ) : FolderItem [ ] { return this . _subFolders ; } set subFolders ( value ) { this . _subFolders = value ; } get files ( ) : CommittedFileItem [ ] { return this . _files ; } set files ( value ) { this . _files = value ; } get infoItem ( ) : InfoItem { return this . _infoItem ; } set infoItem ( value ) { this . _infoItem = value ; } } function getFormattedLabel ( relativePath ) { const name = path . basename ( relativePath ) ; let dir = path . dirname ( relativePath ) ; if ( dir === '.' ) { dir = '' ; } return name + ' \u00a0\u2022\u00a0 ' + dir ; } function createCommittedFileItem ( rootFolder , file ) { return new CommittedFileItem ( rootFolder , file , getFormattedLabel ( file . gitRelativePath ) ) ; } function buildOneFileWithFolder ( rootFolder , file , relativePath = '' ) { const segments = relativePath ? path . relative ( relativePath , file . gitRelativePath ) . split ( /\\|\// ) : file . gitRelativePath . split ( '/' ) ; let gitRelativePath = relativePath ; let parent = rootFolder ; let i = 0 ; for ( ; i < segments . length - 1 ; ++ i ) { gitRelativePath += segments [ i ] + '/' ; let folder = parent . subFolders . find ( item => { return item . label === segments [ i ] ; } ) ; if ( ! folder ) { folder = new FolderItem ( parent , gitRelativePath , segments [ i ] ) ; parent . subFolders . push ( folder ) ; } parent = folder ; } parent . files . push ( new CommittedFileItem ( parent , file , segments [ i ] ) ) ; } function buildFileTree ( rootFolder , files , withFolder ) { if ( withFolder ) { files . forEach ( file => buildOneFileWithFolder ( rootFolder , file ) ) ; } else { rootFolder . files . push ( ... ( files . map ( file => { return createCommittedFileItem ( rootFolder , file ) ; } ) ) ) ; } } function buildFilesWithoutFolder ( rootFolder , folder ) { rootFolder . files . push ( ... ( folder . files . map ( item => { item . label = getFormattedLabel ( path . relative ( rootFolder . gitRelativePath , item . file . gitRelativePath ) . replace ( /\\/g , '/' ) ) ; return item ; } ) ) ) ; folder . subFolders . forEach ( f => buildFilesWithoutFolder ( rootFolder , f ) ) ; folder . files = [ ] ; folder . subFolders = [ ] ; } function buildFilesWithFolder ( rootFolder ) { rootFolder . subFolders . forEach ( folder => buildFilesWithFolder ( folder ) ) ; const files = rootFolder . files ; rootFolder . files = [ ] ; files . forEach ( fileItem => buildOneFileWithFolder ( rootFolder , fileItem . file , rootFolder . gitRelativePath ) ) ; } function setCollapsibleStateOnAll ( rootFolder , state ) { if ( rootFolder ) { rootFolder . collapsibleState = state ; rootFolder . subFolders . forEach ( sub => setCollapsibleStateOnAll ( sub , state ) ) ; } } type CommittedTreeItem = CommittedFileItem | FolderItem | InfoItem ; export class ExplorerViewProvider implements TreeDataProvider < CommittedTreeItem > { private _disposables : Disposable [ ] = [ ] ; private _onDidChange : EventEmitter < CommittedTreeItem > = new EventEmitter < CommittedTreeItem > ( ) ; private _withFolder : boolean ; private _context : FilesViewContext ; private _treeRoot : ( FolderItem | InfoItem ) [ ] = [ ] ; constructor ( model , private _gitService ) { this . _disposables . push ( window . registerTreeDataProvider ( 'committedFiles' , this ) ) ; this . _disposables . push ( commands . registerCommand ( 'githd.showFilesWithFolder' , ( folder ) => this . _showFilesWithFolder ( folder ) ) ) ; this . _disposables . push ( commands . registerCommand ( 'githd.showFilesWithoutFolder' , ( folder ) => this . _showFilesWithoutFolder ( folder ) ) ) ; this . _disposables . push ( commands . registerCommand ( 'githd.collapseFolder' , ( folder ) => this . _setCollapsibleStateOnAll ( folder , TreeItemCollapsibleState . Collapsed ) ) ) ; this . _disposables . push ( commands . registerCommand ( 'githd.expandFolder' , ( folder ) => this . _setCollapsibleStateOnAll ( folder , TreeItemCollapsibleState . Expanded ) ) ) ; this . _disposables . push ( commands . registerCommand ( 'githd.viewFileHistoryFromTree' , ( fileItem ) => model . setHistoryViewContext ( { repo : this . _context . repo , specifiedPath : fileItem . file . fileUri } ) ) ) ; this . _disposables . push ( commands . registerCommand ( 'githd.viewFolderHistoryFromTree' , ( folder ) => model . setHistoryViewContext ( { repo : this . _context . repo , specifiedPath : Uri . file ( path . join ( this . _context . repo . root , folder . gitRelativePath ) ) } ) ) ) ; this . _disposables . push ( this . _onDidChange ) ; model . onDidChangeFilesViewContext ( context => { this . _context = context ; this . _update ( ) ; } , null , this . _disposables ) ; this . _context = model . filesViewContext ; this . _withFolder = model . configuration . withFolder ; this . _update ( ) ; } readonly onDidChangeTreeData : Event < CommittedTreeItem > = this . _onDidChange . event ; dispose ( ) { this . _disposables . forEach ( d => d . dispose ( ) ) ; } getTreeItem ( element ) { return element ; } getChildren ( element ? ) { if ( ! element ) { return this . _treeRoot ; } let folder = element as FolderItem ; if ( folder ) { return [ ] . concat ( folder . subFolders , folder . infoItem , folder . files ) ; } return [ ] ; } getParent ( element ) { return element . parent ; } private get commitOrStashString ( ) : string { return this . _context . isStash ? 'Stash' : 'Commit' ; } private async _update ( ) { this . _treeRoot = [ ] ; if ( ! this . _context ) { return ; } const leftRef = this . _context . leftRef ; const rightRef = this . _context . rightRef ; const specifiedPath = this . _context . specifiedPath ; const lineInfo = this . _context . focusedLineInfo ; if ( ! rightRef ) { this . _onDidChange . fire ( ) ; return ; } const committedFiles = await this . _gitService . getCommittedFiles ( this . _context . repo , leftRef , rightRef , this . _context . isStash ) ; if ( ! leftRef ) { await this . _buildCommitInfo ( rightRef ) ; } if ( ! leftRef && ! specifiedPath ) { this . _buildCommitTree ( committedFiles , rightRef ) ; } else if ( leftRef && ! specifiedPath ) { this . _buildDiffBranchTree ( committedFiles , leftRef , rightRef ) ; } else if ( ! leftRef && specifiedPath ) { await this . _buildPathSpecifiedCommitTree ( committedFiles , specifiedPath , lineInfo , rightRef ) ; } else { await this . _buildPathSpecifiedDiffBranchTree ( committedFiles , this . _context ) ; } this . _onDidChange . fire ( ) ; } private async _buildCommitInfo ( ref ) { await this . _treeRoot . push ( new InfoItem ( await this . _gitService . getCommitDetails ( this . _context . repo , ref , this . _context . isStash ) , `${ this . commitOrStashString } Info` ) ) ; } private _buildCommitTree ( files , ref ) { this . _buildCommitFolder ( `${ this . commitOrStashString } ${ ref } \u00a0 (${ files . length } files changed)` , files ) ; } private _buildDiffBranchTree ( files , leftRef , rightRef ) { this . _buildCommitFolder ( `Diffs between ${ leftRef } and ${ rightRef } \u00a0 (${ files . length } files)` , files ) ; } private async _buildPathSpecifiedCommitTree ( files , specifiedPath , lineInfo , ref ) { await this . _buildFocusFolder ( 'Focus' , files , specifiedPath , lineInfo ) ; this . _buildCommitTree ( files , ref ) ; } private async _buildPathSpecifiedDiffBranchTree ( files , context ) { await this . _buildFocusFolder ( `${ context . leftRef } .. ${ context . rightRef }` , files , context . specifiedPath ) ; } private _buildCommitFolder ( label , committedFiles ) { let folder = new FolderItem ( null , '' , label , rootFolderIcon ) ; buildFileTree ( folder , committedFiles , this . _withFolder ) ; this . _treeRoot . push ( folder ) ; } private async _buildFocusFolder ( label , committedFiles , specifiedPath , lineInfo ? ) { let folder = new FolderItem ( null , '' , label , rootFolderIcon ) ; const relativePath = await this . _gitService . getGitRelativePath ( specifiedPath ) ; if ( fs . lstatSync ( specifiedPath . fsPath ) . isFile ( ) ) { if ( lineInfo ) { folder . infoItem = new InfoItem ( lineInfo , 'line diff' ) ; } let file = committedFiles . find ( value => { return value . gitRelativePath === relativePath ; } ) ; if ( file ) { folder . files . push ( createCommittedFileItem ( folder , file ) ) ; } } else { let focus = [ ] ; committedFiles . forEach ( file => { if ( file . gitRelativePath . search ( relativePath ) === 0 ) { focus . push ( file ) ; } } ) ; buildFileTree ( folder , focus , this . _withFolder ) ; } if ( folder . files . length + folder . subFolders . length > 0 || folder . infoItem ) { this . _treeRoot . push ( folder ) ; } } private _showFilesWithFolder ( parent ) { if ( ! parent ) { this . _withFolder = true ; this . _update ( ) ; } else { buildFilesWithFolder ( parent ) ; this . _onDidChange . fire ( parent ) ; } } private _showFilesWithoutFolder ( parent ) { if ( ! parent ) { this . _withFolder = false ; this . _update ( ) ; } else { parent . subFolders . forEach ( folder => buildFilesWithoutFolder ( parent , folder ) ) ; parent . subFolders = [ ] ; this . _onDidChange . fire ( parent ) ; } } private _setCollapsibleStateOnAll ( folder , state ) { let parent ; if ( ! folder ) { this . _treeRoot . forEach ( sub => { if ( sub instanceof FolderItem ) { setCollapsibleStateOnAll ( sub , state ) ; } } ) ; } else { parent = folder . parent ; folder . collapsibleState = state ; folder . subFolders . forEach ( sub => setCollapsibleStateOnAll ( sub , state ) ) ; } if ( parent ) { const temp = parent . subFolders ; parent . subFolders = [ ] ; this . _onDidChange . fire ( parent ) ; setTimeout ( ( ) => { parent . subFolders = temp ; this . _onDidChange . fire ( parent ) ; } , 250 ) ; } else { const root = this . _treeRoot ; this . _treeRoot = null ; this . _onDidChange . fire ( ) ; setTimeout ( ( ) => { this . _treeRoot = root ; this . _onDidChange . fire ( ) ; } , 250 ) ; } } }