Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

577 lignes
16 KiB

il y a 4 ans
il y a 4 ans
il y a 4 ans
il y a 4 ans
il y a 4 ans
il y a 4 ans
il y a 4 ans
il y a 4 ans
il y a 3 ans
  1. import {
  2. CancellationToken,
  3. ConfigurationChangeEvent,
  4. Disposable,
  5. Event,
  6. EventEmitter,
  7. MarkdownString,
  8. TreeDataProvider,
  9. TreeItem,
  10. TreeItemCollapsibleState,
  11. TreeView,
  12. TreeViewExpansionEvent,
  13. TreeViewVisibilityChangeEvent,
  14. window,
  15. } from 'vscode';
  16. import {
  17. BranchesViewConfig,
  18. CommitsViewConfig,
  19. configuration,
  20. ContributorsViewConfig,
  21. FileHistoryViewConfig,
  22. LineHistoryViewConfig,
  23. RemotesViewConfig,
  24. RepositoriesViewConfig,
  25. SearchAndCompareViewConfig,
  26. StashesViewConfig,
  27. TagsViewConfig,
  28. ViewsCommonConfig,
  29. viewsCommonConfigKeys,
  30. viewsConfigKeys,
  31. ViewsConfigKeys,
  32. WorktreesViewConfig,
  33. } from '../configuration';
  34. import { Container } from '../container';
  35. import { Logger } from '../logger';
  36. import { executeCommand } from '../system/command';
  37. import { debug, log } from '../system/decorators/log';
  38. import { once } from '../system/event';
  39. import { debounce } from '../system/function';
  40. import { cancellable, isPromise } from '../system/promise';
  41. import { BranchesView } from './branchesView';
  42. import { CommitsView } from './commitsView';
  43. import { ContributorsView } from './contributorsView';
  44. import { FileHistoryView } from './fileHistoryView';
  45. import { LineHistoryView } from './lineHistoryView';
  46. import { PageableViewNode, ViewNode } from './nodes';
  47. import { RemotesView } from './remotesView';
  48. import { RepositoriesView } from './repositoriesView';
  49. import { SearchAndCompareView } from './searchAndCompareView';
  50. import { StashesView } from './stashesView';
  51. import { TagsView } from './tagsView';
  52. import { WorktreesView } from './worktreesView';
  53. export type View =
  54. | BranchesView
  55. | CommitsView
  56. | ContributorsView
  57. | FileHistoryView
  58. | LineHistoryView
  59. | RemotesView
  60. | RepositoriesView
  61. | SearchAndCompareView
  62. | StashesView
  63. | TagsView
  64. | WorktreesView;
  65. export type ViewsWithCommits = Exclude<View, FileHistoryView | LineHistoryView | StashesView>;
  66. export type ViewsWithRepositoryFolders = Exclude<View, RepositoriesView | FileHistoryView | LineHistoryView>;
  67. export interface TreeViewNodeCollapsibleStateChangeEvent<T> extends TreeViewExpansionEvent<T> {
  68. state: TreeItemCollapsibleState;
  69. }
  70. export abstract class ViewBase<
  71. RootNode extends ViewNode<View>,
  72. ViewConfig extends
  73. | BranchesViewConfig
  74. | ContributorsViewConfig
  75. | FileHistoryViewConfig
  76. | CommitsViewConfig
  77. | LineHistoryViewConfig
  78. | RemotesViewConfig
  79. | RepositoriesViewConfig
  80. | SearchAndCompareViewConfig
  81. | StashesViewConfig
  82. | TagsViewConfig
  83. | WorktreesViewConfig,
  84. > implements TreeDataProvider<ViewNode>, Disposable
  85. {
  86. protected _onDidChangeTreeData = new EventEmitter<ViewNode | undefined>();
  87. get onDidChangeTreeData(): Event<ViewNode | undefined> {
  88. return this._onDidChangeTreeData.event;
  89. }
  90. private _onDidChangeVisibility = new EventEmitter<TreeViewVisibilityChangeEvent>();
  91. get onDidChangeVisibility(): Event<TreeViewVisibilityChangeEvent> {
  92. return this._onDidChangeVisibility.event;
  93. }
  94. private _onDidChangeNodeCollapsibleState = new EventEmitter<TreeViewNodeCollapsibleStateChangeEvent<ViewNode>>();
  95. get onDidChangeNodeCollapsibleState(): Event<TreeViewNodeCollapsibleStateChangeEvent<ViewNode>> {
  96. return this._onDidChangeNodeCollapsibleState.event;
  97. }
  98. protected disposables: Disposable[] = [];
  99. protected root: RootNode | undefined;
  100. protected tree: TreeView<ViewNode> | undefined;
  101. private readonly _lastKnownLimits = new Map<string, number | undefined>();
  102. constructor(
  103. public readonly id: `gitlens.views.${string}`,
  104. public readonly name: string,
  105. public readonly container: Container,
  106. ) {
  107. this.disposables.push(once(container.onReady)(this.onReady, this));
  108. if (this.container.debugging || this.container.config.debug) {
  109. function addDebuggingInfo(item: TreeItem, node: ViewNode, parent: ViewNode | undefined) {
  110. if (item.tooltip == null) {
  111. item.tooltip = new MarkdownString(
  112. item.label != null && typeof item.label !== 'string' ? item.label.label : item.label ?? '',
  113. );
  114. }
  115. if (typeof item.tooltip === 'string') {
  116. item.tooltip = `${item.tooltip}\n\n---\ncontext: ${item.contextValue}\nnode: ${node.toString()}${
  117. parent != null ? `\nparent: ${parent.toString()}` : ''
  118. }`;
  119. } else {
  120. item.tooltip.appendMarkdown(
  121. `\n\n---\n\ncontext: \`${item.contextValue}\`\\\nnode: \`${node.toString()}\`${
  122. parent != null ? `\\\nparent: \`${parent.toString()}\`` : ''
  123. }`,
  124. );
  125. }
  126. }
  127. const getTreeItemFn = this.getTreeItem;
  128. this.getTreeItem = async function (this: ViewBase<RootNode, ViewConfig>, node: ViewNode) {
  129. const item = await getTreeItemFn.apply(this, [node]);
  130. const parent = node.getParent();
  131. if (node.resolveTreeItem != null) {
  132. if (item.tooltip != null) {
  133. addDebuggingInfo(item, node, parent);
  134. }
  135. const resolveTreeItemFn = node.resolveTreeItem;
  136. node.resolveTreeItem = async function (this: ViewBase<RootNode, ViewConfig>, item: TreeItem) {
  137. const resolvedItem = await resolveTreeItemFn.apply(this, [item]);
  138. addDebuggingInfo(resolvedItem, node, parent);
  139. return resolvedItem;
  140. };
  141. } else {
  142. addDebuggingInfo(item, node, parent);
  143. }
  144. return item;
  145. };
  146. }
  147. this.disposables.push(...this.registerCommands());
  148. }
  149. dispose() {
  150. Disposable.from(...this.disposables).dispose();
  151. }
  152. private onReady() {
  153. this.initialize({ showCollapseAll: this.showCollapseAll });
  154. queueMicrotask(() => this.onConfigurationChanged());
  155. }
  156. get canReveal(): boolean {
  157. return true;
  158. }
  159. protected get showCollapseAll(): boolean {
  160. return true;
  161. }
  162. protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
  163. if (!configuration.changed(e, 'views')) return false;
  164. if (configuration.changed(e, `views.${this.configKey}` as const)) return true;
  165. for (const key of viewsCommonConfigKeys) {
  166. if (configuration.changed(e, `views.${key}` as const)) return true;
  167. }
  168. return false;
  169. }
  170. private _title: string | undefined;
  171. get title(): string | undefined {
  172. return this._title;
  173. }
  174. set title(value: string | undefined) {
  175. this._title = value;
  176. if (this.tree != null) {
  177. this.tree.title = value;
  178. }
  179. }
  180. private _description: string | undefined;
  181. get description(): string | undefined {
  182. return this._description;
  183. }
  184. set description(value: string | undefined) {
  185. this._description = value;
  186. if (this.tree != null) {
  187. this.tree.description = value;
  188. }
  189. }
  190. private _message: string | undefined;
  191. get message(): string | undefined {
  192. return this._message;
  193. }
  194. set message(value: string | undefined) {
  195. this._message = value;
  196. if (this.tree != null) {
  197. this.tree.message = value;
  198. }
  199. }
  200. getQualifiedCommand(command: string) {
  201. return `${this.id}.${command}`;
  202. }
  203. protected abstract getRoot(): RootNode;
  204. protected abstract registerCommands(): Disposable[];
  205. protected onConfigurationChanged(e?: ConfigurationChangeEvent): void {
  206. if (e != null && this.root != null) {
  207. void this.refresh(true);
  208. }
  209. }
  210. protected initialize(options: { showCollapseAll?: boolean } = {}) {
  211. this.tree = window.createTreeView<ViewNode<View>>(this.id, {
  212. ...options,
  213. treeDataProvider: this,
  214. });
  215. this.disposables.push(
  216. configuration.onDidChange(e => {
  217. if (!this.filterConfigurationChanged(e)) return;
  218. this._config = undefined;
  219. this.onConfigurationChanged(e);
  220. }, this),
  221. this.tree,
  222. this.tree.onDidChangeVisibility(debounce(this.onVisibilityChanged, 250), this),
  223. this.tree.onDidCollapseElement(this.onElementCollapsed, this),
  224. this.tree.onDidExpandElement(this.onElementExpanded, this),
  225. );
  226. this._title = this.tree.title;
  227. }
  228. protected ensureRoot(force: boolean = false) {
  229. if (this.root == null || force) {
  230. this.root = this.getRoot();
  231. }
  232. return this.root;
  233. }
  234. getChildren(node?: ViewNode): ViewNode[] | Promise<ViewNode[]> {
  235. if (node != null) return node.getChildren();
  236. const root = this.ensureRoot();
  237. return root.getChildren();
  238. }
  239. getParent(node: ViewNode): ViewNode | undefined {
  240. return node.getParent();
  241. }
  242. getTreeItem(node: ViewNode): TreeItem | Promise<TreeItem> {
  243. return node.getTreeItem();
  244. }
  245. resolveTreeItem(item: TreeItem, node: ViewNode): TreeItem | Promise<TreeItem> {
  246. return node.resolveTreeItem?.(item) ?? item;
  247. }
  248. protected onElementCollapsed(e: TreeViewExpansionEvent<ViewNode>) {
  249. this._onDidChangeNodeCollapsibleState.fire({ ...e, state: TreeItemCollapsibleState.Collapsed });
  250. }
  251. protected onElementExpanded(e: TreeViewExpansionEvent<ViewNode>) {
  252. this._onDidChangeNodeCollapsibleState.fire({ ...e, state: TreeItemCollapsibleState.Expanded });
  253. }
  254. protected onVisibilityChanged(e: TreeViewVisibilityChangeEvent) {
  255. this._onDidChangeVisibility.fire(e);
  256. }
  257. get selection(): readonly ViewNode[] {
  258. if (this.tree == null || this.root == null) return [];
  259. return this.tree.selection;
  260. }
  261. get visible(): boolean {
  262. return this.tree?.visible ?? false;
  263. }
  264. async findNode(
  265. id: string,
  266. options?: {
  267. allowPaging?: boolean;
  268. canTraverse?: (node: ViewNode) => boolean | Promise<boolean>;
  269. maxDepth?: number;
  270. token?: CancellationToken;
  271. },
  272. ): Promise<ViewNode | undefined>;
  273. async findNode(
  274. predicate: (node: ViewNode) => boolean,
  275. options?: {
  276. allowPaging?: boolean;
  277. canTraverse?: (node: ViewNode) => boolean | Promise<boolean>;
  278. maxDepth?: number;
  279. token?: CancellationToken;
  280. },
  281. ): Promise<ViewNode | undefined>;
  282. @log<ViewBase<RootNode, ViewConfig>['findNode']>({
  283. args: {
  284. 0: predicate => (typeof predicate === 'string' ? predicate : '<function>'),
  285. 1: opts => `options=${JSON.stringify({ ...opts, canTraverse: undefined, token: undefined })}`,
  286. },
  287. })
  288. async findNode(
  289. predicate: string | ((node: ViewNode) => boolean),
  290. {
  291. allowPaging = false,
  292. canTraverse,
  293. maxDepth = 2,
  294. token,
  295. }: {
  296. allowPaging?: boolean;
  297. canTraverse?: (node: ViewNode) => boolean | Promise<boolean>;
  298. maxDepth?: number;
  299. token?: CancellationToken;
  300. } = {},
  301. ): Promise<ViewNode | undefined> {
  302. const cc = Logger.getCorrelationContext();
  303. async function find(this: ViewBase<RootNode, ViewConfig>) {
  304. try {
  305. const node = await this.findNodeCoreBFS(
  306. typeof predicate === 'string' ? n => n.id === predicate : predicate,
  307. this.ensureRoot(),
  308. allowPaging,
  309. canTraverse,
  310. maxDepth,
  311. token,
  312. );
  313. return node;
  314. } catch (ex) {
  315. Logger.error(ex, cc);
  316. return undefined;
  317. }
  318. }
  319. if (this.root != null) return find.call(this);
  320. // If we have no root (e.g. never been initialized) force it so the tree will load properly
  321. await this.show({ preserveFocus: true });
  322. // 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
  323. return new Promise<ViewNode | undefined>(resolve => setTimeout(() => resolve(find.call(this)), 100));
  324. }
  325. private async findNodeCoreBFS(
  326. predicate: (node: ViewNode) => boolean,
  327. root: ViewNode,
  328. allowPaging: boolean,
  329. canTraverse: ((node: ViewNode) => boolean | Promise<boolean>) | undefined,
  330. maxDepth: number,
  331. token: CancellationToken | undefined,
  332. ): Promise<ViewNode | undefined> {
  333. const queue: (ViewNode | undefined)[] = [root, undefined];
  334. const defaultPageSize = this.container.config.advanced.maxListItems;
  335. let depth = 0;
  336. let node: ViewNode | undefined;
  337. let children: ViewNode[];
  338. let pagedChildren: ViewNode[];
  339. while (queue.length > 1) {
  340. if (token?.isCancellationRequested) return undefined;
  341. node = queue.shift();
  342. if (node == null) {
  343. depth++;
  344. queue.push(undefined);
  345. if (depth > maxDepth) break;
  346. continue;
  347. }
  348. if (predicate(node)) return node;
  349. if (canTraverse != null) {
  350. const traversable = canTraverse(node);
  351. if (isPromise(traversable)) {
  352. if (!(await traversable)) continue;
  353. } else if (!traversable) {
  354. continue;
  355. }
  356. }
  357. children = await node.getChildren();
  358. if (children.length === 0) continue;
  359. while (node != null && !PageableViewNode.is(node)) {
  360. node = await node.getSplattedChild?.();
  361. }
  362. if (node != null && PageableViewNode.is(node)) {
  363. let child = children.find(predicate);
  364. if (child != null) return child;
  365. if (allowPaging && node.hasMore) {
  366. while (true) {
  367. if (token?.isCancellationRequested) return undefined;
  368. await this.loadMoreNodeChildren(node, defaultPageSize);
  369. pagedChildren = await cancellable(Promise.resolve(node.getChildren()), token ?? 60000, {
  370. onDidCancel: resolve => resolve([]),
  371. });
  372. child = pagedChildren.find(predicate);
  373. if (child != null) return child;
  374. if (!node.hasMore) break;
  375. }
  376. }
  377. // Don't traverse into paged children
  378. continue;
  379. }
  380. queue.push(...children);
  381. }
  382. return undefined;
  383. }
  384. protected async ensureRevealNode(
  385. node: ViewNode,
  386. options?: {
  387. select?: boolean;
  388. focus?: boolean;
  389. expand?: boolean | number;
  390. },
  391. ) {
  392. // Not sure why I need to reveal each parent, but without it the node won't be revealed
  393. const nodes: ViewNode[] = [];
  394. let parent: ViewNode | undefined = node;
  395. while (parent != null) {
  396. nodes.push(parent);
  397. parent = parent.getParent();
  398. }
  399. if (nodes.length > 1) {
  400. nodes.pop();
  401. }
  402. for (const n of nodes.reverse()) {
  403. try {
  404. await this.reveal(n, options);
  405. } catch {}
  406. }
  407. }
  408. @debug()
  409. async refresh(reset: boolean = false) {
  410. await this.root?.refresh?.(reset);
  411. this.triggerNodeChange();
  412. }
  413. @debug<ViewBase<RootNode, ViewConfig>['refreshNode']>({ args: { 0: n => n.toString() } })
  414. async refreshNode(node: ViewNode, reset: boolean = false, force: boolean = false) {
  415. const cancel = await node.refresh?.(reset);
  416. if (!force && cancel === true) return;
  417. this.triggerNodeChange(node);
  418. }
  419. @log<ViewBase<RootNode, ViewConfig>['reveal']>({ args: { 0: n => n.toString() } })
  420. async reveal(
  421. node: ViewNode,
  422. options?: {
  423. select?: boolean;
  424. focus?: boolean;
  425. expand?: boolean | number;
  426. },
  427. ) {
  428. if (this.tree == null) return;
  429. try {
  430. await this.tree.reveal(node, options);
  431. } catch (ex) {
  432. Logger.error(ex);
  433. }
  434. }
  435. @log()
  436. async show(options?: { preserveFocus?: boolean }) {
  437. const cc = Logger.getCorrelationContext();
  438. try {
  439. void (await executeCommand(`${this.id}.focus`, options));
  440. } catch (ex) {
  441. Logger.error(ex, cc);
  442. }
  443. }
  444. // @debug({ args: { 0: (n: ViewNode) => n.toString() }, singleLine: true })
  445. getNodeLastKnownLimit(node: PageableViewNode) {
  446. return this._lastKnownLimits.get(node.id);
  447. }
  448. @debug<ViewBase<RootNode, ViewConfig>['loadMoreNodeChildren']>({
  449. args: { 0: n => n.toString(), 2: n => n?.toString() },
  450. })
  451. async loadMoreNodeChildren(
  452. node: ViewNode & PageableViewNode,
  453. limit: number | { until: string | undefined } | undefined,
  454. previousNode?: ViewNode,
  455. ) {
  456. if (previousNode != null) {
  457. void (await this.reveal(previousNode, { select: true }));
  458. }
  459. await node.loadMore(limit);
  460. this._lastKnownLimits.set(node.id, node.limit);
  461. }
  462. @debug<ViewBase<RootNode, ViewConfig>['resetNodeLastKnownLimit']>({
  463. args: { 0: n => n.toString() },
  464. singleLine: true,
  465. })
  466. resetNodeLastKnownLimit(node: PageableViewNode) {
  467. this._lastKnownLimits.delete(node.id);
  468. }
  469. @debug<ViewBase<RootNode, ViewConfig>['triggerNodeChange']>({ args: { 0: n => n?.toString() } })
  470. triggerNodeChange(node?: ViewNode) {
  471. // Since the root node won't actually refresh, force everything
  472. this._onDidChangeTreeData.fire(node != null && node !== this.root ? node : undefined);
  473. }
  474. protected abstract readonly configKey: ViewsConfigKeys;
  475. private _config: (ViewConfig & ViewsCommonConfig) | undefined;
  476. get config(): ViewConfig & ViewsCommonConfig {
  477. if (this._config == null) {
  478. const cfg = { ...this.container.config.views };
  479. for (const view of viewsConfigKeys) {
  480. delete cfg[view];
  481. }
  482. this._config = {
  483. ...(cfg as ViewsCommonConfig),
  484. ...(this.container.config.views[this.configKey] as ViewConfig),
  485. };
  486. }
  487. return this._config;
  488. }
  489. }