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>;
|
||||
}
|
||||
};
|
||||
}
|
||||
18
packages/scm-extra/src/browser/scm-extra-contribution.ts
Normal file
18
packages/scm-extra/src/browser/scm-extra-contribution.ts
Normal 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'];
|
||||
27
packages/scm-extra/src/browser/scm-extra-frontend-module.ts
Normal file
27
packages/scm-extra/src/browser/scm-extra-frontend-module.ts
Normal 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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
45
packages/scm-extra/src/browser/scm-file-change-node.ts
Normal file
45
packages/scm-extra/src/browser/scm-file-change-node.ts
Normal 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;
|
||||
}
|
||||
197
packages/scm-extra/src/browser/scm-navigable-list-widget.tsx
Normal file
197
packages/scm-extra/src/browser/scm-navigable-list-widget.tsx
Normal 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);
|
||||
}
|
||||
164
packages/scm-extra/src/browser/style/history.css
Normal file
164
packages/scm-extra/src/browser/style/history.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user