mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-31 01:00:29 -04:00
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:
161
src/vs/workbench/contrib/comments/common/commentModel.ts
Normal file
161
src/vs/workbench/contrib/comments/common/commentModel.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
@@ -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;}`);
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user