deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

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

View File

@@ -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);

View File

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

View File

@@ -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();
}

View File

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

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