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

View File

@@ -0,0 +1,18 @@
// *****************************************************************************
// 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 { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser';
export const EDITOR_CONTEXT_MENU_SCM = [...EDITOR_CONTEXT_MENU, '3_scm'];

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// 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 { ContainerModule } from '@theia/core/shared/inversify';
import { bindScmHistoryModule } from './history/scm-history-frontend-module';
import { ScmFileChangeLabelProvider } from './scm-file-change-label-provider';
import { LabelProviderContribution } from '@theia/core/lib/browser';
export default new ContainerModule(bind => {
bindScmHistoryModule(bind);
bind(ScmFileChangeLabelProvider).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(ScmFileChangeLabelProvider);
});

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// 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 { injectable } from '@theia/core/shared/inversify';
import { ApplicationShellLayoutMigration, WidgetDescription, ApplicationShellLayoutMigrationContext } from '@theia/core/lib/browser/shell/shell-layout-restorer';
import { SCM_HISTORY_ID } from './history/scm-history-contribution';
@injectable()
export class ScmExtraLayoutVersion4Migration implements ApplicationShellLayoutMigration {
readonly layoutVersion = 4.0;
onWillInflateWidget(desc: WidgetDescription, { parent }: ApplicationShellLayoutMigrationContext): WidgetDescription | undefined {
if (desc.constructionOptions.factoryId === 'git-history') {
desc.constructionOptions.factoryId = SCM_HISTORY_ID;
return desc;
}
return undefined;
}
}

View File

@@ -0,0 +1,73 @@
// *****************************************************************************
// Copyright (C) 2019 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 } from '@theia/core/shared/inversify';
import { LabelProviderContribution, DidChangeLabelEvent, LabelProvider } from '@theia/core/lib/browser/label-provider';
import { ScmFileChangeNode } from './scm-file-change-node';
import URI from '@theia/core/lib/common/uri';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
@injectable()
export class ScmFileChangeLabelProvider implements LabelProviderContribution {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(ScmService)
protected readonly scmService: ScmService;
canHandle(element: object): number {
return ScmFileChangeNode.is(element) ? 100 : 0;
}
getIcon(node: ScmFileChangeNode): string {
return this.labelProvider.getIcon(new URI(node.fileChange.uri));
}
getName(node: ScmFileChangeNode): string {
return this.labelProvider.getName(new URI(node.fileChange.uri));
}
getDescription(node: ScmFileChangeNode): string {
return this.relativePath(new URI(node.fileChange.uri).parent);
}
affects(node: ScmFileChangeNode, event: DidChangeLabelEvent): boolean {
return event.affects(new URI(node.fileChange.uri));
}
getCaption(node: ScmFileChangeNode): string {
return node.fileChange.getCaption();
}
relativePath(uri: URI | string): string {
const parsedUri = typeof uri === 'string' ? new URI(uri) : uri;
const repo = this.scmService.findRepository(parsedUri);
if (repo) {
const repositoryUri = new URI(repo.provider.rootUri);
const relativePath = repositoryUri.relative(parsedUri);
if (relativePath) {
return relativePath.toString();
}
}
return this.labelProvider.getLongName(parsedUri);
}
getStatusCaption(node: ScmFileChangeNode): string {
return node.fileChange.getStatusCaption();
}
}

View File

@@ -0,0 +1,45 @@
// *****************************************************************************
// Copyright (C) 2018 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 { ScmCommit } from '@theia/scm/lib/browser/scm-provider';
import URI from '@theia/core/lib/common/uri';
import { isObject } from '@theia/core/lib/common';
export interface ScmFileChangeNode {
readonly fileChange: ScmFileChange;
readonly commitId: string;
selected?: boolean;
}
export namespace ScmFileChangeNode {
export function is(node: unknown): node is ScmFileChangeNode {
return isObject(node) && 'fileChange' in node && 'commitId' in node;
}
}
export interface ScmHistoryCommit extends ScmCommit {
readonly commitDetailUri: URI;
readonly fileChanges: ScmFileChange[];
readonly commitDetailOptions: {};
}
export interface ScmFileChange {
readonly uri: string;
getCaption(): string;
getStatusCaption(): string;
getStatusAbbreviation(): string;
getClassNameForStatus(): string;
getUriToOpen(): URI;
}

View File

@@ -0,0 +1,197 @@
// *****************************************************************************
// 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 { SELECTED_CLASS, Key, Widget } from '@theia/core/lib/browser';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import URI from '@theia/core/lib/common/uri';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { Message } from '@theia/core/shared/@lumino/messaging';
import { ElementExt } from '@theia/core/shared/@lumino/domutils';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import * as React from '@theia/core/shared/react';
import { ScmFileChangeLabelProvider } from './scm-file-change-label-provider';
import { ScmFileChangeNode } from './scm-file-change-node';
@injectable()
export abstract class ScmNavigableListWidget<T extends { selected?: boolean }> extends ReactWidget {
protected scmNodes: T[] = [];
private _scrollContainer: string;
@inject(ScmService) protected readonly scmService: ScmService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(ScmFileChangeLabelProvider) protected readonly scmLabelProvider: ScmFileChangeLabelProvider;
constructor() {
super();
this.node.tabIndex = 0;
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.update();
this.node.focus();
}
protected set scrollContainer(id: string) {
this._scrollContainer = id + Date.now();
}
protected get scrollContainer(): string {
return this._scrollContainer;
}
protected override onUpdateRequest(msg: Message): void {
if (!this.isAttached || !this.isVisible) {
return;
}
super.onUpdateRequest(msg);
(async () => {
const selected = this.node.getElementsByClassName(SELECTED_CLASS)[0];
if (selected) {
const container = document.getElementById(this.scrollContainer);
if (container) {
ElementExt.scrollIntoViewIfNeeded(container, selected);
}
}
})();
}
protected override onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.update();
}
protected getRepositoryLabel(uri: string): string | undefined {
const repository = this.scmService.findRepository(new URI(uri));
const isSelectedRepo = this.scmService.selectedRepository && repository && this.scmService.selectedRepository.provider.rootUri === repository.provider.rootUri;
return repository && !isSelectedRepo ? this.labelProvider.getLongName(new URI(repository.provider.rootUri)) : undefined;
}
protected renderHeaderRow({ name, value, classNames, title }: { name: string, value: React.ReactNode, classNames?: string[], title?: string }): React.ReactNode {
if (!value) {
return;
}
const className = ['header-row', ...(classNames || [])].join(' ');
return <div key={name} className={className} title={title}>
<div className='theia-header'>{name}</div>
<div className='header-value'>{value}</div>
</div>;
}
protected addListNavigationKeyListeners(container: HTMLElement): void {
this.addKeyListener(container, Key.ARROW_LEFT, () => this.navigateLeft());
this.addKeyListener(container, Key.ARROW_RIGHT, () => this.navigateRight());
this.addKeyListener(container, Key.ARROW_UP, () => this.navigateUp());
this.addKeyListener(container, Key.ARROW_DOWN, () => this.navigateDown());
this.addKeyListener(container, Key.ENTER, () => this.handleListEnter());
}
protected navigateLeft(): void {
this.selectPreviousNode();
}
protected navigateRight(): void {
this.selectNextNode();
}
protected navigateUp(): void {
this.selectPreviousNode();
}
protected navigateDown(): void {
this.selectNextNode();
}
protected handleListEnter(): void {
}
protected getSelected(): T | undefined {
return this.scmNodes ? this.scmNodes.find(c => c.selected || false) : undefined;
}
protected selectNode(node: T): void {
const n = this.getSelected();
if (n) {
n.selected = false;
}
node.selected = true;
this.update();
}
protected selectNextNode(): void {
const idx = this.indexOfSelected;
if (idx >= 0 && idx < this.scmNodes.length - 1) {
this.selectNode(this.scmNodes[idx + 1]);
} else if (this.scmNodes.length > 0 && idx === -1) {
this.selectNode(this.scmNodes[0]);
}
}
protected selectPreviousNode(): void {
const idx = this.indexOfSelected;
if (idx > 0) {
this.selectNode(this.scmNodes[idx - 1]);
}
}
protected get indexOfSelected(): number {
if (this.scmNodes && this.scmNodes.length > 0) {
return this.scmNodes.findIndex(c => c.selected || false);
}
return -1;
}
}
export namespace ScmItemComponent {
export interface Props {
labelProvider: LabelProvider;
scmLabelProvider: ScmFileChangeLabelProvider;
change: ScmFileChangeNode;
revealChange: (change: ScmFileChangeNode) => void
selectNode: (change: ScmFileChangeNode) => void
}
}
export class ScmItemComponent extends React.Component<ScmItemComponent.Props> {
override render(): JSX.Element {
const { labelProvider, scmLabelProvider, change } = this.props;
const icon = labelProvider.getIcon(change);
const label = labelProvider.getName(change);
const description = labelProvider.getLongName(change);
const caption = scmLabelProvider.getCaption(change);
const statusCaption = scmLabelProvider.getStatusCaption(change);
return <div className={`scmItem noselect${change.selected ? ' ' + SELECTED_CLASS : ''}`}
onDoubleClick={this.revealChange}
onClick={this.selectNode}>
<span className={icon + ' file-icon'}></span>
<div className='noWrapInfo' title={caption} >
<span className='name'>{label + ' '}</span>
<span className='path'>{description}</span>
</div>
<div
title={caption}
className={change.fileChange.getClassNameForStatus()}>
{statusCaption.charAt(0)}
</div>
</div>;
}
protected readonly revealChange = () => this.props.revealChange(this.props.change);
protected readonly selectNode = () => this.props.selectNode(this.props.change);
}

View File

@@ -0,0 +1,164 @@
/********************************************************************************
* 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
********************************************************************************/
.theia-scm-history .history-container {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
}
.theia-scm-history .listContainer {
flex: 1;
position: relative;
}
.theia-scm-history .commitList {
height: 100%;
}
.theia-scm-history .history-container .noWrapInfo {
width: 100%;
}
.theia-scm-history .commitList .commitListElement {
margin: 3px 0;
}
.theia-scm-history .commitListElement.first .containerHead {
border: none;
}
.theia-scm-history .commitListElement .containerHead {
width: calc(100% - 5px);
height: 50px;
display: flex;
align-items: center;
border-top: 1px solid var(--theia-contrastBorder);
}
.theia-scm-history .commitListElement .containerHead:hover {
background-color: var(--theia-list-hoverBackground);
color: var(--theia-list-hoverForeground);
cursor: pointer;
}
.theia-scm-history:focus-within
.commitListElement
.containerHead.theia-mod-selected {
background: var(--theia-list-focusBackground);
color: var(--theia-list-focusForeground);
}
.theia-scm-history:not(:focus-within)
.commitListElement
.containerHead.theia-mod-selected {
background: var(--theia-list-inactiveFocusBackground);
}
.theia-scm-history .commitListElement .containerHead .headContent {
display: flex;
width: 100%;
box-sizing: border-box;
padding: 0 8px 0 2px;
}
.theia-scm-history
.commitListElement
.containerHead
.headContent
.image-container {
margin-right: 5px;
}
.theia-scm-history
.commitListElement
.containerHead
.headContent
.image-container
img {
width: 27px;
}
.theia-scm-history
.commitListElement
.containerHead
.headContent
.headLabelContainer {
min-width: calc(100% - 93px);
}
.theia-scm-history
.commitListElement
.containerHead
.headContent
.headLabelContainer.singleFileMode {
width: 100%;
}
.theia-scm-history
.commitListElement
.containerHead
.headContent
.expansionToggle {
display: flex;
align-items: center;
}
.theia-scm-history
.commitListElement
.containerHead
.headContent
.detailButton {
display: flex;
align-items: center;
visibility: hidden;
margin: 0 5px;
}
.theia-scm-history
.commitListElement
.containerHead:hover
.headContent
.detailButton {
visibility: visible;
}
.theia-scm-history
.commitListElement
.containerHead
.headContent
.expansionToggle
> .toggle {
display: flex;
background: var(--theia-list-focusBackground);
padding: 5px;
border-radius: 7px;
margin-left: 5px;
align-items: center;
justify-content: flex-end;
min-width: 30px;
color: var(--theia-theia-list-focusForeground);
}
.theia-scm-history .commitTime {
color: var(--theia-descriptionForeground);
font-size: smaller;
}
.theia-scm-history .large-spinner {
font-size: 48px;
}