Skip to content

Commit

Permalink
Show file changes as a tree in Source Control view
Browse files Browse the repository at this point in the history
Signed-off-by: Nigel Westbury <nigelipse@miegel.org>
  • Loading branch information
westbury committed Apr 20, 2020
1 parent 983644d commit a6a3905
Show file tree
Hide file tree
Showing 15 changed files with 1,385 additions and 670 deletions.
9 changes: 5 additions & 4 deletions packages/git/src/browser/git-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { WorkspaceService } from '@theia/workspace/lib/browser';
import { GitRepositoryProvider } from './git-repository-provider';
import { GitErrorHandler } from '../browser/git-error-handler';
import { ScmWidget } from '@theia/scm/lib/browser/scm-widget';
import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget';
import { ScmResource, ScmCommand } from '@theia/scm/lib/browser/scm-provider';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { GitPreferences } from './git-preferences';
Expand Down Expand Up @@ -225,8 +226,8 @@ export class GitContribution implements CommandContribution, MenuContribution, T
});

const registerResourceAction = (group: string, action: MenuAction) => {
menus.registerMenuAction(ScmWidget.RESOURCE_INLINE_MENU, action);
menus.registerMenuAction([...ScmWidget.RESOURCE_CONTEXT_MENU, group], action);
menus.registerMenuAction(ScmTreeWidget.RESOURCE_INLINE_MENU, action);
menus.registerMenuAction([...ScmTreeWidget.RESOURCE_CONTEXT_MENU, group], action);
};

registerResourceAction('navigation', {
Expand Down Expand Up @@ -265,8 +266,8 @@ export class GitContribution implements CommandContribution, MenuContribution, T
});

const registerResourceGroupAction = (group: string, action: MenuAction) => {
menus.registerMenuAction(ScmWidget.RESOURCE_GROUP_INLINE_MENU, action);
menus.registerMenuAction([...ScmWidget.RESOURCE_GROUP_CONTEXT_MENU, group], action);
menus.registerMenuAction(ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU, action);
menus.registerMenuAction([...ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU, group], action);
};

registerResourceGroupAction('1_modification', {
Expand Down
11 changes: 8 additions & 3 deletions packages/git/src/browser/git-scm-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,10 @@ export class GitScmProvider implements ScmProvider {
async stage(uri: string): Promise<void> {
try {
const { repository, unstagedChanges, mergeChanges } = this;
const hasUnstagedChanges = unstagedChanges.some(change => change.uri === uri) || mergeChanges.some(change => change.uri === uri);
const resourceUri = new URI(uri);
const hasUnstagedChanges =
unstagedChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri)))
|| mergeChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri)));
if (hasUnstagedChanges) {
// TODO resolve deletion conflicts
// TODO confirm staging of a unresolved file
Expand All @@ -281,7 +284,8 @@ export class GitScmProvider implements ScmProvider {
async unstage(uri: string): Promise<void> {
try {
const { repository, stagedChanges } = this;
if (stagedChanges.some(change => change.uri === uri)) {
const resourceUri = new URI(uri);
if (stagedChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri)))) {
await this.git.unstage(repository, uri);
}
} catch (error) {
Expand All @@ -306,7 +310,8 @@ export class GitScmProvider implements ScmProvider {
async discard(uri: string): Promise<void> {
const { repository } = this;
const status = this.getStatus();
if (!(status && status.changes.some(change => change.uri === uri))) {
const resourceUri = new URI(uri);
if (!(status && status.changes.some(change => resourceUri.isEqualOrParent(new URI(change.uri))))) {
return;
}
// Allow deletion, only iff the same file is not yet in the Git index.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DebugStackFramesWidget } from '@theia/debug/lib/browser/view/debug-stac
import { DebugThreadsWidget } from '@theia/debug/lib/browser/view/debug-threads-widget';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
import { ScmWidget } from '@theia/scm/lib/browser/scm-widget';
import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
import { PluginScmProvider, PluginScmResourceGroup, PluginScmResource } from '../scm-main';
Expand Down Expand Up @@ -131,13 +132,13 @@ export class MenusContributionPointHandler {
} else if (location === 'scm/resourceGroup/context') {
for (const menu of allMenus[location]) {
const inline = menu.group && /^inline/.test(menu.group) || false;
const menuPath = inline ? ScmWidget.RESOURCE_GROUP_INLINE_MENU : ScmWidget.RESOURCE_GROUP_CONTEXT_MENU;
const menuPath = inline ? ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU : ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU;
toDispose.push(this.registerScmMenuAction(menuPath, menu));
}
} else if (location === 'scm/resourceState/context') {
for (const menu of allMenus[location]) {
const inline = menu.group && /^inline/.test(menu.group) || false;
const menuPath = inline ? ScmWidget.RESOURCE_INLINE_MENU : ScmWidget.RESOURCE_CONTEXT_MENU;
const menuPath = inline ? ScmTreeWidget.RESOURCE_INLINE_MENU : ScmTreeWidget.RESOURCE_CONTEXT_MENU;
toDispose.push(this.registerScmMenuAction(menuPath, menu));
}
} else if (location === 'debug/callstack/context') {
Expand Down
2 changes: 1 addition & 1 deletion packages/scm/src/browser/scm-amend-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, S
{
this.state.lastCommit ?
<div>
<div id='lastCommit' className='changesContainer'>
<div id='lastCommit' className='theia-ScmAmend'>
<div className='theia-header scm-theia-header'>
HEAD Commit
</div>
Expand Down
87 changes: 87 additions & 0 deletions packages/scm/src/browser/scm-amend-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/********************************************************************************
* 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 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject } from 'inversify';
import { Message } from '@phosphor/messaging';
import { SelectionService } from '@theia/core/lib/common';
import * as React from 'react';
import {
ContextMenuRenderer, ReactWidget, LabelProvider, KeybindingRegistry, StorageService
} from '@theia/core/lib/browser';
import { ScmService } from './scm-service';
import { ScmAvatarService } from './scm-avatar-service';
import { ScmAmendComponent } from './scm-amend-component';

@injectable()
export class ScmAmendWidget extends ReactWidget {

static ID = 'scm-amend-widget';

@inject(ScmService) protected readonly scmService: ScmService;
@inject(ScmAvatarService) protected readonly avatarService: ScmAvatarService;
@inject(StorageService) protected readonly storageService: StorageService;
@inject(SelectionService) protected readonly selectionService: SelectionService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry;

protected shouldScrollToRow = true;

constructor(
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer,
) {
super();
this.scrollOptions = {
suppressScrollX: true,
minScrollbarLength: 35
};
this.addClass('theia-scm-commit-container');
this.id = ScmAmendWidget.ID;
}

protected onUpdateRequest(msg: Message): void {
if (!this.isAttached || !this.isVisible) {
return;
}
super.onUpdateRequest(msg);
}

protected render(): React.ReactNode {
const repository = this.scmService.selectedRepository;
if (repository && repository.provider.amendSupport) {
return React.createElement(
ScmAmendComponent,
{
key: `amend:${repository.provider.rootUri}`,
style: { flexGrow: 0 },
id: 'amend', // ??? this was hack {this.scrollContainer}
repository: repository,
scmAmendSupport: repository.provider.amendSupport,
setCommitMessage: this.setInputValue,
avatarService: this.avatarService,
storageService: this.storageService,
}
);
}
}

protected setInputValue = (event: React.FormEvent<HTMLTextAreaElement> | React.ChangeEvent<HTMLTextAreaElement> | string) => {
const repository = this.scmService.selectedRepository;
if (repository) {
repository.input.value = typeof event === 'string' ? event : event.currentTarget.value;
}
};

}
170 changes: 170 additions & 0 deletions packages/scm/src/browser/scm-commit-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/********************************************************************************
* 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 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { SelectionService } from '@theia/core/lib/common';
import * as React from 'react';
import TextareaAutosize from 'react-autosize-textarea';
import { ScmInput } from './scm-input';
import {
ContextMenuRenderer, ReactWidget, LabelProvider, KeybindingRegistry, StatefulWidget} from '@theia/core/lib/browser';
import { ScmService } from './scm-service';

@injectable()
export class ScmCommitWidget extends ReactWidget implements StatefulWidget {

static ID = 'scm-commit-widget';

@inject(ScmService) protected readonly scmService: ScmService;
@inject(SelectionService) protected readonly selectionService: SelectionService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry;

protected shouldScrollToRow = true;

/** don't modify DOM use React! only exposed for `focusInput` */
protected readonly inputRef = React.createRef<HTMLTextAreaElement>();

constructor(
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer,
) {
super();
this.scrollOptions = {
suppressScrollX: true,
minScrollbarLength: 35
};
this.addClass('theia-ScmCommit');
this.id = ScmCommitWidget.ID;
}

@postConstruct()
protected init(): void {

}

protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.focus();
}

public focus(): boolean {
(this.inputRef.current || this.node).focus();
return true; // returns false to set parent node focus - tidy this up
}

protected onUpdateRequest(msg: Message): void {
if (!this.isAttached || !this.isVisible) {
return;
}
super.onUpdateRequest(msg);
}

protected render(): React.ReactNode {
const repository = this.scmService.selectedRepository;
if (repository) {
return React.createElement('div', this.createContainerAttributes(), this.renderInput(repository.input));
}
}

/**
* Create the container attributes for the widget.
*/
protected createContainerAttributes(): React.HTMLAttributes<HTMLElement> {
return {
style: { flexGrow: 0 }
};
}

protected renderInput(input: ScmInput): React.ReactNode {
const validationStatus = input.issue ? input.issue.type : 'idle';
const validationMessage = input.issue ? input.issue.message : '';
const format = (value: string, ...args: string[]): string => {
if (args.length !== 0) {
return value.replace(/{(\d+)}/g, (found, n) => {
const i = parseInt(n);
return isNaN(i) || i < 0 || i >= args.length ? found : args[i];
});
}
return value;
};

const keybinding = this.keybindings.acceleratorFor(this.keybindings.getKeybindingsForCommand('scm.acceptInput')[0]).join('+');
const message = format(input.placeholder || '', keybinding);
return <div className={ScmCommitWidget.Styles.INPUT_MESSAGE_CONTAINER}>
<TextareaAutosize
className={`${ScmCommitWidget.Styles.INPUT_MESSAGE} theia-input theia-scm-input-message-${validationStatus}`}
id={ScmCommitWidget.Styles.INPUT_MESSAGE}
placeholder={message}
autoFocus={true}
value={input.value}
onChange={this.setInputValue}
ref={this.inputRef}
rows={1}
maxRows={6} /* from VS Code */>
</TextareaAutosize>
<div
className={
`${ScmCommitWidget.Styles.VALIDATION_MESSAGE} ${ScmCommitWidget.Styles.NO_SELECT}
theia-scm-validation-message-${validationStatus} theia-scm-input-message-${validationStatus}`
}
style={{
display: !!input.issue ? 'block' : 'none'
}}>{validationMessage}</div>
</div>;
}

protected setInputValue = (event: React.FormEvent<HTMLTextAreaElement> | React.ChangeEvent<HTMLTextAreaElement> | string) => {
const repository = this.scmService.selectedRepository;
if (repository) {
repository.input.value = typeof event === 'string' ? event : event.currentTarget.value;
}
};

/**
* Store the tree state.
*/
storeState(): object {
const message = this.inputRef.current ? this.inputRef.current.value : '';
const state: object = {
message
};
return state;
}

/**
* Restore the state.
* @param oldState the old state object.
*/
restoreState(oldState: object): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { message } = (oldState as any);
if (message && this.inputRef.current) {
this.inputRef.current.value = message;
}
}

}

export namespace ScmCommitWidget {

export namespace Styles {
export const INPUT_MESSAGE_CONTAINER = 'theia-scm-input-message-container';
export const INPUT_MESSAGE = 'theia-scm-input-message';
export const VALIDATION_MESSAGE = 'theia-scm-input-validation-message';
export const NO_SELECT = 'no-select';
}
}
Loading

0 comments on commit a6a3905

Please sign in to comment.