Vscode merge (#4582)

* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd

* fix issues with merges

* bump node version in azpipe

* replace license headers

* remove duplicate launch task

* fix build errors

* fix build errors

* fix tslint issues

* working through package and linux build issues

* more work

* wip

* fix packaged builds

* working through linux build errors

* wip

* wip

* wip

* fix mac and linux file limits

* iterate linux pipeline

* disable editor typing

* revert series to parallel

* remove optimize vscode from linux

* fix linting issues

* revert testing change

* add work round for new node

* readd packaging for extensions

* fix issue with angular not resolving decorator dependencies
This commit is contained in:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View File

@@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { Comment, CommentThread, CommentThreadChangedEvent } from 'vs/editor/common/modes';
import { groupBy, firstIndex, flatten } from 'vs/base/common/arrays';
import { localize } from 'vs/nls';
import { values } from 'vs/base/common/map';
export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent {
owner: string;
}
export class CommentNode {
threadId: string;
range: IRange;
comment: Comment;
replies: CommentNode[] = [];
resource: URI;
constructor(threadId: string, resource: URI, comment: Comment, range: IRange) {
this.threadId = threadId;
this.comment = comment;
this.resource = resource;
this.range = range;
}
hasReply(): boolean {
return this.replies && this.replies.length !== 0;
}
}
export class ResourceWithCommentThreads {
id: string;
commentThreads: CommentNode[]; // The top level comments on the file. Replys are nested under each node.
resource: URI;
constructor(resource: URI, commentThreads: CommentThread[]) {
this.id = resource.toString();
this.resource = resource;
this.commentThreads = commentThreads.filter(thread => thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(resource, thread));
}
public static createCommentNode(resource: URI, commentThread: CommentThread): CommentNode {
const { threadId, comments, range } = commentThread;
const commentNodes: CommentNode[] = comments.map(comment => new CommentNode(threadId!, resource, comment, range));
if (commentNodes.length > 1) {
commentNodes[0].replies = commentNodes.slice(1, commentNodes.length);
}
return commentNodes[0];
}
}
export class CommentsModel {
resourceCommentThreads: ResourceWithCommentThreads[];
commentThreadsMap: Map<string, ResourceWithCommentThreads[]>;
constructor() {
this.resourceCommentThreads = [];
this.commentThreadsMap = new Map<string, ResourceWithCommentThreads[]>();
}
public setCommentThreads(owner: string, commentThreads: CommentThread[]): void {
this.commentThreadsMap.set(owner, this.groupByResource(commentThreads));
this.resourceCommentThreads = flatten(values(this.commentThreadsMap));
}
public updateCommentThreads(event: ICommentThreadChangedEvent): boolean {
const { owner, removed, changed, added } = event;
let threadsForOwner = this.commentThreadsMap.get(owner) || [];
removed.forEach(thread => {
// Find resource that has the comment thread
const matchingResourceIndex = firstIndex(threadsForOwner, (resourceData) => resourceData.id === thread.resource);
const matchingResourceData = threadsForOwner[matchingResourceIndex];
// Find comment node on resource that is that thread and remove it
const index = firstIndex(matchingResourceData.commentThreads, (commentThread) => commentThread.threadId === thread.threadId);
matchingResourceData.commentThreads.splice(index, 1);
// If the comment thread was the last thread for a resource, remove that resource from the list
if (matchingResourceData.commentThreads.length === 0) {
threadsForOwner.splice(matchingResourceIndex, 1);
}
});
changed.forEach(thread => {
// Find resource that has the comment thread
const matchingResourceIndex = firstIndex(threadsForOwner, (resourceData) => resourceData.id === thread.resource);
const matchingResourceData = threadsForOwner[matchingResourceIndex];
// Find comment node on resource that is that thread and replace it
const index = firstIndex(matchingResourceData.commentThreads, (commentThread) => commentThread.threadId === thread.threadId);
if (index >= 0) {
matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(URI.parse(matchingResourceData.id), thread);
} else {
matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(URI.parse(matchingResourceData.id), thread));
}
});
added.forEach(thread => {
const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource);
if (existingResource.length) {
const resource = existingResource[0];
if (thread.comments.length) {
resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(resource.resource, thread));
}
} else {
threadsForOwner.push(new ResourceWithCommentThreads(URI.parse(thread.resource!), [thread]));
}
});
this.commentThreadsMap.set(owner, threadsForOwner);
this.resourceCommentThreads = flatten(values(this.commentThreadsMap));
return removed.length > 0 || changed.length > 0 || added.length > 0;
}
public hasCommentThreads(): boolean {
return !!this.resourceCommentThreads.length;
}
public getMessage(): string {
if (!this.resourceCommentThreads.length) {
return localize('noComments', "There are no comments on this review.");
} else {
return '';
}
}
private groupByResource(commentThreads: CommentThread[]): ResourceWithCommentThreads[] {
const resourceCommentThreads: ResourceWithCommentThreads[] = [];
const commentThreadsByResource = new Map<string, ResourceWithCommentThreads>();
for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) {
commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(URI.parse(group[0].resource!), group));
}
commentThreadsByResource.forEach((v, i, m) => {
resourceCommentThreads.push(v);
});
return resourceCommentThreads;
}
private static _compareURIs(a: CommentThread, b: CommentThread) {
const resourceA = a.resource!.toString();
const resourceB = b.resource!.toString();
if (resourceA < resourceB) {
return -1;
} else if (resourceA > resourceB) {
return 1;
} else {
return 0;
}
}
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ICommentThreadWidget {
submitComment: () => Promise<void>;
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Color, RGBA } from 'vs/base/common/color';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { IModelDecorationOptions, OverviewRulerLane } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
const overviewRulerDefault = new Color(new RGBA(197, 197, 197, 1));
export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: overviewRulerDefault, light: overviewRulerDefault, hc: overviewRulerDefault }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges.'));
export class CommentGlyphWidget {
private _lineNumber: number;
private _editor: ICodeEditor;
private commentsDecorations: string[] = [];
private _commentsOptions: ModelDecorationOptions;
constructor(editor: ICodeEditor, lineNumber: number) {
this._commentsOptions = this.createDecorationOptions();
this._editor = editor;
this.setLineNumber(lineNumber);
}
private createDecorationOptions(): ModelDecorationOptions {
const decorationOptions: IModelDecorationOptions = {
isWholeLine: true,
overviewRuler: {
color: themeColorFromId(overviewRulerCommentingRangeForeground),
position: OverviewRulerLane.Center
},
linesDecorationsClassName: `comment-range-glyph comment-thread`
};
return ModelDecorationOptions.createDynamic(decorationOptions);
}
setLineNumber(lineNumber: number): void {
this._lineNumber = lineNumber;
let commentsDecorations = [{
range: {
startLineNumber: lineNumber, startColumn: 1,
endLineNumber: lineNumber, endColumn: 1
},
options: this._commentsOptions
}];
this.commentsDecorations = this._editor.deltaDecorations(this.commentsDecorations, commentsDecorations);
}
getPosition(): IContentWidgetPosition {
const range = this._editor.hasModel() && this.commentsDecorations && this.commentsDecorations.length
? this._editor.getModel().getDecorationRange(this.commentsDecorations[0])
: null;
return {
position: {
lineNumber: range ? range.startLineNumber : this._lineNumber,
column: 1
},
preference: [ContentWidgetPositionPreference.EXACT]
};
}
dispose() {
if (this.commentsDecorations) {
this._editor.deltaDecorations(this.commentsDecorations, []);
}
}
}

View File

@@ -0,0 +1,628 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import * as modes from 'vs/editor/common/modes';
import { ActionsOrientation, ActionItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button } from 'vs/base/browser/ui/button/button';
import { Action, IActionRunner } from 'vs/base/common/actions';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ICommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService';
import { SimpleCommentEditor } from 'vs/workbench/contrib/comments/electron-browser/simpleCommentEditor';
import { Selection } from 'vs/editor/common/core/selection';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { Emitter, Event } from 'vs/base/common/event';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { assign } from 'vs/base/common/objects';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { ToggleReactionsAction, ReactionAction, ReactionActionItem } from './reactionsAction';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment");
const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment...");
export class CommentNode extends Disposable {
private _domNode: HTMLElement;
private _body: HTMLElement;
private _md: HTMLElement;
private _clearTimeout: any;
private _editAction: Action;
private _commentEditContainer: HTMLElement;
private _commentDetailsContainer: HTMLElement;
private _actionsToolbarContainer: HTMLElement;
private _reactionsActionBar?: ActionBar;
private _reactionActionsContainer?: HTMLElement;
private _commentEditor: SimpleCommentEditor | null;
private _commentEditorDisposables: IDisposable[] = [];
private _commentEditorModel: ITextModel;
private _updateCommentButton: Button;
private _errorEditingContainer: HTMLElement;
private _isPendingLabel: HTMLElement;
private _deleteAction: Action;
protected actionRunner?: IActionRunner;
protected toolbar: ToolBar;
private _onDidDelete = new Emitter<CommentNode>();
public get domNode(): HTMLElement {
return this._domNode;
}
public isEditing: boolean;
constructor(
private commentThread: modes.CommentThread | modes.CommentThread2,
public comment: modes.Comment,
private owner: string,
private resource: URI,
private parentEditor: ICodeEditor,
private parentThread: ICommentThreadWidget,
private markdownRenderer: MarkdownRenderer,
@IThemeService private themeService: IThemeService,
@IInstantiationService private instantiationService: IInstantiationService,
@ICommentService private commentService: ICommentService,
@ICommandService private commandService: ICommandService,
@IModelService private modelService: IModelService,
@IModeService private modeService: IModeService,
@IDialogService private dialogService: IDialogService,
@INotificationService private notificationService: INotificationService,
@IContextMenuService private contextMenuService: IContextMenuService
) {
super();
this._domNode = dom.$('div.review-comment');
this._domNode.tabIndex = 0;
const avatar = dom.append(this._domNode, dom.$('div.avatar-container'));
if (comment.userIconPath) {
const img = <HTMLImageElement>dom.append(avatar, dom.$('img.avatar'));
img.src = comment.userIconPath.toString();
img.onerror = _ => img.remove();
}
this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents'));
this.createHeader(this._commentDetailsContainer);
this._body = dom.append(this._commentDetailsContainer, dom.$('div.comment-body'));
this._md = this.markdownRenderer.render(comment.body).element;
this._body.appendChild(this._md);
if (this.comment.commentReactions && this.comment.commentReactions.length) {
this.createReactionsContainer(this._commentDetailsContainer);
}
this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`);
this._domNode.setAttribute('role', 'treeitem');
this._clearTimeout = null;
}
public get onDidDelete(): Event<CommentNode> {
return this._onDidDelete.event;
}
private createHeader(commentDetailsContainer: HTMLElement): void {
const header = dom.append(commentDetailsContainer, dom.$('div.comment-title'));
const author = dom.append(header, dom.$('strong.author'));
author.innerText = this.comment.userName;
this._isPendingLabel = dom.append(header, dom.$('span.isPending'));
if (this.comment.label) {
this._isPendingLabel.innerText = this.comment.label;
} else if (this.comment.isDraft) {
this._isPendingLabel.innerText = 'Pending';
} else {
this._isPendingLabel.innerText = '';
}
this._actionsToolbarContainer = dom.append(header, dom.$('.comment-actions.hidden'));
this.createActionsToolbar();
}
private createActionsToolbar() {
const actions: Action[] = [];
let reactionGroup = this.commentService.getReactionGroup(this.owner);
if (reactionGroup && reactionGroup.length) {
let commentThread = this.commentThread as modes.CommentThread2;
if (commentThread.commentThreadHandle) {
let toggleReactionAction = this.createReactionPicker2();
actions.push(toggleReactionAction);
} else {
let toggleReactionAction = this.createReactionPicker();
actions.push(toggleReactionAction);
}
}
if (this.comment.canEdit || this.comment.editCommand) {
this._editAction = this.createEditAction(this._commentDetailsContainer);
actions.push(this._editAction);
}
if (this.comment.canDelete || this.comment.deleteCommand) {
this._deleteAction = this.createDeleteAction();
actions.push(this._deleteAction);
}
if (actions.length) {
this.toolbar = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {
actionItemProvider: action => {
if (action.id === ToggleReactionsAction.ID) {
return new DropdownMenuActionItem(
action,
(<ToggleReactionsAction>action).menuActions,
this.contextMenuService,
action => {
return this.actionItemProvider(action as Action);
},
this.actionRunner!,
undefined,
'toolbar-toggle-pickReactions',
() => { return AnchorAlignment.RIGHT; }
);
}
return this.actionItemProvider(action as Action);
},
orientation: ActionsOrientation.HORIZONTAL
});
this.registerActionBarListeners(this._actionsToolbarContainer);
this.toolbar.setActions(actions, [])();
this._toDispose.push(this.toolbar);
}
}
actionItemProvider(action: Action) {
let options = {};
if (action.id === 'comment.delete' || action.id === 'comment.edit' || action.id === ToggleReactionsAction.ID) {
options = { label: false, icon: true };
} else {
options = { label: true, icon: true };
}
if (action.id === ReactionAction.ID) {
let item = new ReactionActionItem(action);
return item;
} else {
let item = new ActionItem({}, action, options);
return item;
}
}
private createReactionPicker2(): ToggleReactionsAction {
let toggleReactionActionItem: DropdownMenuActionItem;
let toggleReactionAction = this._register(new ToggleReactionsAction(() => {
if (toggleReactionActionItem) {
toggleReactionActionItem.show();
}
}, nls.localize('commentToggleReaction', "Toggle Reaction")));
let reactionMenuActions: Action[] = [];
let reactionGroup = this.commentService.getReactionGroup(this.owner);
if (reactionGroup && reactionGroup.length) {
reactionMenuActions = reactionGroup.map((reaction) => {
return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
try {
await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread as modes.CommentThread2, this.comment, reaction);
} catch (e) {
const error = e.message
? nls.localize('commentToggleReactionError', "Toggling the comment reaction failed: {0}.", e.message)
: nls.localize('commentToggleReactionDefaultError', "Toggling the comment reaction failed");
this.notificationService.error(error);
}
});
});
}
toggleReactionAction.menuActions = reactionMenuActions;
toggleReactionActionItem = new DropdownMenuActionItem(
toggleReactionAction,
(<ToggleReactionsAction>toggleReactionAction).menuActions,
this.contextMenuService,
action => {
if (action.id === ToggleReactionsAction.ID) {
return toggleReactionActionItem;
}
return this.actionItemProvider(action as Action);
},
this.actionRunner,
undefined,
'toolbar-toggle-pickReactions',
() => { return AnchorAlignment.RIGHT; }
);
return toggleReactionAction;
}
private createReactionPicker(): ToggleReactionsAction {
let toggleReactionActionItem: DropdownMenuActionItem;
let toggleReactionAction = this._register(new ToggleReactionsAction(() => {
if (toggleReactionActionItem) {
toggleReactionActionItem.show();
}
}, nls.localize('commentAddReaction', "Add Reaction")));
let reactionMenuActions: Action[] = [];
let reactionGroup = this.commentService.getReactionGroup(this.owner);
if (reactionGroup && reactionGroup.length) {
reactionMenuActions = reactionGroup.map((reaction) => {
return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
try {
await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction);
} catch (e) {
const error = e.message
? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)
: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");
this.notificationService.error(error);
}
});
});
}
toggleReactionAction.menuActions = reactionMenuActions;
toggleReactionActionItem = new DropdownMenuActionItem(
toggleReactionAction,
(<ToggleReactionsAction>toggleReactionAction).menuActions,
this.contextMenuService,
action => {
if (action.id === ToggleReactionsAction.ID) {
return toggleReactionActionItem;
}
return this.actionItemProvider(action as Action);
},
this.actionRunner!,
undefined,
'toolbar-toggle-pickReactions',
() => { return AnchorAlignment.RIGHT; }
);
return toggleReactionAction;
}
private createReactionsContainer(commentDetailsContainer: HTMLElement): void {
this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));
this._reactionsActionBar = new ActionBar(this._reactionActionsContainer, {
actionItemProvider: action => {
if (action.id === ToggleReactionsAction.ID) {
return new DropdownMenuActionItem(
action,
(<ToggleReactionsAction>action).menuActions,
this.contextMenuService,
action => {
return this.actionItemProvider(action as Action);
},
this.actionRunner!,
undefined,
'toolbar-toggle-pickReactions',
() => { return AnchorAlignment.RIGHT; }
);
}
return this.actionItemProvider(action as Action);
}
});
this._toDispose.push(this._reactionsActionBar);
this.comment.commentReactions!.map(reaction => {
let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => {
try {
let commentThread = this.commentThread as modes.CommentThread2;
if (commentThread.commentThreadHandle) {
await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread as modes.CommentThread2, this.comment, reaction);
} else {
if (reaction.hasReacted) {
await this.commentService.deleteReaction(this.owner, this.resource, this.comment, reaction);
} else {
await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction);
}
}
} catch (e) {
let error: string;
if (reaction.hasReacted) {
error = e.message
? nls.localize('commentDeleteReactionError', "Deleting the comment reaction failed: {0}.", e.message)
: nls.localize('commentDeleteReactionDefaultError', "Deleting the comment reaction failed");
} else {
error = e.message
? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)
: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");
}
this.notificationService.error(error);
}
}, reaction.iconPath, reaction.count);
if (this._reactionsActionBar) {
this._reactionsActionBar.push(action, { label: true, icon: true });
}
});
let reactionGroup = this.commentService.getReactionGroup(this.owner);
if (reactionGroup && reactionGroup.length) {
let commentThread = this.commentThread as modes.CommentThread2;
if (commentThread.commentThreadHandle) {
let toggleReactionAction = this.createReactionPicker2();
this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true });
} else {
let toggleReactionAction = this.createReactionPicker();
this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true });
}
}
}
private createCommentEditor(): void {
const container = dom.append(this._commentEditContainer, dom.$('.edit-textarea'));
this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(), this.parentEditor, this.parentThread);
const resource = URI.parse(`comment:commentinput-${this.comment.commentId}-${Date.now()}.md`);
this._commentEditorModel = this.modelService.createModel('', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
this._commentEditor.setModel(this._commentEditorModel);
this._commentEditor.setValue(this.comment.body.value);
this._commentEditor.layout({ width: container.clientWidth - 14, height: 90 });
this._commentEditor.focus();
const lastLine = this._commentEditorModel.getLineCount();
const lastColumn = this._commentEditorModel.getLineContent(lastLine).length + 1;
this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn));
let commentThread = this.commentThread as modes.CommentThread2;
if (commentThread.commentThreadHandle) {
commentThread.input = {
uri: this._commentEditor.getModel()!.uri,
value: this.comment.body.value
};
this.commentService.setActiveCommentThread(commentThread);
this._commentEditorDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => {
commentThread.input = {
uri: this._commentEditor!.getModel()!.uri,
value: this.comment.body.value
};
this.commentService.setActiveCommentThread(commentThread);
}));
this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => {
if (commentThread.input && this._commentEditor && this._commentEditor.getModel()!.uri === commentThread.input.uri) {
let newVal = this._commentEditor.getValue();
if (newVal !== commentThread.input.value) {
let input = commentThread.input;
input.value = newVal;
commentThread.input = input;
}
}
}));
}
this._toDispose.push(this._commentEditor);
this._toDispose.push(this._commentEditorModel);
}
private removeCommentEditor() {
this.isEditing = false;
this._editAction.enabled = true;
this._body.classList.remove('hidden');
this._commentEditorModel.dispose();
this._commentEditorDisposables.forEach(dispose => dispose.dispose());
this._commentEditorDisposables = [];
if (this._commentEditor) {
this._commentEditor.dispose();
this._commentEditor = null;
}
this._commentEditContainer.remove();
}
async editComment(): Promise<void> {
if (!this._commentEditor) {
throw new Error('No comment editor');
}
this._updateCommentButton.enabled = false;
this._updateCommentButton.label = UPDATE_IN_PROGRESS_LABEL;
try {
const newBody = this._commentEditor.getValue();
if (this.comment.editCommand) {
let commentThread = this.commentThread as modes.CommentThread2;
commentThread.input = {
uri: this._commentEditor.getModel()!.uri,
value: newBody
};
this.commentService.setActiveCommentThread(commentThread);
let commandId = this.comment.editCommand.id;
let args = this.comment.editCommand.arguments || [];
await this.commandService.executeCommand(commandId, ...args);
} else {
await this.commentService.editComment(this.owner, this.resource, this.comment, newBody);
}
this._updateCommentButton.enabled = true;
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
this._commentEditor.getDomNode()!.style.outline = '';
this.removeCommentEditor();
const editedComment = assign({}, this.comment, { body: new MarkdownString(newBody) });
this.update(editedComment);
} catch (e) {
this._updateCommentButton.enabled = true;
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
this._commentEditor.getDomNode()!.style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`;
this._errorEditingContainer.textContent = e.message
? nls.localize('commentEditError', "Updating the comment failed: {0}.", e.message)
: nls.localize('commentEditDefaultError', "Updating the comment failed.");
this._errorEditingContainer.classList.remove('hidden');
this._commentEditor.focus();
}
}
private createDeleteAction(): Action {
return new Action('comment.delete', nls.localize('label.delete', "Delete"), 'octicon octicon-x', true, () => {
return this.dialogService.confirm({
message: nls.localize('confirmDelete', "Delete comment?"),
type: 'question',
primaryButton: nls.localize('label.delete', "Delete")
}).then(async result => {
if (result.confirmed) {
try {
if (this.comment.deleteCommand) {
this.commentService.setActiveCommentThread(this.commentThread as modes.CommentThread2);
let commandId = this.comment.deleteCommand.id;
let args = this.comment.deleteCommand.arguments || [];
await this.commandService.executeCommand(commandId, ...args);
} else {
const didDelete = await this.commentService.deleteComment(this.owner, this.resource, this.comment);
if (didDelete) {
this._onDidDelete.fire(this);
} else {
throw Error();
}
}
} catch (e) {
const error = e.message
? nls.localize('commentDeletionError', "Deleting the comment failed: {0}.", e.message)
: nls.localize('commentDeletionDefaultError', "Deleting the comment failed");
this.notificationService.error(error);
}
}
});
});
}
private createEditAction(commentDetailsContainer: HTMLElement): Action {
return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => {
this.isEditing = true;
this._body.classList.add('hidden');
this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container'));
this.createCommentEditor();
this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden'));
const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions'));
const cancelEditButton = new Button(formActions);
cancelEditButton.label = nls.localize('label.cancel', "Cancel");
this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService));
this._toDispose.push(cancelEditButton.onDidClick(_ => {
this.removeCommentEditor();
}));
this._updateCommentButton = new Button(formActions);
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService));
this._toDispose.push(this._updateCommentButton.onDidClick(_ => {
this.editComment();
}));
this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => {
this._updateCommentButton.enabled = !!this._commentEditor!.getValue();
}));
this._editAction.enabled = false;
return Promise.resolve();
});
}
private registerActionBarListeners(actionsContainer: HTMLElement): void {
this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseenter', () => {
actionsContainer.classList.remove('hidden');
}));
this._toDispose.push(dom.addDisposableListener(this._domNode, 'focus', () => {
actionsContainer.classList.remove('hidden');
}));
this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', () => {
if (!this._domNode.contains(document.activeElement)) {
actionsContainer.classList.add('hidden');
}
}));
this._toDispose.push(dom.addDisposableListener(this._domNode, 'focusout', (e: FocusEvent) => {
if (!this._domNode.contains((<HTMLElement>e.relatedTarget))) {
actionsContainer.classList.add('hidden');
if (this._commentEditor && this._commentEditor.getValue() === this.comment.body.value) {
this.removeCommentEditor();
}
}
}));
}
update(newComment: modes.Comment) {
if (newComment.body !== this.comment.body) {
this._body.removeChild(this._md);
this._md = this.markdownRenderer.render(newComment.body).element;
this._body.appendChild(this._md);
}
const shouldUpdateActions = newComment.editCommand !== this.comment.editCommand || newComment.deleteCommand !== this.comment.deleteCommand;
this.comment = newComment;
if (shouldUpdateActions) {
dom.clearNode(this._actionsToolbarContainer);
this.createActionsToolbar();
}
if (newComment.label) {
this._isPendingLabel.innerText = newComment.label;
} else if (newComment.isDraft) {
this._isPendingLabel.innerText = 'Pending';
} else {
this._isPendingLabel.innerText = '';
}
// update comment reactions
if (this._reactionActionsContainer) {
this._reactionActionsContainer.remove();
}
if (this._reactionsActionBar) {
this._reactionsActionBar.clear();
}
if (this.comment.commentReactions && this.comment.commentReactions.length) {
this.createReactionsContainer(this._commentDetailsContainer);
}
}
focus() {
this.domNode.focus();
if (!this._clearTimeout) {
dom.addClass(this.domNode, 'focus');
this._clearTimeout = setTimeout(() => {
dom.removeClass(this.domNode, 'focus');
}, 3000);
}
}
dispose() {
this._toDispose.forEach(disposeable => disposeable.dispose());
}
}

View File

@@ -0,0 +1,345 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread2 } from 'vs/editor/common/modes';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Range, IRange } from 'vs/editor/common/core/range';
import { keys } from 'vs/base/common/map';
import { CancellationToken } from 'vs/base/common/cancellation';
import { assign } from 'vs/base/common/objects';
import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { MainThreadCommentController } from 'vs/workbench/api/electron-browser/mainThreadComments';
export const ICommentService = createDecorator<ICommentService>('commentService');
export interface IResourceCommentThreadEvent {
resource: URI;
commentInfos: ICommentInfo[];
}
export interface ICommentInfo extends CommentInfo {
owner: string;
}
export interface IWorkspaceCommentThreadsEvent {
ownerId: string;
commentThreads: CommentThread[];
}
export interface ICommentService {
_serviceBrand: any;
readonly onDidSetResourceCommentInfos: Event<IResourceCommentThreadEvent>;
readonly onDidSetAllCommentThreads: Event<IWorkspaceCommentThreadsEvent>;
readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent>;
readonly onDidChangeActiveCommentThread: Event<CommentThread | null>;
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>;
readonly onDidChangeInput: Event<string>;
readonly onDidSetDataProvider: Event<void>;
readonly onDidDeleteDataProvider: Event<string>;
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void;
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void;
removeWorkspaceComments(owner: string): void;
registerCommentController(owner: string, commentControl: MainThreadCommentController): void;
unregisterCommentController(owner: string): void;
registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void;
unregisterDataProvider(owner: string): void;
updateComments(ownerId: string, event: CommentThreadChangedEvent): void;
createNewCommentThread(owner: string, resource: URI, range: Range, text: string): Promise<CommentThread | null>;
replyToCommentThread(owner: string, resource: URI, range: Range, thread: CommentThread, text: string): Promise<CommentThread | null>;
editComment(owner: string, resource: URI, comment: Comment, text: string): Promise<void>;
deleteComment(owner: string, resource: URI, comment: Comment): Promise<boolean>;
getComments(resource: URI): Promise<(ICommentInfo | null)[]>;
getCommentingRanges(resource: URI): Promise<IRange[]>;
startDraft(owner: string, resource: URI): void;
deleteDraft(owner: string, resource: URI): void;
finishDraft(owner: string, resource: URI): void;
getStartDraftLabel(owner: string): string | undefined;
getDeleteDraftLabel(owner: string): string | undefined;
getFinishDraftLabel(owner: string): string | undefined;
addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void>;
deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void>;
getReactionGroup(owner: string): CommentReaction[] | undefined;
toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise<void>;
setActiveCommentThread(commentThread: CommentThread | null);
setInput(input: string);
}
export class CommentService extends Disposable implements ICommentService {
_serviceBrand: any;
private readonly _onDidSetDataProvider: Emitter<void> = this._register(new Emitter<void>());
readonly onDidSetDataProvider: Event<void> = this._onDidSetDataProvider.event;
private readonly _onDidDeleteDataProvider: Emitter<string> = this._register(new Emitter<string>());
readonly onDidDeleteDataProvider: Event<string> = this._onDidDeleteDataProvider.event;
private readonly _onDidSetResourceCommentInfos: Emitter<IResourceCommentThreadEvent> = this._register(new Emitter<IResourceCommentThreadEvent>());
readonly onDidSetResourceCommentInfos: Event<IResourceCommentThreadEvent> = this._onDidSetResourceCommentInfos.event;
private readonly _onDidSetAllCommentThreads: Emitter<IWorkspaceCommentThreadsEvent> = this._register(new Emitter<IWorkspaceCommentThreadsEvent>());
readonly onDidSetAllCommentThreads: Event<IWorkspaceCommentThreadsEvent> = this._onDidSetAllCommentThreads.event;
private readonly _onDidUpdateCommentThreads: Emitter<ICommentThreadChangedEvent> = this._register(new Emitter<ICommentThreadChangedEvent>());
readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent> = this._onDidUpdateCommentThreads.event;
private readonly _onDidChangeActiveCommentThread = this._register(new Emitter<CommentThread | null>());
readonly onDidChangeActiveCommentThread: Event<CommentThread | null> = this._onDidChangeActiveCommentThread.event;
private readonly _onDidChangeInput: Emitter<string> = this._register(new Emitter<string>());
readonly onDidChangeInput: Event<string> = this._onDidChangeInput.event;
private readonly _onDidChangeActiveCommentingRange: Emitter<{
range: Range, commentingRangesInfo:
CommentingRanges
}> = this._register(new Emitter<{
range: Range, commentingRangesInfo:
CommentingRanges
}>());
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }> = this._onDidChangeActiveCommentingRange.event;
private _commentProviders = new Map<string, DocumentCommentProvider>();
private _commentControls = new Map<string, MainThreadCommentController>();
constructor() {
super();
}
setActiveCommentThread(commentThread: CommentThread | null) {
this._onDidChangeActiveCommentThread.fire(commentThread);
}
setInput(input: string) {
this._onDidChangeInput.fire(input);
}
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void {
this._onDidSetResourceCommentInfos.fire({ resource, commentInfos });
}
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void {
this._onDidSetAllCommentThreads.fire({ ownerId: owner, commentThreads: commentsByResource });
}
removeWorkspaceComments(owner: string): void {
this._onDidSetAllCommentThreads.fire({ ownerId: owner, commentThreads: [] });
}
registerCommentController(owner: string, commentControl: MainThreadCommentController): void {
this._commentControls.set(owner, commentControl);
this._onDidSetDataProvider.fire();
}
unregisterCommentController(owner: string): void {
this._commentControls.delete(owner);
this._onDidDeleteDataProvider.fire(owner);
}
registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void {
this._commentProviders.set(owner, commentProvider);
this._onDidSetDataProvider.fire();
}
unregisterDataProvider(owner: string): void {
this._commentProviders.delete(owner);
this._onDidDeleteDataProvider.fire(owner);
}
updateComments(ownerId: string, event: CommentThreadChangedEvent): void {
const evt: ICommentThreadChangedEvent = assign({}, event, { owner: ownerId });
this._onDidUpdateCommentThreads.fire(evt);
}
async createNewCommentThread(owner: string, resource: URI, range: Range, text: string): Promise<CommentThread | null> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return await commentProvider.createNewCommentThread(resource, range, text, CancellationToken.None);
}
return null;
}
async replyToCommentThread(owner: string, resource: URI, range: Range, thread: CommentThread, text: string): Promise<CommentThread | null> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return await commentProvider.replyToCommentThread(resource, range, thread, text, CancellationToken.None);
}
return null;
}
editComment(owner: string, resource: URI, comment: Comment, text: string): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return commentProvider.editComment(resource, comment, text, CancellationToken.None);
}
return Promise.resolve(undefined);
}
deleteComment(owner: string, resource: URI, comment: Comment): Promise<boolean> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return commentProvider.deleteComment(resource, comment, CancellationToken.None).then(() => true);
}
return Promise.resolve(false);
}
async startDraft(owner: string, resource: URI): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider && commentProvider.startDraft) {
return commentProvider.startDraft(resource, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
async deleteDraft(owner: string, resource: URI): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider && commentProvider.deleteDraft) {
return commentProvider.deleteDraft(resource, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
async finishDraft(owner: string, resource: URI): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider && commentProvider.finishDraft) {
return commentProvider.finishDraft(resource, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
async addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider && commentProvider.addReaction) {
return commentProvider.addReaction(resource, comment, reaction, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
async deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider && commentProvider.deleteReaction) {
return commentProvider.deleteReaction(resource, comment, reaction, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
async toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise<void> {
const commentController = this._commentControls.get(owner);
if (commentController) {
return commentController.toggleReaction(resource, thread, comment, reaction, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
getReactionGroup(owner: string): CommentReaction[] | undefined {
const commentProvider = this._commentControls.get(owner);
if (commentProvider) {
return commentProvider.getReactionGroup();
}
const commentController = this._commentControls.get(owner);
if (commentController) {
return commentController.getReactionGroup();
}
return undefined;
}
getStartDraftLabel(owner: string): string | undefined {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return commentProvider.startDraftLabel;
}
return undefined;
}
getDeleteDraftLabel(owner: string): string | undefined {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return commentProvider.deleteDraftLabel;
}
return undefined;
}
getFinishDraftLabel(owner: string): string | undefined {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return commentProvider.finishDraftLabel;
}
return undefined;
}
async getComments(resource: URI): Promise<(ICommentInfo | null)[]> {
const result: Promise<ICommentInfo | null>[] = [];
for (const owner of keys(this._commentProviders)) {
const provider = this._commentProviders.get(owner);
if (provider && provider.provideDocumentComments) {
result.push(provider.provideDocumentComments(resource, CancellationToken.None).then(commentInfo => {
if (commentInfo) {
return <ICommentInfo>{
owner: owner,
threads: commentInfo.threads,
commentingRanges: commentInfo.commentingRanges,
reply: commentInfo.reply,
draftMode: commentInfo.draftMode
};
} else {
return null;
}
}));
}
}
let commentControlResult: Promise<ICommentInfo>[] = [];
this._commentControls.forEach(control => {
commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None));
});
let ret = [...await Promise.all(result), ...await Promise.all(commentControlResult)];
return ret;
}
async getCommentingRanges(resource: URI): Promise<IRange[]> {
let commentControlResult: Promise<IRange[]>[] = [];
this._commentControls.forEach(control => {
commentControlResult.push(control.getCommentingRanges(resource, CancellationToken.None));
});
let ret = await Promise.all(commentControlResult);
return ret.reduce((prev, curr) => { prev.push(...curr); return prev; }, []);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import 'vs/workbench/contrib/comments/electron-browser/commentsEditorContribution';
import { ICommentService, CommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
export interface ICommentsConfiguration {
openPanel: 'neverOpen' | 'openOnSessionStart' | 'openOnSessionStartWithComments';
}
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'comments',
order: 20,
title: nls.localize('commentsConfigurationTitle', "Comments"),
type: 'object',
properties: {
'comments.openPanel': {
enum: ['neverOpen', 'openOnSessionStart', 'openOnSessionStartWithComments'],
default: 'openOnSessionStartWithComments',
description: nls.localize('openComments', "Controls when the comments panel should open.")
}
}
});
registerSingleton(ICommentService, CommentService);

View File

@@ -0,0 +1,871 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/review';
import * as nls from 'vs/nls';
import { $ } from 'vs/base/browser/dom';
import { findFirstInSorted, coalesce } from 'vs/base/common/arrays';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ICodeEditor, IEditorMouseEvent, IViewZone, MouseTargetType, isDiffEditor, isCodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { registerEditorContribution, EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IEditorContribution, IModelChangedEvent } from 'vs/editor/common/editorCommon';
import { IRange, Range } from 'vs/editor/common/core/range';
import * as modes from 'vs/editor/common/modes';
import { peekViewResultsBackground, peekViewResultsSelectionBackground, peekViewTitleBackground } from 'vs/editor/contrib/referenceSearch/referencesWidget';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { CommentThreadCollapsibleState } from 'vs/workbench/api/node/extHostTypes';
import { ReviewZoneWidget, COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/electron-browser/commentThreadWidget';
import { ICommentService, ICommentInfo } from 'vs/workbench/contrib/comments/electron-browser/commentService';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { IModelDecorationOptions } from 'vs/editor/common/model';
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async';
import { overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/electron-browser/commentGlyphWidget';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/electron-browser/simpleCommentEditor';
import { onUnexpectedError } from 'vs/base/common/errors';
export const ctxCommentThreadVisible = new RawContextKey<boolean>('commentThreadVisible', false);
export const ID = 'editor.contrib.review';
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;
this.domNode = $('.review-viewzone');
}
onDomNodeTop(top: number): void {
this.callback(top);
}
}
class CommentingRangeDecoration {
private _decorationId: string;
public get id(): string {
return this._decorationId;
}
constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private commentingRangesInfo?: modes.CommentingRanges) {
const startLineNumber = _range.startLineNumber;
const endLineNumber = _range.endLineNumber;
let commentingRangeDecorations = [{
range: {
startLineNumber: startLineNumber, startColumn: 1,
endLineNumber: endLineNumber, endColumn: 1
},
options: commentingOptions
}];
let model = this._editor.getModel();
if (model) {
this._decorationId = model.deltaDecorations([this._decorationId], commentingRangeDecorations)[0];
}
}
public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined } {
return {
extensionId: this._extensionId,
replyCommand: this._reply,
ownerId: this._ownerId,
commentingRangesInfo: this.commentingRangesInfo
};
}
public getOriginalRange() {
return this._range;
}
public getActiveRange() {
return this._editor.getModel()!.getDecorationRange(this._decorationId);
}
}
class CommentingRangeDecorator {
private decorationOptions: ModelDecorationOptions;
private commentingRangeDecorations: CommentingRangeDecoration[] = [];
private disposables: IDisposable[] = [];
constructor() {
const decorationOptions: IModelDecorationOptions = {
isWholeLine: true,
linesDecorationsClassName: 'comment-range-glyph comment-diff-added'
};
this.decorationOptions = ModelDecorationOptions.createDynamic(decorationOptions);
}
public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) {
let model = editor.getModel();
if (!model) {
return;
}
let commentingRangeDecorations: CommentingRangeDecoration[] = [];
for (const info of commentInfos) {
if (Array.isArray(info.commentingRanges)) {
info.commentingRanges.forEach(range => {
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, range, info.reply, this.decorationOptions));
});
} else {
(info.commentingRanges ? info.commentingRanges.ranges : []).forEach(range => {
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, range, (info.commentingRanges as modes.CommentingRanges).newCommentThreadCommand, this.decorationOptions, info.commentingRanges as modes.CommentingRanges));
});
}
}
let oldDecorations = this.commentingRangeDecorations.map(decoration => decoration.id);
editor.deltaDecorations(oldDecorations, []);
this.commentingRangeDecorations = commentingRangeDecorations;
}
public getMatchedCommentAction(line: number) {
for (const decoration of this.commentingRangeDecorations) {
const range = decoration.getActiveRange();
if (range && range.startLineNumber <= line && line <= range.endLineNumber) {
return decoration.getCommentAction();
}
}
return null;
}
public dispose(): void {
this.disposables = dispose(this.disposables);
this.commentingRangeDecorations = [];
}
}
export class ReviewController implements IEditorContribution {
private globalToDispose: IDisposable[];
private localToDispose: IDisposable[];
private editor: ICodeEditor;
private _newCommentWidget?: ReviewZoneWidget;
private _commentWidgets: ReviewZoneWidget[];
private _commentThreadVisible: IContextKey<boolean>;
private _commentInfos: ICommentInfo[];
private _commentingRangeDecorator: CommentingRangeDecorator;
private mouseDownInfo: { lineNumber: number } | null = null;
private _commentingRangeSpaceReserved = false;
private _computePromise: CancelablePromise<Array<ICommentInfo | null>> | null;
private _computeCommentingRangePromise: CancelablePromise<ICommentInfo[]> | null;
private _computeCommentingRangeScheduler: Delayer<Array<ICommentInfo | null>> | null;
private _pendingCommentCache: { [key: number]: { [key: string]: string } };
private _pendingNewCommentCache: { [key: string]: { lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, pendingComment: string, draftMode: modes.DraftMode | undefined } };
constructor(
editor: ICodeEditor,
@IContextKeyService readonly contextKeyService: IContextKeyService,
@ICommentService private readonly commentService: ICommentService,
@ICommandService private readonly _commandService: ICommandService,
@INotificationService private readonly notificationService: INotificationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
@IContextMenuService readonly contextMenuService: IContextMenuService,
) {
this.editor = editor;
this.globalToDispose = [];
this.localToDispose = [];
this._commentInfos = [];
this._commentWidgets = [];
this._pendingCommentCache = {};
this._pendingNewCommentCache = {};
this._computePromise = null;
this._commentThreadVisible = ctxCommentThreadVisible.bindTo(contextKeyService);
this._commentingRangeDecorator = new CommentingRangeDecorator();
this.globalToDispose.push(this.commentService.onDidDeleteDataProvider(ownerId => {
// Remove new comment widget and glyph, refresh comments
if (this._newCommentWidget && this._newCommentWidget.owner === ownerId) {
this._newCommentWidget.dispose();
this._newCommentWidget = undefined;
}
delete this._pendingCommentCache[ownerId];
this.beginCompute();
}));
this.globalToDispose.push(this.commentService.onDidSetDataProvider(_ => this.beginCompute()));
this.globalToDispose.push(this.commentService.onDidSetResourceCommentInfos(e => {
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
if (editorURI && editorURI.toString() === e.resource.toString()) {
this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null));
}
}));
this.globalToDispose.push(this.editor.onDidChangeModel(e => this.onModelChanged(e)));
this.codeEditorService.registerDecorationType(COMMENTEDITOR_DECORATION_KEY, {});
this.beginCompute();
}
private beginCompute(): Promise<void> {
this._computePromise = createCancelablePromise(token => {
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
if (editorURI) {
return this.commentService.getComments(editorURI);
}
return Promise.resolve([]);
});
return this._computePromise.then(commentInfos => {
this.setComments(coalesce(commentInfos));
this._computePromise = null;
}, error => console.log(error));
}
private beginComputeCommentingRanges() {
if (this._computeCommentingRangeScheduler) {
if (this._computeCommentingRangePromise) {
this._computeCommentingRangePromise.cancel();
this._computeCommentingRangePromise = null;
}
this._computeCommentingRangeScheduler.trigger(() => {
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
if (editorURI) {
return this.commentService.getComments(editorURI);
}
return Promise.resolve([]);
}).then(commentInfos => {
const meaningfulCommentInfos = coalesce(commentInfos);
this._commentingRangeDecorator.update(this.editor, meaningfulCommentInfos);
}, (err) => {
onUnexpectedError(err);
return null;
});
}
}
public static get(editor: ICodeEditor): ReviewController {
return editor.getContribution<ReviewController>(ID);
}
public revealCommentThread(threadId: string, commentId: string, fetchOnceIfNotExist: boolean): void {
const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId);
if (commentThreadWidget.length === 1) {
commentThreadWidget[0].reveal(commentId);
} else if (fetchOnceIfNotExist) {
this.beginCompute().then(_ => {
this.revealCommentThread(threadId, commentId, false);
});
}
}
public nextCommentThread(): void {
if (!this._commentWidgets.length || !this.editor.hasModel()) {
return;
}
const after = this.editor.getSelection().getEndPosition();
const sortedWidgets = this._commentWidgets.sort((a, b) => {
if (a.commentThread.range.startLineNumber < b.commentThread.range.startLineNumber) {
return -1;
}
if (a.commentThread.range.startLineNumber > b.commentThread.range.startLineNumber) {
return 1;
}
if (a.commentThread.range.startColumn < b.commentThread.range.startColumn) {
return -1;
}
if (a.commentThread.range.startColumn > b.commentThread.range.startColumn) {
return 1;
}
return 0;
});
let idx = findFirstInSorted(sortedWidgets, widget => {
if (widget.commentThread.range.startLineNumber > after.lineNumber) {
return true;
}
if (widget.commentThread.range.startLineNumber < after.lineNumber) {
return false;
}
if (widget.commentThread.range.startColumn > after.column) {
return true;
}
return false;
});
if (idx === this._commentWidgets.length) {
this._commentWidgets[0].reveal();
this.editor.setSelection(this._commentWidgets[0].commentThread.range);
} else {
sortedWidgets[idx].reveal();
this.editor.setSelection(sortedWidgets[idx].commentThread.range);
}
}
public getId(): string {
return ID;
}
public dispose(): void {
this.globalToDispose = dispose(this.globalToDispose);
this.localToDispose = dispose(this.localToDispose);
this._commentWidgets.forEach(widget => widget.dispose());
if (this._newCommentWidget) {
this._newCommentWidget.dispose();
this._newCommentWidget = undefined;
}
this.editor = null!; // Strict null override — nulling out in dispose
}
public onModelChanged(e: IModelChangedEvent): void {
this.localToDispose = dispose(this.localToDispose);
if (this._newCommentWidget) {
let pendingNewComment = this._newCommentWidget.getPendingComment();
if (e.oldModelUrl) {
if (pendingNewComment) {
// we can't fetch zone widget's position as the model is already gone
const position = this._newCommentWidget.getPosition();
if (position) {
this._pendingNewCommentCache[e.oldModelUrl.toString()] = {
lineNumber: position.lineNumber,
ownerId: this._newCommentWidget.owner,
extensionId: this._newCommentWidget.extensionId,
replyCommand: this._newCommentWidget.commentThread.reply,
pendingComment: pendingNewComment,
draftMode: this._newCommentWidget.draftMode
};
}
} else {
// clear cache if it is empty
delete this._pendingNewCommentCache[e.oldModelUrl.toString()];
}
}
this._newCommentWidget.dispose();
this._newCommentWidget = undefined;
}
this.removeCommentWidgetsAndStoreCache();
if (e.newModelUrl && this._pendingNewCommentCache[e.newModelUrl.toString()]) {
let newCommentCache = this._pendingNewCommentCache[e.newModelUrl.toString()];
this.addComment(newCommentCache.lineNumber, newCommentCache.replyCommand, newCommentCache.ownerId, newCommentCache.extensionId, newCommentCache.draftMode, newCommentCache.pendingComment);
}
this.localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
this.localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));
this._computeCommentingRangeScheduler = new Delayer<ICommentInfo[]>(200);
this.localToDispose.push({
dispose: () => {
if (this._computeCommentingRangeScheduler) {
this._computeCommentingRangeScheduler.cancel();
}
this._computeCommentingRangeScheduler = null;
}
});
this.localToDispose.push(this.editor.onDidChangeModelContent(async () => {
this.beginComputeCommentingRanges();
}));
this.localToDispose.push(this.commentService.onDidUpdateCommentThreads(e => {
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
if (!editorURI) {
return;
}
let commentInfo = this._commentInfos.filter(info => info.owner === e.owner);
if (!commentInfo || !commentInfo.length) {
return;
}
let added = e.added.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
let removed = e.removed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
let changed = e.changed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
let draftMode = e.draftMode;
commentInfo.forEach(info => info.draftMode = draftMode);
this._commentWidgets.filter(ZoneWidget => ZoneWidget.owner === e.owner).forEach(widget => widget.updateDraftMode(draftMode));
if (this._newCommentWidget && this._newCommentWidget.owner === e.owner) {
this._newCommentWidget.updateDraftMode(draftMode);
}
removed.forEach(thread => {
let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId);
if (matchedZones.length) {
let matchedZone = matchedZones[0];
let index = this._commentWidgets.indexOf(matchedZone);
this._commentWidgets.splice(index, 1);
matchedZone.dispose();
}
});
changed.forEach(thread => {
let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId);
if (matchedZones.length) {
let matchedZone = matchedZones[0];
matchedZone.update(thread);
}
});
added.forEach(thread => {
this.displayCommentThread(e.owner, thread, null, draftMode);
this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread);
});
}));
this.beginCompute();
}
private displayCommentThread(owner: string, thread: modes.CommentThread | modes.CommentThread2, pendingComment: string | null, draftMode: modes.DraftMode | undefined): void {
const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment, draftMode);
zoneWidget.display(thread.range.startLineNumber);
this._commentWidgets.push(zoneWidget);
}
private addComment(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, draftMode: modes.DraftMode | undefined, pendingComment: string | null) {
if (this._newCommentWidget) {
this.notificationService.warn(`Please submit the comment at line ${this._newCommentWidget.position ? this._newCommentWidget.position.lineNumber : -1} before creating a new one.`);
return;
}
// add new comment
this._commentThreadVisible.set(true);
this._newCommentWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, ownerId, {
extensionId: extensionId,
threadId: null,
resource: null,
comments: [],
range: {
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: 0
},
reply: replyCommand,
collapsibleState: CommentThreadCollapsibleState.Expanded,
}, pendingComment, draftMode);
this.localToDispose.push(this._newCommentWidget!.onDidClose(e => {
this.clearNewCommentWidget();
}));
this.localToDispose.push(this._newCommentWidget!.onDidCreateThread(commentWidget => {
const thread = commentWidget.commentThread;
this._commentWidgets.push(commentWidget);
this._commentInfos.filter(info => info.owner === commentWidget.owner)[0].threads.push(thread);
this.clearNewCommentWidget();
}));
this._newCommentWidget!.display(lineNumber);
}
private clearNewCommentWidget() {
this._newCommentWidget = undefined;
if (this.editor && this.editor.hasModel()) {
delete this._pendingNewCommentCache[this.editor.getModel().uri.toString()];
}
}
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;
}
const data = e.target.detail as IMarginData;
const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;
// don't collide with folding and git decorations
if (gutterOffsetX > 14) {
return;
}
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;
}
if (!e.target.element) {
return;
}
if (e.target.element.className.indexOf('comment-diff-added') >= 0) {
const lineNumber = e.target.position!.lineNumber;
this.addCommentAtLine(lineNumber);
}
}
public addOrToggleCommentAtLine(lineNumber: number): void {
// The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead
const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === lineNumber);
if (existingCommentsAtLine.length) {
existingCommentsAtLine.forEach(widget => widget.toggleExpand(lineNumber));
return;
} else {
this.addCommentAtLine(lineNumber);
}
}
public addCommentAtLine(lineNumber: number): void {
const newCommentInfo = this._commentingRangeDecorator.getMatchedCommentAction(lineNumber);
if (!newCommentInfo || !this.editor.hasModel()) {
return;
}
const { replyCommand, ownerId, extensionId, commentingRangesInfo } = newCommentInfo;
if (commentingRangesInfo) {
let range = new Range(lineNumber, 1, lineNumber, 1);
if (commentingRangesInfo.newCommentThreadCommand) {
if (replyCommand) {
const commandId = replyCommand.id;
const args = replyCommand.arguments || [];
this._commandService.executeCommand(commandId, ...args);
}
} else if (commentingRangesInfo.newCommentThreadCallback) {
commentingRangesInfo.newCommentThreadCallback(this.editor.getModel().uri, range);
}
} else {
const commentInfo = this._commentInfos.filter(info => info.owner === ownerId);
if (!commentInfo || !commentInfo.length) {
return;
}
const draftMode = commentInfo[0].draftMode;
this.addComment(lineNumber, replyCommand, ownerId, extensionId, draftMode, null);
}
}
private setComments(commentInfos: ICommentInfo[]): void {
if (!this.editor) {
return;
}
this._commentInfos = commentInfos;
let lineDecorationsWidth: number = this.editor.getConfiguration().layoutInfo.decorationsWidth;
if (this._commentInfos.some(info => Boolean(info.commentingRanges && (Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges.ranges).length))) {
if (!this._commentingRangeSpaceReserved) {
this._commentingRangeSpaceReserved = true;
let extraEditorClassName: string[] = [];
const configuredExtraClassName = this.editor.getRawConfiguration().extraEditorClassName;
if (configuredExtraClassName) {
extraEditorClassName = configuredExtraClassName.split(' ');
}
if (this.editor.getConfiguration().contribInfo.folding) {
lineDecorationsWidth -= 16;
}
lineDecorationsWidth += 9;
extraEditorClassName.push('inline-comment');
this.editor.updateOptions({
extraEditorClassName: extraEditorClassName.join(' '),
lineDecorationsWidth: lineDecorationsWidth
});
// we only update the lineDecorationsWidth property but keep the width of the whole editor.
const originalLayoutInfo = this.editor.getLayoutInfo();
this.editor.layout({
width: originalLayoutInfo.width,
height: originalLayoutInfo.height
});
}
}
// create viewzones
this.removeCommentWidgetsAndStoreCache();
this._commentInfos.forEach(info => {
let providerCacheStore = this._pendingCommentCache[info.owner];
info.threads.forEach(thread => {
let pendingComment: string | null = null;
if (providerCacheStore) {
pendingComment = providerCacheStore[thread.threadId];
}
if (pendingComment) {
thread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded;
}
this.displayCommentThread(info.owner, thread, pendingComment, info.draftMode);
});
});
const commentingRanges: IRange[] = [];
this._commentInfos.forEach(info => {
commentingRanges.push(...(Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges ? info.commentingRanges.ranges : []));
});
this._commentingRangeDecorator.update(this.editor, this._commentInfos);
}
public closeWidget(): void {
this._commentThreadVisible.reset();
if (this._newCommentWidget) {
this._newCommentWidget.dispose();
this._newCommentWidget = undefined;
}
if (this._commentWidgets) {
this._commentWidgets.forEach(widget => widget.hide());
}
this.editor.focus();
this.editor.revealRangeInCenter(this.editor.getSelection()!);
}
private removeCommentWidgetsAndStoreCache() {
if (this._commentWidgets) {
this._commentWidgets.forEach(zone => {
let pendingComment = zone.getPendingComment();
let providerCacheStore = this._pendingCommentCache[zone.owner];
if (pendingComment) {
if (!providerCacheStore) {
this._pendingCommentCache[zone.owner] = {};
}
this._pendingCommentCache[zone.owner][zone.commentThread.threadId] = pendingComment;
} else {
if (providerCacheStore) {
delete providerCacheStore[zone.commentThread.threadId];
}
}
zone.dispose();
});
}
this._commentWidgets = [];
}
}
export class NextCommentThreadAction extends EditorAction {
constructor() {
super({
id: 'editor.action.nextCommentThreadAction',
label: nls.localize('nextCommentThreadAction', "Go to Next Comment Thread"),
alias: 'Go to Next Comment Thread',
precondition: null,
});
}
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
let controller = ReviewController.get(editor);
if (controller) {
controller.nextCommentThread();
}
}
}
registerEditorContribution(ReviewController);
registerEditorAction(NextCommentThreadAction);
CommandsRegistry.registerCommand({
id: 'workbench.action.addComment',
handler: (accessor) => {
const activeEditor = getActiveEditor(accessor);
if (!activeEditor) {
return Promise.resolve();
}
const controller = ReviewController.get(activeEditor);
if (!controller) {
return Promise.resolve();
}
const position = activeEditor.getPosition();
controller.addOrToggleCommentAtLine(position.lineNumber);
return Promise.resolve();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.action.submitComment',
weight: KeybindingWeight.EditorContrib,
primary: KeyMod.CtrlCmd | KeyCode.Enter,
when: ctxCommentEditorFocused,
handler: (accessor, args) => {
const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
if (activeCodeEditor instanceof SimpleCommentEditor) {
activeCodeEditor.getParentThread().submitComment();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'closeReviewPanel',
weight: KeybindingWeight.EditorContrib,
primary: KeyCode.Escape,
secondary: [KeyMod.Shift | KeyCode.Escape],
when: ctxCommentThreadVisible,
handler: closeReviewPanel
});
export function getActiveEditor(accessor: ServicesAccessor): IActiveCodeEditor | null {
let activeTextEditorWidget = accessor.get(IEditorService).activeTextEditorWidget;
if (isDiffEditor(activeTextEditorWidget)) {
if (activeTextEditorWidget.getOriginalEditor().hasTextFocus()) {
activeTextEditorWidget = activeTextEditorWidget.getOriginalEditor();
} else {
activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor();
}
}
if (!isCodeEditor(activeTextEditorWidget) || !activeTextEditorWidget.hasModel()) {
return null;
}
return activeTextEditorWidget;
}
function closeReviewPanel(accessor: ServicesAccessor, args: any) {
const outerEditor = getActiveEditor(accessor);
if (!outerEditor) {
return;
}
const controller = ReviewController.get(outerEditor);
if (!controller) {
return;
}
controller.closeWidget();
}
registerThemingParticipant((theme, collector) => {
const peekViewBackground = theme.getColor(peekViewResultsBackground);
if (peekViewBackground) {
collector.addRule(
`.monaco-editor .review-widget,` +
`.monaco-editor .review-widget {` +
` background-color: ${peekViewBackground};` +
`}`);
}
const monacoEditorBackground = theme.getColor(peekViewTitleBackground);
if (monacoEditorBackground) {
collector.addRule(
`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` +
` background-color: ${monacoEditorBackground}` +
`}`
);
}
const monacoEditorForeground = theme.getColor(editorForeground);
if (monacoEditorForeground) {
collector.addRule(
`.monaco-editor .review-widget .body .monaco-editor {` +
` color: ${monacoEditorForeground}` +
`}` +
`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` +
` color: ${monacoEditorForeground};` +
` font-size: inherit` +
`}`
);
}
const 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;` +
`}`
);
}
const commentingRangeForeground = theme.getColor(overviewRulerCommentingRangeForeground);
if (commentingRangeForeground) {
collector.addRule(`
.monaco-editor .comment-diff-added {
border-left: 3px solid ${commentingRangeForeground};
}
.monaco-editor .comment-diff-added:before {
background: ${commentingRangeForeground};
}
.monaco-editor .comment-thread {
border-left: 3px solid ${commentingRangeForeground};
}
.monaco-editor .comment-thread:before {
background: ${commentingRangeForeground};
}
`);
}
const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);
if (statusBarItemHoverBackground) {
collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active:hover { background-color: ${statusBarItemHoverBackground};}`);
}
const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND);
if (statusBarItemActiveBackground) {
collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid transparent;}`);
}
});

View File

@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/panel';
import * as dom from 'vs/base/browser/dom';
import { IAction } from 'vs/base/common/actions';
import { Event } from 'vs/base/common/event';
import { CollapseAllAction, DefaultAccessibilityProvider, DefaultController, DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults';
import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TreeResourceNavigator, WorkbenchTree } from 'vs/platform/list/browser/listService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Panel } from 'vs/workbench/browser/panel';
import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { ReviewController } from 'vs/workbench/contrib/comments/electron-browser/commentsEditorContribution';
import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'vs/workbench/contrib/comments/electron-browser/commentsTreeViewer';
import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/electron-browser/commentService';
import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ResourceLabels } from 'vs/workbench/browser/labels';
export const COMMENTS_PANEL_ID = 'workbench.panel.comments';
export const COMMENTS_PANEL_TITLE = 'Comments';
export class CommentsPanel extends Panel {
private treeLabels: ResourceLabels;
private tree: WorkbenchTree;
private treeContainer: HTMLElement;
private messageBoxContainer: HTMLElement;
private messageBox: HTMLElement;
private commentsModel: CommentsModel;
private collapseAllAction: IAction;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ICommentService private readonly commentService: ICommentService,
@IEditorService private readonly editorService: IEditorService,
@ICommandService private readonly commandService: ICommandService,
@IOpenerService private readonly openerService: IOpenerService,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService
) {
super(COMMENTS_PANEL_ID, telemetryService, themeService, storageService);
}
public create(parent: HTMLElement): void {
super.create(parent);
dom.addClass(parent, 'comments-panel');
let container = dom.append(parent, dom.$('.comments-panel-container'));
this.treeContainer = dom.append(container, dom.$('.tree-container'));
this.commentsModel = new CommentsModel();
this.createTree();
this.createMessageBox(container);
this._register(this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this));
this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this));
const styleElement = dom.createStyleSheet(parent);
this.applyStyles(styleElement);
this._register(this.themeService.onThemeChange(_ => this.applyStyles(styleElement)));
this._register(this.onDidChangeVisibility(visible => {
if (visible) {
this.refresh();
}
}));
this.render();
}
private applyStyles(styleElement: HTMLStyleElement) {
const content: string[] = [];
const theme = this.themeService.getTheme();
const linkColor = theme.getColor(textLinkForeground);
if (linkColor) {
content.push(`.comments-panel .comments-panel-container a { color: ${linkColor}; }`);
}
const linkActiveColor = theme.getColor(textLinkActiveForeground);
if (linkActiveColor) {
content.push(`.comments-panel .comments-panel-container a:hover, a:active { color: ${linkActiveColor}; }`);
}
const focusColor = theme.getColor(focusBorder);
if (focusColor) {
content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`);
}
const codeTextForegroundColor = theme.getColor(textPreformatForeground);
if (codeTextForegroundColor) {
content.push(`.comments-panel .comments-panel-container .text code { color: ${codeTextForegroundColor}; }`);
}
styleElement.innerHTML = content.join('\n');
}
private async render(): Promise<void> {
dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads());
await this.tree.setInput(this.commentsModel);
this.renderMessage();
}
public getActions(): IAction[] {
if (!this.collapseAllAction) {
this.collapseAllAction = this.instantiationService.createInstance(CollapseAllAction, this.tree, this.commentsModel.hasCommentThreads());
this._register(this.collapseAllAction);
}
return [this.collapseAllAction];
}
public layout(dimensions: dom.Dimension): void {
this.tree.layout(dimensions.height, dimensions.width);
}
public getTitle(): string {
return COMMENTS_PANEL_TITLE;
}
private createMessageBox(parent: HTMLElement): void {
this.messageBoxContainer = dom.append(parent, dom.$('.message-box-container'));
this.messageBox = dom.append(this.messageBoxContainer, dom.$('span'));
this.messageBox.setAttribute('tabindex', '0');
}
private renderMessage(): void {
this.messageBox.textContent = this.commentsModel.getMessage();
dom.toggleClass(this.messageBoxContainer, 'hidden', this.commentsModel.hasCommentThreads());
}
private createTree(): void {
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
this.tree = this._register(this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, {
dataSource: new CommentsDataSource(),
renderer: new CommentsModelRenderer(this.treeLabels, this.openerService),
accessibilityProvider: new DefaultAccessibilityProvider,
controller: new DefaultController(),
dnd: new DefaultDragAndDrop(),
filter: new CommentsDataFilter()
}, {
twistiePixels: 20,
ariaLabel: COMMENTS_PANEL_TITLE
}));
const commentsNavigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: true }));
this._register(Event.debounce(commentsNavigator.openResource, (last, event) => event, 100, true)(options => {
this.openFile(options.element, options.editorOptions.pinned, options.editorOptions.preserveFocus, options.sideBySide);
}));
}
private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): boolean {
if (!element) {
return false;
}
if (!(element instanceof ResourceWithCommentThreads || element instanceof CommentNode)) {
return false;
}
const range = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].range : element.range;
const activeEditor = this.editorService.activeEditor;
let currentActiveResource = activeEditor ? activeEditor.getResource() : undefined;
if (currentActiveResource && currentActiveResource.toString() === element.resource.toString()) {
const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId;
const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.commentId : element.comment.commentId;
const control = this.editorService.activeTextEditorWidget;
if (threadToReveal && isCodeEditor(control)) {
const controller = ReviewController.get(control);
controller.revealCommentThread(threadToReveal, commentToReveal, false);
}
return true;
}
const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId;
const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment : element.comment;
if (commentToReveal.selectCommand) {
this.commandService.executeCommand(commentToReveal.selectCommand.id, ...(commentToReveal.selectCommand.arguments || [])).then(_ => {
let activeWidget = this.editorService.activeTextEditorWidget;
if (isDiffEditor(activeWidget)) {
const originalEditorWidget = activeWidget.getOriginalEditor();
const modifiedEditorWidget = activeWidget.getModifiedEditor();
let controller;
if (originalEditorWidget.getModel()!.uri.toString() === element.resource.toString()) {
controller = ReviewController.get(originalEditorWidget);
} else if (modifiedEditorWidget.getModel()!.uri.toString() === element.resource.toString()) {
controller = ReviewController.get(modifiedEditorWidget);
}
if (controller) {
controller.revealCommentThread(threadToReveal, commentToReveal.commentId, true);
}
} else {
let activeEditor = this.editorService.activeEditor;
let currentActiveResource = activeEditor ? activeEditor.getResource() : undefined;
if (currentActiveResource && currentActiveResource.toString() === element.resource.toString()) {
const control = this.editorService.activeTextEditorWidget;
if (threadToReveal && isCodeEditor(control)) {
const controller = ReviewController.get(control);
controller.revealCommentThread(threadToReveal, commentToReveal.commentId, true);
}
}
}
return true;
});
} else {
this.editorService.openEditor({
resource: element.resource,
options: {
pinned: pinned,
preserveFocus: preserveFocus,
selection: range
}
}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {
if (editor) {
const control = editor.getControl();
if (threadToReveal && isCodeEditor(control)) {
const controller = ReviewController.get(control);
controller.revealCommentThread(threadToReveal, commentToReveal.commentId, true);
}
}
});
}
return true;
}
private refresh(): void {
if (this.isVisible()) {
this.collapseAllAction.enabled = this.commentsModel.hasCommentThreads();
dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads());
this.tree.refresh().then(() => {
this.renderMessage();
}, (e) => {
console.log(e);
});
}
}
private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void {
this.commentsModel.setCommentThreads(e.ownerId, e.commentThreads);
this.refresh();
}
private onCommentsUpdated(e: ICommentThreadChangedEvent): void {
const didUpdate = this.commentsModel.updateCommentThreads(e);
if (didUpdate) {
this.refresh();
}
}
}

View File

@@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import * as nls from 'vs/nls';
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IDataSource, IFilter, IRenderer as ITreeRenderer, ITree } from 'vs/base/parts/tree/browser/tree';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel';
export class CommentsDataSource implements IDataSource {
public getId(tree: ITree, element: any): string {
if (element instanceof CommentsModel) {
return 'root';
}
if (element instanceof ResourceWithCommentThreads) {
return element.id;
}
if (element instanceof CommentNode) {
return `${element.resource.toString()}-${element.comment.commentId}`;
}
return '';
}
public hasChildren(tree: ITree, element: any): boolean {
return element instanceof CommentsModel || element instanceof ResourceWithCommentThreads || (element instanceof CommentNode && !!element.replies.length);
}
public getChildren(tree: ITree, element: any): Promise<ResourceWithCommentThreads[] | CommentNode[]> {
if (element instanceof CommentsModel) {
return Promise.resolve(element.resourceCommentThreads);
}
if (element instanceof ResourceWithCommentThreads) {
return Promise.resolve(element.commentThreads);
}
if (element instanceof CommentNode) {
return Promise.resolve(element.replies);
}
return Promise.resolve([]);
}
public getParent(tree: ITree, element: any): Promise<void> {
return Promise.resolve(undefined);
}
public shouldAutoexpand(tree: ITree, element: any): boolean {
return true;
}
}
interface IResourceTemplateData {
resourceLabel: IResourceLabel;
}
interface ICommentThreadTemplateData {
icon: HTMLImageElement;
userName: HTMLSpanElement;
commentText: HTMLElement;
disposables: Disposable[];
}
export class CommentsModelRenderer implements ITreeRenderer {
private static RESOURCE_ID = 'resource-with-comments';
private static COMMENT_ID = 'comment-node';
constructor(
private labels: ResourceLabels,
@IOpenerService private readonly openerService: IOpenerService
) {
}
public getHeight(tree: ITree, element: any): number {
return 22;
}
public getTemplateId(tree: ITree, element: any): string {
if (element instanceof ResourceWithCommentThreads) {
return CommentsModelRenderer.RESOURCE_ID;
}
if (element instanceof CommentNode) {
return CommentsModelRenderer.COMMENT_ID;
}
return '';
}
public renderTemplate(ITree: ITree, templateId: string, container: HTMLElement): any {
switch (templateId) {
case CommentsModelRenderer.RESOURCE_ID:
return this.renderResourceTemplate(container);
case CommentsModelRenderer.COMMENT_ID:
return this.renderCommentTemplate(container);
}
}
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
switch (templateId) {
case CommentsModelRenderer.RESOURCE_ID:
(<IResourceTemplateData>templateData).resourceLabel.dispose();
break;
case CommentsModelRenderer.COMMENT_ID:
(<ICommentThreadTemplateData>templateData).disposables.forEach(disposeable => disposeable.dispose());
break;
}
}
public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
switch (templateId) {
case CommentsModelRenderer.RESOURCE_ID:
return this.renderResourceElement(tree, element, templateData);
case CommentsModelRenderer.COMMENT_ID:
return this.renderCommentElement(tree, element, templateData);
}
}
private renderResourceTemplate(container: HTMLElement): IResourceTemplateData {
const data = <IResourceTemplateData>Object.create(null);
const labelContainer = dom.append(container, dom.$('.resource-container'));
data.resourceLabel = this.labels.create(labelContainer);
return data;
}
private renderCommentTemplate(container: HTMLElement): ICommentThreadTemplateData {
const data = <ICommentThreadTemplateData>Object.create(null);
const labelContainer = dom.append(container, dom.$('.comment-container'));
data.userName = dom.append(labelContainer, dom.$('.user'));
data.commentText = dom.append(labelContainer, dom.$('.text'));
data.disposables = [];
return data;
}
private renderResourceElement(tree: ITree, element: ResourceWithCommentThreads, templateData: IResourceTemplateData) {
templateData.resourceLabel.setFile(element.resource);
}
private renderCommentElement(tree: ITree, element: CommentNode, templateData: ICommentThreadTemplateData) {
templateData.userName.textContent = element.comment.userName;
templateData.commentText.innerHTML = '';
const renderedComment = renderMarkdown(element.comment.body, {
inline: true,
actionHandler: {
callback: (content) => {
try {
const uri = URI.parse(content);
this.openerService.open(uri).catch(onUnexpectedError);
} catch (err) {
// ignore
}
},
disposeables: templateData.disposables
}
});
const images = renderedComment.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
const image = images[i];
const textDescription = dom.$('');
textDescription.textContent = image.alt ? nls.localize('imageWithLabel', "Image: {0}", image.alt) : nls.localize('image', "Image");
image.parentNode!.replaceChild(textDescription, image);
}
templateData.commentText.appendChild(renderedComment);
}
}
export class CommentsDataFilter implements IFilter {
public isVisible(tree: ITree, element: any): boolean {
if (element instanceof CommentsModel) {
return element.resourceCommentThreads.length > 0;
}
if (element instanceof ResourceWithCommentThreads) {
return element.commentThreads.length > 0;
}
return true;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" aria-hidden="true" height="16" width="16"><path fill="#C5C5C5" fill-rule="evenodd" d="M14 1H2c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1h2v3.5L7.5 11H14c.55 0 1-.45 1-1V2c0-.55-.45-1-1-1zm0 9H7l-2 2v-2H2V2h12v8z"></path></svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.comments-panel .comments-panel-container {
height: 100%;
}
.comments-panel .comments-panel-container .hidden {
display: none;
}
.comments-panel .comments-panel-container .tree-container {
height: 100%;
}
.comments-panel .comments-panel-container .tree-container.hidden {
display: none;
visibility: hidden;
}
.comments-panel .comments-panel-container .tree-container .resource-container,
.comments-panel .comments-panel-container .tree-container .comment-container {
display: flex;
}
.comments-panel .user {
padding-right: 5px;
opacity: 0.5;
}
.comments-panel .comments-panel-container .tree-container .comment-container .text {
flex: 1;
min-width: 0;
}
.comments-panel .comments-panel-container .tree-container .comment-container .text * {
margin: 0;
text-overflow: ellipsis;
overflow: hidden;
}
.comments-panel .comments-panel-container .tree-container .comment-container .text code {
font-family: var(--monaco-monospace-font);
}
.comments-panel .comments-panel-container .message-box-container {
line-height: 22px;
padding-left: 20px;
}
.comments-panel .comments-panel-container .message-box-container span:focus {
outline: none;
}
.comments-panel .comments-panel-container .tree-container .count-badge-wrapper {
margin-left: 10px;
}
.comments-panel .comments-panel-container .tree-container .comment-container {
line-height: 22px;
margin-right: 5px;
}

View File

@@ -0,0 +1,4 @@
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8.25008H4.08333V11.1667H2.91667V8.25008H0V7.08341H2.91667V4.16675H4.08333V7.08341H7V8.25008V8.25008Z" fill="#C8C8C8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 0C13.58 0 10 3.58 10 8C10 12.42 13.58 16 18 16C22.42 16 26 12.42 26 8C26 3.58 22.42 0 18 0V0ZM22.81 12.81C22.18 13.44 21.45 13.92 20.64 14.26C19.81 14.62 18.92 14.79 18 14.79C17.08 14.79 16.19 14.62 15.36 14.26C14.55 13.92 13.81 13.43 13.19 12.81C12.57 12.19 12.08 11.45 11.74 10.64C11.38 9.81 11.21 8.92 11.21 8C11.21 7.08 11.38 6.19 11.74 5.36C12.08 4.55 12.57 3.81 13.19 3.19C13.81 2.57 14.55 2.08 15.36 1.74C16.19 1.38 17.08 1.21 18 1.21C18.92 1.21 19.81 1.38 20.64 1.74C21.45 2.08 22.19 2.57 22.81 3.19C23.43 3.81 23.92 4.55 24.26 5.36C24.62 6.19 24.79 7.08 24.79 8C24.79 8.92 24.62 9.81 24.26 10.64C23.92 11.45 23.43 12.19 22.81 12.81V12.81ZM14 6.8V6.21C14 5.55 14.53 5.02 15.2 5.02H15.79C16.45 5.02 16.98 5.55 16.98 6.21V6.8C16.98 7.47 16.45 8 15.79 8H15.2C14.53 8 14 7.47 14 6.8V6.8ZM19 6.8V6.21C19 5.55 19.53 5.02 20.2 5.02H20.79C21.45 5.02 21.98 5.55 21.98 6.21V6.8C21.98 7.47 21.45 8 20.79 8H20.2C19.53 8 19 7.47 19 6.8V6.8ZM23 10C22.28 11.88 20.09 13 18 13C15.91 13 13.72 11.87 13 10C12.86 9.61 13.23 9 13.66 9H22.25C22.66 9 23.14 9.61 23 10V10Z" fill="#C8C8C8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8.25008H4.08333V11.1667H2.91667V8.25008H0V7.08341H2.91667V4.16675H4.08333V7.08341H7V8.25008V8.25008Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 0C13.58 0 10 3.58 10 8C10 12.42 13.58 16 18 16C22.42 16 26 12.42 26 8C26 3.58 22.42 0 18 0V0ZM22.81 12.81C22.18 13.44 21.45 13.92 20.64 14.26C19.81 14.62 18.92 14.79 18 14.79C17.08 14.79 16.19 14.62 15.36 14.26C14.55 13.92 13.81 13.43 13.19 12.81C12.57 12.19 12.08 11.45 11.74 10.64C11.38 9.81 11.21 8.92 11.21 8C11.21 7.08 11.38 6.19 11.74 5.36C12.08 4.55 12.57 3.81 13.19 3.19C13.81 2.57 14.55 2.08 15.36 1.74C16.19 1.38 17.08 1.21 18 1.21C18.92 1.21 19.81 1.38 20.64 1.74C21.45 2.08 22.19 2.57 22.81 3.19C23.43 3.81 23.92 4.55 24.26 5.36C24.62 6.19 24.79 7.08 24.79 8C24.79 8.92 24.62 9.81 24.26 10.64C23.92 11.45 23.43 12.19 22.81 12.81V12.81ZM14 6.8V6.21C14 5.55 14.53 5.02 15.2 5.02H15.79C16.45 5.02 16.98 5.55 16.98 6.21V6.8C16.98 7.47 16.45 8 15.79 8H15.2C14.53 8 14 7.47 14 6.8V6.8ZM19 6.8V6.21C19 5.55 19.53 5.02 20.2 5.02H20.79C21.45 5.02 21.98 5.55 21.98 6.21V6.8C21.98 7.47 21.45 8 20.79 8H20.2C19.53 8 19 7.47 19 6.8V6.8ZM23 10C22.28 11.88 20.09 13 18 13C15.91 13 13.72 11.87 13 10C12.86 9.61 13.23 9 13.66 9H22.25C22.66 9 23.14 9.61 23 10V10Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8.25008H4.08333V11.1667H2.91667V8.25008H0V7.08341H2.91667V4.16675H4.08333V7.08341H7V8.25008V8.25008Z" fill="#4B4B4B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 0C13.58 0 10 3.58 10 8C10 12.42 13.58 16 18 16C22.42 16 26 12.42 26 8C26 3.58 22.42 0 18 0V0ZM22.81 12.81C22.18 13.44 21.45 13.92 20.64 14.26C19.81 14.62 18.92 14.79 18 14.79C17.08 14.79 16.19 14.62 15.36 14.26C14.55 13.92 13.81 13.43 13.19 12.81C12.57 12.19 12.08 11.45 11.74 10.64C11.38 9.81 11.21 8.92 11.21 8C11.21 7.08 11.38 6.19 11.74 5.36C12.08 4.55 12.57 3.81 13.19 3.19C13.81 2.57 14.55 2.08 15.36 1.74C16.19 1.38 17.08 1.21 18 1.21C18.92 1.21 19.81 1.38 20.64 1.74C21.45 2.08 22.19 2.57 22.81 3.19C23.43 3.81 23.92 4.55 24.26 5.36C24.62 6.19 24.79 7.08 24.79 8C24.79 8.92 24.62 9.81 24.26 10.64C23.92 11.45 23.43 12.19 22.81 12.81V12.81ZM14 6.8V6.21C14 5.55 14.53 5.02 15.2 5.02H15.79C16.45 5.02 16.98 5.55 16.98 6.21V6.8C16.98 7.47 16.45 8 15.79 8H15.2C14.53 8 14 7.47 14 6.8V6.8ZM19 6.8V6.21C19 5.55 19.53 5.02 20.2 5.02H20.79C21.45 5.02 21.98 5.55 21.98 6.21V6.8C21.98 7.47 21.45 8 20.79 8H20.2C19.53 8 19 7.47 19 6.8V6.8ZM23 10C22.28 11.88 20.09 13 18 13C15.91 13 13.72 11.87 13 10C12.86 9.61 13.23 9 13.66 9H22.25C22.66 9 23.14 9.61 23 10V10Z" fill="#4B4B4B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,486 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .margin-view-overlays .review {
background-image: url('comment.svg');
cursor: pointer;
background-repeat: no-repeat;
background-position: center center;
}
.monaco-editor .comment-hint {
height: 20px;
width: 20px;
padding-left: 2px;
background: url('comment.svg') center center no-repeat;
}
.monaco-editor .comment-hint.commenting-disabled {
opacity: 0.5;
}
.monaco-editor .comment-hint:hover {
cursor: pointer;
}
.monaco-editor .review-widget {
width: 100%;
position: absolute;
}
.monaco-editor .review-widget .hidden {
display: none !important;
}
.monaco-editor .review-widget .body {
overflow: hidden;
}
.monaco-editor .review-widget .body .review-comment {
padding: 8px 16px 8px 20px;
display: flex;
}
.monaco-editor .review-widget .body .review-comment .comment-actions {
margin-left: auto;
}
.monaco-editor .review-widget .body .review-comment .comment-actions .monaco-toolbar {
height: 21px;
}
.monaco-editor .review-widget .body .review-comment .comment-actions .action-item {
width: 22px;
}
.monaco-editor .review-widget .body .review-comment .comment-title {
display: flex;
width: 100%;
}
.monaco-editor .review-widget .body .review-comment .comment-title .action-label.octicon {
line-height: 18px;
}
.monaco-editor .review-widget .body .review-comment .comment-title .monaco-dropdown .toolbar-toggle-more {
width: 16px;
height: 18px;
line-height: 18px;
vertical-align: middle;
}
.monaco-editor .review-widget .body .comment-body blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
.monaco-editor .review-widget .body .review-comment .avatar-container {
margin-top: 4px !important;
}
.monaco-editor .review-widget .body .review-comment .avatar-container img.avatar {
height: 28px;
width: 28px;
display: inline-block;
overflow: hidden;
line-height: 1;
vertical-align: middle;
border-radius: 3px;
border-style: none;
}
.monaco-editor .review-widget .body .comment-reactions .monaco-text-button {
margin: 0 7px 0 0;
width: 30px;
background-color: transparent;
border: 1px solid grey;
border-radius: 3px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents {
padding-left: 20px;
user-select: text;
width: 100%;
overflow: hidden;
}
.monaco-editor .review-widget .body pre {
overflow: auto;
word-wrap: normal;
white-space: pre;
}
.monaco-editor.vs-dark .review-widget .body .comment-body h4 {
margin: 0;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .author {
line-height: 22px;
}
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .author {
color: #fff;
font-weight: 600;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .isPending {
margin: 0 5px 0 5px;
padding: 0 2px 0 2px;
font-style: italic;
}
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-body {
padding-top: 4px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions {
margin-top: 8px;
min-height: 25px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .monaco-action-bar .actions-container {
justify-content: flex-start;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label {
padding: 1px 4px;
white-space: pre;
text-align: center;
font-size: 12px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-icon {
background-size: 12px;
background-position: left center;
background-repeat: no-repeat;
width: 16px;
height: 12px;
-webkit-font-smoothing: antialiased;
display: inline-block;
margin-top: 3px;
margin-right: 4px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-label {
line-height: 20px;
margin-right: 4px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label {
display: inline-block;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions {
display: none;
background-image: url(./reaction.svg);
width: 26px;
height: 16px;
background-repeat: no-repeat;
background-position: center;
margin-top: 3px;
border: none;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions:hover .action-item a.action-label.toolbar-toggle-pickReactions {
display: inline-block;
}
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions {
background-image: url(./reaction-dark.svg);
}
.monaco-editor.hc-black .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions {
background-image: url(./reaction-hc.svg);
}
.monaco-editor .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions {
background-image: url(./reaction.svg);
width: 18px;
height: 18px;
background-size: 100% auto;
background-position: center;
background-repeat: no-repeat;
}
.monaco-editor.vs-dark .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions {
background-image: url(./reaction-dark.svg);
}
.monaco-editor.hc-black .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions {
background-image: url(./reaction-hc.svg);
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label{
border: 1px solid transparent;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active {
border: 1px solid grey;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled {
opacity: 0.6;
}
.monaco-editor.vs-dark .review-widget .body span.created_at {
color: #e0e0e0;
}
.monaco-editor .review-widget .body .comment-body p,
.monaco-editor .review-widget .body .comment-body ul {
margin: 8px 0;
}
.monaco-editor .review-widget .body .comment-body p:first-child,
.monaco-editor .review-widget .body .comment-body ul:first-child {
margin-top: 0;
}
.monaco-editor .review-widget .body .comment-body p:last-child,
.monaco-editor .review-widget .body.comment-body ul:last-child {
margin-bottom: 0;
}
.monaco-editor .review-widget .body .comment-body ul {
padding-left: 20px;
}
.monaco-editor .review-widget .body .comment-body li>p {
margin-bottom: 0;
}
.monaco-editor .review-widget .body .comment-body li>ul {
margin-top: 0;
}
.monaco-editor .review-widget .body .comment-body code {
border-radius: 3px;
padding: 0 0.4em;
}
.monaco-editor .review-widget .body .comment-body span {
white-space: pre;
}
.monaco-editor .review-widget .body .comment-body img {
max-width: 100%;
}
.monaco-editor .review-widget .body .comment-form {
margin: 8px 20px;
}
.monaco-editor .review-widget .validation-error {
display: inline-block;
overflow: hidden;
text-align: left;
width: 100%;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-o-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
padding: 0.4em;
font-size: 12px;
line-height: 17px;
min-height: 34px;
margin-top: -1px;
margin-left: -1px;
word-wrap: break-word;
}
.monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button {
display: none;
}
.monaco-editor .review-widget .body .comment-form.expand .monaco-editor,
.monaco-editor .review-widget .body .comment-form.expand .form-actions {
display: block;
box-sizing: content-box;
}
.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {
text-align: left;
display: block;
width: 100%;
resize: vertical;
border-radius: 0;
box-sizing: border-box;
padding: 6px 12px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
border: 0px;
cursor: text;
outline: 1px solid transparent;
}
.monaco-editor .review-widget .body .comment-form .review-thread-reply-button:focus {
outline-style: solid;
outline-width: 1px;
}
.monaco-editor .review-widget .body .comment-form .monaco-editor,
.monaco-editor .review-widget .body .edit-container .monaco-editor {
width: 100%;
min-height: 90px;
max-height: 500px;
border-radius: 3px;
border: 0px;
box-sizing: content-box;
padding: 6px 0 6px 12px;
}
.monaco-editor .review-widget .body .comment-form .monaco-editor,
.monaco-editor .review-widget .body .comment-form .form-actions {
display: none;
}
.monaco-editor .review-widget .body .comment-form .form-actions,
.monaco-editor .review-widget .body .edit-container .form-actions {
overflow: auto;
padding: 10px 0;
}
.monaco-editor .review-widget .body .edit-container .form-actions {
display: flex;
justify-content: flex-end;
}
.monaco-editor .review-widget .body .edit-textarea {
height: 90px;
margin: 5px 0 10px 0;
}
.monaco-editor .review-widget .body .comment-form .monaco-text-button,
.monaco-editor .review-widget .body .edit-container .monaco-text-button {
width: auto;
padding: 4px 10px;
margin-left: 5px;
}
.monaco-editor .review-widget .body .comment-form .monaco-text-button {
float: right;
}
.monaco-editor .review-widget .head {
-webkit-box-sizing: border-box;
-o-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
display: flex;
height: 100%;
}
.monaco-editor .review-widget .head .review-title {
display: inline-block;
font-size: 13px;
margin-left: 20px;
cursor: default;
}
.monaco-editor .review-widget .head .review-title .dirname:not(:empty) {
font-size: 0.9em;
margin-left: 0.5em;
}
.monaco-editor .review-widget .head .review-actions {
flex: 1;
text-align: right;
padding-right: 2px;
}
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar {
display: inline-block;
}
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar,
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar>.actions-container {
height: 100%;
}
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar .action-item {
margin-left: 4px;
}
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar .action-label {
width: 16px;
height: 100%;
margin: 0;
line-height: inherit;
background-repeat: no-repeat;
background-position: center center;
}
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar .action-label.octicon {
margin: 0;
}
.monaco-editor .review-widget .head .review-actions .action-label.icon.close-review-action {
background: url('close.svg') center center no-repeat;
}
.monaco-editor .review-widget>.body {
border-top: 1px solid;
position: relative;
}
.monaco-editor .comment-range-glyph {
margin-left: 5px;
width: 4px !important;
cursor: pointer;
z-index: 10;
}
.monaco-editor .comment-range-glyph:before {
position: absolute;
content: '';
height: 100%;
width: 0;
left: -2px;
transition: width 80ms linear, left 80ms linear;
}
.monaco-editor .margin-view-overlays>div:hover>.comment-range-glyph.comment-diff-added:before,
.monaco-editor .comment-range-glyph.comment-thread:before {
position: absolute;
height: 100%;
width: 9px;
left: -6px;
z-index: 10;
color: black;
text-align: center;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.monaco-editor .margin-view-overlays>div:hover>.comment-range-glyph.comment-diff-added:before {
content: '+';
}
.monaco-editor .comment-range-glyph.comment-thread {
z-index: 20;
}
.monaco-editor .comment-range-glyph.comment-thread:before {
content: '◆';
font-size: 10px;
line-height: 100%;
z-index: 20;
}
.monaco-editor.inline-comment .margin-view-overlays .folding {
margin-left: 7px;
}
.monaco-editor.inline-comment .margin-view-overlays .dirty-diff-glyph {
margin-left: 14px;
}

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { ActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action, IAction } from 'vs/base/common/actions';
import { URI, UriComponents } from 'vs/base/common/uri';
export class ToggleReactionsAction extends Action {
static readonly ID = 'toolbar.toggle.pickReactions';
private _menuActions: IAction[];
private toggleDropdownMenu: () => void;
constructor(toggleDropdownMenu: () => void, title?: string) {
title = title || nls.localize('pickReactions', "Pick Reactions...");
super(ToggleReactionsAction.ID, title, 'toggle-reactions', true);
this.toggleDropdownMenu = toggleDropdownMenu;
}
run(): Promise<any> {
this.toggleDropdownMenu();
return Promise.resolve(true);
}
get menuActions() {
return this._menuActions;
}
set menuActions(actions: IAction[]) {
this._menuActions = actions;
}
}
export class ReactionActionItem extends ActionItem {
constructor(action: ReactionAction) {
super(null, action, {});
}
updateLabel(): void {
let action = this.getAction() as ReactionAction;
if (action.class) {
this.label.classList.add(action.class);
}
if (!action.icon) {
let reactionLabel = dom.append(this.label, dom.$('span.reaction-label'));
reactionLabel.innerText = action.label;
} else {
let reactionIcon = dom.append(this.label, dom.$('.reaction-icon'));
reactionIcon.style.display = '';
let uri = URI.revive(action.icon);
reactionIcon.style.backgroundImage = `url('${uri}')`;
reactionIcon.title = action.label;
}
if (action.count) {
let reactionCount = dom.append(this.label, dom.$('span.reaction-count'));
reactionCount.innerText = `${action.count}`;
}
}
}
export class ReactionAction extends Action {
static readonly ID = 'toolbar.toggle.reaction';
constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: any) => Promise<any>, public icon?: UriComponents, public count?: number) {
super(ReactionAction.ID, label, cssClass, enabled, actionCallback);
}
}

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { EditorAction, EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService } from 'vs/platform/commands/common/commands';
// Allowed Editor Contributions:
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
export const ctxCommentEditorFocused = new RawContextKey<boolean>('commentEditorFocused', false);
export class SimpleCommentEditor extends CodeEditorWidget {
private _parentEditor: ICodeEditor;
private _parentThread: ICommentThreadWidget;
private _commentEditorFocused: IContextKey<boolean>;
constructor(
domElement: HTMLElement,
options: IEditorOptions,
parentEditor: ICodeEditor,
parentThread: ICommentThreadWidget,
@IInstantiationService instantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@ICommandService commandService: ICommandService,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService,
@INotificationService notificationService: INotificationService,
@IAccessibilityService accessibilityService: IAccessibilityService
) {
const codeEditorWidgetOptions = {
contributions: [
MenuPreventer,
ContextMenuController,
SuggestController,
SnippetController2,
TabCompletionController,
]
};
super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService);
this._commentEditorFocused = ctxCommentEditorFocused.bindTo(this._contextKeyService);
this._parentEditor = parentEditor;
this._parentThread = parentThread;
this._register(this.onDidFocusEditorWidget(_ => this._commentEditorFocused.set(true)));
this._register(this.onDidBlurEditorWidget(_ => this._commentEditorFocused.reset()));
}
getParentEditor(): ICodeEditor {
return this._parentEditor;
}
getParentThread(): ICommentThreadWidget {
return this._parentThread;
}
protected _getActions(): EditorAction[] {
return EditorExtensionsRegistry.getEditorActions();
}
public static getEditorOptions(): IEditorOptions {
return {
wordWrap: 'on',
glyphMargin: false,
lineNumbers: 'off',
folding: false,
selectOnLineNumbers: false,
scrollbar: {
vertical: 'visible',
verticalScrollbarSize: 14,
horizontal: 'auto',
useShadows: true,
verticalHasArrows: false,
horizontalHasArrows: false
},
overviewRulerLanes: 2,
lineDecorationsWidth: 0,
scrollBeyondLastLine: false,
renderLineHighlight: 'none',
fixedOverflowWidgets: true,
acceptSuggestionOnEnter: 'smart',
minimap: {
enabled: false
}
};
}
}