Skip to content

Commit

Permalink
Introduce SCM Plugin-Api
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
  • Loading branch information
vinokurig committed Feb 19, 2019
1 parent 2d090d3 commit 20a5ba6
Show file tree
Hide file tree
Showing 18 changed files with 1,554 additions and 27 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ cache:
- packages/preview/node_modules
- packages/process/node_modules
- packages/python/node_modules
- packages/scm/node_modules
- packages/search-in-workspace/node_modules
- packages/task/node_modules
- packages/terminal/node_modules
Expand Down
1 change: 1 addition & 0 deletions examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@theia/preview": "^0.3.19",
"@theia/process": "^0.3.19",
"@theia/python": "^0.3.19",
"@theia/scm": "^0.3.19",
"@theia/search-in-workspace": "^0.3.19",
"@theia/task": "^0.3.19",
"@theia/terminal": "^0.3.19",
Expand Down
1 change: 1 addition & 0 deletions packages/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@theia/filesystem": "^0.3.19",
"@theia/languages": "^0.3.19",
"@theia/navigator": "^0.3.19",
"@theia/scm": "^0.3.19",
"@theia/workspace": "^0.3.19",
"@types/diff": "^3.2.2",
"@types/fs-extra": "^4.0.2",
Expand Down
146 changes: 121 additions & 25 deletions packages/git/src/browser/git-view-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,30 @@
********************************************************************************/
import { injectable, inject } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { DisposableCollection, CommandRegistry, MenuModelRegistry, CommandContribution, MenuContribution, Command } from '@theia/core';
import {
AbstractViewContribution, StatusBar, StatusBarAlignment, DiffUris, StatusBarEntry,
DisposableCollection,
CommandRegistry,
MenuModelRegistry,
CommandContribution,
MenuContribution,
Command,
Emitter
} from '@theia/core';
import {
AbstractViewContribution, StatusBar, DiffUris, StatusBarEntry,
FrontendApplicationContribution, FrontendApplication, Widget
} from '@theia/core/lib/browser';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { EditorManager, EditorWidget, EditorOpenerOptions, EditorContextMenu, EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser';
import { GitFileChange, GitFileStatus } from '../common';
import { GitFileChange, GitFileStatus, Repository } from '../common';
import { GitWidget } from './git-widget';
import { GitRepositoryTracker } from './git-repository-tracker';
import { GitQuickOpenService, GitAction } from './git-quick-open-service';
import { GitSyncService } from './git-sync-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { GitPrompt } from '../common/git-prompt';
import { ScmRepository, ScmService, ScmCommand } from '@theia/scm/lib/browser';
import { GitRepositoryProvider } from './git-repository-provider';

export const GIT_WIDGET_FACTORY_ID = 'git';

Expand Down Expand Up @@ -114,15 +124,23 @@ export class GitViewContribution extends AbstractViewContribution<GitWidget>
static GIT_REPOSITORY_STATUS = 'git-repository-status';
static GIT_SYNC_STATUS = 'git-sync-status';

private static ID_HANDLE = 0;

protected toDispose = new DisposableCollection();

private readonly onDidChangeCommandEmitterMap: Map<string, Emitter<ScmCommand[]>> = new Map();
private readonly onDidChangeRepositoryEmitterMap: Map<string, Emitter<void>> = new Map();
private dirtyRepositories: Repository[] = [];

@inject(StatusBar) protected readonly statusBar: StatusBar;
@inject(EditorManager) protected readonly editorManager: EditorManager;
@inject(GitQuickOpenService) protected readonly quickOpenService: GitQuickOpenService;
@inject(GitRepositoryTracker) protected readonly repositoryTracker: GitRepositoryTracker;
@inject(GitSyncService) protected readonly syncService: GitSyncService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(GitPrompt) protected readonly prompt: GitPrompt;
@inject(ScmService) protected readonly scmService: ScmService;
@inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider;

constructor() {
super({
Expand All @@ -142,17 +160,26 @@ export class GitViewContribution extends AbstractViewContribution<GitWidget>
}

onStart(): void {
this.repositoryProvider.allRepositories.forEach(repository => this.registerScmProvider(repository));
this.dirtyRepositories = this.repositoryProvider.allRepositories;
this.repositoryTracker.onDidChangeRepository(repository => {
if (repository) {
if (this.hasMultipleRepositories()) {
const path = new URI(repository.localUri).path;
this.statusBar.setElement(GitViewContribution.GIT_SELECTED_REPOSITORY, {
text: `$(database) ${path.base}`,
alignment: StatusBarAlignment.LEFT,
priority: 102,
command: GIT_COMMANDS.CHANGE_REPOSITORY.id,
tooltip: path.toString()
});
this.scmService.selectedRepositories.forEach(scmRepo => scmRepo.setSelected(false));
const scmRepository = this.scmService.repositories.find(scmRepo => scmRepo.provider.rootUri === repository.localUri);
if (scmRepository) {
scmRepository.setSelected(true);
}
const onDidChangeCommandEmitter = this.onDidChangeCommandEmitterMap.get(repository.localUri);
if (onDidChangeCommandEmitter) {
onDidChangeCommandEmitter.fire([{
id: GIT_COMMANDS.CHANGE_REPOSITORY.id,
text: `$(database) ${path.base}`,
command: GIT_COMMANDS.CHANGE_REPOSITORY.id,
tooltip: path.toString()
}]);
}
} else {
this.statusBar.removeElement(GitViewContribution.GIT_SELECTED_REPOSITORY);
}
Expand All @@ -163,6 +190,7 @@ export class GitViewContribution extends AbstractViewContribution<GitWidget>
}
});
this.repositoryTracker.onGitEvent(event => {
this.checkNewOrRemovedRepositories();
const { status } = event;
const branch = status.branch ? status.branch : status.currentHead ? status.currentHead.substring(0, 8) : 'NO-HEAD';
let dirty = '';
Expand All @@ -179,15 +207,79 @@ export class GitViewContribution extends AbstractViewContribution<GitWidget>
dirty = '*';
}
}
this.statusBar.setElement(GitViewContribution.GIT_REPOSITORY_STATUS, {
text: `$(code-fork) ${branch}${dirty}`,
alignment: StatusBarAlignment.LEFT,
priority: 101,
command: GIT_COMMANDS.CHECKOUT.id
});
this.updateSyncStatusBarEntry();
const onDidChangeCommandEmitter = this.onDidChangeCommandEmitterMap.get(event.source.localUri);
if (onDidChangeCommandEmitter) {
onDidChangeCommandEmitter.fire([{
id: GIT_COMMANDS.CHECKOUT.id,
text: `$(code-fork) ${branch}${dirty}`,
command: GIT_COMMANDS.CHECKOUT.id
}]);
}
const onDidChangeRepositoryEmitter = this.onDidChangeRepositoryEmitterMap.get(event.source.localUri);
if (onDidChangeRepositoryEmitter) {
onDidChangeRepositoryEmitter.fire(undefined);
}
this.updateSyncStatusBarEntry(event.source.localUri);
});
this.syncService.onDidChange(() => this.updateSyncStatusBarEntry(
this.repositoryProvider.selectedRepository
? this.repositoryProvider.selectedRepository.localUri
: undefined)
);
}

/** Detect and handle added or removed repositories. */
private checkNewOrRemovedRepositories() {
const added =
this.repositoryProvider
.allRepositories
.find(repo => this.dirtyRepositories.every(dirtyRepo => dirtyRepo.localUri !== repo.localUri));
if (added) {
this.registerScmProvider(added);
}
const removed =
this.dirtyRepositories
.find(dirtyRepo => this.repositoryProvider.allRepositories.every(repo => repo.localUri !== dirtyRepo.localUri));
if (removed) {
const removedScmRepo = this.scmService.repositories.find(scmRepo => scmRepo.provider.rootUri === removed.localUri);
if (removedScmRepo) {
removedScmRepo.dispose();
}
}
this.dirtyRepositories = this.repositoryProvider.allRepositories;
}

private registerScmProvider(repository: Repository): ScmRepository {
const uri = repository.localUri;
const disposableCollection = new DisposableCollection();
const onDidChangeStatusBarCommandsEmitter = new Emitter<ScmCommand[]>();
const onDidChangeResourcesEmitter = new Emitter<void>();
const onDidChangeRepositoryEmitter = new Emitter<void>();
this.onDidChangeCommandEmitterMap.set(uri, onDidChangeStatusBarCommandsEmitter);
this.onDidChangeRepositoryEmitterMap.set(uri, onDidChangeRepositoryEmitter);
disposableCollection.push(onDidChangeRepositoryEmitter);
disposableCollection.push(onDidChangeResourcesEmitter);
const dispose = () => {
disposableCollection.dispose();
this.onDidChangeCommandEmitterMap.delete(uri);
this.onDidChangeRepositoryEmitterMap.delete(uri);
};
return this.scmService.registerScmProvider({
label: 'Git',
id: `git_provider_${ GitViewContribution.ID_HANDLE ++ }`,
contextValue: 'git',
onDidChange: onDidChangeRepositoryEmitter.event,
onDidChangeStatusBarCommands: onDidChangeStatusBarCommandsEmitter.event,
onDidChangeResources: onDidChangeRepositoryEmitter.event,
rootUri: uri,
groups: [],
async getOriginalResource() {
return undefined;
},
dispose(): void {
dispose();
}
});
this.syncService.onDidChange(() => this.updateSyncStatusBarEntry());
}

registerMenus(menus: MenuModelRegistry): void {
Expand Down Expand Up @@ -412,14 +504,18 @@ export class GitViewContribution extends AbstractViewContribution<GitWidget>
return this.repositoryTracker.allRepositories.length > 1;
}

protected updateSyncStatusBarEntry(): void {
protected updateSyncStatusBarEntry(repositoryUri: string | undefined): void {
const entry = this.getStatusBarEntry();
if (entry) {
this.statusBar.setElement(GitViewContribution.GIT_SYNC_STATUS, {
alignment: StatusBarAlignment.LEFT,
priority: 100,
...entry
});
if (entry && repositoryUri) {
const onDidChangeCommandEmitter = this.onDidChangeCommandEmitterMap.get(repositoryUri);
if (onDidChangeCommandEmitter) {
onDidChangeCommandEmitter.fire([{
id: 'vcs-sync-status',
text: entry.text,
tooltip: entry.tooltip,
command: entry.command,
}]);
}
} else {
this.statusBar.removeElement(GitViewContribution.GIT_SYNC_STATUS);
}
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@theia/plugin": "^0.3.19",
"@theia/preferences": "^0.3.19",
"@theia/search-in-workspace": "^0.3.19",
"@theia/scm": "^0.3.19",
"@theia/task": "^0.3.19",
"@theia/workspace": "^0.3.19",
"decompress": "^4.2.0",
Expand Down
89 changes: 87 additions & 2 deletions packages/plugin-ext/src/api/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

/* tslint:disable:no-any */

import { Plugin as InternalPlugin } from '../api/plugin-api';
import { createProxyIdentifier, ProxyIdentifier } from './rpc-protocol';
import * as theia from '@theia/plugin';
import { PluginLifecycle, PluginModel, PluginMetadata, PluginPackage } from '../common/plugin-protocol';
Expand Down Expand Up @@ -61,6 +62,7 @@ import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-sch
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
import { DebugProtocol } from 'vscode-debugprotocol';
import { SymbolInformation } from 'vscode-languageserver-types';
import { ScmCommand } from '@theia/scm/lib/browser';

export interface PluginInitData {
plugins: PluginMetadata[];
Expand Down Expand Up @@ -448,6 +450,87 @@ export interface NotificationExt {
$onCancel(id: string): void;
}

export interface ScmExt {
createSourceControl(plugin: InternalPlugin, id: string, label: string, rootUri?: theia.Uri): theia.SourceControl;
getLastInputBox(plugin: InternalPlugin): theia.SourceControlInputBox | undefined;
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, resourceHandle: number): Promise<void>;
$provideOriginalResource(sourceControlHandle: number, uri: string, token: CancellationToken): Promise<UriComponents | undefined>;
}

export interface ScmMain {
$registerSourceControl(sourceControlHandle: number, id: string, label: string, rootUri?: string): Promise<void>
$updateSourceControl(sourceControlHandle: number, features: SourceControlProviderFeatures): Promise<void>;
$unregisterSourceControl(sourceControlHandle: number): Promise<void>;

$registerGroup(sourceControlHandle: number, groupHandle: number, id: string, label: string): Promise<void>;
$updateGroup(sourceControlHandle: number, groupHandle: number, features: SourceControlGroupFeatures): Promise<void>;
$updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): Promise<void>;
$updateResourceState(sourceControlHandle: number, groupHandle: number, resources: SourceControlResourceState[]): Promise<void>;
$unregisterGroup(sourceControlHandle: number, groupHandle: number): Promise<void>;

$setInputBoxValue(sourceControlHandle: number, value: string): Promise<void>;
$setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): Promise<void>;
}

export interface SourceControlProviderFeatures {
hasQuickDiffProvider?: boolean;
count?: number;
commitTemplate?: string;
acceptInputCommand?: ScmCommand;
statusBarCommands?: ScmCommand[];
}

export interface SourceControlGroupFeatures {
hideWhenEmpty: boolean | undefined;
}

export interface SourceControlResourceState {
readonly handle: number
/**
* The uri of the underlying resource inside the workspace.
*/
readonly resourceUri: string;

/**
* The command which should be run when the resource
* state is open in the Source Control viewlet.
*/
readonly command?: Command;

/**
* The decorations for this source control
* resource state.
*/
readonly decorations?: SourceControlResourceDecorations;
}

/**
* The decorations for a [source control resource state](#SourceControlResourceState).
* Can be independently specified for light and dark themes.
*/
export interface SourceControlResourceDecorations {

/**
* Whether the source control resource state should be striked-through in the UI.
*/
readonly strikeThrough?: boolean;

/**
* Whether the source control resource state should be faded in the UI.
*/
readonly faded?: boolean;

/**
* The title for a specific source control resource state.
*/
readonly tooltip?: string;

/**
* The icon path for a specific source control resource state.
*/
readonly iconPath?: string;
}

export interface NotificationMain {
$startProgress(message: string): Promise<string | undefined>;
$stopProgress(id: string): void;
Expand Down Expand Up @@ -1012,7 +1095,8 @@ export const PLUGIN_RPC_CONTEXT = {
STORAGE_MAIN: createProxyIdentifier<StorageMain>('StorageMain'),
TASKS_MAIN: createProxyIdentifier<TasksMain>('TasksMain'),
LANGUAGES_CONTRIBUTION_MAIN: createProxyIdentifier<LanguagesContributionMain>('LanguagesContributionMain'),
DEBUG_MAIN: createProxyIdentifier<DebugMain>('DebugMain')
DEBUG_MAIN: createProxyIdentifier<DebugMain>('DebugMain'),
SCM_MAIN: createProxyIdentifier<ScmMain>('ScmMain')
};

export const MAIN_RPC_CONTEXT = {
Expand All @@ -1034,7 +1118,8 @@ export const MAIN_RPC_CONTEXT = {
STORAGE_EXT: createProxyIdentifier<StorageExt>('StorageExt'),
TASKS_EXT: createProxyIdentifier<TasksExt>('TasksExt'),
LANGUAGES_CONTRIBUTION_EXT: createProxyIdentifier<LanguagesContributionExt>('LanguagesContributionExt'),
DEBUG_EXT: createProxyIdentifier<DebugExt>('DebugExt')
DEBUG_EXT: createProxyIdentifier<DebugExt>('DebugExt'),
SCM_EXT: createProxyIdentifier<ScmExt>('ScmExt')
};

export interface TasksExt {
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-ext/src/main/browser/main-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { TasksMainImpl } from './tasks-main';
import { StorageMainImpl } from './plugin-storage';
import { LanguagesContributionMainImpl } from './languages-contribution-main';
import { DebugMainImpl } from './debug/debug-main';
import { ScmMainImpl } from './scm-main';

export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void {
const commandRegistryMain = new CommandRegistryMainImpl(rpc, container);
Expand Down Expand Up @@ -100,4 +101,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container

const debugMain = new DebugMainImpl(rpc, connectionMain, container);
rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain);

const scmMain = new ScmMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.SCM_MAIN, scmMain);
}
Loading

0 comments on commit 20a5ba6

Please sign in to comment.