deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
21
packages/scm-extra/src/browser/history/index.ts
Normal file
21
packages/scm-extra/src/browser/history/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Arm and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './scm-history-provider';
|
||||
import { ScmHistorySupport } from './scm-history-widget';
|
||||
|
||||
export { ScmHistorySupport };
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Command, Event, nls } from '@theia/core';
|
||||
import { OpenViewArguments } from '@theia/core/lib/browser';
|
||||
import { ScmFileChangeNode, ScmHistoryCommit } from '../scm-file-change-node';
|
||||
|
||||
export const SCM_HISTORY_ID = 'scm-history';
|
||||
export const SCM_HISTORY_LABEL = nls.localize('theia/scm/history', 'History');
|
||||
export const SCM_HISTORY_TOGGLE_KEYBINDING = 'alt+h';
|
||||
export const SCM_HISTORY_MAX_COUNT = 100;
|
||||
|
||||
export namespace ScmHistoryCommands {
|
||||
export const OPEN_FILE_HISTORY: Command = {
|
||||
id: 'scm-history:open-file-history',
|
||||
};
|
||||
export const OPEN_BRANCH_HISTORY: Command = {
|
||||
id: 'scm-history:open-branch-history',
|
||||
label: SCM_HISTORY_LABEL
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScmHistoryOpenViewArguments extends OpenViewArguments {
|
||||
uri: string | undefined;
|
||||
}
|
||||
|
||||
export const ScmHistorySupport = Symbol('scm-history-support');
|
||||
export interface ScmHistorySupport {
|
||||
getCommitHistory(options?: HistoryWidgetOptions): Promise<ScmHistoryCommit[]>;
|
||||
readonly onDidChangeHistory: Event<void>;
|
||||
}
|
||||
|
||||
export interface ScmCommitNode {
|
||||
commitDetails: ScmHistoryCommit;
|
||||
authorAvatar: string;
|
||||
fileChangeNodes: ScmFileChangeNode[];
|
||||
expanded: boolean;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export namespace ScmCommitNode {
|
||||
export function is(node: unknown): node is ScmCommitNode {
|
||||
return !!node && typeof node === 'object' && 'commitDetails' in node && 'expanded' in node && 'selected' in node;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HistoryWidgetOptions {
|
||||
range?: {
|
||||
toRevision?: string;
|
||||
fromRevision?: string;
|
||||
};
|
||||
uri?: string;
|
||||
maxCount?: number;
|
||||
}
|
||||
|
||||
export type ScmHistoryListNode = (ScmCommitNode | ScmFileChangeNode);
|
||||
@@ -0,0 +1,90 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { MenuModelRegistry, CommandRegistry, SelectionService } from '@theia/core';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser';
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ScmHistoryWidget } from './scm-history-widget';
|
||||
import { ScmService } from '@theia/scm/lib/browser/scm-service';
|
||||
import { EDITOR_CONTEXT_MENU_SCM } from '../scm-extra-contribution';
|
||||
import { SCM_HISTORY_ID, SCM_HISTORY_LABEL, ScmHistoryCommands, SCM_HISTORY_TOGGLE_KEYBINDING, ScmHistoryOpenViewArguments } from './scm-history-constants';
|
||||
export { SCM_HISTORY_ID, SCM_HISTORY_LABEL, ScmHistoryCommands, SCM_HISTORY_TOGGLE_KEYBINDING, ScmHistoryOpenViewArguments };
|
||||
|
||||
@injectable()
|
||||
export class ScmHistoryContribution extends AbstractViewContribution<ScmHistoryWidget> {
|
||||
|
||||
@inject(SelectionService)
|
||||
protected readonly selectionService: SelectionService;
|
||||
@inject(ScmService)
|
||||
protected readonly scmService: ScmService;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: SCM_HISTORY_ID,
|
||||
widgetName: SCM_HISTORY_LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'left',
|
||||
rank: 500
|
||||
},
|
||||
toggleCommandId: ScmHistoryCommands.OPEN_BRANCH_HISTORY.id,
|
||||
toggleKeybinding: SCM_HISTORY_TOGGLE_KEYBINDING
|
||||
});
|
||||
}
|
||||
|
||||
override async openView(args?: Partial<ScmHistoryOpenViewArguments>): Promise<ScmHistoryWidget> {
|
||||
const widget = await super.openView(args);
|
||||
this.refreshWidget(args!.uri);
|
||||
return widget;
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(NavigatorContextMenu.SEARCH, {
|
||||
commandId: ScmHistoryCommands.OPEN_FILE_HISTORY.id,
|
||||
label: SCM_HISTORY_LABEL
|
||||
});
|
||||
menus.registerMenuAction(EDITOR_CONTEXT_MENU_SCM, {
|
||||
commandId: ScmHistoryCommands.OPEN_FILE_HISTORY.id,
|
||||
label: SCM_HISTORY_LABEL
|
||||
});
|
||||
super.registerMenus(menus);
|
||||
}
|
||||
|
||||
override registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(ScmHistoryCommands.OPEN_FILE_HISTORY, this.newUriAwareCommandHandler({
|
||||
isEnabled: (uri: URI) => !!this.scmService.findRepository(uri),
|
||||
isVisible: (uri: URI) => !!this.scmService.findRepository(uri),
|
||||
execute: async uri => this.openView({ activate: true, uri: uri.toString() }),
|
||||
}));
|
||||
super.registerCommands(commands);
|
||||
}
|
||||
|
||||
protected async refreshWidget(uri: string | undefined): Promise<void> {
|
||||
const widget = this.tryGetWidget();
|
||||
if (!widget) {
|
||||
// the widget doesn't exist, so don't wake it up
|
||||
return;
|
||||
}
|
||||
await widget.setContent({ uri });
|
||||
}
|
||||
|
||||
protected newUriAwareCommandHandler(handler: UriCommandHandler<URI>): UriAwareCommandHandler<URI> {
|
||||
return UriAwareCommandHandler.MonoSelect(this.selectionService, handler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Arm and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { ScmHistoryContribution, SCM_HISTORY_ID } from './scm-history-contribution';
|
||||
import { WidgetFactory, bindViewContribution, ApplicationShellLayoutMigration } from '@theia/core/lib/browser';
|
||||
import { ScmHistoryWidget } from './scm-history-widget';
|
||||
import { ScmExtraLayoutVersion4Migration } from '../scm-extra-layout-migrations';
|
||||
|
||||
import '../../../src/browser/style/history.css';
|
||||
|
||||
export function bindScmHistoryModule(bind: interfaces.Bind): void {
|
||||
|
||||
bind(ScmHistoryWidget).toSelf();
|
||||
bind(WidgetFactory).toDynamicValue(ctx => ({
|
||||
id: SCM_HISTORY_ID,
|
||||
createWidget: () => ctx.container.get<ScmHistoryWidget>(ScmHistoryWidget)
|
||||
}));
|
||||
|
||||
bindViewContribution(bind, ScmHistoryContribution);
|
||||
|
||||
bind(ApplicationShellLayoutMigration).to(ScmExtraLayoutVersion4Migration).inSingletonScope();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Arm and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ScmProvider } from '@theia/scm/lib/browser/scm-provider';
|
||||
import { ScmHistorySupport } from './scm-history-constants';
|
||||
|
||||
export interface ScmHistoryProvider extends ScmProvider {
|
||||
historySupport?: ScmHistorySupport;
|
||||
}
|
||||
export namespace ScmHistoryProvider {
|
||||
export function is(scmProvider: ScmProvider | undefined): scmProvider is ScmHistoryProvider {
|
||||
return !!scmProvider && 'historySupport' in scmProvider;
|
||||
}
|
||||
}
|
||||
571
packages/scm-extra/src/browser/history/scm-history-widget.tsx
Normal file
571
packages/scm-extra/src/browser/history/scm-history-widget.tsx
Normal file
@@ -0,0 +1,571 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { DisposableCollection } from '@theia/core';
|
||||
import { OpenerService, open, StatefulWidget, SELECTED_CLASS, WidgetManager, ApplicationShell, codicon } from '@theia/core/lib/browser';
|
||||
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||
import { Message } from '@theia/core/shared/@lumino/messaging';
|
||||
import { Virtuoso, VirtuosoHandle } from '@theia/core/shared/react-virtuoso';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ScmFileChange, ScmFileChangeNode } from '../scm-file-change-node';
|
||||
import { ScmAvatarService } from '@theia/scm/lib/browser/scm-avatar-service';
|
||||
import { ScmItemComponent, ScmNavigableListWidget } from '../scm-navigable-list-widget';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { ScmHistoryProvider } from './scm-history-provider';
|
||||
import throttle = require('@theia/core/shared/lodash.throttle');
|
||||
import { HistoryWidgetOptions, ScmCommitNode, ScmHistoryListNode, ScmHistorySupport, SCM_HISTORY_ID, SCM_HISTORY_LABEL, SCM_HISTORY_MAX_COUNT } from './scm-history-constants';
|
||||
export { HistoryWidgetOptions, ScmCommitNode, ScmHistoryListNode, ScmHistorySupport };
|
||||
|
||||
@injectable()
|
||||
export class ScmHistoryWidget extends ScmNavigableListWidget<ScmHistoryListNode> implements StatefulWidget {
|
||||
protected options: HistoryWidgetOptions;
|
||||
protected singleFileMode: boolean;
|
||||
private cancelIndicator: CancellationTokenSource;
|
||||
protected listView: ScmHistoryList | undefined;
|
||||
protected hasMoreCommits: boolean;
|
||||
protected allowScrollToSelected: boolean;
|
||||
|
||||
protected status: {
|
||||
state: 'loading',
|
||||
} | {
|
||||
state: 'ready',
|
||||
commits: ScmCommitNode[];
|
||||
} | {
|
||||
state: 'error',
|
||||
errorMessage: React.ReactNode
|
||||
};
|
||||
|
||||
protected readonly toDisposeOnRepositoryChange = new DisposableCollection();
|
||||
|
||||
protected historySupport: ScmHistorySupport | undefined;
|
||||
|
||||
constructor(
|
||||
@inject(OpenerService) protected readonly openerService: OpenerService,
|
||||
@inject(ApplicationShell) protected readonly shell: ApplicationShell,
|
||||
@inject(FileService) protected readonly fileService: FileService,
|
||||
@inject(ScmAvatarService) protected readonly avatarService: ScmAvatarService,
|
||||
@inject(WidgetManager) protected readonly widgetManager: WidgetManager,
|
||||
) {
|
||||
super();
|
||||
this.id = SCM_HISTORY_ID;
|
||||
this.scrollContainer = 'scm-history-list-container';
|
||||
this.title.label = SCM_HISTORY_LABEL;
|
||||
this.title.caption = SCM_HISTORY_LABEL;
|
||||
this.title.iconClass = codicon('history');
|
||||
this.title.closable = true;
|
||||
this.addClass('theia-scm');
|
||||
this.addClass('theia-scm-history');
|
||||
this.status = { state: 'loading' };
|
||||
this.resetState();
|
||||
this.cancelIndicator = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.refreshOnRepositoryChange();
|
||||
this.toDispose.push(this.scmService.onDidChangeSelectedRepository(() => this.refreshOnRepositoryChange()));
|
||||
this.toDispose.push(this.labelProvider.onDidChange(event => {
|
||||
if (this.scmNodes.some(node => ScmFileChangeNode.is(node) && event.affects(new URI(node.fileChange.uri)))) {
|
||||
this.update();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected refreshOnRepositoryChange(): void {
|
||||
this.toDisposeOnRepositoryChange.dispose();
|
||||
|
||||
const repository = this.scmService.selectedRepository;
|
||||
if (repository && ScmHistoryProvider.is(repository.provider)) {
|
||||
this.historySupport = repository.provider.historySupport;
|
||||
if (this.historySupport) {
|
||||
this.toDisposeOnRepositoryChange.push(this.historySupport.onDidChangeHistory(() => this.setContent(this.options)));
|
||||
}
|
||||
} else {
|
||||
this.historySupport = undefined;
|
||||
}
|
||||
this.setContent(this.options);
|
||||
|
||||
// If switching repository, discard options because they are specific to a repository
|
||||
this.options = this.createHistoryOptions();
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
protected createHistoryOptions(): HistoryWidgetOptions {
|
||||
return {
|
||||
maxCount: SCM_HISTORY_MAX_COUNT
|
||||
};
|
||||
}
|
||||
|
||||
protected readonly toDisposeOnRefresh = new DisposableCollection();
|
||||
protected refresh(): void {
|
||||
this.toDisposeOnRefresh.dispose();
|
||||
this.toDispose.push(this.toDisposeOnRefresh);
|
||||
const repository = this.scmService.selectedRepository;
|
||||
this.title.label = SCM_HISTORY_LABEL;
|
||||
if (repository) {
|
||||
this.title.label += ': ' + repository.provider.label;
|
||||
}
|
||||
const area = this.shell.getAreaFor(this);
|
||||
if (area === 'left') {
|
||||
this.shell.leftPanelHandler.refresh();
|
||||
} else if (area === 'right') {
|
||||
this.shell.rightPanelHandler.refresh();
|
||||
}
|
||||
this.update();
|
||||
|
||||
if (repository) {
|
||||
this.toDisposeOnRefresh.push(repository.onDidChange(() => this.update()));
|
||||
// render synchronously to avoid cursor jumping
|
||||
// see https://stackoverflow.com/questions/28922275/in-reactjs-why-does-setstate-behave-differently-when-called-synchronously/28922465#28922465
|
||||
this.toDisposeOnRefresh.push(repository.input.onDidChange(() => this.setContent(this.options)));
|
||||
}
|
||||
}
|
||||
|
||||
protected override onAfterAttach(msg: Message): void {
|
||||
super.onAfterAttach(msg);
|
||||
this.addListNavigationKeyListeners(this.node);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.addEventListener<any>(this.node, 'ps-scroll-y', (e: Event & { target: { scrollTop: number } }) => {
|
||||
if (this.listView?.list) {
|
||||
const { scrollTop } = e.target;
|
||||
this.listView.list.scrollTo({
|
||||
top: scrollTop
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setContent = throttle((options?: HistoryWidgetOptions) => this.doSetContent(options), 100);
|
||||
|
||||
protected async doSetContent(options?: HistoryWidgetOptions): Promise<void> {
|
||||
this.resetState(options);
|
||||
if (options && options.uri) {
|
||||
try {
|
||||
const fileStat = await this.fileService.resolve(new URI(options.uri));
|
||||
this.singleFileMode = !fileStat.isDirectory;
|
||||
} catch {
|
||||
this.singleFileMode = true;
|
||||
}
|
||||
}
|
||||
await this.addCommits(options);
|
||||
this.onDataReady();
|
||||
if (this.scmNodes.length > 0) {
|
||||
this.selectNode(this.scmNodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
protected resetState(options?: HistoryWidgetOptions): void {
|
||||
this.options = options || this.createHistoryOptions();
|
||||
this.hasMoreCommits = true;
|
||||
this.allowScrollToSelected = true;
|
||||
}
|
||||
|
||||
protected async addCommits(options?: HistoryWidgetOptions): Promise<void> {
|
||||
const repository = this.scmService.selectedRepository;
|
||||
|
||||
this.cancelIndicator.cancel();
|
||||
this.cancelIndicator = new CancellationTokenSource();
|
||||
const token = this.cancelIndicator.token;
|
||||
|
||||
if (repository) {
|
||||
if (this.historySupport) {
|
||||
try {
|
||||
const history = await this.historySupport.getCommitHistory(options);
|
||||
if (token.isCancellationRequested || !this.hasMoreCommits) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options && (options.maxCount && history.length < options.maxCount)) {
|
||||
this.hasMoreCommits = false;
|
||||
}
|
||||
|
||||
const avatarCache = new Map<string, string>();
|
||||
|
||||
const commits: ScmCommitNode[] = [];
|
||||
for (const commit of history) {
|
||||
const fileChangeNodes: ScmFileChangeNode[] = [];
|
||||
await Promise.all(commit.fileChanges.map(async fileChange => {
|
||||
fileChangeNodes.push({
|
||||
fileChange, commitId: commit.id
|
||||
});
|
||||
}));
|
||||
|
||||
let avatarUrl = '';
|
||||
if (avatarCache.has(commit.authorEmail)) {
|
||||
avatarUrl = avatarCache.get(commit.authorEmail)!;
|
||||
} else {
|
||||
avatarUrl = await this.avatarService.getAvatar(commit.authorEmail);
|
||||
avatarCache.set(commit.authorEmail, avatarUrl);
|
||||
}
|
||||
|
||||
commits.push({
|
||||
commitDetails: commit,
|
||||
authorAvatar: avatarUrl,
|
||||
fileChangeNodes,
|
||||
expanded: false,
|
||||
selected: false
|
||||
});
|
||||
}
|
||||
this.status = { state: 'ready', commits };
|
||||
} catch (error) {
|
||||
if (options && options.uri && repository) {
|
||||
this.hasMoreCommits = false;
|
||||
}
|
||||
this.status = { state: 'error', errorMessage: <React.Fragment> {error.message} </React.Fragment> };
|
||||
}
|
||||
} else {
|
||||
this.status = { state: 'error', errorMessage: <React.Fragment>History is not supported for {repository.provider.label} source control.</React.Fragment> };
|
||||
}
|
||||
} else {
|
||||
this.status = {
|
||||
state: 'error',
|
||||
errorMessage: <React.Fragment>{nls.localizeByDefault('No source control providers registered.')}</React.Fragment>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async addOrRemoveFileChangeNodes(commit: ScmCommitNode): Promise<void> {
|
||||
const id = this.scmNodes.findIndex(node => node === commit);
|
||||
if (commit.expanded) {
|
||||
this.removeFileChangeNodes(commit, id);
|
||||
} else {
|
||||
await this.addFileChangeNodes(commit, id);
|
||||
}
|
||||
commit.expanded = !commit.expanded;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected async addFileChangeNodes(commit: ScmCommitNode, scmNodesArrayIndex: number): Promise<void> {
|
||||
if (commit.fileChangeNodes) {
|
||||
this.scmNodes.splice(scmNodesArrayIndex + 1, 0, ...commit.fileChangeNodes.map(node =>
|
||||
Object.assign(node, { commitSha: commit.commitDetails.id })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
protected removeFileChangeNodes(commit: ScmCommitNode, scmNodesArrayIndex: number): void {
|
||||
if (commit.fileChangeNodes) {
|
||||
this.scmNodes.splice(scmNodesArrayIndex + 1, commit.fileChangeNodes.length);
|
||||
}
|
||||
}
|
||||
|
||||
storeState(): object {
|
||||
const { options, singleFileMode } = this;
|
||||
return {
|
||||
options,
|
||||
singleFileMode
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
restoreState(oldState: any): void {
|
||||
this.options = oldState['options'];
|
||||
this.options.maxCount = SCM_HISTORY_MAX_COUNT;
|
||||
this.singleFileMode = oldState['singleFileMode'];
|
||||
this.setContent(this.options);
|
||||
}
|
||||
|
||||
protected onDataReady(): void {
|
||||
if (this.status.state === 'ready') {
|
||||
this.scmNodes = this.status.commits;
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
let content: React.ReactNode;
|
||||
switch (this.status.state) {
|
||||
case 'ready':
|
||||
content = < React.Fragment >
|
||||
{this.renderHistoryHeader()}
|
||||
{this.renderCommitList()}
|
||||
</React.Fragment>;
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
const reason: React.ReactNode = this.status.errorMessage;
|
||||
let path: React.ReactNode = '';
|
||||
if (this.options.uri) {
|
||||
const relPathEncoded = this.scmLabelProvider.relativePath(this.options.uri);
|
||||
const relPath = relPathEncoded ? `${decodeURIComponent(relPathEncoded)}` : '';
|
||||
|
||||
const repo = this.scmService.findRepository(new URI(this.options.uri));
|
||||
const repoName = repo ? `${this.labelProvider.getName(new URI(repo.provider.rootUri))}` : '';
|
||||
|
||||
const relPathAndRepo = [relPath, repoName].filter(Boolean).join(
|
||||
` ${nls.localize('theia/git/prepositionIn', 'in')} `
|
||||
);
|
||||
path = `${relPathAndRepo}`;
|
||||
}
|
||||
content = <AlertMessage
|
||||
type='WARNING'
|
||||
header={nls.localize('theia/git/noHistoryForError', 'There is no history available for {0}', `${path}`)}>
|
||||
{reason}
|
||||
</AlertMessage>;
|
||||
break;
|
||||
|
||||
case 'loading':
|
||||
content = <div className='spinnerContainer'>
|
||||
<span className={`${codicon('loading')} theia-animation-spin large-spinner`}></span>
|
||||
</div>;
|
||||
break;
|
||||
}
|
||||
return <div className='history-container'>
|
||||
{content}
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected renderHistoryHeader(): React.ReactNode {
|
||||
if (this.options.uri) {
|
||||
const path = this.scmLabelProvider.relativePath(this.options.uri);
|
||||
const fileName = path.split('/').pop();
|
||||
return <div className='diff-header'>
|
||||
{
|
||||
this.renderHeaderRow({ name: 'repository', value: this.getRepositoryLabel(this.options.uri) })
|
||||
}
|
||||
{
|
||||
this.renderHeaderRow({ name: 'file', value: fileName, title: path })
|
||||
}
|
||||
<div className='theia-header'>
|
||||
Commits
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
protected renderCommitList(): React.ReactNode {
|
||||
const list = <div className='listContainer' id={this.scrollContainer}>
|
||||
<ScmHistoryList
|
||||
ref={listView => this.listView = (listView || undefined)}
|
||||
rows={this.scmNodes}
|
||||
hasMoreRows={this.hasMoreCommits}
|
||||
loadMoreRows={this.loadMoreRows}
|
||||
renderCommit={this.renderCommit}
|
||||
renderFileChangeList={this.renderFileChangeList}
|
||||
></ScmHistoryList>
|
||||
</div>;
|
||||
this.allowScrollToSelected = true;
|
||||
return list;
|
||||
}
|
||||
|
||||
protected readonly loadMoreRows = (index: number) => this.doLoadMoreRows(index);
|
||||
protected doLoadMoreRows(index: number): Promise<void> {
|
||||
let resolver: () => void;
|
||||
const promise = new Promise<void>(resolve => resolver = resolve);
|
||||
const lastRow = this.scmNodes[index - 1];
|
||||
if (ScmCommitNode.is(lastRow)) {
|
||||
const toRevision = lastRow.commitDetails.id;
|
||||
this.addCommits({
|
||||
range: { toRevision },
|
||||
maxCount: SCM_HISTORY_MAX_COUNT,
|
||||
uri: this.options.uri
|
||||
}).then(() => {
|
||||
this.allowScrollToSelected = false;
|
||||
this.onDataReady();
|
||||
resolver();
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
protected readonly renderCommit = (commit: ScmCommitNode) => this.doRenderCommit(commit);
|
||||
protected doRenderCommit(commit: ScmCommitNode): React.ReactNode {
|
||||
let expansionToggleIcon = codicon('chevron-right');
|
||||
if (commit && commit.expanded) {
|
||||
expansionToggleIcon = codicon('chevron-down');
|
||||
}
|
||||
return <div
|
||||
className={`containerHead${commit.selected ? ' ' + SELECTED_CLASS : ''}`}
|
||||
onClick={
|
||||
e => {
|
||||
if (commit.selected && !this.singleFileMode) {
|
||||
this.addOrRemoveFileChangeNodes(commit);
|
||||
} else {
|
||||
this.selectNode(commit);
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
onDoubleClick={
|
||||
e => {
|
||||
if (this.singleFileMode && commit.fileChangeNodes.length > 0) {
|
||||
this.openFile(commit.fileChangeNodes[0].fileChange);
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className='headContent'><div className='image-container'>
|
||||
<img className='gravatar' src={commit.authorAvatar}></img>
|
||||
</div>
|
||||
<div className={`headLabelContainer${this.singleFileMode ? ' singleFileMode' : ''}`}>
|
||||
<div className='headLabel noWrapInfo noselect'>
|
||||
{commit.commitDetails.summary}
|
||||
</div>
|
||||
<div className='commitTime noWrapInfo noselect'>
|
||||
{commit.commitDetails.authorDateRelative + ' by ' + commit.commitDetails.authorName}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${codicon('eye')} detailButton`} onClick={() => this.openDetailWidget(commit)}></div>
|
||||
{!this.singleFileMode && <div className='expansionToggle noselect'>
|
||||
<div className='toggle'>
|
||||
<div className='number'>{commit.commitDetails.fileChanges.length.toString()}</div>
|
||||
<div className={'icon ' + expansionToggleIcon}></div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div >;
|
||||
}
|
||||
|
||||
protected async openDetailWidget(commitNode: ScmCommitNode): Promise<void> {
|
||||
const options = {
|
||||
...commitNode.commitDetails.commitDetailOptions,
|
||||
mode: 'reveal'
|
||||
};
|
||||
open(
|
||||
this.openerService,
|
||||
commitNode.commitDetails.commitDetailUri,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
protected readonly renderFileChangeList = (fileChange: ScmFileChangeNode) => this.doRenderFileChangeList(fileChange);
|
||||
protected doRenderFileChangeList(fileChange: ScmFileChangeNode): React.ReactNode {
|
||||
const fileChangeElement: React.ReactNode = this.renderScmItem(fileChange, fileChange.commitId);
|
||||
return fileChangeElement;
|
||||
}
|
||||
|
||||
protected renderScmItem(change: ScmFileChangeNode, commitSha: string): React.ReactNode {
|
||||
return <ScmItemComponent key={change.fileChange.uri.toString()} {...{
|
||||
labelProvider: this.labelProvider,
|
||||
scmLabelProvider: this.scmLabelProvider,
|
||||
change,
|
||||
revealChange: () => this.openFile(change.fileChange),
|
||||
selectNode: () => this.selectNode(change)
|
||||
}} />;
|
||||
}
|
||||
|
||||
protected override navigateLeft(): void {
|
||||
const selected = this.getSelected();
|
||||
if (selected && this.status.state === 'ready') {
|
||||
if (ScmCommitNode.is(selected)) {
|
||||
const idx = this.status.commits.findIndex(c => c.commitDetails.id === selected.commitDetails.id);
|
||||
if (selected.expanded) {
|
||||
this.addOrRemoveFileChangeNodes(selected);
|
||||
} else {
|
||||
if (idx > 0) {
|
||||
this.selectNode(this.status.commits[idx - 1]);
|
||||
}
|
||||
}
|
||||
} else if (ScmFileChangeNode.is(selected)) {
|
||||
const idx = this.status.commits.findIndex(c => c.commitDetails.id === selected.commitId);
|
||||
this.selectNode(this.status.commits[idx]);
|
||||
}
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected override navigateRight(): void {
|
||||
const selected = this.getSelected();
|
||||
if (selected) {
|
||||
if (ScmCommitNode.is(selected) && !selected.expanded && !this.singleFileMode) {
|
||||
this.addOrRemoveFileChangeNodes(selected);
|
||||
} else {
|
||||
this.selectNextNode();
|
||||
}
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected override handleListEnter(): void {
|
||||
const selected = this.getSelected();
|
||||
if (selected) {
|
||||
if (ScmCommitNode.is(selected)) {
|
||||
if (this.singleFileMode) {
|
||||
this.openFile(selected.fileChangeNodes[0].fileChange);
|
||||
} else {
|
||||
this.openDetailWidget(selected);
|
||||
}
|
||||
} else if (ScmFileChangeNode.is(selected)) {
|
||||
this.openFile(selected.fileChange);
|
||||
}
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected openFile(change: ScmFileChange): void {
|
||||
const uriToOpen = change.getUriToOpen();
|
||||
open(this.openerService, uriToOpen, { mode: 'reveal' });
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ScmHistoryList {
|
||||
export interface Props {
|
||||
readonly rows: ScmHistoryListNode[]
|
||||
readonly hasMoreRows: boolean
|
||||
readonly loadMoreRows: (index: number) => Promise<void>
|
||||
readonly renderCommit: (commit: ScmCommitNode) => React.ReactNode
|
||||
readonly renderFileChangeList: (fileChange: ScmFileChangeNode) => React.ReactNode
|
||||
}
|
||||
}
|
||||
export class ScmHistoryList extends React.Component<ScmHistoryList.Props> {
|
||||
list: VirtuosoHandle | undefined;
|
||||
|
||||
protected readonly checkIfRowIsLoaded = (opts: { index: number }) => this.doCheckIfRowIsLoaded(opts);
|
||||
protected doCheckIfRowIsLoaded(opts: { index: number }): boolean {
|
||||
const row = this.props.rows[opts.index];
|
||||
return !!row;
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { hasMoreRows, loadMoreRows, rows } = this.props;
|
||||
return <Virtuoso
|
||||
ref={list => this.list = (list || undefined)}
|
||||
data={rows}
|
||||
itemContent={index => this.renderRow(index)}
|
||||
endReached={hasMoreRows ? loadMoreRows : undefined}
|
||||
overscan={500}
|
||||
style={{
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
protected renderRow(index: number): React.ReactNode {
|
||||
if (this.checkIfRowIsLoaded({ index })) {
|
||||
const row = this.props.rows[index];
|
||||
if (ScmCommitNode.is(row)) {
|
||||
const head = this.props.renderCommit(row);
|
||||
return <div className={`commitListElement${index === 0 ? ' first' : ''}`} >
|
||||
{head}
|
||||
</div>;
|
||||
} else if (ScmFileChangeNode.is(row)) {
|
||||
return <div className='fileChangeListElement'>
|
||||
{this.props.renderFileChangeList(row)}
|
||||
</div>;
|
||||
}
|
||||
} else {
|
||||
return <div className={`commitListElement${index === 0 ? ' first' : ''}`} >
|
||||
<span className={`${codicon('loading')} theia-animation-spin`}></span>
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user