dirtydiffDecorator.ts 44.8 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import * as nls from 'vs/nls';
7

J
Joao Moreno 已提交
8
import 'vs/css!./media/dirtydiffDecorator';
J
Joao Moreno 已提交
9
import { ThrottledDelayer, first } from 'vs/base/common/async';
10
import { IDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
11
import { Event, Emitter } from 'vs/base/common/event';
12
import * as ext from 'vs/workbench/common/contributions';
A
Alex Dima 已提交
13
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
14
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
15
import { ITextModelService } from 'vs/editor/common/services/resolverService';
16
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
17
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
18
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
19
import { URI } from 'vs/base/common/uri';
J
Joao Moreno 已提交
20
import { ISCMService, ISCMRepository, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm';
A
Alex Dima 已提交
21
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
J
Joao Moreno 已提交
22
import { registerThemingParticipant, ITheme, ICssStyleCollector, themeColorFromId, IThemeService } from 'vs/platform/theme/common/themeService';
23
import { registerColor, transparent } from 'vs/platform/theme/common/colorRegistry';
24
import { Color, RGBA } from 'vs/base/common/color';
J
Joao Moreno 已提交
25
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
26
import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions';
27
import { PeekViewWidget, getOuterEditor, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/peekView';
J
Joao Moreno 已提交
28
import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
J
Joao Moreno 已提交
29 30
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
J
Joao Moreno 已提交
31 32
import { Position } from 'vs/editor/common/core/position';
import { rot } from 'vs/base/common/numbers';
33
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
J
Joao Moreno 已提交
34
import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
A
Alex Dima 已提交
35
import { IDiffEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions';
J
Joao Moreno 已提交
36
import { Action, IAction, ActionRunner } from 'vs/base/common/actions';
37
import { IActionBarOptions, ActionsOrientation, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
38
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
J
Joao Moreno 已提交
39
import { basename, isEqualOrParent } from 'vs/base/common/resources';
40
import { MenuId, IMenuService, IMenu, MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions';
41
import { createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
M
Matt Bierner 已提交
42
import { IChange, IEditorModel, ScrollType, IEditorContribution, IDiffEditorModel } from 'vs/editor/common/editorCommon';
43
import { OverviewRulerLane, ITextModel, IModelDecorationOptions, MinimapPosition } from 'vs/editor/common/model';
J
Joao Moreno 已提交
44
import { sortedDiff, firstIndex } from 'vs/base/common/arrays';
J
Joao Moreno 已提交
45
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
46
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
J
Joao Moreno 已提交
47
import { ISplice } from 'vs/base/common/sequence';
I
isidor 已提交
48
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
49
import { INotificationService } from 'vs/platform/notification/common/notification';
50
import { createStyleSheet } from 'vs/base/browser/dom';
J
Joao Moreno 已提交
51

J
Joao Moreno 已提交
52 53
class DiffActionRunner extends ActionRunner {

J
Johannes Rieken 已提交
54
	runAction(action: IAction, context: any): Promise<any> {
J
Joao Moreno 已提交
55 56 57 58 59 60 61 62
		if (action instanceof MenuItemAction) {
			return action.run(...context);
		}

		return super.runAction(action, context);
	}
}

J
Joao Moreno 已提交
63
export interface IModelRegistry {
M
Matt Bierner 已提交
64
	getModel(editorModel: IEditorModel): DirtyDiffModel | null;
J
Joao Moreno 已提交
65 66
}

J
Joao Moreno 已提交
67 68
export const isDirtyDiffVisible = new RawContextKey<boolean>('dirtyDiffVisible', false);

J
Joao Moreno 已提交
69
function getChangeHeight(change: IChange): number {
J
Joao Moreno 已提交
70 71 72 73 74 75 76 77 78 79 80 81
	const modified = change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1;
	const original = change.originalEndLineNumber - change.originalStartLineNumber + 1;

	if (change.originalEndLineNumber === 0) {
		return modified;
	} else if (change.modifiedEndLineNumber === 0) {
		return original;
	} else {
		return modified + original;
	}
}

J
Joao Moreno 已提交
82
function getModifiedEndLineNumber(change: IChange): number {
J
Joao Moreno 已提交
83
	if (change.modifiedEndLineNumber === 0) {
J
Joao Moreno 已提交
84
		return change.modifiedStartLineNumber === 0 ? 1 : change.modifiedStartLineNumber;
J
Joao Moreno 已提交
85 86 87 88 89
	} else {
		return change.modifiedEndLineNumber;
	}
}

J
Joao Moreno 已提交
90 91 92 93 94 95 96 97 98
function lineIntersectsChange(lineNumber: number, change: IChange): boolean {
	// deletion at the beginning of the file
	if (lineNumber === 1 && change.modifiedStartLineNumber === 0 && change.modifiedEndLineNumber === 0) {
		return true;
	}

	return lineNumber >= change.modifiedStartLineNumber && lineNumber <= (change.modifiedEndLineNumber || change.modifiedStartLineNumber);
}

J
Joao Moreno 已提交
99 100
class UIEditorAction extends Action {

101
	private editor: ICodeEditor;
102 103 104
	private action: EditorAction;
	private instantiationService: IInstantiationService;

J
Joao Moreno 已提交
105
	constructor(
106
		editor: ICodeEditor,
107
		action: EditorAction,
J
Joao Moreno 已提交
108
		cssClass: string,
109 110
		@IKeybindingService keybindingService: IKeybindingService,
		@IInstantiationService instantiationService: IInstantiationService
J
Joao Moreno 已提交
111
	) {
112 113 114 115 116 117 118 119
		const keybinding = keybindingService.lookupKeybinding(action.id);
		const label = action.label + (keybinding ? ` (${keybinding.getLabel()})` : '');

		super(action.id, label, cssClass);

		this.instantiationService = instantiationService;
		this.action = action;
		this.editor = editor;
J
Joao Moreno 已提交
120 121
	}

J
Johannes Rieken 已提交
122
	run(): Promise<any> {
J
Joao Moreno 已提交
123
		return Promise.resolve(this.instantiationService.invokeFunction(accessor => this.action.run(accessor, this.editor, null)));
J
Joao Moreno 已提交
124 125 126
	}
}

127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
enum ChangeType {
	Modify,
	Add,
	Delete
}

function getChangeType(change: IChange): ChangeType {
	if (change.originalEndLineNumber === 0) {
		return ChangeType.Add;
	} else if (change.modifiedEndLineNumber === 0) {
		return ChangeType.Delete;
	} else {
		return ChangeType.Modify;
	}
}

143
function getChangeTypeColor(theme: ITheme, changeType: ChangeType): Color | undefined {
144 145 146 147 148 149 150
	switch (changeType) {
		case ChangeType.Modify: return theme.getColor(editorGutterModifiedBackground);
		case ChangeType.Add: return theme.getColor(editorGutterAddedBackground);
		case ChangeType.Delete: return theme.getColor(editorGutterDeletedBackground);
	}
}

151
function getOuterEditorFromDiffEditor(accessor: ServicesAccessor): ICodeEditor | null {
J
Joao Moreno 已提交
152 153 154
	const diffEditors = accessor.get(ICodeEditorService).listDiffEditors();

	for (const diffEditor of diffEditors) {
A
Alex Dima 已提交
155
		if (diffEditor.hasTextFocus() && diffEditor instanceof EmbeddedDiffEditorWidget) {
J
Joao Moreno 已提交
156 157 158 159 160 161 162
			return diffEditor.getParentEditor();
		}
	}

	return getOuterEditor(accessor);
}

J
Joao Moreno 已提交
163 164
class DirtyDiffWidget extends PeekViewWidget {

J
Joao Moreno 已提交
165
	private diffEditor!: EmbeddedDiffEditorWidget;
J
Joao Moreno 已提交
166
	private title: string;
J
Joao Moreno 已提交
167
	private menu: IMenu;
J
Joao Moreno 已提交
168 169
	private index: number = 0;
	private change: IChange | undefined;
J
Joao Moreno 已提交
170
	private height: number | undefined = undefined;
J
Joao Moreno 已提交
171
	private contextKeyService: IContextKeyService;
172

J
Joao Moreno 已提交
173 174 175
	constructor(
		editor: ICodeEditor,
		private model: DirtyDiffModel,
176 177
		@IThemeService private readonly themeService: IThemeService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
J
Joao Moreno 已提交
178
		@IMenuService menuService: IMenuService,
179 180
		@IKeybindingService private readonly keybindingService: IKeybindingService,
		@INotificationService private readonly notificationService: INotificationService,
I
isidor 已提交
181
		@IContextKeyService contextKeyService: IContextKeyService,
182
		@IContextMenuService private readonly contextMenuService: IContextMenuService
J
Joao Moreno 已提交
183
	) {
J
Joao Moreno 已提交
184
		super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true });
J
Joao Moreno 已提交
185

M
Matt Bierner 已提交
186
		this._disposables.add(themeService.onThemeChange(this._applyTheme, this));
J
Joao Moreno 已提交
187 188
		this._applyTheme(themeService.getTheme());

J
Joao Moreno 已提交
189
		this.contextKeyService = contextKeyService.createScoped();
M
Matt Bierner 已提交
190
		this.contextKeyService.createKey('originalResourceScheme', this.model.original!.uri.scheme);
J
Joao Moreno 已提交
191
		this.menu = menuService.createMenu(MenuId.SCMChangeContext, this.contextKeyService);
J
Joao Moreno 已提交
192

J
Joao Moreno 已提交
193
		this.create();
M
Matt Bierner 已提交
194
		if (editor.hasModel()) {
B
Benjamin Pasero 已提交
195
			this.title = basename(editor.getModel().uri);
M
Matt Bierner 已提交
196 197 198
		} else {
			this.title = '';
		}
J
Joao Moreno 已提交
199
		this.setTitle(this.title);
J
Joao Moreno 已提交
200

M
Matt Bierner 已提交
201
		this._disposables.add(model.onDidChange(this.renderTitle, this));
J
Joao Moreno 已提交
202 203
	}

J
Joao Moreno 已提交
204 205
	showChange(index: number): void {
		const change = this.model.changes[index];
J
Joao Moreno 已提交
206
		this.index = index;
J
Joao Moreno 已提交
207 208 209
		this.change = change;

		const originalModel = this.model.original;
210 211 212 213 214

		if (!originalModel) {
			return;
		}

J
Joao Moreno 已提交
215
		const onFirstDiffUpdate = Event.once(this.diffEditor.onDidUpdateDiff);
216 217 218 219 220

		// TODO@joao TODO@alex need this setTimeout probably because the
		// non-side-by-side diff still hasn't created the view zones
		onFirstDiffUpdate(() => setTimeout(() => this.revealChange(change), 0));

M
Matt Bierner 已提交
221
		this.diffEditor.setModel(this.model as IDiffEditorModel);
222

J
Joao Moreno 已提交
223
		const position = new Position(getModifiedEndLineNumber(change), 1);
J
Joao Moreno 已提交
224

A
Alex Dima 已提交
225
		const lineHeight = this.editor.getOption(EditorOption.lineHeight);
J
Joao Moreno 已提交
226 227 228
		const editorHeight = this.editor.getLayoutInfo().height;
		const editorHeightInLines = Math.floor(editorHeight / lineHeight);
		const height = Math.min(getChangeHeight(change) + /* padding */ 8, Math.floor(editorHeightInLines / 3));
J
Joao Moreno 已提交
229

J
Joao Moreno 已提交
230
		this.renderTitle();
231 232 233

		const changeType = getChangeType(change);
		const changeTypeColor = getChangeTypeColor(this.themeService.getTheme(), changeType);
J
Joao Moreno 已提交
234
		this.style({ frameColor: changeTypeColor, arrowColor: changeTypeColor });
235

J
Johannes Rieken 已提交
236
		this._actionbarWidget!.context = [this.model.modified!.uri, this.model.changes, index];
J
Joao Moreno 已提交
237
		this.show(position, height);
J
Joao Moreno 已提交
238
		this.editor.focus();
J
Joao Moreno 已提交
239 240 241
	}

	private renderTitle(): void {
J
Joao Moreno 已提交
242
		const detail = this.model.changes.length > 1
243 244
			? nls.localize('changes', "{0} of {1} changes", this.index + 1, this.model.changes.length)
			: nls.localize('change', "{0} of {1} change", this.index + 1, this.model.changes.length);
J
Joao Moreno 已提交
245

J
Joao Moreno 已提交
246
		this.setTitle(this.title, detail);
247 248
	}

J
Joao Moreno 已提交
249 250 251
	protected _fillHead(container: HTMLElement): void {
		super._fillHead(container);

252 253
		const previous = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowPreviousChangeAction(), 'codicon-arrow-up');
		const next = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowNextChangeAction(), 'codicon-arrow-down');
J
Joao Moreno 已提交
254

M
Matt Bierner 已提交
255 256
		this._disposables.add(previous);
		this._disposables.add(next);
J
Johannes Rieken 已提交
257
		this._actionbarWidget!.push([previous, next], { label: false, icon: true });
J
Joao Moreno 已提交
258 259

		const actions: IAction[] = [];
J
Joao Moreno 已提交
260
		this._disposables.add(createAndFillInActionBarActions(this.menu, { shouldForwardArgs: true }, actions));
J
Johannes Rieken 已提交
261
		this._actionbarWidget!.push(actions, { label: false, icon: true });
J
Joao Moreno 已提交
262 263 264
	}

	protected _getActionBarOptions(): IActionBarOptions {
J
Joao Moreno 已提交
265 266 267 268 269 270 271 272 273
		const actionRunner = new DiffActionRunner();

		// close widget on successful action
		actionRunner.onDidRun(e => {
			if (!(e.action instanceof UIEditorAction) && !e.error) {
				this.dispose();
			}
		});

J
Joao Moreno 已提交
274
		return {
J
Joao Moreno 已提交
275
			actionRunner,
276
			actionViewItemProvider: action => this.getActionViewItem(action),
J
Joao Moreno 已提交
277 278 279 280
			orientation: ActionsOrientation.HORIZONTAL_REVERSE
		};
	}

281
	getActionViewItem(action: IAction): IActionViewItem | undefined {
J
Joao Moreno 已提交
282
		if (!(action instanceof MenuItemAction)) {
283
			return undefined;
J
Joao Moreno 已提交
284 285
		}

286
		return new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
J
Joao Moreno 已提交
287 288
	}

289
	protected _fillBody(container: HTMLElement): void {
J
Joao Moreno 已提交
290
		const options: IDiffEditorOptions = {
J
Joao Moreno 已提交
291
			scrollBeyondLastLine: true,
J
Joao Moreno 已提交
292 293 294 295 296 297 298 299 300 301
			scrollbar: {
				verticalScrollbarSize: 14,
				horizontal: 'auto',
				useShadows: true,
				verticalHasArrows: false,
				horizontalHasArrows: false
			},
			overviewRulerLanes: 2,
			fixedOverflowWidgets: true,
			minimap: { enabled: false },
J
Joao Moreno 已提交
302
			renderSideBySide: false,
J
Joao Moreno 已提交
303 304
			readOnly: true,
			ignoreTrimWhitespace: false
J
Joao Moreno 已提交
305 306 307 308 309
		};

		this.diffEditor = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, container, options, this.editor);
	}

J
Joao Moreno 已提交
310 311 312 313 314 315 316 317 318 319 320
	_onWidth(width: number): void {
		if (typeof this.height === 'undefined') {
			return;
		}

		this.diffEditor.layout({ height: this.height, width });
	}

	protected _doLayoutBody(height: number, width: number): void {
		super._doLayoutBody(height, width);
		this.diffEditor.layout({ height, width });
J
Joao Moreno 已提交
321

J
Joao Moreno 已提交
322
		if (typeof this.height === 'undefined' && this.change) {
J
Joao Moreno 已提交
323 324
			this.revealChange(this.change);
		}
J
Joao Moreno 已提交
325 326

		this.height = height;
J
Joao Moreno 已提交
327 328
	}

J
Joao Moreno 已提交
329
	private revealChange(change: IChange): void {
J
Joao Moreno 已提交
330 331 332 333 334 335 336 337 338 339 340 341 342 343
		let start: number, end: number;

		if (change.modifiedEndLineNumber === 0) { // deletion
			start = change.modifiedStartLineNumber;
			end = change.modifiedStartLineNumber + 1;
		} else if (change.originalEndLineNumber > 0) { // modification
			start = change.modifiedStartLineNumber - 1;
			end = change.modifiedEndLineNumber + 1;
		} else { // insertion
			start = change.modifiedStartLineNumber;
			end = change.modifiedEndLineNumber;
		}

		this.diffEditor.revealLinesInCenter(start, end, ScrollType.Immediate);
J
Joao Moreno 已提交
344
	}
J
Joao Moreno 已提交
345 346

	private _applyTheme(theme: ITheme) {
M
Matt Bierner 已提交
347
		const borderColor = theme.getColor(peekViewBorder) || Color.transparent;
J
Joao Moreno 已提交
348 349 350 351
		this.style({
			arrowColor: borderColor,
			frameColor: borderColor,
			headerBackgroundColor: theme.getColor(peekViewTitleBackground) || Color.transparent,
352 353
			primaryHeadingColor: theme.getColor(peekViewTitleForeground),
			secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground)
J
Joao Moreno 已提交
354 355
		});
	}
J
Joao Moreno 已提交
356 357 358 359

	protected revealLine(lineNumber: number) {
		this.editor.revealLineInCenterIfOutsideViewport(lineNumber, ScrollType.Smooth);
	}
J
Joao Moreno 已提交
360 361
}

J
Joao Moreno 已提交
362
export class ShowPreviousChangeAction extends EditorAction {
J
Joao Moreno 已提交
363 364 365

	constructor() {
		super({
J
Joao Moreno 已提交
366 367 368
			id: 'editor.action.dirtydiff.previous',
			label: nls.localize('show previous change', "Show Previous Change"),
			alias: 'Show Previous Change',
369
			precondition: undefined,
370
			kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib }
J
Joao Moreno 已提交
371 372 373
		});
	}

374
	run(accessor: ServicesAccessor, editor: ICodeEditor): void {
J
Joao Moreno 已提交
375 376 377 378 379 380 381
		const outerEditor = getOuterEditorFromDiffEditor(accessor);

		if (!outerEditor) {
			return;
		}

		const controller = DirtyDiffController.get(outerEditor);
J
Joao Moreno 已提交
382 383 384 385 386

		if (!controller) {
			return;
		}

J
Joao Moreno 已提交
387 388 389 390
		if (!controller.canNavigate()) {
			return;
		}

J
Joao Moreno 已提交
391 392 393
		controller.previous();
	}
}
394
registerEditorAction(ShowPreviousChangeAction);
J
Joao Moreno 已提交
395

J
Joao Moreno 已提交
396
export class ShowNextChangeAction extends EditorAction {
J
Joao Moreno 已提交
397 398 399

	constructor() {
		super({
J
Joao Moreno 已提交
400 401 402
			id: 'editor.action.dirtydiff.next',
			label: nls.localize('show next change', "Show Next Change"),
			alias: 'Show Next Change',
403
			precondition: undefined,
404
			kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib }
J
Joao Moreno 已提交
405 406 407
		});
	}

408
	run(accessor: ServicesAccessor, editor: ICodeEditor): void {
J
Joao Moreno 已提交
409 410 411 412 413 414 415
		const outerEditor = getOuterEditorFromDiffEditor(accessor);

		if (!outerEditor) {
			return;
		}

		const controller = DirtyDiffController.get(outerEditor);
J
Joao Moreno 已提交
416 417 418 419 420

		if (!controller) {
			return;
		}

J
Joao Moreno 已提交
421 422 423 424
		if (!controller.canNavigate()) {
			return;
		}

J
Joao Moreno 已提交
425
		controller.next();
J
Joao Moreno 已提交
426 427
	}
}
428
registerEditorAction(ShowNextChangeAction);
J
Joao Moreno 已提交
429

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
// Go to menu
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
	group: '7_change_nav',
	command: {
		id: 'editor.action.dirtydiff.next',
		title: nls.localize({ key: 'miGotoNextChange', comment: ['&& denotes a mnemonic'] }, "Next &&Change")
	},
	order: 1
});

MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
	group: '7_change_nav',
	command: {
		id: 'editor.action.dirtydiff.previous',
		title: nls.localize({ key: 'miGotoPreviousChange', comment: ['&& denotes a mnemonic'] }, "Previous &&Change")
	},
	order: 2
});

J
Joao Moreno 已提交
449 450 451 452 453 454 455
export class MoveToPreviousChangeAction extends EditorAction {

	constructor() {
		super({
			id: 'workbench.action.editor.previousChange',
			label: nls.localize('move to previous change', "Move to Previous Change"),
			alias: 'Move to Previous Change',
456
			precondition: undefined,
457
			kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib }
J
Joao Moreno 已提交
458 459 460 461 462 463
		});
	}

	run(accessor: ServicesAccessor, editor: ICodeEditor): void {
		const outerEditor = getOuterEditorFromDiffEditor(accessor);

M
Matt Bierner 已提交
464
		if (!outerEditor || !outerEditor.hasModel()) {
J
Joao Moreno 已提交
465 466 467 468 469 470 471 472 473 474 475
			return;
		}

		const controller = DirtyDiffController.get(outerEditor);

		if (!controller || !controller.modelRegistry) {
			return;
		}

		const lineNumber = outerEditor.getPosition().lineNumber;
		const model = controller.modelRegistry.getModel(outerEditor.getModel());
J
Joao Moreno 已提交
476

M
Matt Bierner 已提交
477
		if (!model || model.changes.length === 0) {
J
Joao Moreno 已提交
478 479 480
			return;
		}

J
Joao Moreno 已提交
481 482 483
		const index = model.findPreviousClosestChange(lineNumber, false);
		const change = model.changes[index];

J
Joao Moreno 已提交
484 485
		const position = new Position(change.modifiedStartLineNumber, 1);
		outerEditor.setPosition(position);
S
Sribalaji M 已提交
486
		outerEditor.revealPositionInCenter(position);
J
Joao Moreno 已提交
487 488 489 490 491 492 493 494 495 496 497
	}
}
registerEditorAction(MoveToPreviousChangeAction);

export class MoveToNextChangeAction extends EditorAction {

	constructor() {
		super({
			id: 'workbench.action.editor.nextChange',
			label: nls.localize('move to next change', "Move to Next Change"),
			alias: 'Move to Next Change',
498
			precondition: undefined,
499
			kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib }
J
Joao Moreno 已提交
500 501 502 503 504 505
		});
	}

	run(accessor: ServicesAccessor, editor: ICodeEditor): void {
		const outerEditor = getOuterEditorFromDiffEditor(accessor);

M
Matt Bierner 已提交
506
		if (!outerEditor || !outerEditor.hasModel()) {
J
Joao Moreno 已提交
507 508 509 510 511 512 513 514 515 516 517
			return;
		}

		const controller = DirtyDiffController.get(outerEditor);

		if (!controller || !controller.modelRegistry) {
			return;
		}

		const lineNumber = outerEditor.getPosition().lineNumber;
		const model = controller.modelRegistry.getModel(outerEditor.getModel());
J
Joao Moreno 已提交
518

M
Matt Bierner 已提交
519
		if (!model || model.changes.length === 0) {
J
Joao Moreno 已提交
520 521 522
			return;
		}

J
Joao Moreno 已提交
523 524 525
		const index = model.findNextClosestChange(lineNumber, false);
		const change = model.changes[index];

J
Joao Moreno 已提交
526 527
		const position = new Position(change.modifiedStartLineNumber, 1);
		outerEditor.setPosition(position);
528
		outerEditor.revealPositionInCenter(position);
J
Joao Moreno 已提交
529 530 531 532
	}
}
registerEditorAction(MoveToNextChangeAction);

J
Joao Moreno 已提交
533 534
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: 'closeDirtyDiff',
535
	weight: KeybindingWeight.EditorContrib + 50,
J
Joao Moreno 已提交
536 537
	primary: KeyCode.Escape,
	secondary: [KeyMod.Shift | KeyCode.Escape],
J
Joao Moreno 已提交
538
	when: ContextKeyExpr.and(isDirtyDiffVisible),
J
Joao Moreno 已提交
539
	handler: (accessor: ServicesAccessor) => {
J
Joao Moreno 已提交
540 541 542
		const outerEditor = getOuterEditorFromDiffEditor(accessor);

		if (!outerEditor) {
J
Joao Moreno 已提交
543 544 545
			return;
		}

J
Joao Moreno 已提交
546
		const controller = DirtyDiffController.get(outerEditor);
J
Joao Moreno 已提交
547 548 549 550 551 552 553 554 555

		if (!controller) {
			return;
		}

		controller.close();
	}
});

556
export class DirtyDiffController extends Disposable implements IEditorContribution {
J
Joao Moreno 已提交
557

558
	public static readonly ID = 'editor.contrib.dirtydiff';
J
Joao Moreno 已提交
559

560
	static get(editor: ICodeEditor): DirtyDiffController {
J
Joao Moreno 已提交
561 562 563
		return editor.getContribution<DirtyDiffController>(DirtyDiffController.ID);
	}

J
Joao Moreno 已提交
564
	modelRegistry: IModelRegistry | null = null;
J
Joao Moreno 已提交
565

J
Joao Moreno 已提交
566
	private model: DirtyDiffModel | null = null;
J
Joao Moreno 已提交
567
	private widget: DirtyDiffWidget | null = null;
J
Joao Moreno 已提交
568
	private currentIndex: number = -1;
J
Joao Moreno 已提交
569
	private readonly isDirtyDiffVisible!: IContextKey<boolean>;
J
Joao Moreno 已提交
570
	private session: IDisposable = Disposable.None;
J
Joao Moreno 已提交
571
	private mouseDownInfo: { lineNumber: number } | null = null;
J
Joao Moreno 已提交
572
	private enabled = false;
J
Joao Moreno 已提交
573

J
Joao Moreno 已提交
574 575
	constructor(
		private editor: ICodeEditor,
J
Joao Moreno 已提交
576
		@IContextKeyService contextKeyService: IContextKeyService,
577
		@IInstantiationService private readonly instantiationService: IInstantiationService
J
Joao Moreno 已提交
578
	) {
579
		super();
J
Joao Moreno 已提交
580 581 582 583
		this.enabled = !contextKeyService.getContextKeyValue('isInDiffEditor');

		if (this.enabled) {
			this.isDirtyDiffVisible = isDirtyDiffVisible.bindTo(contextKeyService);
584 585 586
			this._register(editor.onMouseDown(e => this.onEditorMouseDown(e)));
			this._register(editor.onMouseUp(e => this.onEditorMouseUp(e)));
			this._register(editor.onDidChangeModel(() => this.close()));
J
Joao Moreno 已提交
587
		}
J
Joao Moreno 已提交
588
	}
J
Joao Moreno 已提交
589

J
Joao Moreno 已提交
590
	canNavigate(): boolean {
M
Matt Bierner 已提交
591
		return this.currentIndex === -1 || (!!this.model && this.model.changes.length > 1);
J
Joao Moreno 已提交
592 593
	}

J
Joao Moreno 已提交
594
	next(lineNumber?: number): void {
J
Joao Moreno 已提交
595
		if (!this.assertWidget()) {
J
Joao Moreno 已提交
596 597
			return;
		}
M
Matt Bierner 已提交
598 599 600
		if (!this.widget || !this.model) {
			return;
		}
J
Joao Moreno 已提交
601

M
Matt Bierner 已提交
602
		if (this.editor.hasModel() && (typeof lineNumber === 'number' || this.currentIndex === -1)) {
J
Joao Moreno 已提交
603
			this.currentIndex = this.model.findNextClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber);
J
Joao Moreno 已提交
604
		} else {
J
Joao Moreno 已提交
605
			this.currentIndex = rot(this.currentIndex + 1, this.model.changes.length);
J
Joao Moreno 已提交
606 607
		}

J
Joao Moreno 已提交
608
		this.widget.showChange(this.currentIndex);
J
Joao Moreno 已提交
609 610
	}

J
Joao Moreno 已提交
611
	previous(lineNumber?: number): void {
J
Joao Moreno 已提交
612
		if (!this.assertWidget()) {
J
Joao Moreno 已提交
613 614
			return;
		}
M
Matt Bierner 已提交
615 616 617
		if (!this.widget || !this.model) {
			return;
		}
J
Joao Moreno 已提交
618

M
Matt Bierner 已提交
619
		if (this.editor.hasModel() && (typeof lineNumber === 'number' || this.currentIndex === -1)) {
J
Joao Moreno 已提交
620
			this.currentIndex = this.model.findPreviousClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber);
J
Joao Moreno 已提交
621
		} else {
J
Joao Moreno 已提交
622
			this.currentIndex = rot(this.currentIndex - 1, this.model.changes.length);
J
Joao Moreno 已提交
623 624
		}

J
Joao Moreno 已提交
625
		this.widget.showChange(this.currentIndex);
J
Joao Moreno 已提交
626 627 628 629
	}

	close(): void {
		this.session.dispose();
J
Joao Moreno 已提交
630
		this.session = Disposable.None;
J
Joao Moreno 已提交
631 632 633
	}

	private assertWidget(): boolean {
J
Joao Moreno 已提交
634 635 636 637
		if (!this.enabled) {
			return false;
		}

J
Joao Moreno 已提交
638
		if (this.widget) {
M
Matt Bierner 已提交
639
			if (!this.model || this.model.changes.length === 0) {
J
Joao Moreno 已提交
640 641 642 643 644 645 646 647 648 649 650
				this.close();
				return false;
			}

			return true;
		}

		if (!this.modelRegistry) {
			return false;
		}

J
Joao Moreno 已提交
651 652 653
		const editorModel = this.editor.getModel();

		if (!editorModel) {
J
Joao Moreno 已提交
654
			return false;
J
Joao Moreno 已提交
655 656
		}

J
Joao Moreno 已提交
657
		const model = this.modelRegistry.getModel(editorModel);
J
Joao Moreno 已提交
658 659

		if (!model) {
J
Joao Moreno 已提交
660 661 662 663 664
			return false;
		}

		if (model.changes.length === 0) {
			return false;
J
Joao Moreno 已提交
665 666
		}

J
Joao Moreno 已提交
667
		this.currentIndex = -1;
J
Joao Moreno 已提交
668
		this.model = model;
J
Joao Moreno 已提交
669
		this.widget = this.instantiationService.createInstance(DirtyDiffWidget, this.editor, model);
J
Joao Moreno 已提交
670 671
		this.isDirtyDiffVisible.set(true);

672
		const disposables = new DisposableStore();
673
		disposables.add(Event.once(this.widget.onDidClose)(this.close, this));
J
Joao Moreno 已提交
674 675 676 677
		Event.chain(model.onDidChange)
			.filter(e => e.diff.length > 0)
			.map(e => e.diff)
			.event(this.onDidModelChange, this, disposables);
678

679 680
		disposables.add(this.widget);
		disposables.add(toDisposable(() => {
681 682 683 684 685 686
			this.model = null;
			this.widget = null;
			this.currentIndex = -1;
			this.isDirtyDiffVisible.set(false);
			this.editor.focus();
		}));
J
Joao Moreno 已提交
687

688
		this.session = disposables;
J
Joao Moreno 已提交
689
		return true;
J
Joao Moreno 已提交
690 691
	}

J
Joao Moreno 已提交
692
	private onDidModelChange(splices: ISplice<IChange>[]): void {
M
Matt Bierner 已提交
693 694 695
		if (!this.model) {
			return;
		}
J
Joao Moreno 已提交
696 697 698 699 700 701
		for (const splice of splices) {
			if (splice.start <= this.currentIndex) {
				if (this.currentIndex < splice.start + splice.deleteCount) {
					this.currentIndex = -1;
					this.next();
				} else {
J
Joao Moreno 已提交
702
					this.currentIndex = rot(this.currentIndex + splice.toInsert.length - splice.deleteCount - 1, this.model.changes.length);
J
Joao Moreno 已提交
703 704 705 706
					this.next();
				}
			}
		}
J
Joao Moreno 已提交
707 708
	}

J
Joao Moreno 已提交
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
	private onEditorMouseDown(e: IEditorMouseEvent): void {
		this.mouseDownInfo = null;

		const range = e.target.range;

		if (!range) {
			return;
		}

		if (!e.event.leftButton) {
			return;
		}

		if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
			return;
		}
M
Matt Bierner 已提交
725 726 727
		if (!e.target.element) {
			return;
		}
728 729 730 731
		if (e.target.element.className.indexOf('dirty-diff-glyph') < 0) {
			return;
		}

J
Joao Moreno 已提交
732
		const data = e.target.detail as IMarginData;
733 734
		const offsetLeftInGutter = (e.target.element as HTMLElement).offsetLeft;
		const gutterOffsetX = data.offsetX - offsetLeftInGutter;
J
Joao Moreno 已提交
735 736

		// TODO@joao TODO@alex TODO@martin this is such that we don't collide with folding
737
		if (gutterOffsetX < -3 || gutterOffsetX > 6) { // dirty diff decoration on hover is 9px wide
J
Joao Moreno 已提交
738 739 740
			return;
		}

J
Joao Moreno 已提交
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
		this.mouseDownInfo = { lineNumber: range.startLineNumber };
	}

	private onEditorMouseUp(e: IEditorMouseEvent): void {
		if (!this.mouseDownInfo) {
			return;
		}

		const { lineNumber } = this.mouseDownInfo;
		this.mouseDownInfo = null;

		const range = e.target.range;

		if (!range || range.startLineNumber !== lineNumber) {
			return;
		}

		if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
			return;
		}

J
Joao Moreno 已提交
762 763 764 765
		if (!this.modelRegistry) {
			return;
		}

J
Joao Moreno 已提交
766 767 768 769 770 771 772 773 774 775 776 777
		const editorModel = this.editor.getModel();

		if (!editorModel) {
			return;
		}

		const model = this.modelRegistry.getModel(editorModel);

		if (!model) {
			return;
		}

J
Joao Moreno 已提交
778 779 780
		const index = firstIndex(model.changes, change => lineIntersectsChange(lineNumber, change));

		if (index < 0) {
J
Joao Moreno 已提交
781 782 783
			return;
		}

J
Joao Moreno 已提交
784 785 786 787 788
		if (index === this.currentIndex) {
			this.close();
		} else {
			this.next(lineNumber);
		}
J
Joao Moreno 已提交
789 790
	}

J
Joao Moreno 已提交
791 792 793 794
	getChanges(): IChange[] {
		if (!this.modelRegistry) {
			return [];
		}
M
Matt Bierner 已提交
795 796 797
		if (!this.editor.hasModel()) {
			return [];
		}
J
Joao Moreno 已提交
798 799 800 801 802 803 804 805 806

		const model = this.modelRegistry.getModel(this.editor.getModel());

		if (!model) {
			return [];
		}

		return model.changes;
	}
J
Joao Moreno 已提交
807
}
808

809
export const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackground', {
J
Joao Moreno 已提交
810 811
	dark: new Color(new RGBA(12, 125, 157)),
	light: new Color(new RGBA(102, 175, 224)),
812
	hc: new Color(new RGBA(0, 155, 249))
813
}, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified."));
814 815

export const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', {
J
Joao Moreno 已提交
816 817
	dark: new Color(new RGBA(88, 124, 12)),
	light: new Color(new RGBA(129, 184, 139)),
818
	hc: new Color(new RGBA(51, 171, 78))
819
}, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added."));
820 821

export const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', {
J
Joao Moreno 已提交
822 823
	dark: new Color(new RGBA(148, 21, 27)),
	light: new Color(new RGBA(202, 75, 81)),
824
	hc: new Color(new RGBA(252, 93, 109))
825
}, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted."));
826

827 828 829
export const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', {
	dark: new Color(new RGBA(12, 125, 157)),
	light: new Color(new RGBA(102, 175, 224)),
830
	hc: new Color(new RGBA(0, 155, 249))
831 832 833 834 835
}, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified."));

export const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', {
	dark: new Color(new RGBA(88, 124, 12)),
	light: new Color(new RGBA(129, 184, 139)),
836
	hc: new Color(new RGBA(51, 171, 78))
837 838 839 840 841
}, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added."));

export const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', {
	dark: new Color(new RGBA(148, 21, 27)),
	light: new Color(new RGBA(202, 75, 81)),
842
	hc: new Color(new RGBA(252, 93, 109))
843 844
}, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted."));

845 846 847
export const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', { dark: transparent(editorGutterModifiedBackground, 0.6), light: transparent(editorGutterModifiedBackground, 0.6), hc: transparent(editorGutterModifiedBackground, 0.6) }, nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.'));
export const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', { dark: transparent(editorGutterAddedBackground, 0.6), light: transparent(editorGutterAddedBackground, 0.6), hc: transparent(editorGutterAddedBackground, 0.6) }, nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.'));
export const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', { dark: transparent(editorGutterDeletedBackground, 0.6), light: transparent(editorGutterDeletedBackground, 0.6), hc: transparent(editorGutterDeletedBackground, 0.6) }, nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.'));
848

849
class DirtyDiffDecorator extends Disposable {
850

851
	static createDecoration(className: string, options: { gutter: boolean, overview: { active: boolean, color: string }, minimap: { active: boolean, color: string }, isWholeLine: boolean }): ModelDecorationOptions {
J
Joao Moreno 已提交
852
		const decorationOptions: IModelDecorationOptions = {
J
Joao Moreno 已提交
853
			isWholeLine: options.isWholeLine,
J
Joao Moreno 已提交
854 855 856 857
		};

		if (options.gutter) {
			decorationOptions.linesDecorationsClassName = `dirty-diff-glyph ${className}`;
858
		}
J
Joao Moreno 已提交
859

860
		if (options.overview.active) {
J
Joao Moreno 已提交
861
			decorationOptions.overviewRuler = {
862
				color: themeColorFromId(options.overview.color),
J
Joao Moreno 已提交
863 864
				position: OverviewRulerLane.Left
			};
865 866
		}

867 868 869 870 871 872 873
		if (options.minimap.active) {
			decorationOptions.minimap = {
				color: themeColorFromId(options.minimap.color),
				position: MinimapPosition.Gutter
			};
		}

J
Joao Moreno 已提交
874 875 876 877 878 879
		return ModelDecorationOptions.createDynamic(decorationOptions);
	}

	private modifiedOptions: ModelDecorationOptions;
	private addedOptions: ModelDecorationOptions;
	private deletedOptions: ModelDecorationOptions;
J
Joao Moreno 已提交
880
	private decorations: string[] = [];
M
Matt Bierner 已提交
881
	private editorModel: ITextModel | null;
J
Joao Moreno 已提交
882 883

	constructor(
M
Matt Bierner 已提交
884
		editorModel: ITextModel,
J
Joao Moreno 已提交
885 886
		private model: DirtyDiffModel,
		@IConfigurationService configurationService: IConfigurationService
J
Joao Moreno 已提交
887
	) {
888
		super();
M
Matt Bierner 已提交
889
		this.editorModel = editorModel;
J
Joao Moreno 已提交
890 891 892
		const decorations = configurationService.getValue<string>('scm.diffDecorations');
		const gutter = decorations === 'all' || decorations === 'gutter';
		const overview = decorations === 'all' || decorations === 'overview';
893
		const minimap = decorations === 'all' || decorations === 'minimap';
J
Joao Moreno 已提交
894

895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912
		this.modifiedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified', {
			gutter,
			overview: { active: overview, color: overviewRulerModifiedForeground },
			minimap: { active: minimap, color: minimapGutterModifiedBackground },
			isWholeLine: true
		});
		this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', {
			gutter,
			overview: { active: overview, color: overviewRulerAddedForeground },
			minimap: { active: minimap, color: minimapGutterAddedBackground },
			isWholeLine: true
		});
		this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', {
			gutter,
			overview: { active: overview, color: overviewRulerDeletedForeground },
			minimap: { active: minimap, color: minimapGutterDeletedBackground },
			isWholeLine: false
		});
J
Joao Moreno 已提交
913

914
		this._register(model.onDidChange(this.onDidChange, this));
J
Joao Moreno 已提交
915 916
	}

J
Joao Moreno 已提交
917
	private onDidChange(): void {
M
Matt Bierner 已提交
918 919 920
		if (!this.editorModel) {
			return;
		}
J
Joao Moreno 已提交
921
		const decorations = this.model.changes.map((change) => {
922
			const changeType = getChangeType(change);
J
Joao Moreno 已提交
923 924 925
			const startLineNumber = change.modifiedStartLineNumber;
			const endLineNumber = change.modifiedEndLineNumber || startLineNumber;

926 927 928 929 930 931 932
			switch (changeType) {
				case ChangeType.Add:
					return {
						range: {
							startLineNumber: startLineNumber, startColumn: 1,
							endLineNumber: endLineNumber, endColumn: 1
						},
J
Joao Moreno 已提交
933
						options: this.addedOptions
934 935 936 937
					};
				case ChangeType.Delete:
					return {
						range: {
J
Joao Moreno 已提交
938 939
							startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE,
							endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE
940
						},
J
Joao Moreno 已提交
941
						options: this.deletedOptions
942 943 944 945 946 947 948
					};
				case ChangeType.Modify:
					return {
						range: {
							startLineNumber: startLineNumber, startColumn: 1,
							endLineNumber: endLineNumber, endColumn: 1
						},
J
Joao Moreno 已提交
949
						options: this.modifiedOptions
950
					};
J
Joao Moreno 已提交
951 952 953 954 955 956 957
			}
		});

		this.decorations = this.editorModel.deltaDecorations(this.decorations, decorations);
	}

	dispose(): void {
958
		super.dispose();
J
Joao Moreno 已提交
959 960 961 962 963 964 965 966 967 968

		if (this.editorModel && !this.editorModel.isDisposed()) {
			this.editorModel.deltaDecorations(this.decorations, []);
		}

		this.editorModel = null;
		this.decorations = [];
	}
}

J
Joao Moreno 已提交
969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990
function compareChanges(a: IChange, b: IChange): number {
	let result = a.modifiedStartLineNumber - b.modifiedStartLineNumber;

	if (result !== 0) {
		return result;
	}

	result = a.modifiedEndLineNumber - b.modifiedEndLineNumber;

	if (result !== 0) {
		return result;
	}

	result = a.originalStartLineNumber - b.originalStartLineNumber;

	if (result !== 0) {
		return result;
	}

	return a.originalEndLineNumber - b.originalEndLineNumber;
}

J
Joao Moreno 已提交
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007
function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => number {
	return (a, b) => {
		const aIsParent = isEqualOrParent(uri, a.rootUri!);
		const bIsParent = isEqualOrParent(uri, b.rootUri!);

		if (aIsParent && bIsParent) {
			return a.rootUri!.fsPath.length - b.rootUri!.fsPath.length;
		} else if (aIsParent) {
			return -1;
		} else if (bIsParent) {
			return 1;
		} else {
			return 0;
		}
	};
}

1008
export class DirtyDiffModel extends Disposable {
J
Joao Moreno 已提交
1009

J
Joao Moreno 已提交
1010
	private _originalModel: ITextModel | null = null;
M
Matt Bierner 已提交
1011 1012
	get original(): ITextModel | null { return this._originalModel; }
	get modified(): ITextModel | null { return this._editorModel; }
1013

M
Matt Bierner 已提交
1014 1015
	private diffDelayer: ThrottledDelayer<IChange[] | null> | null;
	private _originalURIPromise?: Promise<URI | null>;
1016
	private repositoryDisposables = new Set<IDisposable>();
1017
	private readonly originalModelDisposables = this._register(new DisposableStore());
1018

J
Joao Moreno 已提交
1019 1020
	private readonly _onDidChange = new Emitter<{ changes: IChange[], diff: ISplice<IChange>[] }>();
	readonly onDidChange: Event<{ changes: IChange[], diff: ISplice<IChange>[] }> = this._onDidChange.event;
J
Joao Moreno 已提交
1021

J
Joao Moreno 已提交
1022
	private _changes: IChange[] = [];
J
Joao Moreno 已提交
1023
	get changes(): IChange[] { return this._changes; }
J
Joao Moreno 已提交
1024

M
Matt Bierner 已提交
1025 1026
	private _editorModel: ITextModel | null;

1027
	constructor(
M
Matt Bierner 已提交
1028
		editorModel: ITextModel,
1029 1030 1031
		@ISCMService private readonly scmService: ISCMService,
		@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
		@ITextModelService private readonly textModelResolverService: ITextModelService
1032
	) {
1033
		super();
M
Matt Bierner 已提交
1034
		this._editorModel = editorModel;
J
Joao Moreno 已提交
1035
		this.diffDelayer = new ThrottledDelayer<IChange[]>(200);
J
Joao Moreno 已提交
1036

1037 1038
		this._register(editorModel.onDidChangeContent(() => this.triggerDiff()));
		this._register(scmService.onDidAddRepository(this.onDidAddRepository, this));
J
Joao Moreno 已提交
1039 1040 1041 1042 1043 1044
		scmService.repositories.forEach(r => this.onDidAddRepository(r));

		this.triggerDiff();
	}

	private onDidAddRepository(repository: ISCMRepository): void {
1045
		const disposables = new DisposableStore();
J
Joao Moreno 已提交
1046 1047

		this.repositoryDisposables.add(disposables);
1048
		disposables.add(toDisposable(() => this.repositoryDisposables.delete(disposables)));
J
Joao Moreno 已提交
1049

J
Joao Moreno 已提交
1050
		const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources);
1051
		disposables.add(onDidChange(this.triggerDiff, this));
J
Joao Moreno 已提交
1052

J
Joao Moreno 已提交
1053
		const onDidRemoveThis = Event.filter(this.scmService.onDidRemoveRepository, r => r === repository);
1054
		disposables.add(onDidRemoveThis(() => dispose(disposables), null));
J
Joao Moreno 已提交
1055 1056

		this.triggerDiff();
1057 1058
	}

J
Johannes Rieken 已提交
1059
	private triggerDiff(): Promise<any> {
1060
		if (!this.diffDelayer) {
J
Joao Moreno 已提交
1061
			return Promise.resolve(null);
1062 1063
		}

J
Joao Moreno 已提交
1064 1065
		return this.diffDelayer
			.trigger(() => this.diff())
J
Joao Moreno 已提交
1066
			.then((changes: IChange[]) => {
J
Joao Moreno 已提交
1067
				if (!this._editorModel || this._editorModel.isDisposed() || !this._originalModel || this._originalModel.isDisposed()) {
M
Matt Bierner 已提交
1068
					return; // disposed
J
Joao Moreno 已提交
1069
				}
J
Joao Moreno 已提交
1070

1071
				if (this._originalModel.getValueLength() === 0) {
J
Joao Moreno 已提交
1072
					changes = [];
J
Joao Moreno 已提交
1073
				}
1074

J
Joao Moreno 已提交
1075
				const diff = sortedDiff(this._changes, changes, compareChanges);
J
Joao Moreno 已提交
1076
				this._changes = changes;
J
Joao Moreno 已提交
1077
				this._onDidChange.fire({ changes, diff });
J
Joao Moreno 已提交
1078 1079
			});
	}
1080

M
Matt Bierner 已提交
1081
	private diff(): Promise<IChange[] | null> {
J
Joao Moreno 已提交
1082
		return this.getOriginalURIPromise().then(originalURI => {
J
Joao Moreno 已提交
1083
			if (!this._editorModel || this._editorModel.isDisposed() || !originalURI) {
J
Joao Moreno 已提交
1084
				return Promise.resolve([]); // disposed
J
Joao Moreno 已提交
1085
			}
1086

J
Joao Moreno 已提交
1087
			if (!this.editorWorkerService.canComputeDirtyDiff(originalURI, this._editorModel.uri)) {
J
Joao Moreno 已提交
1088
				return Promise.resolve([]); // Files too large
1089 1090
			}

J
Joao Moreno 已提交
1091
			return this.editorWorkerService.computeDirtyDiff(originalURI, this._editorModel.uri, false);
J
Joao Moreno 已提交
1092
		});
1093 1094
	}

M
Matt Bierner 已提交
1095
	private getOriginalURIPromise(): Promise<URI | null> {
J
Joao Moreno 已提交
1096 1097 1098 1099
		if (this._originalURIPromise) {
			return this._originalURIPromise;
		}

J
Joao Moreno 已提交
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112
		this._originalURIPromise = this.getOriginalResource().then(originalUri => {
			if (!this._editorModel) { // disposed
				return null;
			}

			if (!originalUri) {
				this._originalModel = null;
				return null;
			}

			if (this._originalModel && this._originalModel.uri.toString() === originalUri.toString()) {
				return originalUri;
			}
1113

J
Joao Moreno 已提交
1114 1115
			return this.textModelResolverService.createModelReference(originalUri).then(ref => {
				if (!this._editorModel) { // disposed
J
Joao Moreno 已提交
1116 1117 1118
					return null;
				}

J
Joao Moreno 已提交
1119
				this._originalModel = ref.object.textEditorModel;
1120

1121
				this.originalModelDisposables.clear();
1122 1123
				this.originalModelDisposables.add(ref);
				this.originalModelDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => this.triggerDiff()));
J
Joao Moreno 已提交
1124

J
Joao Moreno 已提交
1125
				return originalUri;
J
Joao Moreno 已提交
1126
			});
J
Joao Moreno 已提交
1127
		});
J
Joao Moreno 已提交
1128

J
Joao Moreno 已提交
1129
		return this._originalURIPromise.finally(() => {
M
Matt Bierner 已提交
1130
			this._originalURIPromise = undefined;
J
Joao Moreno 已提交
1131 1132 1133
		});
	}

J
Joao Moreno 已提交
1134
	private async getOriginalResource(): Promise<URI | null> {
J
Joao Moreno 已提交
1135
		if (!this._editorModel) {
J
Joao Moreno 已提交
1136
			return Promise.resolve(null);
J
Joao Moreno 已提交
1137 1138
		}

J
Joao Moreno 已提交
1139
		const uri = this._editorModel.uri;
J
Joao Moreno 已提交
1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
		const providers = this.scmService.repositories.map(r => r.provider);
		const rootedProviders = providers.filter(p => !!p.rootUri);

		rootedProviders.sort(createProviderComparer(uri));

		const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri)));

		if (result) {
			return result;
		}

		const nonRootedProviders = providers.filter(p => !p.rootUri);
		return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri)));
1153 1154
	}

J
Joao Moreno 已提交
1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
	findNextClosestChange(lineNumber: number, inclusive = true): number {
		for (let i = 0; i < this.changes.length; i++) {
			const change = this.changes[i];

			if (inclusive) {
				if (getModifiedEndLineNumber(change) >= lineNumber) {
					return i;
				}
			} else {
				if (change.modifiedStartLineNumber > lineNumber) {
					return i;
				}
			}
		}

		return 0;
	}

	findPreviousClosestChange(lineNumber: number, inclusive = true): number {
		for (let i = this.changes.length - 1; i >= 0; i--) {
			const change = this.changes[i];

			if (inclusive) {
				if (change.modifiedStartLineNumber <= lineNumber) {
					return i;
				}
			} else {
				if (getModifiedEndLineNumber(change) < lineNumber) {
					return i;
				}
			}
		}

		return this.changes.length - 1;
	}

1191
	dispose(): void {
1192
		super.dispose();
J
Joao Moreno 已提交
1193

J
Joao Moreno 已提交
1194
		this._editorModel = null;
1195
		this._originalModel = null;
J
Joao Moreno 已提交
1196

1197 1198 1199 1200
		if (this.diffDelayer) {
			this.diffDelayer.cancel();
			this.diffDelayer = null;
		}
J
Joao Moreno 已提交
1201 1202 1203

		this.repositoryDisposables.forEach(d => dispose(d));
		this.repositoryDisposables.clear();
1204 1205 1206
	}
}

J
Joao Moreno 已提交
1207 1208 1209 1210 1211 1212 1213 1214 1215 1216
class DirtyDiffItem {

	constructor(readonly model: DirtyDiffModel, readonly decorator: DirtyDiffDecorator) { }

	dispose(): void {
		this.decorator.dispose();
		this.model.dispose();
	}
}

J
Joao Moreno 已提交
1217 1218 1219 1220 1221
interface IViewState {
	readonly width: number;
	readonly visibility: 'always' | 'hover';
}

1222
export class DirtyDiffWorkbenchController extends Disposable implements ext.IWorkbenchContribution, IModelRegistry {
1223

1224
	private enabled = false;
J
Joao Moreno 已提交
1225
	private viewState: IViewState = { width: 3, visibility: 'always' };
A
Alex Dima 已提交
1226
	private models: ITextModel[] = [];
J
Joao Moreno 已提交
1227
	private items: { [modelId: string]: DirtyDiffItem; } = Object.create(null);
M
Matt Bierner 已提交
1228
	private readonly transientDisposables = this._register(new DisposableStore());
J
Joao Moreno 已提交
1229
	private stylesheet: HTMLStyleElement;
1230 1231

	constructor(
1232 1233 1234
		@IEditorService private readonly editorService: IEditorService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IConfigurationService private readonly configurationService: IConfigurationService
1235
	) {
1236
		super();
J
Joao Moreno 已提交
1237
		this.stylesheet = createStyleSheet();
1238
		this._register(toDisposable(() => this.stylesheet.parentElement!.removeChild(this.stylesheet)));
1239

J
Joao Moreno 已提交
1240
		const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations'));
1241
		this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this));
J
Joao Moreno 已提交
1242
		this.onDidChangeConfiguration();
1243

J
Joao Moreno 已提交
1244
		const onDidChangeDiffWidthConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterWidth'));
1245 1246
		onDidChangeDiffWidthConfiguration(this.onDidChangeDiffWidthConfiguration, this);
		this.onDidChangeDiffWidthConfiguration();
1247 1248 1249 1250

		const onDidChangeDiffVisibilityConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterVisibility'));
		onDidChangeDiffVisibilityConfiguration(this.onDidChangeDiffVisibiltiyConfiguration, this);
		this.onDidChangeDiffVisibiltiyConfiguration();
1251 1252
	}

J
Joao Moreno 已提交
1253 1254
	private onDidChangeConfiguration(): void {
		const enabled = this.configurationService.getValue<string>('scm.diffDecorations') !== 'none';
1255 1256 1257 1258 1259 1260 1261 1262

		if (enabled) {
			this.enable();
		} else {
			this.disable();
		}
	}

1263
	private onDidChangeDiffWidthConfiguration(): void {
J
Joao Moreno 已提交
1264
		let width = this.configurationService.getValue<number>('scm.diffDecorationsGutterWidth');
1265 1266 1267 1268 1269

		if (isNaN(width) || width <= 0 || width > 5) {
			width = 3;
		}

J
Joao Moreno 已提交
1270
		this.setViewState({ ...this.viewState, width });
1271 1272
	}

1273
	private onDidChangeDiffVisibiltiyConfiguration(): void {
J
Joao Moreno 已提交
1274 1275 1276
		const visibility = this.configurationService.getValue<'always' | 'hover'>('scm.diffDecorationsGutterVisibility');
		this.setViewState({ ...this.viewState, visibility });
	}
1277

J
Joao Moreno 已提交
1278 1279
	private setViewState(state: IViewState): void {
		this.viewState = state;
1280
		this.stylesheet.innerHTML = `
J
Joao Moreno 已提交
1281
			.monaco-editor .dirty-diff-modified,.monaco-editor .dirty-diff-added{border-left-width:${state.width}px;}
1282
			.monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added, .monaco-editor .dirty-diff-deleted {
J
Joao Moreno 已提交
1283
				opacity: ${state.visibility === 'always' ? 1 : 0};
1284 1285 1286 1287
			}
		`;
	}

1288 1289
	private enable(): void {
		if (this.enabled) {
J
Joao Moreno 已提交
1290
			this.disable();
1291
		}
1292

M
Matt Bierner 已提交
1293
		this.transientDisposables.add(this.editorService.onDidVisibleEditorsChange(() => this.onEditorsChanged()));
1294 1295 1296
		this.onEditorsChanged();
		this.enabled = true;
	}
1297

1298 1299
	private disable(): void {
		if (!this.enabled) {
1300 1301 1302
			return;
		}

M
Matt Bierner 已提交
1303
		this.transientDisposables.clear();
1304 1305 1306 1307 1308 1309 1310 1311 1312 1313
		this.models.forEach(m => this.items[m.id].dispose());
		this.models = [];
		this.items = Object.create(null);
		this.enabled = false;
	}

	// HACK: This is the best current way of figuring out whether to draw these decorations
	// or not. Needs context from the editor, to know whether it is a diff editor, in place editor
	// etc.
	private onEditorsChanged(): void {
1314
		const models = this.editorService.visibleTextEditorWidgets
1315 1316

			// only interested in code editor widgets
A
Alex Dima 已提交
1317
			.filter(c => c instanceof CodeEditorWidget)
1318

J
Joao Moreno 已提交
1319 1320
			// set model registry and map to models
			.map(editor => {
A
Alex Dima 已提交
1321
				const codeEditor = editor as CodeEditorWidget;
J
Joao Moreno 已提交
1322 1323 1324 1325
				const controller = DirtyDiffController.get(codeEditor);
				controller.modelRegistry = this;
				return codeEditor.getModel();
			})
1326 1327

			// remove nulls and duplicates
M
Matt Bierner 已提交
1328
			.filter((m, i, a) => !!m && !!m.uri && a.indexOf(m, i + 1) === -1) as ITextModel[];
1329

J
Joao Moreno 已提交
1330 1331
		const newModels = models.filter(o => this.models.every(m => o !== m));
		const oldModels = this.models.filter(m => models.every(o => o !== m));
1332 1333

		oldModels.forEach(m => this.onModelInvisible(m));
J
Joao Moreno 已提交
1334
		newModels.forEach(m => this.onModelVisible(m));
1335

J
Joao Moreno 已提交
1336
		this.models = models;
1337 1338
	}

A
Alex Dima 已提交
1339
	private onModelVisible(editorModel: ITextModel): void {
J
Joao Moreno 已提交
1340
		const model = this.instantiationService.createInstance(DirtyDiffModel, editorModel);
J
Joao Moreno 已提交
1341
		const decorator = new DirtyDiffDecorator(editorModel, model, this.configurationService);
J
Joao Moreno 已提交
1342 1343

		this.items[editorModel.id] = new DirtyDiffItem(model, decorator);
1344 1345
	}

A
Alex Dima 已提交
1346
	private onModelInvisible(editorModel: ITextModel): void {
J
Joao Moreno 已提交
1347 1348 1349 1350
		this.items[editorModel.id].dispose();
		delete this.items[editorModel.id];
	}

A
Alex Dima 已提交
1351
	getModel(editorModel: ITextModel): DirtyDiffModel | null {
J
Joao Moreno 已提交
1352 1353 1354 1355 1356 1357 1358
		const item = this.items[editorModel.id];

		if (!item) {
			return null;
		}

		return item.model;
1359 1360 1361
	}

	dispose(): void {
1362
		this.disable();
1363
		super.dispose();
1364 1365
	}
}
B
Benjamin Pasero 已提交
1366

1367
registerEditorContribution(DirtyDiffController.ID, DirtyDiffController);
1368

B
Benjamin Pasero 已提交
1369 1370 1371
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
	const editorGutterModifiedBackgroundColor = theme.getColor(editorGutterModifiedBackground);
	if (editorGutterModifiedBackgroundColor) {
J
Joao Moreno 已提交
1372
		collector.addRule(`
J
Joao Moreno 已提交
1373
			.monaco-editor .dirty-diff-modified {
J
Joao Moreno 已提交
1374
				border-left: 3px solid ${editorGutterModifiedBackgroundColor};
1375
				transition: opacity 0.5s;
J
Joao Moreno 已提交
1376
			}
J
Joao Moreno 已提交
1377
			.monaco-editor .dirty-diff-modified:before {
J
Joao Moreno 已提交
1378 1379
				background: ${editorGutterModifiedBackgroundColor};
			}
1380 1381 1382
			.monaco-editor .margin:hover .dirty-diff-modified {
				opacity: 1;
			}
J
Joao Moreno 已提交
1383
		`);
B
Benjamin Pasero 已提交
1384 1385 1386 1387
	}

	const editorGutterAddedBackgroundColor = theme.getColor(editorGutterAddedBackground);
	if (editorGutterAddedBackgroundColor) {
J
Joao Moreno 已提交
1388
		collector.addRule(`
J
Joao Moreno 已提交
1389
			.monaco-editor .dirty-diff-added {
J
Joao Moreno 已提交
1390
				border-left: 3px solid ${editorGutterAddedBackgroundColor};
1391
				transition: opacity 0.5s;
J
Joao Moreno 已提交
1392
			}
J
Joao Moreno 已提交
1393
			.monaco-editor .dirty-diff-added:before {
J
Joao Moreno 已提交
1394 1395
				background: ${editorGutterAddedBackgroundColor};
			}
1396 1397 1398
			.monaco-editor .margin:hover .dirty-diff-added {
				opacity: 1;
			}
J
Joao Moreno 已提交
1399
		`);
B
Benjamin Pasero 已提交
1400 1401
	}

1402
	const editorGutteDeletedBackgroundColor = theme.getColor(editorGutterDeletedBackground);
B
Benjamin Pasero 已提交
1403 1404
	if (editorGutteDeletedBackgroundColor) {
		collector.addRule(`
J
Joao Moreno 已提交
1405
			.monaco-editor .dirty-diff-deleted:after {
B
Benjamin Pasero 已提交
1406
				border-left: 4px solid ${editorGutteDeletedBackgroundColor};
1407
				transition: opacity 0.5s;
B
Benjamin Pasero 已提交
1408
			}
J
Joao Moreno 已提交
1409
			.monaco-editor .dirty-diff-deleted:before {
J
Joao Moreno 已提交
1410 1411
				background: ${editorGutteDeletedBackgroundColor};
			}
1412 1413 1414
			.monaco-editor .margin:hover .dirty-diff-added {
				opacity: 1;
			}
B
Benjamin Pasero 已提交
1415 1416
		`);
	}
1417
});