commentsEditorContribution.ts 16.7 KB
Newer Older
P
Peng Lyu 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

P
Peng Lyu 已提交
7
import 'vs/css!./media/review';
8
import { $ } from 'vs/base/browser/builder';
9
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
P
Peng Lyu 已提交
10
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
R
Rachel Macfarlane 已提交
11
import { ICodeEditor, IEditorMouseEvent, IViewZone } from 'vs/editor/browser/editorBrowser';
P
Peng Lyu 已提交
12 13 14
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
15
import { IEditorContribution } from 'vs/editor/common/editorCommon';
16
import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model';
17 18
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import * as modes from 'vs/editor/common/modes';
P
Peng Lyu 已提交
19
import { peekViewEditorBackground, peekViewResultsBackground, peekViewResultsSelectionBackground } from 'vs/editor/contrib/referenceSearch/referencesWidget';
20 21
import * as nls from 'vs/nls';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
P
Peng Lyu 已提交
22
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
23
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
24
import { INotificationService } from 'vs/platform/notification/common/notification';
P
Peng Lyu 已提交
25
import { editorForeground, registerColor } from 'vs/platform/theme/common/colorRegistry';
26
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
27
import { CommentThreadCollapsibleState } from 'vs/workbench/api/node/extHostTypes';
28
import { ReviewModel } from 'vs/workbench/parts/comments/common/reviewModel';
29
import { CommentGlyphWidget } from 'vs/workbench/parts/comments/electron-browser/commentGlyphWidget';
P
Peng Lyu 已提交
30
import { ReviewZoneWidget, COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/parts/comments/electron-browser/commentThreadWidget';
31
import { ICommentService } from 'vs/workbench/services/comments/electron-browser/commentService';
P
Peng Lyu 已提交
32 33
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
P
Peng Lyu 已提交
34 35

export const ctxReviewPanelVisible = new RawContextKey<boolean>('reviewPanelVisible', false);
36 37
export const overviewRulerReviewForeground = registerColor('editorOverviewRuler.reviewForeground', { dark: '#ff646480', light: '#ff646480', hc: '#ff646480' }, nls.localize('overviewRulerWordHighlightStrongForeground', 'Overview ruler marker color for write-access symbol highlights. The color must not be opaque to not hide underlying decorations.'), true);

P
Peng Lyu 已提交
38 39
export const ID = 'editor.contrib.review';

40 41
const COMMENTING_RANGE_DECORATION = ModelDecorationOptions.register({
	stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
42
	linesDecorationsClassName: 'commenting-range',
P
Peng Lyu 已提交
43 44
});

P
Peng Lyu 已提交
45 46 47 48 49 50 51 52 53
export class ReviewViewZone implements IViewZone {
	public readonly afterLineNumber: number;
	public readonly domNode: HTMLElement;
	private callback: (top: number) => void;

	constructor(afterLineNumber: number, onDomNodeTop: (top: number) => void) {
		this.afterLineNumber = afterLineNumber;
		this.callback = onDomNodeTop;

P
use $.  
Peng Lyu 已提交
54
		this.domNode = $('.review-viewzone').getHTMLElement();
P
Peng Lyu 已提交
55 56 57 58 59 60 61 62 63 64 65 66
	}

	onDomNodeTop(top: number): void {
		this.callback(top);
	}
}

export class ReviewController implements IEditorContribution {
	private globalToDispose: IDisposable[];
	private localToDispose: IDisposable[];
	private editor: ICodeEditor;
	private decorationIDs: string[];
67 68
	private commentingRangeDecorationMap: Map<number, string[]>;
	private commentingRangeDecorations: string[];
R
Rachel Macfarlane 已提交
69 70
	private _newCommentWidget: ReviewZoneWidget;
	private _commentWidgets: ReviewZoneWidget[];
P
Peng Lyu 已提交
71
	private _reviewPanelVisible: IContextKey<boolean>;
72
	private _commentInfos: modes.CommentInfo[];
73
	private _reviewModel: ReviewModel;
74
	private _newCommentGlyph: CommentGlyphWidget;
75
	private _hasSetComments: boolean;
P
Peng Lyu 已提交
76 77 78

	constructor(
		editor: ICodeEditor,
79
		@IContextKeyService contextKeyService: IContextKeyService,
80
		@IThemeService private themeService: IThemeService,
81
		@ICommentService private commentService: ICommentService,
P
Peng Lyu 已提交
82 83 84 85 86 87
		@INotificationService private notificationService: INotificationService,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IModeService private modeService: IModeService,
		@IModelService private modelService: IModelService,
		@ICodeEditorService private codeEditorService: ICodeEditorService,

P
Peng Lyu 已提交
88 89 90 91 92
	) {
		this.editor = editor;
		this.globalToDispose = [];
		this.localToDispose = [];
		this.decorationIDs = [];
93 94
		this.commentingRangeDecorations = [];
		this.commentingRangeDecorationMap = new Map();
95
		this._commentInfos = [];
R
Rachel Macfarlane 已提交
96 97
		this._commentWidgets = [];
		this._newCommentWidget = null;
98
		this._newCommentGlyph = null;
99
		this._hasSetComments = false;
P
Peng Lyu 已提交
100 101

		this._reviewPanelVisible = ctxReviewPanelVisible.bindTo(contextKeyService);
102 103 104
		this._reviewModel = new ReviewModel();

		this._reviewModel.onDidChangeStyle(style => {
105 106 107
			this.editor.changeDecorations(accessor => {
				this.decorationIDs = accessor.deltaDecorations(this.decorationIDs, []);
			});
108

R
Rachel Macfarlane 已提交
109 110 111
			if (this._newCommentWidget) {
				this._newCommentWidget.dispose();
				this._newCommentWidget = null;
112
			}
113

R
Rachel Macfarlane 已提交
114
			this._commentWidgets.forEach(zone => {
115 116
				zone.dispose();
			});
117

118 119
			this._commentInfos.forEach(info => {
				info.threads.forEach(thread => {
120
					let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, info.owner, thread, {});
121
					zoneWidget.display(thread.range.startLineNumber);
R
Rachel Macfarlane 已提交
122
					this._commentWidgets.push(zoneWidget);
123
				});
124
			});
125
		});
P
Peng Lyu 已提交
126

127
		this.globalToDispose.push(this.commentService.onDidSetResourceCommentInfos(e => {
128 129
			const editorURI = this.editor && this.editor.getModel() && this.editor.getModel().uri;
			if (editorURI && editorURI.toString() === e.resource.toString()) {
130
				this.setComments(e.commentInfos);
131
			}
P
Peng Lyu 已提交
132 133
		}));

134 135 136 137 138 139 140 141 142 143
		this.globalToDispose.push(this.commentService.onDidSetDataProvider(async () => {
			const editorURI = this.editor && this.editor.getModel() && this.editor.getModel().uri;


			if (editorURI) {
				let commentInfos = await this.commentService.getComments(editorURI);
				this.setComments(commentInfos);
			}
		}));

P
Peng Lyu 已提交
144
		this.globalToDispose.push(this.editor.onDidChangeModel(() => this.onModelChanged()));
P
Peng Lyu 已提交
145
		this.codeEditorService.registerDecorationType(COMMENTEDITOR_DECORATION_KEY, {});
P
Peng Lyu 已提交
146 147 148 149 150 151
	}

	public static get(editor: ICodeEditor): ReviewController {
		return editor.getContribution<ReviewController>(ID);
	}

P
Peng Lyu 已提交
152
	public revealCommentThread(threadId: string, commentId?: string): void {
R
Rachel Macfarlane 已提交
153
		const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId);
154
		if (commentThreadWidget.length === 1) {
P
Peng Lyu 已提交
155
			commentThreadWidget[0].reveal(commentId);
156 157 158
		}
	}

P
Peng Lyu 已提交
159 160 161
	getId(): string {
		return ID;
	}
162

P
Peng Lyu 已提交
163 164 165 166
	dispose(): void {
		this.globalToDispose = dispose(this.globalToDispose);
		this.localToDispose = dispose(this.localToDispose);

167 168
		this._commentWidgets.forEach(widget => widget.dispose());

R
Rachel Macfarlane 已提交
169 170 171
		if (this._newCommentWidget) {
			this._newCommentWidget.dispose();
			this._newCommentWidget = null;
P
Peng Lyu 已提交
172 173 174 175 176 177
		}
		this.editor = null;
	}

	public onModelChanged(): void {
		this.localToDispose = dispose(this.localToDispose);
R
Rachel Macfarlane 已提交
178
		if (this._newCommentWidget) {
P
Peng Lyu 已提交
179
			// todo store view state.
R
Rachel Macfarlane 已提交
180 181
			this._newCommentWidget.dispose();
			this._newCommentWidget = null;
P
Peng Lyu 已提交
182
		}
183

184 185 186 187 188
		if (this._newCommentGlyph) {
			this.editor.removeContentWidget(this._newCommentGlyph);
			this._newCommentGlyph = null;
		}

R
Rachel Macfarlane 已提交
189
		this._commentWidgets.forEach(zone => {
190 191
			zone.dispose();
		});
R
Rachel Macfarlane 已提交
192
		this._commentWidgets = [];
193

P
Peng Lyu 已提交
194
		this.localToDispose.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e)));
195 196 197 198 199 200
		this.localToDispose.push(this.editor.onDidChangeModelContent(() => {
			if (this._newCommentGlyph) {
				this.editor.removeContentWidget(this._newCommentGlyph);
				this._newCommentGlyph = null;
			}
		}));
P
Peng Lyu 已提交
201 202 203 204 205 206 207 208 209 210
		this.localToDispose.push(this.commentService.onDidUpdateCommentThreads(e => {
			const editorURI = this.editor && this.editor.getModel() && this.editor.getModel().uri;
			if (!editorURI) {
				return;
			}
			let added = e.added.filter(thread => thread.resource.toString() === editorURI.toString());
			let removed = e.removed.filter(thread => thread.resource.toString() === editorURI.toString());
			let changed = e.changed.filter(thread => thread.resource.toString() === editorURI.toString());

			removed.forEach(thread => {
R
Rachel Macfarlane 已提交
211
				let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId);
P
Peng Lyu 已提交
212 213
				if (matchedZones.length) {
					let matchedZone = matchedZones[0];
R
Rachel Macfarlane 已提交
214 215
					let index = this._commentWidgets.indexOf(matchedZone);
					this._commentWidgets.splice(index, 1);
P
Peng Lyu 已提交
216 217 218 219
				}
			});

			changed.forEach(thread => {
R
Rachel Macfarlane 已提交
220
				let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId);
P
Peng Lyu 已提交
221 222 223 224 225 226
				if (matchedZones.length) {
					let matchedZone = matchedZones[0];
					matchedZone.update(thread);
				}
			});
			added.forEach(thread => {
227
				let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, e.owner, thread, {});
P
Peng Lyu 已提交
228
				zoneWidget.display(thread.range.startLineNumber);
R
Rachel Macfarlane 已提交
229
				this._commentWidgets.push(zoneWidget);
230
				this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread);
P
Peng Lyu 已提交
231 232
			});
		}));
P
Peng Lyu 已提交
233 234
	}

235 236 237 238
	private addComment(lineNumber: number) {
		let newCommentInfo = this.getNewCommentAction(lineNumber);
		if (!newCommentInfo) {
			return;
P
Peng Lyu 已提交
239
		}
240 241 242

		// add new comment
		this._reviewPanelVisible.set(true);
243
		const { replyCommand, ownerId } = newCommentInfo;
244
		this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, ownerId, {
245 246 247 248 249 250 251 252 253
			threadId: null,
			resource: null,
			comments: [],
			range: {
				startLineNumber: lineNumber,
				startColumn: 0,
				endLineNumber: lineNumber,
				endColumn: 0
			},
254
			reply: replyCommand,
255
			collapsibleState: CommentThreadCollapsibleState.Expanded,
256
		}, {});
257

R
Rachel Macfarlane 已提交
258 259
		this._newCommentWidget.onDidClose(e => {
			this._newCommentWidget = null;
260
		});
R
Rachel Macfarlane 已提交
261
		this._newCommentWidget.display(lineNumber);
P
Peng Lyu 已提交
262 263
	}

264
	private onEditorMouseMove(e: IEditorMouseEvent): void {
265 266 267 268 269 270
		if (!this._hasSetComments) {
			return;
		}

		const hasCommentingRanges = this._commentInfos.length && this._commentInfos.some(info => !!info.commentingRanges.length);
		if (hasCommentingRanges && e.target.position && e.target.position.lineNumber !== undefined) {
271
			if (this._newCommentGlyph && e.target.element.className !== 'comment-hint') {
272 273
				this.editor.removeContentWidget(this._newCommentGlyph);
			}
274

R
Rachel Macfarlane 已提交
275 276 277 278 279 280 281
			const lineNumber = e.target.position.lineNumber;
			if (!this.isExistingCommentThreadAtLine(lineNumber)) {
				this._newCommentGlyph = this.isLineInCommentingRange(lineNumber)
					? this._newCommentGlyph = new CommentGlyphWidget('comment-hint', this.editor, lineNumber, false, () => {
						this.addComment(lineNumber);
					})
					: this._newCommentGlyph = new CommentGlyphWidget('comment-hint', this.editor, lineNumber, true, () => {
282
						this.notificationService.warn('Commenting is not supported outside of diff hunk areas.');
R
Rachel Macfarlane 已提交
283
					});
284

R
Rachel Macfarlane 已提交
285 286
				this.editor.layoutContentWidget(this._newCommentGlyph);
			}
287
		}
P
Peng Lyu 已提交
288 289
	}

290
	private getNewCommentAction(line: number): { replyCommand: modes.Command, ownerId: number } {
291 292 293 294 295
		for (let i = 0; i < this._commentInfos.length; i++) {
			const commentInfo = this._commentInfos[i];
			const lineWithinRange = commentInfo.commentingRanges.some(range =>
				range.startLineNumber <= line && line <= range.endLineNumber
			);
P
Peng Lyu 已提交
296

297 298 299 300 301
			if (lineWithinRange) {
				return {
					replyCommand: commentInfo.reply,
					ownerId: commentInfo.owner
				};
P
Peng Lyu 已提交
302
			}
303
		}
P
Peng Lyu 已提交
304

305
		return null;
306
	}
P
Peng Lyu 已提交
307

R
Rachel Macfarlane 已提交
308
	private isLineInCommentingRange(line: number): boolean {
309
		return this._commentInfos.some(commentInfo => {
R
Rachel Macfarlane 已提交
310
			return commentInfo.commentingRanges.some(range =>
311 312
				range.startLineNumber <= line && line <= range.endLineNumber
			);
R
Rachel Macfarlane 已提交
313 314
		});
	}
P
Peng Lyu 已提交
315

R
Rachel Macfarlane 已提交
316 317 318
	private isExistingCommentThreadAtLine(line: number): boolean {
		const existingThread = this._commentInfos.some(commentInfo => {
			return commentInfo.threads.some(thread =>
319 320 321
				thread.range.startLineNumber === line
			);
		});
R
Rachel Macfarlane 已提交
322 323 324 325

		const existingNewComment = this._newCommentWidget && this._newCommentWidget.position && this._newCommentWidget.position.lineNumber === line;

		return existingThread || existingNewComment;
P
Peng Lyu 已提交
326 327
	}

328 329
	setComments(commentInfos: modes.CommentInfo[]): void {
		this._commentInfos = commentInfos;
330
		this._hasSetComments = true;
331

332 333 334 335
		this.editor.changeDecorations(accessor => {
			this.commentingRangeDecorationMap.forEach((val, index) => {
				accessor.deltaDecorations(val, []);
				this.commentingRangeDecorationMap.delete(index);
336
			});
337

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
			if (this._commentInfos.length === 0) {
				return;
			}

			commentInfos.forEach(info => {
				let ranges = [];
				if (info.commentingRanges) {
					ranges.push(...info.commentingRanges);
				}

				const commentingRangeDecorations: IModelDeltaDecoration[] = [];

				ranges.forEach(range => {
					commentingRangeDecorations.push({
						options: COMMENTING_RANGE_DECORATION,
						range: range
					});
355
				});
356 357 358

				let commentingRangeDecorationIds = accessor.deltaDecorations(this.commentingRangeDecorations, commentingRangeDecorations);
				this.commentingRangeDecorationMap.set(info.owner, commentingRangeDecorationIds);
359
			});
360 361 362
		});

		// create viewzones
R
Rachel Macfarlane 已提交
363
		this._commentWidgets.forEach(zone => {
364 365 366 367 368
			zone.dispose();
		});

		this._commentInfos.forEach(info => {
			info.threads.forEach(thread => {
369
				let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, info.owner, thread, {});
370
				zoneWidget.display(thread.range.startLineNumber);
R
Rachel Macfarlane 已提交
371
				this._commentWidgets.push(zoneWidget);
372 373
			});
		});
M
Matt Bierner 已提交
374 375 376
	}


P
Peng Lyu 已提交
377 378 379
	public closeWidget(): void {
		this._reviewPanelVisible.reset();

R
Rachel Macfarlane 已提交
380 381 382
		if (this._newCommentWidget) {
			this._newCommentWidget.dispose();
			this._newCommentWidget = null;
P
Peng Lyu 已提交
383 384
		}

R
Rachel Macfarlane 已提交
385
		if (this._commentWidgets) {
386
			this._commentWidgets.forEach(widget => widget.hide());
387 388
		}

P
Peng Lyu 已提交
389
		this.editor.focus();
390
		this.editor.revealRangeInCenter(this.editor.getSelection());
P
Peng Lyu 已提交
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
	}
}

registerEditorContribution(ReviewController);


KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: 'closeReviewPanel',
	weight: KeybindingsRegistry.WEIGHT.editorContrib(),
	primary: KeyCode.Escape,
	secondary: [KeyMod.Shift | KeyCode.Escape],
	when: ctxReviewPanelVisible,
	handler: closeReviewPanel
});

export function getOuterEditor(accessor: ServicesAccessor): ICodeEditor {
	let editor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
	if (editor instanceof EmbeddedCodeEditorWidget) {
		return editor.getParentEditor();
	}
	return editor;
}

function closeReviewPanel(accessor: ServicesAccessor, args: any) {
	var outerEditor = getOuterEditor(accessor);
	if (!outerEditor) {
		return;
	}

	let controller = ReviewController.get(outerEditor);

	if (!controller) {
		return;
	}

	controller.closeWidget();
427 428 429 430
}


registerThemingParticipant((theme, collector) => {
P
Peng Lyu 已提交
431
	let peekViewBackground = theme.getColor(peekViewResultsBackground);
P
Peng Lyu 已提交
432
	if (peekViewBackground) {
433 434 435
		collector.addRule(
			`.monaco-editor .review-widget,` +
			`.monaco-editor .review-widget {` +
P
Peng Lyu 已提交
436
			`	background-color: ${peekViewBackground};` +
437 438
			`}`);
	}
P
Peng Lyu 已提交
439

P
Peng Lyu 已提交
440
	let monacoEditorBackground = theme.getColor(peekViewEditorBackground);
P
Peng Lyu 已提交
441 442
	if (monacoEditorBackground) {
		collector.addRule(
P
Peng Lyu 已提交
443 444
			`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` +
			`	background-color: ${monacoEditorBackground}` +
P
Peng Lyu 已提交
445 446 447 448 449 450 451
			`}`
		);
	}

	let monacoEditorForeground = theme.getColor(editorForeground);
	if (monacoEditorForeground) {
		collector.addRule(
P
Peng Lyu 已提交
452
			`.monaco-editor .review-widget .body .monaco-editor {` +
P
Peng Lyu 已提交
453
			`	color: ${monacoEditorForeground}` +
P
Peng Lyu 已提交
454 455 456
			`}` +
			`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` +
			`	color: ${monacoEditorForeground}` +
P
Peng Lyu 已提交
457 458 459
			`}`
		);
	}
P
Peng Lyu 已提交
460 461 462 463 464 465 466 467 468 469 470 471 472 473

	let selectionBackground = theme.getColor(peekViewResultsSelectionBackground);

	if (selectionBackground) {
		collector.addRule(
			`@keyframes monaco-review-widget-focus {` +
			`	0% { background: ${selectionBackground}; }` +
			`	100% { background: transparent; }` +
			`}` +
			`.monaco-editor .review-widget .body .review-comment.focus {` +
			`	animation: monaco-review-widget-focus 3s ease 0s;` +
			`}`
		);
	}
474
});