Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 

577 righe
16 KiB

import {
CancellationToken,
ConfigurationChangeEvent,
Disposable,
Event,
EventEmitter,
MarkdownString,
TreeDataProvider,
TreeItem,
TreeItemCollapsibleState,
TreeView,
TreeViewExpansionEvent,
TreeViewVisibilityChangeEvent,
window,
} from 'vscode';
import {
BranchesViewConfig,
CommitsViewConfig,
configuration,
ContributorsViewConfig,
FileHistoryViewConfig,
LineHistoryViewConfig,
RemotesViewConfig,
RepositoriesViewConfig,
SearchAndCompareViewConfig,
StashesViewConfig,
TagsViewConfig,
ViewsCommonConfig,
viewsCommonConfigKeys,
viewsConfigKeys,
ViewsConfigKeys,
WorktreesViewConfig,
} from '../configuration';
import { Container } from '../container';
import { Logger } from '../logger';
import { executeCommand } from '../system/command';
import { debug, log } from '../system/decorators/log';
import { once } from '../system/event';
import { debounce } from '../system/function';
import { cancellable, isPromise } from '../system/promise';
import { BranchesView } from './branchesView';
import { CommitsView } from './commitsView';
import { ContributorsView } from './contributorsView';
import { FileHistoryView } from './fileHistoryView';
import { LineHistoryView } from './lineHistoryView';
import { PageableViewNode, ViewNode } from './nodes';
import { RemotesView } from './remotesView';
import { RepositoriesView } from './repositoriesView';
import { SearchAndCompareView } from './searchAndCompareView';
import { StashesView } from './stashesView';
import { TagsView } from './tagsView';
import { WorktreesView } from './worktreesView';
export type View =
| BranchesView
| CommitsView
| ContributorsView
| FileHistoryView
| LineHistoryView
| RemotesView
| RepositoriesView
| SearchAndCompareView
| StashesView
| TagsView
| WorktreesView;
export type ViewsWithCommits = Exclude<View, FileHistoryView | LineHistoryView | StashesView>;
export type ViewsWithRepositoryFolders = Exclude<View, RepositoriesView | FileHistoryView | LineHistoryView>;
export interface TreeViewNodeCollapsibleStateChangeEvent<T> extends TreeViewExpansionEvent<T> {
state: TreeItemCollapsibleState;
}
export abstract class ViewBase<
RootNode extends ViewNode<View>,
ViewConfig extends
| BranchesViewConfig
| ContributorsViewConfig
| FileHistoryViewConfig
| CommitsViewConfig
| LineHistoryViewConfig
| RemotesViewConfig
| RepositoriesViewConfig
| SearchAndCompareViewConfig
| StashesViewConfig
| TagsViewConfig
| WorktreesViewConfig,
> implements TreeDataProvider<ViewNode>, Disposable
{
protected _onDidChangeTreeData = new EventEmitter<ViewNode | undefined>();
get onDidChangeTreeData(): Event<ViewNode | undefined> {
return this._onDidChangeTreeData.event;
}
private _onDidChangeVisibility = new EventEmitter<TreeViewVisibilityChangeEvent>();
get onDidChangeVisibility(): Event<TreeViewVisibilityChangeEvent> {
return this._onDidChangeVisibility.event;
}
private _onDidChangeNodeCollapsibleState = new EventEmitter<TreeViewNodeCollapsibleStateChangeEvent<ViewNode>>();
get onDidChangeNodeCollapsibleState(): Event<TreeViewNodeCollapsibleStateChangeEvent<ViewNode>> {
return this._onDidChangeNodeCollapsibleState.event;
}
protected disposables: Disposable[] = [];
protected root: RootNode | undefined;
protected tree: TreeView<ViewNode> | undefined;
private readonly _lastKnownLimits = new Map<string, number | undefined>();
constructor(
public readonly id: `gitlens.views.${string}`,
public readonly name: string,
public readonly container: Container,
) {
this.disposables.push(once(container.onReady)(this.onReady, this));
if (this.container.debugging || this.container.config.debug) {
function addDebuggingInfo(item: TreeItem, node: ViewNode, parent: ViewNode | undefined) {
if (item.tooltip == null) {
item.tooltip = new MarkdownString(
item.label != null && typeof item.label !== 'string' ? item.label.label : item.label ?? '',
);
}
if (typeof item.tooltip === 'string') {
item.tooltip = `${item.tooltip}\n\n---\ncontext: ${item.contextValue}\nnode: ${node.toString()}${
parent != null ? `\nparent: ${parent.toString()}` : ''
}`;
} else {
item.tooltip.appendMarkdown(
`\n\n---\n\ncontext: \`${item.contextValue}\`\\\nnode: \`${node.toString()}\`${
parent != null ? `\\\nparent: \`${parent.toString()}\`` : ''
}`,
);
}
}
const getTreeItemFn = this.getTreeItem;
this.getTreeItem = async function (this: ViewBase<RootNode, ViewConfig>, node: ViewNode) {
const item = await getTreeItemFn.apply(this, [node]);
const parent = node.getParent();
if (node.resolveTreeItem != null) {
if (item.tooltip != null) {
addDebuggingInfo(item, node, parent);
}
const resolveTreeItemFn = node.resolveTreeItem;
node.resolveTreeItem = async function (this: ViewBase<RootNode, ViewConfig>, item: TreeItem) {
const resolvedItem = await resolveTreeItemFn.apply(this, [item]);
addDebuggingInfo(resolvedItem, node, parent);
return resolvedItem;
};
} else {
addDebuggingInfo(item, node, parent);
}
return item;
};
}
this.disposables.push(...this.registerCommands());
}
dispose() {
Disposable.from(...this.disposables).dispose();
}
private onReady() {
this.initialize({ showCollapseAll: this.showCollapseAll });
queueMicrotask(() => this.onConfigurationChanged());
}
get canReveal(): boolean {
return true;
}
protected get showCollapseAll(): boolean {
return true;
}
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
if (!configuration.changed(e, 'views')) return false;
if (configuration.changed(e, `views.${this.configKey}` as const)) return true;
for (const key of viewsCommonConfigKeys) {
if (configuration.changed(e, `views.${key}` as const)) return true;
}
return false;
}
private _title: string | undefined;
get title(): string | undefined {
return this._title;
}
set title(value: string | undefined) {
this._title = value;
if (this.tree != null) {
this.tree.title = value;
}
}
private _description: string | undefined;
get description(): string | undefined {
return this._description;
}
set description(value: string | undefined) {
this._description = value;
if (this.tree != null) {
this.tree.description = value;
}
}
private _message: string | undefined;
get message(): string | undefined {
return this._message;
}
set message(value: string | undefined) {
this._message = value;
if (this.tree != null) {
this.tree.message = value;
}
}
getQualifiedCommand(command: string) {
return `${this.id}.${command}`;
}
protected abstract getRoot(): RootNode;
protected abstract registerCommands(): Disposable[];
protected onConfigurationChanged(e?: ConfigurationChangeEvent): void {
if (e != null && this.root != null) {
void this.refresh(true);
}
}
protected initialize(options: { showCollapseAll?: boolean } = {}) {
this.tree = window.createTreeView<ViewNode<View>>(this.id, {
...options,
treeDataProvider: this,
});
this.disposables.push(
configuration.onDidChange(e => {
if (!this.filterConfigurationChanged(e)) return;
this._config = undefined;
this.onConfigurationChanged(e);
}, this),
this.tree,
this.tree.onDidChangeVisibility(debounce(this.onVisibilityChanged, 250), this),
this.tree.onDidCollapseElement(this.onElementCollapsed, this),
this.tree.onDidExpandElement(this.onElementExpanded, this),
);
this._title = this.tree.title;
}
protected ensureRoot(force: boolean = false) {
if (this.root == null || force) {
this.root = this.getRoot();
}
return this.root;
}
getChildren(node?: ViewNode): ViewNode[] | Promise<ViewNode[]> {
if (node != null) return node.getChildren();
const root = this.ensureRoot();
return root.getChildren();
}
getParent(node: ViewNode): ViewNode | undefined {
return node.getParent();
}
getTreeItem(node: ViewNode): TreeItem | Promise<TreeItem> {
return node.getTreeItem();
}
resolveTreeItem(item: TreeItem, node: ViewNode): TreeItem | Promise<TreeItem> {
return node.resolveTreeItem?.(item) ?? item;
}
protected onElementCollapsed(e: TreeViewExpansionEvent<ViewNode>) {
this._onDidChangeNodeCollapsibleState.fire({ ...e, state: TreeItemCollapsibleState.Collapsed });
}
protected onElementExpanded(e: TreeViewExpansionEvent<ViewNode>) {
this._onDidChangeNodeCollapsibleState.fire({ ...e, state: TreeItemCollapsibleState.Expanded });
}
protected onVisibilityChanged(e: TreeViewVisibilityChangeEvent) {
this._onDidChangeVisibility.fire(e);
}
get selection(): readonly ViewNode[] {
if (this.tree == null || this.root == null) return [];
return this.tree.selection;
}
get visible(): boolean {
return this.tree?.visible ?? false;
}
async findNode(
id: string,
options?: {
allowPaging?: boolean;
canTraverse?: (node: ViewNode) => boolean | Promise<boolean>;
maxDepth?: number;
token?: CancellationToken;
},
): Promise<ViewNode | undefined>;
async findNode(
predicate: (node: ViewNode) => boolean,
options?: {
allowPaging?: boolean;
canTraverse?: (node: ViewNode) => boolean | Promise<boolean>;
maxDepth?: number;
token?: CancellationToken;
},
): Promise<ViewNode | undefined>;
@log<ViewBase<RootNode, ViewConfig>['findNode']>({
args: {
0: predicate => (typeof predicate === 'string' ? predicate : '<function>'),
1: opts => `options=${JSON.stringify({ ...opts, canTraverse: undefined, token: undefined })}`,
},
})
async findNode(
predicate: string | ((node: ViewNode) => boolean),
{
allowPaging = false,
canTraverse,
maxDepth = 2,
token,
}: {
allowPaging?: boolean;
canTraverse?: (node: ViewNode) => boolean | Promise<boolean>;
maxDepth?: number;
token?: CancellationToken;
} = {},
): Promise<ViewNode | undefined> {
const cc = Logger.getCorrelationContext();
async function find(this: ViewBase<RootNode, ViewConfig>) {
try {
const node = await this.findNodeCoreBFS(
typeof predicate === 'string' ? n => n.id === predicate : predicate,
this.ensureRoot(),
allowPaging,
canTraverse,
maxDepth,
token,
);
return node;
} catch (ex) {
Logger.error(ex, cc);
return undefined;
}
}
if (this.root != null) return find.call(this);
// If we have no root (e.g. never been initialized) force it so the tree will load properly
await this.show({ preserveFocus: true });
// Since we have to show the view, give the view time to load and let the callstack unwind before we try to find the node
return new Promise<ViewNode | undefined>(resolve => setTimeout(() => resolve(find.call(this)), 100));
}
private async findNodeCoreBFS(
predicate: (node: ViewNode) => boolean,
root: ViewNode,
allowPaging: boolean,
canTraverse: ((node: ViewNode) => boolean | Promise<boolean>) | undefined,
maxDepth: number,
token: CancellationToken | undefined,
): Promise<ViewNode | undefined> {
const queue: (ViewNode | undefined)[] = [root, undefined];
const defaultPageSize = this.container.config.advanced.maxListItems;
let depth = 0;
let node: ViewNode | undefined;
let children: ViewNode[];
let pagedChildren: ViewNode[];
while (queue.length > 1) {
if (token?.isCancellationRequested) return undefined;
node = queue.shift();
if (node == null) {
depth++;
queue.push(undefined);
if (depth > maxDepth) break;
continue;
}
if (predicate(node)) return node;
if (canTraverse != null) {
const traversable = canTraverse(node);
if (isPromise(traversable)) {
if (!(await traversable)) continue;
} else if (!traversable) {
continue;
}
}
children = await node.getChildren();
if (children.length === 0) continue;
while (node != null && !PageableViewNode.is(node)) {
node = await node.getSplattedChild?.();
}
if (node != null && PageableViewNode.is(node)) {
let child = children.find(predicate);
if (child != null) return child;
if (allowPaging && node.hasMore) {
while (true) {
if (token?.isCancellationRequested) return undefined;
await this.loadMoreNodeChildren(node, defaultPageSize);
pagedChildren = await cancellable(Promise.resolve(node.getChildren()), token ?? 60000, {
onDidCancel: resolve => resolve([]),
});
child = pagedChildren.find(predicate);
if (child != null) return child;
if (!node.hasMore) break;
}
}
// Don't traverse into paged children
continue;
}
queue.push(...children);
}
return undefined;
}
protected async ensureRevealNode(
node: ViewNode,
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
},
) {
// Not sure why I need to reveal each parent, but without it the node won't be revealed
const nodes: ViewNode[] = [];
let parent: ViewNode | undefined = node;
while (parent != null) {
nodes.push(parent);
parent = parent.getParent();
}
if (nodes.length > 1) {
nodes.pop();
}
for (const n of nodes.reverse()) {
try {
await this.reveal(n, options);
} catch {}
}
}
@debug()
async refresh(reset: boolean = false) {
await this.root?.refresh?.(reset);
this.triggerNodeChange();
}
@debug<ViewBase<RootNode, ViewConfig>['refreshNode']>({ args: { 0: n => n.toString() } })
async refreshNode(node: ViewNode, reset: boolean = false, force: boolean = false) {
const cancel = await node.refresh?.(reset);
if (!force && cancel === true) return;
this.triggerNodeChange(node);
}
@log<ViewBase<RootNode, ViewConfig>['reveal']>({ args: { 0: n => n.toString() } })
async reveal(
node: ViewNode,
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
},
) {
if (this.tree == null) return;
try {
await this.tree.reveal(node, options);
} catch (ex) {
Logger.error(ex);
}
}
@log()
async show(options?: { preserveFocus?: boolean }) {
const cc = Logger.getCorrelationContext();
try {
void (await executeCommand(`${this.id}.focus`, options));
} catch (ex) {
Logger.error(ex, cc);
}
}
// @debug({ args: { 0: (n: ViewNode) => n.toString() }, singleLine: true })
getNodeLastKnownLimit(node: PageableViewNode) {
return this._lastKnownLimits.get(node.id);
}
@debug<ViewBase<RootNode, ViewConfig>['loadMoreNodeChildren']>({
args: { 0: n => n.toString(), 2: n => n?.toString() },
})
async loadMoreNodeChildren(
node: ViewNode & PageableViewNode,
limit: number | { until: string | undefined } | undefined,
previousNode?: ViewNode,
) {
if (previousNode != null) {
void (await this.reveal(previousNode, { select: true }));
}
await node.loadMore(limit);
this._lastKnownLimits.set(node.id, node.limit);
}
@debug<ViewBase<RootNode, ViewConfig>['resetNodeLastKnownLimit']>({
args: { 0: n => n.toString() },
singleLine: true,
})
resetNodeLastKnownLimit(node: PageableViewNode) {
this._lastKnownLimits.delete(node.id);
}
@debug<ViewBase<RootNode, ViewConfig>['triggerNodeChange']>({ args: { 0: n => n?.toString() } })
triggerNodeChange(node?: ViewNode) {
// Since the root node won't actually refresh, force everything
this._onDidChangeTreeData.fire(node != null && node !== this.root ? node : undefined);
}
protected abstract readonly configKey: ViewsConfigKeys;
private _config: (ViewConfig & ViewsCommonConfig) | undefined;
get config(): ViewConfig & ViewsCommonConfig {
if (this._config == null) {
const cfg = { ...this.container.config.views };
for (const view of viewsConfigKeys) {
delete cfg[view];
}
this._config = {
...(cfg as ViewsCommonConfig),
...(this.container.config.views[this.configKey] as ViewConfig),
};
}
return this._config;
}
}