Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dirty Diff Peek View #13104

Merged
merged 14 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@

- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)

<!-- ## not yet released
## not yet released

<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a> -->
- [scm] added support for dirty diff peek view [#13104](https://github.com/eclipse-theia/theia/pull/13104)

<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a>
- [scm] revised some of the dirty diff related types [#13104](https://github.com/eclipse-theia/theia/pull/13104)
- replaced `DirtyDiff.added/removed/modified` with `changes`, which provides more detailed information about the changes
- changed the semantics of `LineRange` to represent a range that spans up to but not including the `end` line (previously, it included the `end` line)
- changed the signature of `DirtyDiffDecorator.toDeltaDecoration(LineRange | number, EditorDecorationOptions)` to `toDeltaDecoration(Change)`

## v1.48.0 - 03/28/2024

Expand Down Expand Up @@ -95,7 +101,7 @@
- Moved `ThemaIcon` and `ThemeColor` to the common folder
- Minor typing adjustments in QuickPickService: in parti
- FileUploadService: moved id field from data transfer item to the corresponding file info
- The way we instantiate monaco services has changed completely: if you touch monaco services in your code, please read the description in the
- The way we instantiate monaco services has changed completely: if you touch monaco services in your code, please read the description in the
file comment in `monaco-init.ts`.

## v1.46.0 - 01/25/2024
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/browser/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable
setEncoding(encoding: string, mode: EncodingMode): void;

readonly onEncodingChanged: Event<string>;

shouldDisplayDirtyDiff(): boolean;
}

export interface Dimension {
Expand Down
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);
});
}

}
45 changes: 29 additions & 16 deletions packages/git/src/browser/dirty-diff/dirty-diff-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ export class DirtyDiffManager {

protected async handleEditorCreated(editorWidget: EditorWidget): Promise<void> {
const editor = editorWidget.editor;
const uri = editor.uri.toString();
if (editor.uri.scheme !== 'file') {
if (!this.supportsDirtyDiff(editor)) {
return;
}
const toDispose = new DisposableCollection();
const model = this.createNewModel(editor);
toDispose.push(model);
const uri = editor.uri.toString();
this.models.set(uri, model);
toDispose.push(editor.onDocumentContentChanged(throttle((event: TextDocumentChangeEvent) => model.handleDocumentChanged(event.document), 1000)));
editorWidget.disposed.connect(() => {
Expand All @@ -93,6 +93,10 @@ export class DirtyDiffManager {
model.handleDocumentChanged(editor.document);
}

protected supportsDirtyDiff(editor: TextEditor): boolean {
return editor.uri.scheme === 'file' && editor.shouldDisplayDirtyDiff();
}

protected createNewModel(editor: TextEditor): DirtyDiffModel {
const previousRevision = this.createPreviousFileRevision(editor.uri);
const model = new DirtyDiffModel(editor, this.preferences, previousRevision);
Expand All @@ -101,11 +105,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 +122,8 @@ export class DirtyDiffManager {
return this.git.lsFiles(repository, fileUri.toString(), { errorUnmatch: true });
}
return false;
}
},
getOriginalUri
};
}

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

}

export class DirtyDiffModel implements Disposable {
Expand All @@ -137,7 +144,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 @@ -181,7 +188,7 @@ export class DirtyDiffModel implements Disposable {
update(): void {
const editor = this.editor;
if (!this.shouldRender()) {
this.onDirtyDiffUpdateEmitter.fire({ editor, added: [], removed: [], modified: [] });
this.onDirtyDiffUpdateEmitter.fire({ editor, changes: [] });
return;
}
if (this.updateTimeout) {
Expand All @@ -200,7 +207,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 +258,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 Down Expand Up @@ -282,16 +293,18 @@ export namespace DirtyDiffModel {
}

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
Loading
Loading