Closes #354 - Adds line history explorer (wip) Closes #456 - Adds repository status to the repository nodesmain
@ -0,0 +1,5 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg"> | |||||
<path fill="#C5C5C5" d="m6,12l-1,0l0,-1l1,0l0,1l0,0zm0,-3l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm8,-1l0,12c0,0.55 -0.45,1 -1,1l-5,0l0,2l-1.5,-1.5l-1.5,1.5l0,-2l-2,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l10,0c0.55,0 1,0.45 1,1l0,0zm-1,10l-10,0l0,2l2,0l0,-1l3,0l0,1l5,0l0,-2l0,0zm0,-10l-9,0l0,9l9,0l0,-9l0,0z" /> | |||||
<ellipse fill="#0366d6" stroke="#C5C5C5" stroke-width="0.5" rx="3" ry="3" cx="13" cy="4" /> | |||||
</svg> |
@ -0,0 +1,5 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg"> | |||||
<path fill="#424242" d="m6,12l-1,0l0,-1l1,0l0,1l0,0zm0,-3l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm8,-1l0,12c0,0.55 -0.45,1 -1,1l-5,0l0,2l-1.5,-1.5l-1.5,1.5l0,-2l-2,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l10,0c0.55,0 1,0.45 1,1l0,0zm-1,10l-10,0l0,2l2,0l0,-1l3,0l0,1l5,0l0,-2l0,0zm0,-10l-9,0l0,9l9,0l0,-9l0,0z" /> | |||||
<ellipse fill="#0366d6" stroke="#424242" stroke-width="0.5" rx="3" ry="3" cx="13" cy="4" /> | |||||
</svg> |
@ -0,0 +1,33 @@ | |||||
'use strict'; | |||||
import { Container } from '../container'; | |||||
import { Command, CommandContext, Commands } from './common'; | |||||
export class ShowExplorerCommand extends Command { | |||||
constructor() { | |||||
super([ | |||||
Commands.ShowGitExplorer, | |||||
Commands.ShowFileHistoryExplorer, | |||||
Commands.ShowLineHistoryExplorer, | |||||
Commands.ShowResultsExplorer | |||||
]); | |||||
} | |||||
protected async preExecute(context: CommandContext): Promise<any> { | |||||
return this.execute(context.command as Commands); | |||||
} | |||||
execute(command: Commands) { | |||||
switch (command) { | |||||
case Commands.ShowGitExplorer: | |||||
return Container.gitExplorer.show(); | |||||
case Commands.ShowFileHistoryExplorer: | |||||
return Container.fileHistoryExplorer.show(); | |||||
case Commands.ShowLineHistoryExplorer: | |||||
return Container.lineHistoryExplorer.show(); | |||||
case Commands.ShowResultsExplorer: | |||||
return Container.resultsExplorer.show(); | |||||
} | |||||
return undefined; | |||||
} | |||||
} |
@ -1,13 +0,0 @@ | |||||
'use strict'; | |||||
import { Container } from '../container'; | |||||
import { Command, Commands } from './common'; | |||||
export class ShowGitExplorerCommand extends Command { | |||||
constructor() { | |||||
super(Commands.ShowGitExplorer); | |||||
} | |||||
execute() { | |||||
return Container.gitExplorer.show(); | |||||
} | |||||
} |
@ -1,13 +0,0 @@ | |||||
'use strict'; | |||||
import { Container } from '../container'; | |||||
import { Command, Commands } from './common'; | |||||
export class ShowHistoryExplorerCommand extends Command { | |||||
constructor() { | |||||
super(Commands.ShowHistoryExplorer); | |||||
} | |||||
execute() { | |||||
return Container.historyExplorer.show(); | |||||
} | |||||
} |
@ -1,13 +0,0 @@ | |||||
'use strict'; | |||||
import { Container } from '../container'; | |||||
import { Command, Commands } from './common'; | |||||
export class ShowResultsExplorerCommand extends Command { | |||||
constructor() { | |||||
super(Commands.ShowResultsExplorer); | |||||
} | |||||
execute() { | |||||
return Container.resultsExplorer.show(); | |||||
} | |||||
} |
@ -0,0 +1,80 @@ | |||||
'use strict'; | |||||
import { commands, ConfigurationChangeEvent } from 'vscode'; | |||||
import { configuration, IExplorersConfig, IFileHistoryExplorerConfig } from '../configuration'; | |||||
import { CommandContext, setCommandContext } from '../constants'; | |||||
import { Container } from '../container'; | |||||
import { ExplorerBase, RefreshReason } from './explorer'; | |||||
import { RefreshNodeCommandArgs } from './explorerCommands'; | |||||
import { ActiveFileHistoryNode, ExplorerNode } from './nodes'; | |||||
export class FileHistoryExplorer extends ExplorerBase<ActiveFileHistoryNode> { | |||||
constructor() { | |||||
super('gitlens.fileHistoryExplorer'); | |||||
} | |||||
getRoot() { | |||||
return new ActiveFileHistoryNode(this); | |||||
} | |||||
protected registerCommands() { | |||||
Container.explorerCommands; | |||||
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this); | |||||
commands.registerCommand( | |||||
this.getQualifiedCommand('refreshNode'), | |||||
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args), | |||||
this | |||||
); | |||||
commands.registerCommand( | |||||
this.getQualifiedCommand('setRenameFollowingOn'), | |||||
() => this.setRenameFollowing(true), | |||||
this | |||||
); | |||||
commands.registerCommand( | |||||
this.getQualifiedCommand('setRenameFollowingOff'), | |||||
() => this.setRenameFollowing(false), | |||||
this | |||||
); | |||||
} | |||||
protected onConfigurationChanged(e: ConfigurationChangeEvent) { | |||||
const initializing = configuration.initializing(e); | |||||
if ( | |||||
!initializing && | |||||
!configuration.changed(e, configuration.name('fileHistoryExplorer').value) && | |||||
!configuration.changed(e, configuration.name('explorers').value) && | |||||
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) && | |||||
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value) | |||||
) { | |||||
return; | |||||
} | |||||
if ( | |||||
initializing || | |||||
configuration.changed(e, configuration.name('fileHistoryExplorer')('enabled').value) || | |||||
configuration.changed(e, configuration.name('fileHistoryExplorer')('location').value) | |||||
) { | |||||
setCommandContext(CommandContext.FileHistoryExplorer, this.config.enabled ? this.config.location : false); | |||||
} | |||||
if (initializing || configuration.changed(e, configuration.name('fileHistoryExplorer')('location').value)) { | |||||
this.initialize(this.config.location); | |||||
} | |||||
if (!initializing && this._root !== undefined) { | |||||
void this.refresh(RefreshReason.ConfigurationChanged); | |||||
} | |||||
} | |||||
get config(): IExplorersConfig & IFileHistoryExplorerConfig { | |||||
return { ...Container.config.explorers, ...Container.config.fileHistoryExplorer }; | |||||
} | |||||
private setRenameFollowing(enabled: boolean) { | |||||
return configuration.updateEffective( | |||||
configuration.name('advanced')('fileHistoryFollowsRenames').value, | |||||
enabled | |||||
); | |||||
} | |||||
} |
@ -1,251 +0,0 @@ | |||||
'use strict'; | |||||
import * as path from 'path'; | |||||
import { | |||||
commands, | |||||
ConfigurationChangeEvent, | |||||
Disposable, | |||||
Event, | |||||
EventEmitter, | |||||
TextEditor, | |||||
TreeDataProvider, | |||||
TreeItem, | |||||
TreeView, | |||||
Uri, | |||||
window | |||||
} from 'vscode'; | |||||
import { UriComparer } from '../comparers'; | |||||
import { configuration, IExplorersConfig, IHistoryExplorerConfig } from '../configuration'; | |||||
import { CommandContext, GlyphChars, setCommandContext } from '../constants'; | |||||
import { Container } from '../container'; | |||||
import { GitUri } from '../git/gitUri'; | |||||
import { Logger } from '../logger'; | |||||
import { Functions } from '../system'; | |||||
import { RefreshNodeCommandArgs } from '../views/explorerCommands'; | |||||
import { ExplorerNode, HistoryNode, MessageNode, RefreshReason } from './nodes'; | |||||
export * from './nodes'; | |||||
export class HistoryExplorer implements TreeDataProvider<ExplorerNode>, Disposable { | |||||
private _disposable: Disposable | undefined; | |||||
private _root?: ExplorerNode; | |||||
private _tree: TreeView<ExplorerNode> | undefined; | |||||
private _onDidChangeTreeData = new EventEmitter<ExplorerNode>(); | |||||
public get onDidChangeTreeData(): Event<ExplorerNode> { | |||||
return this._onDidChangeTreeData.event; | |||||
} | |||||
constructor() { | |||||
Container.explorerCommands; | |||||
commands.registerCommand('gitlens.historyExplorer.refresh', this.refresh, this); | |||||
commands.registerCommand('gitlens.historyExplorer.refreshNode', this.refreshNode, this); | |||||
commands.registerCommand( | |||||
'gitlens.historyExplorer.setRenameFollowingOn', | |||||
() => this.setRenameFollowing(true), | |||||
this | |||||
); | |||||
commands.registerCommand( | |||||
'gitlens.historyExplorer.setRenameFollowingOff', | |||||
() => this.setRenameFollowing(false), | |||||
this | |||||
); | |||||
Container.context.subscriptions.push( | |||||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this), | |||||
window.onDidChangeVisibleTextEditors(Functions.debounce(this.onVisibleEditorsChanged, 500), this), | |||||
configuration.onDidChange(this.onConfigurationChanged, this) | |||||
); | |||||
void this.onConfigurationChanged(configuration.initializingChangeEvent); | |||||
} | |||||
dispose() { | |||||
this._disposable && this._disposable.dispose(); | |||||
} | |||||
private async onConfigurationChanged(e: ConfigurationChangeEvent) { | |||||
const initializing = configuration.initializing(e); | |||||
if ( | |||||
!initializing && | |||||
!configuration.changed(e, configuration.name('historyExplorer').value) && | |||||
!configuration.changed(e, configuration.name('explorers').value) && | |||||
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) && | |||||
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value) | |||||
) { | |||||
return; | |||||
} | |||||
if ( | |||||
initializing || | |||||
configuration.changed(e, configuration.name('historyExplorer')('enabled').value) || | |||||
configuration.changed(e, configuration.name('historyExplorer')('location').value) | |||||
) { | |||||
setCommandContext(CommandContext.HistoryExplorer, this.config.enabled ? this.config.location : false); | |||||
} | |||||
if (initializing) { | |||||
this.setRoot(await this.getRootNode(window.activeTextEditor)); | |||||
} | |||||
if (initializing || configuration.changed(e, configuration.name('historyExplorer')('location').value)) { | |||||
if (this._disposable) { | |||||
this._disposable.dispose(); | |||||
this._onDidChangeTreeData = new EventEmitter<ExplorerNode>(); | |||||
} | |||||
this._tree = window.createTreeView(`gitlens.historyExplorer:${this.config.location}`, { | |||||
treeDataProvider: this | |||||
}); | |||||
this._disposable = this._tree; | |||||
} | |||||
if (!initializing && this._root !== undefined) { | |||||
void this.refresh(RefreshReason.ConfigurationChanged); | |||||
} | |||||
} | |||||
private async onActiveEditorChanged(editor: TextEditor | undefined) { | |||||
const root = await this.getRootNode(editor); | |||||
if (!this.setRoot(root)) return; | |||||
void this.refresh(RefreshReason.ActiveEditorChanged, root); | |||||
} | |||||
private onVisibleEditorsChanged(editors: TextEditor[]) { | |||||
if (this._root === undefined) return; | |||||
// If we have no visible editors, or no trackable visible editors reset the view | |||||
if (editors.length === 0 || !editors.some(e => e.document && Container.git.isTrackable(e.document.uri))) { | |||||
this.clearRoot(); | |||||
void this.refresh(RefreshReason.VisibleEditorsChanged); | |||||
} | |||||
} | |||||
get config(): IExplorersConfig & IHistoryExplorerConfig { | |||||
return { ...Container.config.explorers, ...Container.config.historyExplorer }; | |||||
} | |||||
getParent(element: ExplorerNode): ExplorerNode | undefined { | |||||
return undefined; | |||||
} | |||||
async getChildren(node?: ExplorerNode): Promise<ExplorerNode[]> { | |||||
if (this._root === undefined) return [new MessageNode(`No active file ${GlyphChars.Dash} no history to show`)]; | |||||
if (node === undefined) return this._root.getChildren(); | |||||
return node.getChildren(); | |||||
} | |||||
async getTreeItem(node: ExplorerNode): Promise<TreeItem> { | |||||
return node.getTreeItem(); | |||||
} | |||||
getQualifiedCommand(command: string) { | |||||
return `gitlens.historyExplorer.${command}`; | |||||
} | |||||
async refresh(reason?: RefreshReason, root?: ExplorerNode) { | |||||
if (reason === undefined) { | |||||
reason = RefreshReason.Command; | |||||
} | |||||
Logger.log(`HistoryExplorer.refresh`, `reason='${reason}'`); | |||||
if (this._root === undefined || root === undefined) { | |||||
this.clearRoot(); | |||||
this.setRoot(await this.getRootNode(window.activeTextEditor)); | |||||
} | |||||
this._onDidChangeTreeData.fire(); | |||||
} | |||||
refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) { | |||||
Logger.log(`HistoryExplorer.refreshNode(${(node as { id?: string }).id || ''})`); | |||||
if (args !== undefined && node.supportsPaging) { | |||||
node.maxCount = args.maxCount; | |||||
} | |||||
node.refresh(); | |||||
// Since a root node won't actually refresh, force everything | |||||
this._onDidChangeTreeData.fire(this._root === node ? undefined : node); | |||||
} | |||||
async show() { | |||||
if (this._root === undefined || this._tree === undefined) return; | |||||
try { | |||||
await this._tree.reveal(this._root, { select: false }); | |||||
} | |||||
catch (ex) { | |||||
Logger.error(ex); | |||||
} | |||||
} | |||||
private clearRoot() { | |||||
if (this._root === undefined) return; | |||||
this._root.dispose(); | |||||
this._root = undefined; | |||||
} | |||||
private async getRootNode(editor: TextEditor | undefined): Promise<ExplorerNode | undefined> { | |||||
// If we have no active editor, or no visible editors, or no trackable visible editors reset the view | |||||
if ( | |||||
editor == null || | |||||
window.visibleTextEditors.length === 0 || | |||||
!window.visibleTextEditors.some(e => e.document && Container.git.isTrackable(e.document.uri)) | |||||
) { | |||||
return undefined; | |||||
} | |||||
// If we do have a visible trackable editor, don't change from the last state (avoids issues when focus switches to the problems/output/debug console panes) | |||||
if (editor.document === undefined || !Container.git.isTrackable(editor.document.uri)) return this._root; | |||||
let gitUri = await GitUri.fromUri(editor.document.uri); | |||||
const repo = await Container.git.getRepository(gitUri); | |||||
if (repo === undefined) return undefined; | |||||
let uri; | |||||
if (gitUri.sha !== undefined) { | |||||
// If we have a sha, normalize the history to the working file (so we get a full history all the time) | |||||
const [fileName, repoPath] = await Container.git.findWorkingFileName( | |||||
gitUri.fsPath, | |||||
gitUri.repoPath, | |||||
gitUri.sha | |||||
); | |||||
if (fileName !== undefined) { | |||||
uri = Uri.file(repoPath !== undefined ? path.join(repoPath, fileName) : fileName); | |||||
} | |||||
} | |||||
if (UriComparer.equals(uri || gitUri, this._root && this._root.uri)) return this._root; | |||||
if (uri !== undefined) { | |||||
gitUri = await GitUri.fromUri(uri); | |||||
} | |||||
return new HistoryNode(gitUri, repo, this); | |||||
} | |||||
private setRenameFollowing(enabled: boolean) { | |||||
return configuration.updateEffective( | |||||
configuration.name('advanced')('fileHistoryFollowsRenames').value, | |||||
enabled | |||||
); | |||||
} | |||||
private setRoot(root: ExplorerNode | undefined): boolean { | |||||
if (this._root === root) return false; | |||||
if (this._root !== undefined) { | |||||
this._root.dispose(); | |||||
} | |||||
this._root = root; | |||||
return true; | |||||
} | |||||
} |
@ -0,0 +1,80 @@ | |||||
'use strict'; | |||||
import { commands, ConfigurationChangeEvent } from 'vscode'; | |||||
import { configuration, IExplorersConfig, ILineHistoryExplorerConfig } from '../configuration'; | |||||
import { CommandContext, setCommandContext } from '../constants'; | |||||
import { Container } from '../container'; | |||||
import { ExplorerBase, RefreshReason } from './explorer'; | |||||
import { RefreshNodeCommandArgs } from './explorerCommands'; | |||||
import { ExplorerNode } from './nodes'; | |||||
import { ActiveLineHistoryNode } from './nodes/activeLineHistoryNode'; | |||||
export class LineHistoryExplorer extends ExplorerBase<ActiveLineHistoryNode> { | |||||
constructor() { | |||||
super('gitlens.lineHistoryExplorer'); | |||||
} | |||||
getRoot() { | |||||
return new ActiveLineHistoryNode(this); | |||||
} | |||||
protected registerCommands() { | |||||
Container.explorerCommands; | |||||
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this); | |||||
commands.registerCommand( | |||||
this.getQualifiedCommand('refreshNode'), | |||||
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args), | |||||
this | |||||
); | |||||
commands.registerCommand( | |||||
this.getQualifiedCommand('setRenameFollowingOn'), | |||||
() => this.setRenameFollowing(true), | |||||
this | |||||
); | |||||
commands.registerCommand( | |||||
this.getQualifiedCommand('setRenameFollowingOff'), | |||||
() => this.setRenameFollowing(false), | |||||
this | |||||
); | |||||
} | |||||
protected onConfigurationChanged(e: ConfigurationChangeEvent) { | |||||
const initializing = configuration.initializing(e); | |||||
if ( | |||||
!initializing && | |||||
!configuration.changed(e, configuration.name('lineHistoryExplorer').value) && | |||||
!configuration.changed(e, configuration.name('explorers').value) && | |||||
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) && | |||||
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value) | |||||
) { | |||||
return; | |||||
} | |||||
if ( | |||||
initializing || | |||||
configuration.changed(e, configuration.name('lineHistoryExplorer')('enabled').value) || | |||||
configuration.changed(e, configuration.name('lineHistoryExplorer')('location').value) | |||||
) { | |||||
setCommandContext(CommandContext.LineHistoryExplorer, this.config.enabled ? this.config.location : false); | |||||
} | |||||
if (initializing || configuration.changed(e, configuration.name('lineHistoryExplorer')('location').value)) { | |||||
this.initialize(this.config.location); | |||||
} | |||||
if (!initializing && this._root !== undefined) { | |||||
void this.refresh(RefreshReason.ConfigurationChanged); | |||||
} | |||||
} | |||||
get config(): IExplorersConfig & ILineHistoryExplorerConfig { | |||||
return { ...Container.config.explorers, ...Container.config.lineHistoryExplorer }; | |||||
} | |||||
private setRenameFollowing(enabled: boolean) { | |||||
return configuration.updateEffective( | |||||
configuration.name('advanced')('fileHistoryFollowsRenames').value, | |||||
enabled | |||||
); | |||||
} | |||||
} |
@ -0,0 +1,108 @@ | |||||
'use strict'; | |||||
import * as path from 'path'; | |||||
import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, Uri, window } from 'vscode'; | |||||
import { UriComparer } from '../../comparers'; | |||||
import { Container } from '../../container'; | |||||
import { GitUri } from '../../git/gitService'; | |||||
import { Functions } from '../../system'; | |||||
import { FileHistoryExplorer } from '../fileHistoryExplorer'; | |||||
import { MessageNode } from './common'; | |||||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode'; | |||||
import { FileHistoryNode } from './fileHistoryNode'; | |||||
export class ActiveFileHistoryNode extends SubscribeableExplorerNode<FileHistoryExplorer> { | |||||
private _child: FileHistoryNode | undefined; | |||||
constructor(explorer: FileHistoryExplorer) { | |||||
super(unknownGitUri, explorer); | |||||
} | |||||
dispose() { | |||||
super.dispose(); | |||||
this.resetChild(); | |||||
} | |||||
resetChild() { | |||||
if (this._child !== undefined) { | |||||
this._child.dispose(); | |||||
this._child = undefined; | |||||
} | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
if (this._child === undefined) { | |||||
if (this.uri === unknownGitUri) { | |||||
return [new MessageNode('There are no editors open that can provide file history')]; | |||||
} | |||||
this._child = new FileHistoryNode(this.uri, this.explorer); | |||||
} | |||||
return [this._child]; | |||||
} | |||||
getTreeItem(): TreeItem { | |||||
const item = new TreeItem('File History', TreeItemCollapsibleState.Expanded); | |||||
item.contextValue = ResourceType.ActiveFileHistory; | |||||
void this.ensureSubscription(); | |||||
return item; | |||||
} | |||||
async refresh() { | |||||
const editor = window.activeTextEditor; | |||||
if (editor == null || !Container.git.isTrackable(editor.document.uri)) { | |||||
if ( | |||||
this.uri === unknownGitUri || | |||||
(Container.git.isTrackable(this.uri) && | |||||
window.visibleTextEditors.some(e => e.document && UriComparer.equals(e.document.uri, this.uri))) | |||||
) { | |||||
return; | |||||
} | |||||
this._uri = unknownGitUri; | |||||
this.resetChild(); | |||||
return; | |||||
} | |||||
if (UriComparer.equals(editor!.document.uri, this.uri)) return; | |||||
let gitUri = await GitUri.fromUri(editor!.document.uri); | |||||
let uri; | |||||
if (gitUri.sha !== undefined) { | |||||
// If we have a sha, normalize the history to the working file (so we get a full history all the time) | |||||
const [fileName, repoPath] = await Container.git.findWorkingFileName( | |||||
gitUri.fsPath, | |||||
gitUri.repoPath, | |||||
gitUri.sha | |||||
); | |||||
if (fileName !== undefined) { | |||||
uri = Uri.file(repoPath !== undefined ? path.join(repoPath, fileName) : fileName); | |||||
} | |||||
} | |||||
if (this.uri !== unknownGitUri && UriComparer.equals(uri || gitUri, this.uri)) return; | |||||
if (uri !== undefined) { | |||||
gitUri = await GitUri.fromUri(uri); | |||||
} | |||||
this._uri = gitUri; | |||||
this.resetChild(); | |||||
} | |||||
protected async subscribe() { | |||||
return Disposable.from( | |||||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this) | |||||
); | |||||
} | |||||
private onActiveEditorChanged(editor: TextEditor | undefined) { | |||||
void this.explorer.refreshNode(this); | |||||
} | |||||
} |
@ -0,0 +1,116 @@ | |||||
'use strict'; | |||||
import { | |||||
Disposable, | |||||
Selection, | |||||
TextEditor, | |||||
TextEditorSelectionChangeEvent, | |||||
TreeItem, | |||||
TreeItemCollapsibleState, | |||||
window | |||||
} from 'vscode'; | |||||
import { UriComparer } from '../../comparers'; | |||||
import { Container } from '../../container'; | |||||
import { GitUri } from '../../git/gitService'; | |||||
import { Functions } from '../../system'; | |||||
import { LineHistoryExplorer } from '../lineHistoryExplorer'; | |||||
import { MessageNode } from './common'; | |||||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode'; | |||||
import { LineHistoryNode } from './lineHistoryNode'; | |||||
export class ActiveLineHistoryNode extends SubscribeableExplorerNode<LineHistoryExplorer> { | |||||
private _child: LineHistoryNode | undefined; | |||||
private _selection: Selection | undefined; | |||||
constructor(explorer: LineHistoryExplorer) { | |||||
super(unknownGitUri, explorer); | |||||
} | |||||
dispose() { | |||||
super.dispose(); | |||||
this.resetChild(); | |||||
} | |||||
resetChild() { | |||||
if (this._child !== undefined) { | |||||
this._child.dispose(); | |||||
this._child = undefined; | |||||
} | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
if (this._child === undefined) { | |||||
if (this.uri === unknownGitUri) { | |||||
return [new MessageNode('There are no editors open that can provide line history')]; | |||||
} | |||||
this._child = new LineHistoryNode(this.uri, this._selection!, this.explorer); | |||||
} | |||||
return [this._child]; | |||||
} | |||||
getTreeItem(): TreeItem { | |||||
const item = new TreeItem('Line History', TreeItemCollapsibleState.Expanded); | |||||
item.contextValue = ResourceType.ActiveLineHistory; | |||||
void this.ensureSubscription(); | |||||
return item; | |||||
} | |||||
async refresh() { | |||||
const editor = window.activeTextEditor; | |||||
if (editor == null || !Container.git.isTrackable(editor.document.uri)) { | |||||
if ( | |||||
this.uri === unknownGitUri || | |||||
(Container.git.isTrackable(this.uri) && | |||||
window.visibleTextEditors.some(e => e.document && UriComparer.equals(e.document.uri, this.uri))) | |||||
) { | |||||
return; | |||||
} | |||||
this._uri = unknownGitUri; | |||||
this._selection = undefined; | |||||
this.resetChild(); | |||||
return; | |||||
} | |||||
if ( | |||||
UriComparer.equals(editor!.document.uri, this.uri) && | |||||
(this._selection !== undefined && editor.selection.isEqual(this._selection)) | |||||
) { | |||||
return; | |||||
} | |||||
const gitUri = await GitUri.fromUri(editor!.document.uri); | |||||
if ( | |||||
this.uri !== unknownGitUri && | |||||
UriComparer.equals(gitUri, this.uri) && | |||||
(this._selection !== undefined && editor.selection.isEqual(this._selection)) | |||||
) { | |||||
return; | |||||
} | |||||
this._uri = gitUri; | |||||
this._selection = editor.selection; | |||||
this.resetChild(); | |||||
} | |||||
protected async subscribe() { | |||||
return Disposable.from( | |||||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this), | |||||
window.onDidChangeTextEditorSelection(Functions.debounce(this.onSelectionChanged, 500), this) | |||||
); | |||||
} | |||||
private onActiveEditorChanged(editor: TextEditor | undefined) { | |||||
void this.explorer.refreshNode(this); | |||||
} | |||||
private onSelectionChanged(e: TextEditorSelectionChangeEvent) { | |||||
void this.explorer.refreshNode(this); | |||||
} | |||||
} |
@ -1,97 +0,0 @@ | |||||
'use strict'; | |||||
import { TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; | |||||
import { isTextEditor } from '../../constants'; | |||||
import { Container } from '../../container'; | |||||
import { GitUri } from '../../git/gitService'; | |||||
import { Functions } from '../../system'; | |||||
import { GitExplorer } from '../gitExplorer'; | |||||
import { ExplorerNode } from './explorerNode'; | |||||
import { RepositoryNode } from './repositoryNode'; | |||||
export class ActiveRepositoryNode extends ExplorerNode { | |||||
private _repositoryNode: RepositoryNode | undefined; | |||||
constructor( | |||||
private readonly explorer: GitExplorer | |||||
) { | |||||
super(undefined!); | |||||
Container.context.subscriptions.push( | |||||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this) | |||||
); | |||||
void this.onActiveEditorChanged(window.activeTextEditor); | |||||
} | |||||
dispose() { | |||||
super.dispose(); | |||||
if (this._repositoryNode !== undefined) { | |||||
this._repositoryNode.dispose(); | |||||
this._repositoryNode = undefined; | |||||
} | |||||
} | |||||
get id(): string { | |||||
return 'gitlens:repository:active'; | |||||
} | |||||
private async onActiveEditorChanged(editor: TextEditor | undefined) { | |||||
if (editor !== undefined && !isTextEditor(editor)) return; | |||||
let changed = false; | |||||
try { | |||||
const repoPath = await Container.git.getActiveRepoPath(editor); | |||||
if (repoPath === undefined) { | |||||
if (this._repositoryNode !== undefined) { | |||||
changed = true; | |||||
this._repositoryNode.dispose(); | |||||
this._repositoryNode = undefined; | |||||
} | |||||
return; | |||||
} | |||||
if (this._repositoryNode !== undefined && this._repositoryNode.repo.path === repoPath) return; | |||||
const repo = await Container.git.getRepository(repoPath); | |||||
if (repo === undefined || repo.closed) { | |||||
if (this._repositoryNode !== undefined) { | |||||
changed = true; | |||||
this._repositoryNode.dispose(); | |||||
this._repositoryNode = undefined; | |||||
} | |||||
return; | |||||
} | |||||
changed = true; | |||||
if (this._repositoryNode !== undefined) { | |||||
this._repositoryNode.dispose(); | |||||
} | |||||
this._repositoryNode = new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer, true, this); | |||||
} | |||||
finally { | |||||
if (changed) { | |||||
this.explorer.refreshNode(this); | |||||
} | |||||
} | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
return this._repositoryNode !== undefined ? this._repositoryNode.getChildren() : []; | |||||
} | |||||
getTreeItem(): TreeItem { | |||||
const item = | |||||
this._repositoryNode !== undefined | |||||
? this._repositoryNode.getTreeItem() | |||||
: new TreeItem('No active repository', TreeItemCollapsibleState.None); | |||||
item.id = this.id; | |||||
return item; | |||||
} | |||||
} |
@ -1,38 +0,0 @@ | |||||
'use strict'; | |||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { GitLog, GitUri } from '../../git/gitService'; | |||||
import { Iterables } from '../../system'; | |||||
import { CommitNode } from './commitNode'; | |||||
import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; | |||||
export class CommitsNode extends ExplorerNode { | |||||
readonly supportsPaging: boolean = true; | |||||
constructor( | |||||
public readonly repoPath: string, | |||||
private readonly logFn: (maxCount: number | undefined) => Promise<GitLog | undefined>, | |||||
private readonly explorer: Explorer | |||||
) { | |||||
super(GitUri.fromRepoPath(repoPath)); | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
const log = await this.logFn(this.maxCount); | |||||
if (log === undefined) return []; | |||||
const children: (CommitNode | ShowAllNode)[] = [ | |||||
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer)) | |||||
]; | |||||
if (log.truncated) { | |||||
children.push(new ShowAllNode('Show All Commits', this, this.explorer)); | |||||
} | |||||
return children; | |||||
} | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
const item = new TreeItem('Commits', TreeItemCollapsibleState.Collapsed); | |||||
item.contextValue = ResourceType.Commits; | |||||
return item; | |||||
} | |||||
} |
@ -1,74 +0,0 @@ | |||||
'use strict'; | |||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { GitLog, GitUri } from '../../git/gitService'; | |||||
import { Iterables } from '../../system'; | |||||
import { CommitNode } from './commitNode'; | |||||
import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; | |||||
export class CommitsResultsNode extends ExplorerNode { | |||||
readonly supportsPaging: boolean = true; | |||||
private _cache: { label: string; log: GitLog | undefined } | undefined; | |||||
constructor( | |||||
public readonly repoPath: string, | |||||
private readonly labelFn: (log: GitLog | undefined) => Promise<string>, | |||||
private readonly logFn: (maxCount: number | undefined) => Promise<GitLog | undefined>, | |||||
private readonly explorer: Explorer, | |||||
private readonly contextValue: ResourceType = ResourceType.ResultsCommits | |||||
) { | |||||
super(GitUri.fromRepoPath(repoPath)); | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
const log = await this.getLog(); | |||||
if (log === undefined) return []; | |||||
const children: (CommitNode | ShowAllNode)[] = [ | |||||
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer)) | |||||
]; | |||||
if (log.truncated) { | |||||
children.push(new ShowAllNode('Show All Results', this, this.explorer)); | |||||
} | |||||
return children; | |||||
} | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
const log = await this.getLog(); | |||||
const item = new TreeItem( | |||||
await this.getLabel(), | |||||
log && log.count > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None | |||||
); | |||||
item.contextValue = this.contextValue; | |||||
return item; | |||||
} | |||||
refresh() { | |||||
this._cache = undefined; | |||||
} | |||||
private async ensureCache() { | |||||
if (this._cache === undefined) { | |||||
const log = await this.logFn(this.maxCount); | |||||
this._cache = { | |||||
label: await this.labelFn(log), | |||||
log: log | |||||
}; | |||||
} | |||||
return this._cache; | |||||
} | |||||
private async getLabel() { | |||||
const cache = await this.ensureCache(); | |||||
return cache.label; | |||||
} | |||||
private async getLog() { | |||||
const cache = await this.ensureCache(); | |||||
return cache.log; | |||||
} | |||||
} |
@ -0,0 +1,94 @@ | |||||
import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; | |||||
import { GlyphChars } from '../../constants'; | |||||
import { Container } from '../../container'; | |||||
import { Explorer } from '../explorer'; | |||||
import { RefreshNodeCommandArgs } from '../explorerCommands'; | |||||
import { ExplorerNode, ResourceType, unknownGitUri } from '../nodes/explorerNode'; | |||||
export class MessageNode extends ExplorerNode { | |||||
constructor( | |||||
private readonly message: string, | |||||
private readonly tooltip?: string, | |||||
private readonly iconPath?: | |||||
| string | |||||
| Uri | |||||
| { | |||||
light: string | Uri; | |||||
dark: string | Uri; | |||||
} | |||||
| ThemeIcon | |||||
) { | |||||
super(unknownGitUri); | |||||
} | |||||
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> { | |||||
return []; | |||||
} | |||||
getTreeItem(): TreeItem | Promise<TreeItem> { | |||||
const item = new TreeItem(this.message, TreeItemCollapsibleState.None); | |||||
item.contextValue = ResourceType.Message; | |||||
item.tooltip = this.tooltip; | |||||
item.iconPath = this.iconPath; | |||||
return item; | |||||
} | |||||
} | |||||
export abstract class PagerNode extends ExplorerNode { | |||||
protected _args: RefreshNodeCommandArgs = {}; | |||||
constructor( | |||||
protected readonly message: string, | |||||
protected readonly node: ExplorerNode, | |||||
protected readonly explorer: Explorer | |||||
) { | |||||
super(unknownGitUri); | |||||
} | |||||
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> { | |||||
return []; | |||||
} | |||||
getTreeItem(): TreeItem | Promise<TreeItem> { | |||||
const item = new TreeItem(this.message, TreeItemCollapsibleState.None); | |||||
item.contextValue = ResourceType.Pager; | |||||
item.command = this.getCommand(); | |||||
item.iconPath = { | |||||
dark: Container.context.asAbsolutePath('images/dark/icon-unfold.svg'), | |||||
light: Container.context.asAbsolutePath('images/light/icon-unfold.svg') | |||||
}; | |||||
return item; | |||||
} | |||||
getCommand(): Command | undefined { | |||||
return { | |||||
title: 'Refresh', | |||||
command: this.explorer.getQualifiedCommand('refreshNode'), | |||||
arguments: [this.node, this._args] | |||||
} as Command; | |||||
} | |||||
} | |||||
export class ShowMoreNode extends PagerNode { | |||||
constructor( | |||||
type: string, | |||||
node: ExplorerNode, | |||||
explorer: Explorer, | |||||
maxCount: number = Container.config.advanced.maxListItems | |||||
) { | |||||
super( | |||||
maxCount === 0 | |||||
? `Show All ${type} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while` | |||||
: `Show More ${type}`, | |||||
node, | |||||
explorer | |||||
); | |||||
this._args.maxCount = maxCount; | |||||
} | |||||
} | |||||
export class ShowAllNode extends ShowMoreNode { | |||||
constructor(type: string, node: ExplorerNode, explorer: Explorer) { | |||||
super(type, node, explorer, 0); | |||||
} | |||||
} |
@ -1,59 +0,0 @@ | |||||
'use strict'; | |||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { GlyphChars } from '../../constants'; | |||||
import { Container } from '../../container'; | |||||
import { GitLog, GitService, GitUri } from '../../git/gitService'; | |||||
import { Strings } from '../../system'; | |||||
import { CommitsResultsNode } from './commitsResultsNode'; | |||||
import { Explorer, ExplorerNode, NamedRef, ResourceType } from './explorerNode'; | |||||
import { StatusFilesResultsNode } from './statusFilesResultsNode'; | |||||
export class ComparisonResultsNode extends ExplorerNode { | |||||
constructor( | |||||
public readonly repoPath: string, | |||||
public readonly ref1: NamedRef, | |||||
public readonly ref2: NamedRef, | |||||
private readonly explorer: Explorer | |||||
) { | |||||
super(GitUri.fromRepoPath(repoPath)); | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
this.resetChildren(); | |||||
const commitsQueryFn = (maxCount: number | undefined) => | |||||
Container.git.getLog(this.uri.repoPath!, { | |||||
maxCount: maxCount, | |||||
ref: `${this.ref1.ref}...${this.ref2.ref || 'HEAD'}` | |||||
}); | |||||
const commitsLabelFn = async (log: GitLog | undefined) => { | |||||
const count = log !== undefined ? log.count : 0; | |||||
const truncated = log !== undefined ? log.truncated : false; | |||||
return Strings.pluralize('commit', count, { number: truncated ? `${count}+` : undefined, zero: 'No' }); | |||||
}; | |||||
this.children = [ | |||||
new CommitsResultsNode(this.uri.repoPath!, commitsLabelFn, commitsQueryFn, this.explorer), | |||||
new StatusFilesResultsNode(this.uri.repoPath!, this.ref1.ref, this.ref2.ref, this.explorer) | |||||
]; | |||||
return this.children; | |||||
} | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
let repository = ''; | |||||
if ((await Container.git.getRepositoryCount()) > 1) { | |||||
const repo = await Container.git.getRepository(this.uri.repoPath!); | |||||
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) || this.uri.repoPath}`; | |||||
} | |||||
const item = new TreeItem( | |||||
`Comparing ${this.ref1.label || GitService.shortenSha(this.ref1.ref, { working: 'Working Tree' })} to ${this | |||||
.ref2.label || GitService.shortenSha(this.ref2.ref, { working: 'Working Tree' })}${repository}`, | |||||
TreeItemCollapsibleState.Expanded | |||||
); | |||||
item.contextValue = ResourceType.ComparisonResults; | |||||
return item; | |||||
} | |||||
} |
@ -1,35 +0,0 @@ | |||||
'use strict'; | |||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { Container } from '../../container'; | |||||
import { GitUri, Repository } from '../../git/gitService'; | |||||
import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; | |||||
import { FileHistoryNode } from './fileHistoryNode'; | |||||
export class HistoryNode extends ExplorerNode { | |||||
constructor( | |||||
uri: GitUri, | |||||
private readonly repo: Repository, | |||||
private readonly explorer: Explorer | |||||
) { | |||||
super(uri); | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
this.resetChildren(); | |||||
this.children = [new FileHistoryNode(this.uri, this.repo, this.explorer)]; | |||||
return this.children; | |||||
} | |||||
getTreeItem(): TreeItem { | |||||
const item = new TreeItem(`${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded); | |||||
item.contextValue = ResourceType.History; | |||||
item.iconPath = { | |||||
dark: Container.context.asAbsolutePath('images/dark/icon-history.svg'), | |||||
light: Container.context.asAbsolutePath('images/light/icon-history.svg') | |||||
}; | |||||
return item; | |||||
} | |||||
} |
@ -0,0 +1,131 @@ | |||||
'use strict'; | |||||
import { Disposable, Selection, TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { Container } from '../../container'; | |||||
import { GitCommitType, GitLogCommit, IGitStatusFile } from '../../git/git'; | |||||
import { GitUri, RepositoryChange, RepositoryChangeEvent, RepositoryFileSystemChangeEvent } from '../../git/gitService'; | |||||
import { Logger } from '../../logger'; | |||||
import { Iterables } from '../../system'; | |||||
import { LineHistoryExplorer } from '../lineHistoryExplorer'; | |||||
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; | |||||
import { MessageNode } from './common'; | |||||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode } from './explorerNode'; | |||||
export class LineHistoryNode extends SubscribeableExplorerNode<LineHistoryExplorer> { | |||||
constructor( | |||||
uri: GitUri, | |||||
public readonly selection: Selection, | |||||
explorer: LineHistoryExplorer | |||||
) { | |||||
super(uri, explorer); | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
const children: ExplorerNode[] = []; | |||||
const displayAs = | |||||
CommitFileNodeDisplayAs.CommitLabel | | |||||
(this.explorer.config.avatars ? CommitFileNodeDisplayAs.Gravatar : CommitFileNodeDisplayAs.StatusIcon); | |||||
const log = await Container.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, { | |||||
ref: this.uri.sha, | |||||
range: this.selection | |||||
}); | |||||
if (log !== undefined) { | |||||
children.push( | |||||
...Iterables.filterMap( | |||||
log.commits.values(), | |||||
c => new CommitFileNode(c.fileStatuses[0], c, this.explorer, displayAs, this.selection) | |||||
) | |||||
); | |||||
} | |||||
const blame = await Container.git.getBlameForLine(this.uri, this.selection.active.line); | |||||
if (blame !== undefined) { | |||||
const first = children[0] as CommitFileNode | undefined; | |||||
if (first === undefined || first.commit.sha !== blame.commit.sha) { | |||||
const status: IGitStatusFile = { | |||||
fileName: blame.commit.fileName, | |||||
indexStatus: '?', | |||||
originalFileName: blame.commit.originalFileName, | |||||
repoPath: this.uri.repoPath!, | |||||
status: 'M', | |||||
workTreeStatus: '?' | |||||
}; | |||||
const commit = new GitLogCommit( | |||||
GitCommitType.File, | |||||
this.uri.repoPath!, | |||||
blame.commit.sha, | |||||
'You', | |||||
blame.commit.email, | |||||
blame.commit.date, | |||||
blame.commit.message, | |||||
blame.commit.fileName, | |||||
[status], | |||||
'M', | |||||
blame.commit.originalFileName, | |||||
blame.commit.previousSha, | |||||
blame.commit.originalFileName || blame.commit.fileName | |||||
); | |||||
children.splice(0, 0, new CommitFileNode(status, commit, this.explorer, displayAs, this.selection)); | |||||
} | |||||
} | |||||
if (children.length === 0) return [new MessageNode('No line history')]; | |||||
return children; | |||||
} | |||||
getTreeItem(): TreeItem { | |||||
const lines = this.selection.isSingleLine | |||||
? ` #${this.selection.start.line + 1}` | |||||
: ` #${this.selection.start.line + 1}-${this.selection.end.line + 1}`; | |||||
const item = new TreeItem( | |||||
`${this.uri.getFormattedPath({ suffix: `${lines}${this.uri.sha ? ` (${this.uri.shortSha})` : ''}` })}`, | |||||
TreeItemCollapsibleState.Expanded | |||||
); | |||||
item.contextValue = ResourceType.FileHistory; | |||||
item.tooltip = `History of ${this.uri.getFilename()}${lines}\n${this.uri.getDirectory()}/`; | |||||
item.iconPath = { | |||||
dark: Container.context.asAbsolutePath('images/dark/icon-history.svg'), | |||||
light: Container.context.asAbsolutePath('images/light/icon-history.svg') | |||||
}; | |||||
void this.ensureSubscription(); | |||||
return item; | |||||
} | |||||
protected async subscribe() { | |||||
const repo = await Container.git.getRepository(this.uri); | |||||
if (repo === undefined) return undefined; | |||||
const subscription = Disposable.from( | |||||
repo.onDidChange(this.onRepoChanged, this), | |||||
repo.onDidChangeFileSystem(this.onRepoFileSystemChanged, this), | |||||
{ dispose: () => repo.stopWatchingFileSystem() } | |||||
); | |||||
repo.startWatchingFileSystem(); | |||||
return subscription; | |||||
} | |||||
private onRepoChanged(e: RepositoryChangeEvent) { | |||||
if (!e.changed(RepositoryChange.Repository)) return; | |||||
Logger.log(`LineHistoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`); | |||||
void this.explorer.refreshNode(this); | |||||
} | |||||
private onRepoFileSystemChanged(e: RepositoryFileSystemChangeEvent) { | |||||
if (!e.uris.some(uri => uri.toString(true) === this.uri.toString(true))) return; | |||||
Logger.log(`LineHistoryNode.onRepoFileSystemChanged; triggering node refresh`); | |||||
void this.explorer.refreshNode(this); | |||||
} | |||||
} |
@ -1,41 +1,127 @@ | |||||
'use strict'; | 'use strict'; | ||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { GitUri, Repository } from '../../git/gitService'; | |||||
import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; | |||||
import { Container } from '../../container'; | |||||
import { GitUri } from '../../git/gitService'; | |||||
import { Logger } from '../../logger'; | |||||
import { Functions } from '../../system'; | |||||
import { GitExplorer } from '../gitExplorer'; | import { GitExplorer } from '../gitExplorer'; | ||||
import { ActiveRepositoryNode } from './activeRepositoryNode'; | |||||
import { ExplorerNode, ResourceType } from './explorerNode'; | |||||
import { MessageNode } from './common'; | |||||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode'; | |||||
import { RepositoryNode } from './repositoryNode'; | import { RepositoryNode } from './repositoryNode'; | ||||
export class RepositoriesNode extends ExplorerNode { | |||||
constructor( | |||||
private readonly repositories: Repository[], | |||||
private readonly explorer: GitExplorer | |||||
) { | |||||
super(undefined!); | |||||
export class RepositoriesNode extends SubscribeableExplorerNode<GitExplorer> { | |||||
private _children: (RepositoryNode | MessageNode)[] | undefined; | |||||
constructor(explorer: GitExplorer) { | |||||
super(unknownGitUri, explorer); | |||||
} | } | ||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
if (this.children === undefined) { | |||||
this.children = this.repositories | |||||
.sort((a, b) => a.index - b.index) | |||||
.filter(repo => !repo.closed) | |||||
.map(repo => new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer)); | |||||
if (this.children.length > 1) { | |||||
this.children.splice(0, 0, new ActiveRepositoryNode(this.explorer)); | |||||
dispose() { | |||||
super.dispose(); | |||||
if (this._children !== undefined) { | |||||
for (const child of this._children) { | |||||
if (child instanceof RepositoryNode) { | |||||
child.dispose(); | |||||
} | |||||
} | } | ||||
this._children = undefined; | |||||
} | } | ||||
return this.children; | |||||
} | } | ||||
refresh() { | |||||
this.resetChildren(); | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
if (this._children === undefined) { | |||||
const repositories = [...(await Container.git.getRepositories())]; | |||||
if (repositories.length === 0) return [new MessageNode('No repositories found')]; | |||||
const children = []; | |||||
for (const repo of repositories.sort((a, b) => a.index - b.index)) { | |||||
if (repo.closed) continue; | |||||
children.push(new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer)); | |||||
} | |||||
this._children = children; | |||||
} | |||||
return this._children; | |||||
} | } | ||||
getTreeItem(): TreeItem { | getTreeItem(): TreeItem { | ||||
const item = new TreeItem(`Repositories`, TreeItemCollapsibleState.Expanded); | const item = new TreeItem(`Repositories`, TreeItemCollapsibleState.Expanded); | ||||
item.contextValue = ResourceType.Repositories; | item.contextValue = ResourceType.Repositories; | ||||
void this.ensureSubscription(); | |||||
return item; | return item; | ||||
} | } | ||||
async refresh() { | |||||
if (this._children === undefined) return; | |||||
const repositories = [...(await Container.git.getRepositories())]; | |||||
if (repositories.length === 0 && (this._children === undefined || this._children.length === 0)) return; | |||||
if (repositories.length === 0) { | |||||
this._children = [new MessageNode('No repositories found')]; | |||||
return; | |||||
} | |||||
const children = []; | |||||
for (const repo of repositories.sort((a, b) => a.index - b.index)) { | |||||
const normalizedPath = repo.normalizedPath; | |||||
const child = (this._children as RepositoryNode[]).find(c => c.repo.normalizedPath === normalizedPath); | |||||
if (child !== undefined) { | |||||
children.push(child); | |||||
child.refresh(); | |||||
} | |||||
else { | |||||
children.push(new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer)); | |||||
} | |||||
} | |||||
for (const child of this._children as RepositoryNode[]) { | |||||
if (children.includes(child)) continue; | |||||
child.dispose(); | |||||
} | |||||
this._children = children; | |||||
void this.ensureSubscription(); | |||||
} | |||||
protected async subscribe() { | |||||
return Disposable.from( | |||||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this), | |||||
Container.git.onDidChangeRepositories(this.onRepositoriesChanged, this) | |||||
); | |||||
} | |||||
private async onActiveEditorChanged(editor: TextEditor | undefined) { | |||||
if (editor == null || this._children === undefined || this._children.length === 1) { | |||||
return; | |||||
} | |||||
try { | |||||
const uri = editor.document.uri; | |||||
const gitUri = await Container.git.getVersionedUri(uri); | |||||
const node = this._children.find(n => n instanceof RepositoryNode && n.repo.containsUri(gitUri || uri)) as | |||||
| RepositoryNode | |||||
| undefined; | |||||
if (node === undefined) return; | |||||
// HACK: Since we have no expand/collapse api, reveal the first child to force an expand | |||||
// See https://github.com/Microsoft/vscode/issues/55879 | |||||
const children = await node.getChildren(); | |||||
await this.explorer.reveal(children !== undefined && children.length !== 0 ? children[0] : node); | |||||
} | |||||
catch (ex) { | |||||
Logger.error(ex); | |||||
} | |||||
} | |||||
private onRepositoriesChanged() { | |||||
void this.explorer.refreshNode(this); | |||||
} | |||||
} | } |
@ -1,119 +1,222 @@ | |||||
'use strict'; | 'use strict'; | ||||
import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; | import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; | ||||
import { GlyphChars } from '../../constants'; | import { GlyphChars } from '../../constants'; | ||||
import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../../git/gitService'; | |||||
import { Container } from '../../container'; | |||||
import { | |||||
GitBranch, | |||||
GitStatus, | |||||
GitUri, | |||||
Repository, | |||||
RepositoryChange, | |||||
RepositoryChangeEvent, | |||||
RepositoryFileSystemChangeEvent | |||||
} from '../../git/gitService'; | |||||
import { Logger } from '../../logger'; | import { Logger } from '../../logger'; | ||||
import { Strings } from '../../system'; | import { Strings } from '../../system'; | ||||
import { GitExplorer } from '../gitExplorer'; | import { GitExplorer } from '../gitExplorer'; | ||||
import { BranchesNode } from './branchesNode'; | import { BranchesNode } from './branchesNode'; | ||||
import { ExplorerNode, ResourceType } from './explorerNode'; | |||||
import { BranchNode } from './branchNode'; | |||||
import { MessageNode } from './common'; | |||||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode } from './explorerNode'; | |||||
import { RemotesNode } from './remotesNode'; | import { RemotesNode } from './remotesNode'; | ||||
import { StashesNode } from './stashesNode'; | import { StashesNode } from './stashesNode'; | ||||
import { StatusNode } from './statusNode'; | |||||
import { StatusFilesNode } from './statusFilesNode'; | |||||
import { StatusUpstreamNode } from './statusUpstreamNode'; | |||||
import { TagsNode } from './tagsNode'; | import { TagsNode } from './tagsNode'; | ||||
export class RepositoryNode extends ExplorerNode { | |||||
export class RepositoryNode extends SubscribeableExplorerNode<GitExplorer> { | |||||
private _children: ExplorerNode[] | undefined; | |||||
private _status: Promise<GitStatus | undefined>; | |||||
constructor( | constructor( | ||||
uri: GitUri, | uri: GitUri, | ||||
public readonly repo: Repository, | public readonly repo: Repository, | ||||
private readonly explorer: GitExplorer, | |||||
private readonly active: boolean = false, | |||||
private readonly activeParent?: ExplorerNode | |||||
explorer: GitExplorer | |||||
) { | ) { | ||||
super(uri); | |||||
super(uri, explorer); | |||||
this._status = this.repo.getStatus(); | |||||
} | } | ||||
get id(): string { | get id(): string { | ||||
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}`; | |||||
return `gitlens:repository(${this.repo.path})`; | |||||
} | } | ||||
async getChildren(): Promise<ExplorerNode[]> { | async getChildren(): Promise<ExplorerNode[]> { | ||||
if (this.children === undefined) { | |||||
this.updateSubscription(); | |||||
this.children = [ | |||||
new StatusNode(this.uri, this.repo, this.explorer, this.active), | |||||
new BranchesNode(this.uri, this.repo, this.explorer, this.active), | |||||
new RemotesNode(this.uri, this.repo, this.explorer, this.active), | |||||
new StashesNode(this.uri, this.repo, this.explorer, this.active), | |||||
new TagsNode(this.uri, this.repo, this.explorer, this.active) | |||||
]; | |||||
if (this._children === undefined) { | |||||
const children = []; | |||||
const status = await this._status; | |||||
if (status !== undefined) { | |||||
const branch = new GitBranch( | |||||
status.repoPath, | |||||
status.branch, | |||||
true, | |||||
status.sha, | |||||
status.upstream, | |||||
status.state.ahead, | |||||
status.state.behind, | |||||
status.detached | |||||
); | |||||
children.push(new BranchNode(branch, this.uri, this.explorer, false)); | |||||
if (status.state.behind) { | |||||
children.push(new StatusUpstreamNode(status, 'behind', this.explorer)); | |||||
} | |||||
if (status.state.ahead) { | |||||
children.push(new StatusUpstreamNode(status, 'ahead', this.explorer)); | |||||
} | |||||
if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) { | |||||
const range = status.upstream ? `${status.upstream}..${branch.ref}` : undefined; | |||||
children.push(new StatusFilesNode(status, range, this.explorer)); | |||||
} | |||||
children.push(new MessageNode(GlyphChars.Dash.repeat(2), '')); | |||||
} | |||||
children.push( | |||||
new BranchesNode(this.uri, this.repo, this.explorer), | |||||
new RemotesNode(this.uri, this.repo, this.explorer), | |||||
new StashesNode(this.uri, this.repo, this.explorer), | |||||
new TagsNode(this.uri, this.repo, this.explorer) | |||||
); | |||||
this._children = children; | |||||
} | } | ||||
return this.children; | |||||
return this._children; | |||||
} | } | ||||
getTreeItem(): TreeItem { | |||||
this.updateSubscription(); | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
let label = this.repo.formattedName || this.uri.repoPath || ''; | |||||
let tooltip = this.repo.formattedName ? `${this.repo.formattedName}\n${this.uri.repoPath}` : this.uri.repoPath; | |||||
let iconSuffix = ''; | |||||
let workingStatus = ''; | |||||
const status = await this._status; | |||||
if (status !== undefined) { | |||||
tooltip += `\n\n${status.branch}`; | |||||
if (status.files.length !== 0 && this.includeWorkingTree) { | |||||
workingStatus = status.getFormattedDiffStatus({ | |||||
compact: true, | |||||
prefix: Strings.pad(GlyphChars.Dot, 2, 2) | |||||
}); | |||||
} | |||||
const label = this.active | |||||
? `Active Repository ${Strings.pad(GlyphChars.Dash, 1, 1)} ${this.repo.formattedName || this.uri.repoPath}` | |||||
: `${this.repo.formattedName || this.uri.repoPath}`; | |||||
const upstreamStatus = status.getUpstreamStatus({ | |||||
prefix: `${GlyphChars.Space} ` | |||||
}); | |||||
label += ` ${Strings.pad(GlyphChars.Dash, 2, 3)}${status.branch}${upstreamStatus}${workingStatus}`; | |||||
iconSuffix = workingStatus ? '-blue' : ''; | |||||
if (status.upstream !== undefined) { | |||||
tooltip += ` is tracking ${status.upstream}\n${status.getUpstreamStatus({ | |||||
empty: 'up-to-date', | |||||
expand: true, | |||||
separator: '\n', | |||||
suffix: '\n' | |||||
})}`; | |||||
if (status.state.behind) { | |||||
iconSuffix = '-red'; | |||||
} | |||||
if (status.state.ahead) { | |||||
iconSuffix = status.state.behind ? '-yellow' : '-green'; | |||||
} | |||||
} | |||||
const item = new TreeItem( | |||||
label, | |||||
this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed | |||||
); | |||||
if (workingStatus) { | |||||
tooltip += `\nWorking tree has uncommitted changes${status.getFormattedDiffStatus({ | |||||
expand: true, | |||||
prefix: `\n`, | |||||
separator: '\n' | |||||
})}`; | |||||
} | |||||
} | |||||
const item = new TreeItem(label, TreeItemCollapsibleState.Expanded); | |||||
item.id = this.id; | item.id = this.id; | ||||
item.contextValue = ResourceType.Repository; | item.contextValue = ResourceType.Repository; | ||||
item.tooltip = tooltip; | |||||
item.iconPath = { | |||||
dark: Container.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), | |||||
light: Container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`) | |||||
}; | |||||
void this.ensureSubscription(); | |||||
return item; | return item; | ||||
} | } | ||||
refresh() { | refresh() { | ||||
this.resetChildren(); | |||||
this.updateSubscription(); | |||||
this._status = this.repo.getStatus(); | |||||
this._children = undefined; | |||||
void this.ensureSubscription(); | |||||
} | } | ||||
private updateSubscription() { | |||||
// We only need to subscribe if auto-refresh is enabled, because if it becomes enabled we will be refreshed | |||||
if (this.explorer.autoRefresh) { | |||||
this.disposable = | |||||
this.disposable || | |||||
Disposable.from( | |||||
this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this), | |||||
this.repo.onDidChange(this.onRepoChanged, this) | |||||
); | |||||
} | |||||
else if (this.disposable !== undefined) { | |||||
this.disposable.dispose(); | |||||
this.disposable = undefined; | |||||
protected async subscribe() { | |||||
const disposables = [this.repo.onDidChange(this.onRepoChanged, this)]; | |||||
if (this.includeWorkingTree) { | |||||
disposables.push(this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), { | |||||
dispose: () => this.repo.stopWatchingFileSystem() | |||||
}); | |||||
this.repo.startWatchingFileSystem(); | |||||
} | } | ||||
return Disposable.from(...disposables); | |||||
} | |||||
private get includeWorkingTree(): boolean { | |||||
return this.explorer.config.includeWorkingTree; | |||||
} | } | ||||
private onAutoRefreshChanged() { | |||||
this.updateSubscription(); | |||||
private onFileSystemChanged(e: RepositoryFileSystemChangeEvent) { | |||||
void this.explorer.refreshNode(this); | |||||
} | } | ||||
private onRepoChanged(e: RepositoryChangeEvent) { | private onRepoChanged(e: RepositoryChangeEvent) { | ||||
Logger.log(`RepositoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`); | Logger.log(`RepositoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`); | ||||
if (e.changed(RepositoryChange.Closed)) { | |||||
this.dispose(); | |||||
return; | |||||
} | |||||
if ( | if ( | ||||
this.children === undefined || | |||||
this._children === undefined || | |||||
e.changed(RepositoryChange.Repository) || | e.changed(RepositoryChange.Repository) || | ||||
e.changed(RepositoryChange.Config) | e.changed(RepositoryChange.Config) | ||||
) { | ) { | ||||
this.explorer.refreshNode(this.active && this.activeParent !== undefined ? this.activeParent : this); | |||||
void this.explorer.refreshNode(this); | |||||
return; | return; | ||||
} | } | ||||
if (e.changed(RepositoryChange.Stashes)) { | if (e.changed(RepositoryChange.Stashes)) { | ||||
const node = this.children.find(c => c instanceof StashesNode); | |||||
const node = this._children.find(c => c instanceof StashesNode); | |||||
if (node !== undefined) { | if (node !== undefined) { | ||||
this.explorer.refreshNode(node); | |||||
void this.explorer.refreshNode(node); | |||||
} | } | ||||
} | } | ||||
if (e.changed(RepositoryChange.Remotes)) { | if (e.changed(RepositoryChange.Remotes)) { | ||||
const node = this.children.find(c => c instanceof RemotesNode); | |||||
const node = this._children.find(c => c instanceof RemotesNode); | |||||
if (node !== undefined) { | if (node !== undefined) { | ||||
this.explorer.refreshNode(node); | |||||
void this.explorer.refreshNode(node); | |||||
} | } | ||||
} | } | ||||
if (e.changed(RepositoryChange.Tags)) { | if (e.changed(RepositoryChange.Tags)) { | ||||
const node = this.children.find(c => c instanceof TagsNode); | |||||
const node = this._children.find(c => c instanceof TagsNode); | |||||
if (node !== undefined) { | if (node !== undefined) { | ||||
this.explorer.refreshNode(node); | |||||
void this.explorer.refreshNode(node); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@ -0,0 +1,68 @@ | |||||
'use strict'; | |||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { GitLog, GitUri } from '../../git/gitService'; | |||||
import { Iterables } from '../../system'; | |||||
import { ResultsExplorer } from '../resultsExplorer'; | |||||
import { CommitNode } from './commitNode'; | |||||
import { ShowAllNode } from './common'; | |||||
import { ExplorerNode, PageableExplorerNode, ResourceType } from './explorerNode'; | |||||
export interface CommitsQueryResults { | |||||
label: string; | |||||
log: GitLog | undefined; | |||||
} | |||||
export class ResultsCommitsNode extends ExplorerNode implements PageableExplorerNode { | |||||
readonly supportsPaging: boolean = true; | |||||
maxCount: number | undefined; | |||||
constructor( | |||||
public readonly repoPath: string, | |||||
private readonly commitsQuery: (maxCount: number | undefined) => Promise<CommitsQueryResults>, | |||||
private readonly explorer: ResultsExplorer, | |||||
private readonly contextValue: ResourceType = ResourceType.ResultsCommits | |||||
) { | |||||
super(GitUri.fromRepoPath(repoPath)); | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
const { log } = await this.getCommitsQueryResults(); | |||||
if (log === undefined) return []; | |||||
const children: (CommitNode | ShowAllNode)[] = [ | |||||
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer)) | |||||
]; | |||||
if (log.truncated) { | |||||
children.push(new ShowAllNode('Results', this, this.explorer)); | |||||
} | |||||
return children; | |||||
} | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
const { label, log } = await this.getCommitsQueryResults(); | |||||
const item = new TreeItem( | |||||
label, | |||||
log && log.count > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None | |||||
); | |||||
item.contextValue = this.contextValue; | |||||
return item; | |||||
} | |||||
async refresh() { | |||||
this._commitsQueryResults = this.commitsQuery(this.maxCount); | |||||
} | |||||
private _commitsQueryResults: Promise<CommitsQueryResults> | undefined; | |||||
private getCommitsQueryResults() { | |||||
if (this._commitsQueryResults === undefined) { | |||||
this._commitsQueryResults = this.commitsQuery(this.maxCount); | |||||
} | |||||
return this._commitsQueryResults; | |||||
} | |||||
} |
@ -0,0 +1,84 @@ | |||||
'use strict'; | |||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { GlyphChars } from '../../constants'; | |||||
import { Container } from '../../container'; | |||||
import { GitService, GitUri } from '../../git/gitService'; | |||||
import { Strings } from '../../system'; | |||||
import { ResultsExplorer } from '../resultsExplorer'; | |||||
import { ExplorerNode, NamedRef, ResourceType } from './explorerNode'; | |||||
import { CommitsQueryResults, ResultsCommitsNode } from './resultsCommitsNode'; | |||||
import { StatusFilesResultsNode } from './statusFilesResultsNode'; | |||||
export class ResultsComparisonNode extends ExplorerNode { | |||||
constructor( | |||||
public readonly repoPath: string, | |||||
ref1: NamedRef, | |||||
ref2: NamedRef, | |||||
private readonly explorer: ResultsExplorer | |||||
) { | |||||
super(GitUri.fromRepoPath(repoPath)); | |||||
this._ref1 = ref1; | |||||
this._ref2 = ref2; | |||||
} | |||||
private _ref1: NamedRef; | |||||
get ref1(): NamedRef { | |||||
return this._ref1; | |||||
} | |||||
private _ref2: NamedRef; | |||||
get ref2(): NamedRef { | |||||
return this._ref2; | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
return [ | |||||
new ResultsCommitsNode(this.uri.repoPath!, this.getCommitsQuery.bind(this), this.explorer), | |||||
new StatusFilesResultsNode(this.uri.repoPath!, this._ref1.ref, this._ref2.ref, this.explorer) | |||||
]; | |||||
} | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
let repository = ''; | |||||
if ((await Container.git.getRepositoryCount()) > 1) { | |||||
const repo = await Container.git.getRepository(this.uri.repoPath!); | |||||
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) || this.uri.repoPath}`; | |||||
} | |||||
const item = new TreeItem( | |||||
`Comparing ${this._ref1.label || | |||||
GitService.shortenSha(this._ref1.ref, { working: 'Working Tree' })} to ${this._ref2.label || | |||||
GitService.shortenSha(this._ref2.ref, { working: 'Working Tree' })}${repository}`, | |||||
TreeItemCollapsibleState.Expanded | |||||
); | |||||
item.contextValue = ResourceType.ComparisonResults; | |||||
return item; | |||||
} | |||||
swap() { | |||||
const ref1 = this._ref1; | |||||
this._ref1 = this._ref2; | |||||
this._ref2 = ref1; | |||||
this.explorer.triggerNodeUpdate(this); | |||||
} | |||||
private async getCommitsQuery(maxCount: number | undefined): Promise<CommitsQueryResults> { | |||||
const log = await Container.git.getLog(this.uri.repoPath!, { | |||||
maxCount: maxCount, | |||||
ref: `${this._ref1.ref}...${this._ref2.ref || 'HEAD'}` | |||||
}); | |||||
const count = log !== undefined ? log.count : 0; | |||||
const truncated = log !== undefined ? log.truncated : false; | |||||
const label = Strings.pluralize('commit', count, { number: truncated ? `${count}+` : undefined, zero: 'No' }); | |||||
return { | |||||
label: label, | |||||
log: log | |||||
}; | |||||
} | |||||
} |
@ -0,0 +1,64 @@ | |||||
'use strict'; | |||||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { ResultsExplorer } from '../resultsExplorer'; | |||||
import { MessageNode } from './common'; | |||||
import { ExplorerNode, ResourceType, unknownGitUri } from './explorerNode'; | |||||
export class ResultsNode extends ExplorerNode { | |||||
private _children: (ExplorerNode | MessageNode)[] = []; | |||||
constructor( | |||||
public readonly explorer: ResultsExplorer | |||||
) { | |||||
super(unknownGitUri); | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
if (this._children.length === 0) return [new MessageNode('No results')]; | |||||
return this._children; | |||||
} | |||||
getTreeItem(): TreeItem { | |||||
const item = new TreeItem(`Results`, TreeItemCollapsibleState.Expanded); | |||||
item.contextValue = ResourceType.Results; | |||||
return item; | |||||
} | |||||
addOrReplace(results: ExplorerNode, replace: boolean) { | |||||
if (this._children.includes(results)) return; | |||||
if (this._children.length !== 0 && replace) { | |||||
this._children.length = 0; | |||||
this._children.push(results); | |||||
} | |||||
else { | |||||
this._children.splice(0, 0, results); | |||||
} | |||||
this.explorer.triggerNodeUpdate(); | |||||
} | |||||
clear() { | |||||
if (this._children.length === 0) return; | |||||
this._children.length = 0; | |||||
this.explorer.triggerNodeUpdate(); | |||||
} | |||||
dismiss(node: ExplorerNode) { | |||||
if (this._children.length === 0) return; | |||||
const index = this._children.findIndex(n => n === node); | |||||
if (index === -1) return; | |||||
this._children.splice(index, 1); | |||||
this.explorer.triggerNodeUpdate(); | |||||
} | |||||
async refresh() { | |||||
if (this._children.length === 0) return; | |||||
this._children.forEach(c => c.refresh()); | |||||
} | |||||
} |
@ -1,180 +0,0 @@ | |||||
import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||||
import { GlyphChars } from '../../constants'; | |||||
import { Container } from '../../container'; | |||||
import { GitBranch, GitUri, Repository, RepositoryFileSystemChangeEvent } from '../../git/gitService'; | |||||
import { GitExplorer } from '../gitExplorer'; | |||||
import { BranchNode } from './branchNode'; | |||||
import { ExplorerNode, ResourceType } from './explorerNode'; | |||||
import { StatusFilesNode } from './statusFilesNode'; | |||||
import { StatusUpstreamNode } from './statusUpstreamNode'; | |||||
export class StatusNode extends ExplorerNode { | |||||
constructor( | |||||
uri: GitUri, | |||||
public readonly repo: Repository, | |||||
private readonly explorer: GitExplorer, | |||||
private readonly active: boolean = false | |||||
) { | |||||
super(uri); | |||||
} | |||||
get id(): string { | |||||
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:status`; | |||||
} | |||||
async getChildren(): Promise<ExplorerNode[]> { | |||||
this.resetChildren(); | |||||
const children = []; | |||||
const status = await this.repo.getStatus(); | |||||
if (status !== undefined) { | |||||
if (status.state.behind) { | |||||
children.push(new StatusUpstreamNode(status, 'behind', this.explorer, this.active)); | |||||
} | |||||
if (status.state.ahead) { | |||||
children.push(new StatusUpstreamNode(status, 'ahead', this.explorer, this.active)); | |||||
} | |||||
if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) { | |||||
const range = status.upstream ? `${status.upstream}..${status.ref}` : undefined; | |||||
children.push(new StatusFilesNode(status, range, this.explorer, this.active)); | |||||
} | |||||
} | |||||
let branch = await this.repo.getBranch(); | |||||
if (branch !== undefined) { | |||||
if (status !== undefined) { | |||||
branch = new GitBranch( | |||||
branch.repoPath, | |||||
branch.name, | |||||
branch.current, | |||||
branch.sha, | |||||
branch.tracking, | |||||
status.state.ahead, | |||||
status.state.behind, | |||||
branch.detached | |||||
); | |||||
} | |||||
children.push(new StatusBranchNode(branch, this.uri, this.explorer)); | |||||
} | |||||
this.children = children; | |||||
return this.children; | |||||
} | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
if (this.disposable !== undefined) { | |||||
this.disposable.dispose(); | |||||
this.disposable = undefined; | |||||
} | |||||
const status = await this.repo.getStatus(); | |||||
if (status === undefined) return new TreeItem('No repo status'); | |||||
if (this.explorer.autoRefresh && this.includeWorkingTree) { | |||||
this.disposable = Disposable.from( | |||||
this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this), | |||||
this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), | |||||
{ dispose: () => this.repo.stopWatchingFileSystem() } | |||||
); | |||||
this.repo.startWatchingFileSystem(); | |||||
} | |||||
let hasChildren = false; | |||||
const hasWorkingChanges = status.files.length !== 0 && this.includeWorkingTree; | |||||
let label = `${status.getUpstreamStatus({ prefix: `${GlyphChars.Space} ` })}${ | |||||
hasWorkingChanges ? status.getFormattedDiffStatus({ prefix: `${GlyphChars.Space} ` }) : '' | |||||
}`; | |||||
let tooltip = `${status.branch} (current)`; | |||||
let iconSuffix = ''; | |||||
if (status.upstream) { | |||||
if (this.explorer.config.showTrackingBranch) { | |||||
label += `${GlyphChars.Space} ${GlyphChars.ArrowLeftRightLong}${GlyphChars.Space} ${status.upstream}`; | |||||
} | |||||
tooltip += `\n\nTracking ${GlyphChars.Dash} ${status.upstream} | |||||
${status.getUpstreamStatus({ empty: 'up-to-date', expand: true, separator: '\n' })}`; | |||||
if (status.state.ahead || status.state.behind) { | |||||
hasChildren = true; | |||||
if (status.state.behind) { | |||||
iconSuffix = '-red'; | |||||
} | |||||
if (status.state.ahead) { | |||||
iconSuffix = status.state.behind ? '-yellow' : '-green'; | |||||
} | |||||
} | |||||
} | |||||
if (hasWorkingChanges) { | |||||
tooltip += `\n\nHas uncommitted changes${status.getFormattedDiffStatus({ | |||||
expand: true, | |||||
prefix: `\n`, | |||||
separator: '\n' | |||||
})}`; | |||||
} | |||||
let state: TreeItemCollapsibleState; | |||||
if (hasChildren || hasWorkingChanges) { | |||||
// HACK: Until https://github.com/Microsoft/vscode/issues/30918 is fixed | |||||
state = this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed; | |||||
} | |||||
else { | |||||
state = TreeItemCollapsibleState.Collapsed; | |||||
} | |||||
const item = new TreeItem(`${status.branch}${label}`, state); | |||||
item.id = this.id; | |||||
item.contextValue = ResourceType.Status; | |||||
item.tooltip = tooltip; | |||||
item.iconPath = { | |||||
dark: Container.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), | |||||
light: Container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`) | |||||
}; | |||||
return item; | |||||
} | |||||
private get includeWorkingTree(): boolean { | |||||
return this.explorer.config.includeWorkingTree; | |||||
} | |||||
private onAutoRefreshChanged() { | |||||
if (this.disposable === undefined) return; | |||||
// If auto-refresh changes, just kill the subscriptions | |||||
// (if it was enabled -- we will get refreshed so we don't have to worry about re-hooking it up here) | |||||
this.disposable.dispose(); | |||||
this.disposable = undefined; | |||||
} | |||||
private async onFileSystemChanged(e: RepositoryFileSystemChangeEvent) { | |||||
this.explorer.refreshNode(this); | |||||
} | |||||
} | |||||
export class StatusBranchNode extends BranchNode { | |||||
constructor(branch: GitBranch, uri: GitUri, explorer: GitExplorer) { | |||||
super(branch, uri, explorer); | |||||
} | |||||
get markCurrent() { | |||||
return false; | |||||
} | |||||
async getTreeItem(): Promise<TreeItem> { | |||||
const item = await super.getTreeItem(); | |||||
if (item.label!.startsWith('(') && item.label!.endsWith(')')) { | |||||
item.label = `History ${item.label}`; | |||||
} | |||||
else { | |||||
item.label = `History (${item.label})`; | |||||
} | |||||
return item; | |||||
} | |||||
} |