未验证 提交 1d1105e9 编写于 作者: R Rachel Macfarlane 提交者: GitHub

Add support for editing comments, #58078

上级 ec058d3d
...@@ -1008,6 +1008,7 @@ export interface Comment { ...@@ -1008,6 +1008,7 @@ export interface Comment {
readonly body: IMarkdownString; readonly body: IMarkdownString;
readonly userName: string; readonly userName: string;
readonly gravatar: string; readonly gravatar: string;
readonly canEdit?: boolean;
readonly command?: Command; readonly command?: Command;
} }
...@@ -1039,6 +1040,7 @@ export interface DocumentCommentProvider { ...@@ -1039,6 +1040,7 @@ export interface DocumentCommentProvider {
provideDocumentComments(resource: URI, token: CancellationToken): Promise<CommentInfo>; provideDocumentComments(resource: URI, token: CancellationToken): Promise<CommentInfo>;
createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise<CommentThread>; createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise<CommentThread>;
replyToCommentThread(resource: URI, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>; replyToCommentThread(resource: URI, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>;
editComment(resource: URI, comment: Comment, text: string, token: CancellationToken): Promise<Comment>;
onDidChangeCommentThreads(): Event<CommentThreadChangedEvent>; onDidChangeCommentThreads(): Event<CommentThreadChangedEvent>;
} }
...@@ -1047,8 +1049,6 @@ export interface DocumentCommentProvider { ...@@ -1047,8 +1049,6 @@ export interface DocumentCommentProvider {
*/ */
export interface WorkspaceCommentProvider { export interface WorkspaceCommentProvider {
provideWorkspaceComments(token: CancellationToken): Promise<CommentThread[]>; provideWorkspaceComments(token: CancellationToken): Promise<CommentThread[]>;
createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise<CommentThread>;
replyToCommentThread(resource: URI, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>;
onDidChangeCommentThreads(): Event<CommentThreadChangedEvent>; onDidChangeCommentThreads(): Event<CommentThreadChangedEvent>;
} }
......
...@@ -656,10 +656,37 @@ declare module 'vscode' { ...@@ -656,10 +656,37 @@ declare module 'vscode' {
} }
interface Comment { interface Comment {
/**
* The id of the comment
*/
commentId: string; commentId: string;
/**
* The text of the comment
*/
body: MarkdownString; body: MarkdownString;
/**
* The display name of the user who created the comment
*/
userName: string; userName: string;
/**
* The avatar src of the user who created the comment
*/
gravatar: string; gravatar: string;
/**
* Whether the current user has permission to edit the comment.
*
* This will be treated as false if the comment is provided by a `WorkspaceCommentProvider`, or
* if it is provided by a `DocumentCommentProvider` and no `editComment` method is given.
*/
canEdit?: boolean;
/**
* The command to be executed if the comment is selected in the Comments Panel
*/
command?: Command; command?: Command;
} }
...@@ -681,14 +708,42 @@ declare module 'vscode' { ...@@ -681,14 +708,42 @@ declare module 'vscode' {
} }
interface DocumentCommentProvider { interface DocumentCommentProvider {
/**
* Provide the commenting ranges and comment threads for the given document. The comments are displayed within the editor.
*/
provideDocumentComments(document: TextDocument, token: CancellationToken): Promise<CommentInfo>; provideDocumentComments(document: TextDocument, token: CancellationToken): Promise<CommentInfo>;
/**
* Called when a user adds a new comment thread in the document at the specified range, with body text.
*/
createNewCommentThread(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise<CommentThread>; createNewCommentThread(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise<CommentThread>;
/**
* Called when a user replies to a new comment thread in the document at the specified range, with body text.
*/
replyToCommentThread(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>; replyToCommentThread(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>;
/**
* Called when a user edits the comment body to the be new text text.
*/
editComment?(document: TextDocument, comment: Comment, text: string, token: CancellationToken): Promise<Comment>;
/**
* Notify of updates to comment threads.
*/
onDidChangeCommentThreads: Event<CommentThreadChangedEvent>; onDidChangeCommentThreads: Event<CommentThreadChangedEvent>;
} }
interface WorkspaceCommentProvider { interface WorkspaceCommentProvider {
/**
* Provide all comments for the workspace. Comments are shown within the comments panel. Selecting a comment
* from the panel runs the comment's command.
*/
provideWorkspaceComments(token: CancellationToken): Promise<CommentThread[]>; provideWorkspaceComments(token: CancellationToken): Promise<CommentThread[]>;
/**
* Notify of updates to comment threads.
*/
onDidChangeCommentThreads: Event<CommentThreadChangedEvent>; onDidChangeCommentThreads: Event<CommentThreadChangedEvent>;
} }
......
...@@ -77,6 +77,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments ...@@ -77,6 +77,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments
}, },
replyToCommentThread: async (uri, range, thread, text, token) => { replyToCommentThread: async (uri, range, thread, text, token) => {
return this._proxy.$replyToCommentThread(handle, uri, range, thread, text); return this._proxy.$replyToCommentThread(handle, uri, range, thread, text);
},
editComment: async (uri, comment, text, token) => {
return this._proxy.$editComment(handle, uri, comment, text);
} }
} }
); );
......
...@@ -995,6 +995,7 @@ export interface ExtHostCommentsShape { ...@@ -995,6 +995,7 @@ export interface ExtHostCommentsShape {
$provideDocumentComments(handle: number, document: UriComponents): Thenable<modes.CommentInfo>; $provideDocumentComments(handle: number, document: UriComponents): Thenable<modes.CommentInfo>;
$createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Thenable<modes.CommentThread>; $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Thenable<modes.CommentThread>;
$replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Thenable<modes.CommentThread>; $replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Thenable<modes.CommentThread>;
$editComment(handle: number, document: UriComponents, comment: modes.Comment, text: string): Thenable<modes.Comment>;
$provideWorkspaceComments(handle: number): Thenable<modes.CommentThread[]>; $provideWorkspaceComments(handle: number): Thenable<modes.CommentThread[]>;
} }
......
...@@ -73,10 +73,10 @@ export class ExtHostComments implements ExtHostCommentsShape { ...@@ -73,10 +73,10 @@ export class ExtHostComments implements ExtHostCommentsShape {
return TPromise.as(null); return TPromise.as(null);
} }
const provider = this._documentProviders.get(handle);
return asThenable(() => { return asThenable(() => {
let provider = this._documentProviders.get(handle);
return provider.createNewCommentThread(data.document, ran, text, CancellationToken.None); return provider.createNewCommentThread(data.document, ran, text, CancellationToken.None);
}).then(commentThread => commentThread ? convertToCommentThread(commentThread, this._commandsConverter) : null); }).then(commentThread => commentThread ? convertToCommentThread(provider, commentThread, this._commandsConverter) : null);
} }
$replyToCommentThread(handle: number, uri: UriComponents, range: IRange, thread: modes.CommentThread, text: string): Thenable<modes.CommentThread> { $replyToCommentThread(handle: number, uri: UriComponents, range: IRange, thread: modes.CommentThread, text: string): Thenable<modes.CommentThread> {
...@@ -87,10 +87,23 @@ export class ExtHostComments implements ExtHostCommentsShape { ...@@ -87,10 +87,23 @@ export class ExtHostComments implements ExtHostCommentsShape {
return TPromise.as(null); return TPromise.as(null);
} }
const provider = this._documentProviders.get(handle);
return asThenable(() => { return asThenable(() => {
let provider = this._documentProviders.get(handle);
return provider.replyToCommentThread(data.document, ran, convertFromCommentThread(thread), text, CancellationToken.None); return provider.replyToCommentThread(data.document, ran, convertFromCommentThread(thread), text, CancellationToken.None);
}).then(commentThread => commentThread ? convertToCommentThread(commentThread, this._commandsConverter) : null); }).then(commentThread => commentThread ? convertToCommentThread(provider, commentThread, this._commandsConverter) : null);
}
$editComment(handle: number, uri: UriComponents, comment: modes.Comment, text: string): Thenable<modes.Comment> {
const data = this._documents.getDocumentData(URI.revive(uri));
if (!data || !data.document) {
throw new Error('Unable to retrieve document from URI');
}
const provider = this._documentProviders.get(handle);
return asThenable(() => {
return provider.editComment(data.document, convertFromComment(comment), text, CancellationToken.None);
}).then(comment => convertToComment(provider, comment, this._commandsConverter));
} }
$provideDocumentComments(handle: number, uri: UriComponents): Thenable<modes.CommentInfo> { $provideDocumentComments(handle: number, uri: UriComponents): Thenable<modes.CommentInfo> {
...@@ -99,11 +112,10 @@ export class ExtHostComments implements ExtHostCommentsShape { ...@@ -99,11 +112,10 @@ export class ExtHostComments implements ExtHostCommentsShape {
return TPromise.as(null); return TPromise.as(null);
} }
const provider = this._documentProviders.get(handle);
return asThenable(() => { return asThenable(() => {
let provider = this._documentProviders.get(handle);
return provider.provideDocumentComments(data.document, CancellationToken.None); return provider.provideDocumentComments(data.document, CancellationToken.None);
}) }).then(commentInfo => commentInfo ? convertCommentInfo(handle, provider, commentInfo, this._commandsConverter) : null);
.then(commentInfo => commentInfo ? convertCommentInfo(handle, commentInfo, this._commandsConverter) : null);
} }
$provideWorkspaceComments(handle: number): Thenable<modes.CommentThread[]> { $provideWorkspaceComments(handle: number): Thenable<modes.CommentThread[]> {
...@@ -115,7 +127,7 @@ export class ExtHostComments implements ExtHostCommentsShape { ...@@ -115,7 +127,7 @@ export class ExtHostComments implements ExtHostCommentsShape {
return asThenable(() => { return asThenable(() => {
return provider.provideWorkspaceComments(CancellationToken.None); return provider.provideWorkspaceComments(CancellationToken.None);
}).then(comments => }).then(comments =>
comments.map(x => convertToCommentThread(x, this._commandsConverter) comments.map(comment => convertToCommentThread(provider, comment, this._commandsConverter)
)); ));
} }
...@@ -124,28 +136,28 @@ export class ExtHostComments implements ExtHostCommentsShape { ...@@ -124,28 +136,28 @@ export class ExtHostComments implements ExtHostCommentsShape {
this._proxy.$onDidCommentThreadsChange(handle, { this._proxy.$onDidCommentThreadsChange(handle, {
owner: handle, owner: handle,
changed: event.changed.map(x => convertToCommentThread(x, this._commandsConverter)), changed: event.changed.map(thread => convertToCommentThread(provider, thread, this._commandsConverter)),
added: event.added.map(x => convertToCommentThread(x, this._commandsConverter)), added: event.added.map(thread => convertToCommentThread(provider, thread, this._commandsConverter)),
removed: event.removed.map(x => convertToCommentThread(x, this._commandsConverter)) removed: event.removed.map(thread => convertToCommentThread(provider, thread, this._commandsConverter))
}); });
}); });
} }
} }
function convertCommentInfo(owner: number, vscodeCommentInfo: vscode.CommentInfo, commandsConverter: CommandsConverter): modes.CommentInfo { function convertCommentInfo(owner: number, provider: vscode.DocumentCommentProvider, vscodeCommentInfo: vscode.CommentInfo, commandsConverter: CommandsConverter): modes.CommentInfo {
return { return {
owner: owner, owner: owner,
threads: vscodeCommentInfo.threads.map(x => convertToCommentThread(x, commandsConverter)), threads: vscodeCommentInfo.threads.map(x => convertToCommentThread(provider, x, commandsConverter)),
commentingRanges: vscodeCommentInfo.commentingRanges ? vscodeCommentInfo.commentingRanges.map(range => extHostTypeConverter.Range.from(range)) : [] commentingRanges: vscodeCommentInfo.commentingRanges ? vscodeCommentInfo.commentingRanges.map(range => extHostTypeConverter.Range.from(range)) : []
}; };
} }
function convertToCommentThread(vscodeCommentThread: vscode.CommentThread, commandsConverter: CommandsConverter): modes.CommentThread { function convertToCommentThread(provider: vscode.DocumentCommentProvider | vscode.WorkspaceCommentProvider, vscodeCommentThread: vscode.CommentThread, commandsConverter: CommandsConverter): modes.CommentThread {
return { return {
threadId: vscodeCommentThread.threadId, threadId: vscodeCommentThread.threadId,
resource: vscodeCommentThread.resource.toString(), resource: vscodeCommentThread.resource.toString(),
range: extHostTypeConverter.Range.from(vscodeCommentThread.range), range: extHostTypeConverter.Range.from(vscodeCommentThread.range),
comments: vscodeCommentThread.comments.map(comment => convertToComment(comment, commandsConverter)), comments: vscodeCommentThread.comments.map(comment => convertToComment(provider, comment, commandsConverter)),
collapsibleState: vscodeCommentThread.collapsibleState collapsibleState: vscodeCommentThread.collapsibleState
}; };
} }
...@@ -165,16 +177,19 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { ...@@ -165,16 +177,19 @@ function convertFromComment(comment: modes.Comment): vscode.Comment {
commentId: comment.commentId, commentId: comment.commentId,
body: extHostTypeConverter.MarkdownString.to(comment.body), body: extHostTypeConverter.MarkdownString.to(comment.body),
userName: comment.userName, userName: comment.userName,
gravatar: comment.gravatar gravatar: comment.gravatar,
canEdit: comment.canEdit
}; };
} }
function convertToComment(vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment { function convertToComment(provider: vscode.DocumentCommentProvider | vscode.WorkspaceCommentProvider, vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment {
const canEdit = !!(provider as vscode.DocumentCommentProvider).editComment && vscodeComment.canEdit;
return { return {
commentId: vscodeComment.commentId, commentId: vscodeComment.commentId,
body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), body: extHostTypeConverter.MarkdownString.from(vscodeComment.body),
userName: vscodeComment.userName, userName: vscodeComment.userName,
gravatar: vscodeComment.gravatar, gravatar: vscodeComment.gravatar,
canEdit: canEdit,
command: vscodeComment.command ? commandsConverter.toInternal(vscodeComment.command) : null command: vscodeComment.command ? commandsConverter.toInternal(vscodeComment.command) : null
}; };
} }
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import * as modes from 'vs/editor/common/modes';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button } from 'vs/base/browser/ui/button/button';
import { Action } from 'vs/base/common/actions';
import { Disposable } 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/parts/comments/electron-browser/commentService';
import { SimpleCommentEditor } from 'vs/workbench/parts/comments/electron-browser/simpleCommentEditor';
import { KeyCode } from 'vs/base/common/keyCodes';
import { isMacintosh } from 'vs/base/common/platform';
import { Selection } from 'vs/editor/common/core/selection';
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 _commentEditor: SimpleCommentEditor;
private _commentEditorModel: ITextModel;
private _updateCommentButton: Button;
private _errorEditingContainer: HTMLElement;
public get domNode(): HTMLElement {
return this._domNode;
}
constructor(
public comment: modes.Comment,
private owner: number,
private resource: URI,
private markdownRenderer: MarkdownRenderer,
private themeService: IThemeService,
private instantiationService: IInstantiationService,
private commentService: ICommentService,
private modelService: IModelService,
private modeService: IModeService
) {
super();
this._domNode = dom.$('div.review-comment');
this._domNode.tabIndex = 0;
const avatar = dom.append(this._domNode, dom.$('div.avatar-container'));
const img = <HTMLImageElement>dom.append(avatar, dom.$('img.avatar'));
img.src = comment.gravatar;
const commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents'));
this.createHeader(commentDetailsContainer);
this._body = dom.append(commentDetailsContainer, dom.$('div.comment-body'));
this._md = this.markdownRenderer.render(comment.body).element;
this._body.appendChild(this._md);
this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`);
this._domNode.setAttribute('role', 'treeitem');
this._clearTimeout = null;
}
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;
const actions: Action[] = [];
if (this.comment.canEdit) {
this._editAction = this.createEditAction(commentDetailsContainer);
actions.push(this._editAction);
}
if (actions.length) {
const actionsContainer = dom.append(header, dom.$('.comment-actions.hidden'));
const actionBar = new ActionBar(actionsContainer, {});
this._toDispose.push(actionBar);
this.registerActionBarListeners(actionsContainer);
actions.forEach(action => actionBar.push(action, { 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());
const resource = URI.parse(`comment:commentinput-${this.comment.commentId}-${Date.now()}.md`);
this._commentEditorModel = this.modelService.createModel('', this.modeService.getOrCreateModeByFilenameOrFirstLine(resource.path), resource, true);
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));
this._toDispose.push(this._commentEditor.onKeyDown((e: IKeyboardEvent) => {
const isCmdOrCtrl = isMacintosh ? e.metaKey : e.ctrlKey;
if (this._updateCommentButton.enabled && e.keyCode === KeyCode.Enter && isCmdOrCtrl) {
this.editComment();
}
}));
this._toDispose.push(this._commentEditor);
this._toDispose.push(this._commentEditorModel);
}
private removeCommentEditor() {
this._editAction.enabled = true;
this._body.classList.remove('hidden');
this._commentEditorModel.dispose();
this._commentEditor.dispose();
this._commentEditor = null;
this._commentEditContainer.remove();
}
private editComment(): void {
this._updateCommentButton.enabled = false;
this._updateCommentButton.label = UPDATE_IN_PROGRESS_LABEL;
try {
this.commentService.editComment(this.owner, this.resource, this.comment, this._commentEditor.getValue()).then(editedComment => {
this._updateCommentButton.enabled = true;
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
this._commentEditor.getDomNode().style.outline = '';
this.removeCommentEditor();
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 = nls.localize('commentCreationError', "Updating the comment failed: {0}.", e.message);
this._errorEditingContainer.classList.remove('hidden');
this._commentEditor.focus();
}
}
private createEditAction(commentDetailsContainer: HTMLElement): Action {
return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', 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");
attachButtonStyler(cancelEditButton, this.themeService);
this._toDispose.push(cancelEditButton.onDidClick(_ => {
this.removeCommentEditor();
}));
this._updateCommentButton = new Button(formActions);
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
attachButtonStyler(this._updateCommentButton, this.themeService);
this._toDispose.push(this._updateCommentButton.onDidClick(_ => {
this.editComment();
}));
this._toDispose.push(this._commentEditor.onDidChangeModelContent(_ => {
this._updateCommentButton.enabled = !!this._commentEditor.getValue();
}));
this._editAction.enabled = false;
return null;
});
}
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', (e: MouseEvent) => {
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);
}
this.comment = newComment;
}
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());
}
}
\ No newline at end of file
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
'use strict'; 'use strict';
import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo } from 'vs/editor/common/modes'; import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment } from 'vs/editor/common/modes';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event'; import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
...@@ -41,6 +41,7 @@ export interface ICommentService { ...@@ -41,6 +41,7 @@ export interface ICommentService {
updateComments(event: CommentThreadChangedEvent): void; updateComments(event: CommentThreadChangedEvent): void;
createNewCommentThread(owner: number, resource: URI, range: Range, text: string): Promise<CommentThread>; createNewCommentThread(owner: number, resource: URI, range: Range, text: string): Promise<CommentThread>;
replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): Promise<CommentThread>; replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): Promise<CommentThread>;
editComment(owner: number, resource: URI, comment: Comment, text: string): Promise<Comment>;
getComments(resource: URI): Promise<CommentInfo[]>; getComments(resource: URI): Promise<CommentInfo[]>;
} }
...@@ -114,6 +115,16 @@ export class CommentService extends Disposable implements ICommentService { ...@@ -114,6 +115,16 @@ export class CommentService extends Disposable implements ICommentService {
return null; return null;
} }
editComment(owner: number, resource: URI, comment: Comment, text: string): Promise<Comment> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return commentProvider.editComment(resource, comment, text, CancellationToken.None);
}
return null;
}
getComments(resource: URI): Promise<CommentInfo[]> { getComments(resource: URI): Promise<CommentInfo[]> {
const result = []; const result = [];
for (const handle of keys(this._commentProviders)) { for (const handle of keys(this._commentProviders)) {
......
...@@ -37,65 +37,12 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; ...@@ -37,65 +37,12 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { CommentNode } from 'vs/workbench/parts/comments/electron-browser/commentNode';
export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';
const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x'; const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x';
const COMMENT_SCHEME = 'comment'; const COMMENT_SCHEME = 'comment';
export class CommentNode {
private _domNode: HTMLElement;
private _body: HTMLElement;
private _md: HTMLElement;
private _clearTimeout: any;
public get domNode(): HTMLElement {
return this._domNode;
}
constructor(
public comment: modes.Comment,
private markdownRenderer: MarkdownRenderer,
) {
this._domNode = dom.$('div.review-comment');
this._domNode.tabIndex = 0;
let avatar = dom.append(this._domNode, dom.$('div.avatar-container'));
let img = <HTMLImageElement>dom.append(avatar, dom.$('img.avatar'));
img.src = comment.gravatar;
let commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents'));
let header = dom.append(commentDetailsContainer, dom.$('div'));
let author = dom.append(header, dom.$('strong.author'));
author.innerText = comment.userName;
this._body = dom.append(commentDetailsContainer, dom.$('div.comment-body'));
this._md = this.markdownRenderer.render(comment.body).element;
this._body.appendChild(this._md);
this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`);
this._domNode.setAttribute('role', 'treeitem');
this._clearTimeout = null;
}
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);
}
this.comment = newComment;
}
focus() {
this.domNode.focus();
if (!this._clearTimeout) {
dom.addClass(this.domNode, 'focus');
this._clearTimeout = setTimeout(() => {
dom.removeClass(this.domNode, 'focus');
}, 3000);
}
}
}
let INMEM_MODEL_ID = 0; let INMEM_MODEL_ID = 0;
export class ReviewZoneWidget extends ZoneWidget { export class ReviewZoneWidget extends ZoneWidget {
...@@ -219,9 +166,9 @@ export class ReviewZoneWidget extends ZoneWidget { ...@@ -219,9 +166,9 @@ export class ReviewZoneWidget extends ZoneWidget {
this.dispose(); this.dispose();
return null; return null;
} }
this._isCollapsed = true; this._isCollapsed = true;
this.hide(); this.hide();
return null; return null;
}); });
...@@ -265,7 +212,18 @@ export class ReviewZoneWidget extends ZoneWidget { ...@@ -265,7 +212,18 @@ export class ReviewZoneWidget extends ZoneWidget {
lastCommentElement = oldCommentNode[0].domNode; lastCommentElement = oldCommentNode[0].domNode;
newCommentNodeList.unshift(oldCommentNode[0]); newCommentNodeList.unshift(oldCommentNode[0]);
} else { } else {
let newElement = new CommentNode(currentComment, this._markdownRenderer); let newElement = new CommentNode(
currentComment,
this.owner,
this.editor.getModel().uri,
this._markdownRenderer,
this.themeService,
this.instantiationService,
this.commentService,
this.modelService,
this.modeService);
this._disposables.push(newElement);
newCommentNodeList.unshift(newElement); newCommentNodeList.unshift(newElement);
if (lastCommentElement) { if (lastCommentElement) {
this._commentsElement.insertBefore(newElement.domNode, lastCommentElement); this._commentsElement.insertBefore(newElement.domNode, lastCommentElement);
...@@ -302,7 +260,16 @@ export class ReviewZoneWidget extends ZoneWidget { ...@@ -302,7 +260,16 @@ export class ReviewZoneWidget extends ZoneWidget {
this._commentElements = []; this._commentElements = [];
for (let i = 0; i < this._commentThread.comments.length; i++) { for (let i = 0; i < this._commentThread.comments.length; i++) {
let newCommentNode = new CommentNode(this._commentThread.comments[i], this._markdownRenderer); let newCommentNode = new CommentNode(this._commentThread.comments[i],
this.owner,
this.editor.getModel().uri,
this._markdownRenderer,
this.themeService,
this.instantiationService,
this.commentService,
this.modelService,
this.modeService);
this._disposables.push(newCommentNode);
this._commentElements.push(newCommentNode); this._commentElements.push(newCommentNode);
this._commentsElement.appendChild(newCommentNode.domNode); this._commentsElement.appendChild(newCommentNode.domNode);
} }
...@@ -592,18 +559,18 @@ export class ReviewZoneWidget extends ZoneWidget { ...@@ -592,18 +559,18 @@ export class ReviewZoneWidget extends ZoneWidget {
const content: string[] = []; const content: string[] = [];
const linkColor = theme.getColor(textLinkForeground); const linkColor = theme.getColor(textLinkForeground);
if (linkColor) { if (linkColor) {
content.push(`.monaco-editor .review-widget .body .review-comment a { color: ${linkColor} }`); content.push(`.monaco-editor .review-widget .body .comment-body a { color: ${linkColor} }`);
} }
const linkActiveColor = theme.getColor(textLinkActiveForeground); const linkActiveColor = theme.getColor(textLinkActiveForeground);
if (linkActiveColor) { if (linkActiveColor) {
content.push(`.monaco-editor .review-widget .body .review-comment a:hover, a:active { color: ${linkActiveColor} }`); content.push(`.monaco-editor .review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`);
} }
const focusColor = theme.getColor(focusBorder); const focusColor = theme.getColor(focusBorder);
if (focusColor) { if (focusColor) {
content.push(`.monaco-editor .review-widget .body .review-comment a:focus { outline: 1px solid ${focusColor}; }`); content.push(`.monaco-editor .review-widget .body .comment-body a:focus { outline: 1px solid ${focusColor}; }`);
content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); content.push(`.monaco-editor .review-widget .body .monaco-editor.focused { outline: 1px solid ${focusColor}; }`);
} }
const blockQuoteBackground = theme.getColor(textBlockQuoteBackground); const blockQuoteBackground = theme.getColor(textBlockQuoteBackground);
...@@ -619,17 +586,17 @@ export class ReviewZoneWidget extends ZoneWidget { ...@@ -619,17 +586,17 @@ export class ReviewZoneWidget extends ZoneWidget {
const hcBorder = theme.getColor(contrastBorder); const hcBorder = theme.getColor(contrastBorder);
if (hcBorder) { if (hcBorder) {
content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`);
content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor { outline: 1px solid ${hcBorder}; }`); content.push(`.monaco-editor .review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`);
} }
const errorBorder = theme.getColor(inputValidationErrorBorder); const errorBorder = theme.getColor(inputValidationErrorBorder);
if (errorBorder) { if (errorBorder) {
content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { border: 1px solid ${errorBorder}; }`); content.push(`.monaco-editor .review-widget .validation-error { border: 1px solid ${errorBorder}; }`);
} }
const errorBackground = theme.getColor(inputValidationErrorBackground); const errorBackground = theme.getColor(inputValidationErrorBackground);
if (errorBackground) { if (errorBackground) {
content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { background: ${errorBackground}; }`); content.push(`.monaco-editor .review-widget .validation-error { background: ${errorBackground}; }`);
} }
const errorForeground = theme.getColor(inputValidationErrorForeground); const errorForeground = theme.getColor(inputValidationErrorForeground);
......
...@@ -30,12 +30,33 @@ ...@@ -30,12 +30,33 @@
position: absolute; position: absolute;
} }
.monaco-editor .review-widget .hidden {
display: none !important;
}
.monaco-editor .review-widget .body .review-comment { .monaco-editor .review-widget .body .review-comment {
padding: 8px 16px 8px 20px; padding: 8px 16px 8px 20px;
display: flex; display: flex;
} }
.monaco-editor .review-widget .body .review-comment blockquote { .monaco-editor .review-widget .body .review-comment .comment-actions {
margin-left: auto;
}
.monaco-editor .review-widget .body .review-comment .comment-actions .action-item {
width: 30px;
}
.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 .comment-body blockquote {
margin: 0 7px 0 5px; margin: 0 7px 0 5px;
padding: 0 16px 0 10px; padding: 0 16px 0 10px;
border-left-width: 5px; border-left-width: 5px;
...@@ -58,8 +79,10 @@ ...@@ -58,8 +79,10 @@
} }
.monaco-editor .review-widget .body .review-comment .review-comment-contents { .monaco-editor .review-widget .body .review-comment .review-comment-contents {
margin-left: 20px; padding-left: 20px;
user-select: text; user-select: text;
width: 100%;
overflow: hidden;
} }
.monaco-editor .review-widget .body pre { .monaco-editor .review-widget .body pre {
...@@ -68,13 +91,14 @@ ...@@ -68,13 +91,14 @@
white-space: pre; white-space: pre;
} }
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents h4 { .monaco-editor.vs-dark .review-widget .body .comment-body h4 {
margin: 0; margin: 0;
} }
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .author { .monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .author {
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
line-height: 19px;
} }
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-body { .monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-body {
...@@ -85,39 +109,39 @@ ...@@ -85,39 +109,39 @@
color: #e0e0e0; color: #e0e0e0;
} }
.monaco-editor .review-widget .body p, .monaco-editor .review-widget .body .comment-body p,
.monaco-editor .review-widget .body ul { .monaco-editor .review-widget .body .comment-body ul {
margin: 8px 0; margin: 8px 0;
} }
.monaco-editor .review-widget .body p:first-child, .monaco-editor .review-widget .body .comment-body p:first-child,
.monaco-editor .review-widget .body ul:first-child { .monaco-editor .review-widget .body .comment-body ul:first-child {
margin-top: 0; margin-top: 0;
} }
.monaco-editor .review-widget .body p:last-child, .monaco-editor .review-widget .body .comment-body p:last-child,
.monaco-editor .review-widget .body ul:last-child { .monaco-editor .review-widget .body.comment-body ul:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.monaco-editor .review-widget .body ul { .monaco-editor .review-widget .body .comment-body ul {
padding-left: 20px; padding-left: 20px;
} }
.monaco-editor .review-widget .body li>p { .monaco-editor .review-widget .body .comment-body li>p {
margin-bottom: 0; margin-bottom: 0;
} }
.monaco-editor .review-widget .body li>ul { .monaco-editor .review-widget .body .comment-body li>ul {
margin-top: 0; margin-top: 0;
} }
.monaco-editor .review-widget .body code { .monaco-editor .review-widget .body .comment-body code {
border-radius: 3px; border-radius: 3px;
padding: 0 0.4em; padding: 0 0.4em;
} }
.monaco-editor .review-widget .body span { .monaco-editor .review-widget .body .comment-body span {
white-space: pre; white-space: pre;
} }
...@@ -130,7 +154,7 @@ ...@@ -130,7 +154,7 @@
padding: 8px 0; padding: 8px 0;
} }
.monaco-editor .review-widget .body .comment-form .validation-error { .monaco-editor .review-widget .validation-error {
display: inline-block; display: inline-block;
overflow: hidden; overflow: hidden;
text-align: left; text-align: left;
...@@ -149,10 +173,6 @@ ...@@ -149,10 +173,6 @@
word-wrap: break-word; word-wrap: break-word;
} }
.monaco-editor .review-widget .body .comment-form .validation-error.hidden {
display: none;
}
.monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button { .monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button {
display: none; display: none;
} }
...@@ -184,25 +204,46 @@ ...@@ -184,25 +204,46 @@
outline-width: 1px; outline-width: 1px;
} }
.monaco-editor .review-widget .body .comment-form .monaco-editor { .monaco-editor .review-widget .body .comment-form .monaco-editor,
display: none; .monaco-editor .review-widget .body .edit-container .monaco-editor {
width: 100%; width: 100%;
min-height: 90px; min-height: 90px;
max-height: 500px; max-height: 500px;
border-radius: 3px; border-radius: 3px;
border: 0px; border: 0px;
box-sizing: content-box;
padding: 6px 0 6px 12px; padding: 6px 0 6px 12px;
} }
.monaco-editor .review-widget .body .comment-form .monaco-editor,
.monaco-editor .review-widget .body .comment-form .form-actions { .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; overflow: auto;
padding: 10px 0; padding: 10px 0;
display: none;
} }
.monaco-editor .review-widget .body .comment-form .monaco-text-button { .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; width: auto;
padding: 4px 10px; padding: 4px 10px;
margin-left: 5px;
}
.monaco-editor .review-widget .body .comment-form .monaco-text-button {
float: right; float: right;
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册