- 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;
- }
- }
|