Skip to content

Commit

Permalink
Add Dirty Diff Peek View
Browse files Browse the repository at this point in the history
  • Loading branch information
pisv committed Feb 2, 2024
1 parent 028b2e9 commit 37a783d
Show file tree
Hide file tree
Showing 21 changed files with 1,293 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { inject, injectable } from '@theia/core/shared/inversify';
import { DirtyDiffDecorator } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-decorator';
import { DirtyDiffNavigator } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-navigator';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
import { DirtyDiffManager } from './dirty-diff-manager';

Expand All @@ -25,10 +26,13 @@ export class DirtyDiffContribution implements FrontendApplicationContribution {
constructor(
@inject(DirtyDiffManager) protected readonly dirtyDiffManager: DirtyDiffManager,
@inject(DirtyDiffDecorator) protected readonly dirtyDiffDecorator: DirtyDiffDecorator,
@inject(DirtyDiffNavigator) protected readonly dirtyDiffNavigator: DirtyDiffNavigator,
) { }

onStart(app: FrontendApplication): void {
this.dirtyDiffManager.onDirtyDiffUpdate(update => this.dirtyDiffDecorator.applyDecorations(update));
this.dirtyDiffManager.onDirtyDiffUpdate(update => {
this.dirtyDiffDecorator.applyDecorations(update);
this.dirtyDiffNavigator.handleDirtyDiffUpdate(update);
});
}

}
38 changes: 24 additions & 14 deletions packages/git/src/browser/dirty-diff/dirty-diff-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,14 @@ export class DirtyDiffManager {
}

protected createPreviousFileRevision(fileUri: URI): DirtyDiffModel.PreviousFileRevision {
const getOriginalUri = (staged: boolean): URI => {
const query = staged ? '' : 'HEAD';
return fileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery(query);
};
return <DirtyDiffModel.PreviousFileRevision>{
fileUri,
getContents: async (staged: boolean) => {
const query = staged ? '' : 'HEAD';
const uri = fileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery(query);
const uri = getOriginalUri(staged);
const gitResource = await this.gitResourceResolver.getResource(uri);
return gitResource.readContents();
},
Expand All @@ -115,7 +118,8 @@ export class DirtyDiffManager {
return this.git.lsFiles(repository, fileUri.toString(), { errorUnmatch: true });
}
return false;
}
},
getOriginalUri
};
}

Expand All @@ -128,7 +132,6 @@ export class DirtyDiffManager {
await model.handleGitStatusUpdate(repository, changes);
}
}

}

export class DirtyDiffModel implements Disposable {
Expand All @@ -137,7 +140,7 @@ export class DirtyDiffModel implements Disposable {

protected enabled = true;
protected staged: boolean;
protected previousContent: ContentLines | undefined;
protected previousContent: DirtyDiffModel.PreviousRevisionContent | undefined;
protected currentContent: ContentLines | undefined;

protected readonly onDirtyDiffUpdateEmitter = new Emitter<DirtyDiffUpdate>();
Expand Down Expand Up @@ -200,7 +203,7 @@ export class DirtyDiffModel implements Disposable {
// a new update task should be scheduled anyway.
return;
}
const dirtyDiffUpdate = <DirtyDiffUpdate>{ editor, ...dirtyDiff };
const dirtyDiffUpdate = <DirtyDiffUpdate>{ editor, previousRevisionUri: previous.uri, ...dirtyDiff };
this.onDirtyDiffUpdateEmitter.fire(dirtyDiffUpdate);
}, 100);
}
Expand Down Expand Up @@ -251,9 +254,13 @@ export class DirtyDiffModel implements Disposable {
return modelUri.startsWith(repoUri) && this.previousRevision.isVersionControlled();
}

protected async getPreviousRevisionContent(): Promise<ContentLines | undefined> {
const contents = await this.previousRevision.getContents(this.staged);
return contents ? ContentLines.fromString(contents) : undefined;
protected async getPreviousRevisionContent(): Promise<DirtyDiffModel.PreviousRevisionContent | undefined> {
const { previousRevision, staged } = this;
const contents = await previousRevision.getContents(staged);
if (contents) {
const uri = previousRevision.getOriginalUri?.(staged);
return { ...ContentLines.fromString(contents), uri };
}
}

dispose(): void {
Expand All @@ -275,23 +282,26 @@ export namespace DirtyDiffModel {
*/
export function computeDirtyDiff(previous: ContentLines, current: ContentLines): DirtyDiff | undefined {
try {
return diffComputer.computeDirtyDiff(ContentLines.arrayLike(previous), ContentLines.arrayLike(current));
return diffComputer.computeDirtyDiff(ContentLines.arrayLike(previous), ContentLines.arrayLike(current),
{ rangeMappings: true });
} catch {
return undefined;
}
}

export function documentContentLines(document: TextEditorDocument): ContentLines {
return {
length: document.lineCount,
getLineContent: line => document.getLineContent(line + 1),
};
return ContentLines.fromTextEditorDocument(document);
}

export interface PreviousFileRevision {
readonly fileUri: URI;
getContents(staged: boolean): Promise<string>;
isVersionControlled(): Promise<boolean>;
getOriginalUri?(staged: boolean): URI;
}

export interface PreviousRevisionContent extends ContentLines {
readonly uri?: URI;
}

}
90 changes: 89 additions & 1 deletion packages/git/src/browser/git-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
TabBarToolbarRegistry
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { EditorContextMenu, EditorManager, EditorOpenerOptions, EditorWidget } from '@theia/editor/lib/browser';
import { Git, GitFileChange, GitFileStatus } from '../common';
import { Git, GitFileChange, GitFileStatus, GitWatcher, Repository } from '../common';
import { GitRepositoryTracker } from './git-repository-tracker';
import { GitAction, GitQuickOpenService } from './git-quick-open-service';
import { GitSyncService } from './git-sync-service';
Expand All @@ -42,6 +42,8 @@ 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 { ScmCommand, ScmResource } from '@theia/scm/lib/browser/scm-provider';
import { LineRange } from '@theia/scm/lib/browser/dirty-diff/diff-computer';
import { DirtyDiffWidget, SCM_CHANGE_TITLE_MENU } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { GitPreferences } from './git-preferences';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
Expand Down Expand Up @@ -166,6 +168,18 @@ export namespace GIT_COMMANDS {
label: 'Stage All Changes',
iconClass: codicon('add')
}, 'vscode.git/package/command.stageAll', GIT_CATEGORY_KEY);
export const STAGE_CHANGE = Command.toLocalizedCommand({
id: 'git.stage.change',
category: GIT_CATEGORY,
label: 'Stage Change',
iconClass: codicon('add')
}, 'vscode.git/package/command.stageChange', GIT_CATEGORY_KEY);
export const REVERT_CHANGE = Command.toLocalizedCommand({
id: 'git.revert.change',
category: GIT_CATEGORY,
label: 'Revert Change',
iconClass: codicon('discard')
}, 'vscode.git/package/command.revertChange', GIT_CATEGORY_KEY);
export const UNSTAGE = Command.toLocalizedCommand({
id: 'git.unstage',
category: GIT_CATEGORY,
Expand Down Expand Up @@ -280,6 +294,7 @@ export class GitContribution implements CommandContribution, MenuContribution, T
@inject(GitPreferences) protected readonly gitPreferences: GitPreferences;
@inject(DecorationsService) protected readonly decorationsService: DecorationsService;
@inject(GitDecorationProvider) protected readonly gitDecorationProvider: GitDecorationProvider;
@inject(GitWatcher) protected readonly gitWatcher: GitWatcher;

onStart(): void {
this.updateStatusBar();
Expand Down Expand Up @@ -385,6 +400,15 @@ export class GitContribution implements CommandContribution, MenuContribution, T
commandId: GIT_COMMANDS.DISCARD_ALL.id,
when: 'scmProvider == git && scmResourceGroup == workingTree || scmProvider == git && scmResourceGroup == untrackedChanges',
});

menus.registerMenuAction(SCM_CHANGE_TITLE_MENU, {
commandId: GIT_COMMANDS.STAGE_CHANGE.id,
when: 'scmProvider == git'
});
menus.registerMenuAction(SCM_CHANGE_TITLE_MENU, {
commandId: GIT_COMMANDS.REVERT_CHANGE.id,
when: 'scmProvider == git'
});
}

registerCommands(registry: CommandRegistry): void {
Expand Down Expand Up @@ -573,6 +597,14 @@ export class GitContribution implements CommandContribution, MenuContribution, T
isEnabled: widget => this.workspaceService.opened && (!widget || widget instanceof ScmWidget) && !this.repositoryProvider.selectedRepository,
isVisible: widget => this.workspaceService.opened && (!widget || widget instanceof ScmWidget) && !this.repositoryProvider.selectedRepository
});
registry.registerCommand(GIT_COMMANDS.STAGE_CHANGE, {
execute: (widget: DirtyDiffWidget) => this.withProgress(() => this.stageChange(widget)),
isEnabled: widget => widget instanceof DirtyDiffWidget
});
registry.registerCommand(GIT_COMMANDS.REVERT_CHANGE, {
execute: (widget: DirtyDiffWidget) => this.withProgress(() => this.revertChange(widget)),
isEnabled: widget => widget instanceof DirtyDiffWidget
});
}
async amend(): Promise<void> {
{
Expand Down Expand Up @@ -922,6 +954,62 @@ export class GitContribution implements CommandContribution, MenuContribution, T

}

async stageChange(widget: DirtyDiffWidget): Promise<void> {
const scmRepository = this.repositoryProvider.selectedScmRepository;
if (!scmRepository) {
return;
}

const repository = scmRepository.provider.repository;

const path = Repository.relativePath(repository, widget.uri)?.toString();
if (!path) {
return;
}

const { currentChange } = widget;
if (!currentChange) {
return;
}

const dataToStage = await widget.getContentWithSelectedChanges(change => change === currentChange);

try {
const hash = (await this.git.exec(repository, ['hash-object', '--stdin', '-w', '--path', path], { stdin: dataToStage, stdinEncoding: 'utf8' })).stdout.trim();

let mode = (await this.git.exec(repository, ['ls-files', '--format=%(objectmode)', '--', path])).stdout.split('\n').filter(line => !!line.trim())[0];
if (!mode) {
mode = '100644'; // regular non-executable file
}

await this.git.exec(repository, ['update-index', '--add', '--cacheinfo', mode, hash, path]);

// enforce a notification as there would be no status update if the file had been staged already
this.gitWatcher.onGitChanged({ source: repository, status: await this.git.status(repository) });
} catch (error) {
this.gitErrorHandler.handleError(error);
}

widget.editor.cursor = LineRange.getStartPosition(currentChange.currentRange);
}

async revertChange(widget: DirtyDiffWidget): Promise<void> {
const { currentChange } = widget;
if (!currentChange) {
return;
}

const editor = widget.editor.getControl();
editor.pushUndoStop();
editor.executeEdits('Revert Change', [{
range: editor.getModel()!.getFullModelRange(),
text: await widget.getContentWithSelectedChanges(change => change !== currentChange)
}]);
editor.pushUndoStop();

widget.editor.cursor = LineRange.getStartPosition(currentChange.currentRange);
}

/**
* It should be aligned with https://code.visualstudio.com/api/references/theme-color#git-colors
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/git/src/node/git-repository-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ export class GitRepositoryWatcher implements Disposable {
} else {
const idleTimeout = this.watching ? 5000 : /* super long */ 1000 * 60 * 60 * 24;
await new Promise<void>(resolve => {
this.idle = true;
const id = setTimeout(resolve, idleTimeout);
this.interruptIdle = () => { clearTimeout(id); resolve(); };
}).then(() => {
this.idle = false;
this.interruptIdle = undefined;
});
}
Expand Down
1 change: 0 additions & 1 deletion packages/monaco/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
.monaco-editor .zone-widget {
position: absolute;
z-index: 10;
background-color: var(--theia-editorWidget-background);
}

.monaco-editor .zone-widget .zone-widget-container {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import { URI as CodeUri } from '@theia/core/shared/vscode-uri';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import { DirtyDiffWidget } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget';
import { ChangeRangeMapping, LineRange, NormalizedEmptyLineRange } from '@theia/scm/lib/browser/dirty-diff/diff-computer';
import { IChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/smartLinesDiffComputer';
import { TimelineItem } from '@theia/timeline/lib/common/timeline-model';
import { ScmCommandArg, TimelineCommandArg, TreeViewItemReference } from '../../../common';
import { TestItemReference, TestMessageArg } from '../../../common/test-types';
Expand Down Expand Up @@ -105,6 +108,7 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter {
['scm/resourceState/context', toScmArgs],
['scm/title', () => [this.toScmArg(this.scmService.selectedRepository)]],
['testing/message/context', toTestMessageArgs],
['scm/change/title', (...args) => this.toScmChangeArgs(...args)],
['timeline/item/context', (...args) => this.toTimelineArgs(...args)],
['view/item/context', (...args) => this.toTreeArgs(...args)],
['view/title', noArgs],
Expand Down Expand Up @@ -226,6 +230,41 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter {
}
}

protected toScmChangeArgs(...args: any[]): any[] {
const arg = args[0];
if (arg instanceof DirtyDiffWidget) {
const toIChange = (change: ChangeRangeMapping): IChange => {
const convert = (range: LineRange | NormalizedEmptyLineRange): [number, number] => {
let startLineNumber;
let endLineNumber;
if (!LineRange.isEmpty(range)) {
startLineNumber = range.start + 1;
endLineNumber = range.end + 1;
} else {
startLineNumber = range.start === 0 ? 0 : range.end + 1;
endLineNumber = 0;
}
return [startLineNumber, endLineNumber];
};
const { previousRange, currentRange } = change;
const [originalStartLineNumber, originalEndLineNumber] = convert(previousRange);
const [modifiedStartLineNumber, modifiedEndLineNumber] = convert(currentRange);
return {
originalStartLineNumber,
originalEndLineNumber,
modifiedStartLineNumber,
modifiedEndLineNumber
};
};
return [
arg.uri['codeUri'],
arg.changes.map(toIChange),
arg.currentChangeIndex
];
}
return [];
}

protected toTimelineArgs(...args: any[]): any[] {
const timelineArgs: any[] = [];
const arg = args[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DebugVariablesWidget } from '@theia/debug/lib/browser/view/debug-variab
import { EditorWidget, EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser';
import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution';
import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget';
import { PLUGIN_SCM_CHANGE_TITLE_MENU } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget';
import { TIMELINE_ITEM_CONTEXT_MENU } from '@theia/timeline/lib/browser/timeline-tree-widget';
import { COMMENT_CONTEXT, COMMENT_THREAD_CONTEXT, COMMENT_TITLE } from '../comments/comment-thread-widget';
import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-view-widget';
Expand All @@ -52,6 +53,7 @@ export const implementedVSCodeContributionPoints = [
'editor/title/run',
'editor/lineNumber/context',
'explorer/context',
'scm/change/title',
'scm/resourceFolder/context',
'scm/resourceGroup/context',
'scm/resourceState/context',
Expand Down Expand Up @@ -80,6 +82,7 @@ export const codeToTheiaMappings = new Map<ContributionPoint, MenuPath[]>([
['editor/title/run', [PLUGIN_EDITOR_TITLE_RUN_MENU]],
['editor/lineNumber/context', [EDITOR_LINENUMBER_CONTEXT_MENU]],
['explorer/context', [NAVIGATOR_CONTEXT_MENU]],
['scm/change/title', [PLUGIN_SCM_CHANGE_TITLE_MENU]],
['scm/resourceFolder/context', [ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU]],
['scm/resourceGroup/context', [ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU]],
['scm/resourceState/context', [ScmTreeWidget.RESOURCE_CONTEXT_MENU]],
Expand Down
2 changes: 2 additions & 0 deletions packages/scm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"@theia/core": "1.46.0",
"@theia/editor": "1.46.0",
"@theia/filesystem": "1.46.0",
"@theia/monaco": "1.46.0",
"@theia/monaco-editor-core": "1.72.3",
"@types/diff": "^3.2.2",
"diff": "^3.4.0",
"p-debounce": "^2.1.0",
Expand Down
Loading

0 comments on commit 37a783d

Please sign in to comment.