mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-31 12:16:22 +01:00
399 lines
14 KiB
TypeScript
399 lines
14 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import 'vs/css!./media/review';
|
|
import * as dom from 'vs/base/browser/dom';
|
|
import { Emitter } from 'vs/base/common/event';
|
|
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import * as languages from 'vs/editor/common/languages';
|
|
import { IMarkdownRendererOptions } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer';
|
|
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus';
|
|
import { CommentReply } from 'vs/workbench/contrib/comments/browser/commentReply';
|
|
import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
|
|
import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody';
|
|
import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader';
|
|
import { CommentThreadAdditionalActions } from 'vs/workbench/contrib/comments/browser/commentThreadAdditionalActions';
|
|
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
|
|
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
|
|
import { IColorTheme } from 'vs/platform/theme/common/themeService';
|
|
import { contrastBorder, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
|
|
import { PANEL_BORDER } from 'vs/workbench/common/theme';
|
|
import { IRange } from 'vs/editor/common/core/range';
|
|
import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar } from 'vs/workbench/contrib/comments/browser/commentColors';
|
|
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
|
|
import { FontInfo } from 'vs/editor/common/config/fontInfo';
|
|
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
|
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
|
|
|
|
export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';
|
|
|
|
|
|
export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends Disposable implements ICommentThreadWidget {
|
|
private _header!: CommentThreadHeader<T>;
|
|
private _body: CommentThreadBody<T>;
|
|
private _commentReply?: CommentReply<T>;
|
|
private _additionalActions?: CommentThreadAdditionalActions<T>;
|
|
private _commentMenus: CommentMenus;
|
|
private _commentThreadDisposables: IDisposable[] = [];
|
|
private _threadIsEmpty: IContextKey<boolean>;
|
|
private _styleElement: HTMLStyleElement;
|
|
private _commentThreadContextValue: IContextKey<string | undefined>;
|
|
private _onDidResize = new Emitter<dom.Dimension>();
|
|
onDidResize = this._onDidResize.event;
|
|
|
|
get commentThread() {
|
|
return this._commentThread;
|
|
}
|
|
|
|
constructor(
|
|
readonly container: HTMLElement,
|
|
private _owner: string,
|
|
private _parentResourceUri: URI,
|
|
private _contextKeyService: IContextKeyService,
|
|
private _scopedInstatiationService: IInstantiationService,
|
|
private _commentThread: languages.CommentThread<T>,
|
|
private _pendingComment: string | undefined,
|
|
private _pendingEdits: { [key: number]: string } | undefined,
|
|
private _markdownOptions: IMarkdownRendererOptions,
|
|
private _commentOptions: languages.CommentOptions | undefined,
|
|
private _containerDelegate: {
|
|
actionRunner: (() => void) | null;
|
|
collapse: () => void;
|
|
},
|
|
@ICommentService private commentService: ICommentService,
|
|
@IContextMenuService contextMenuService: IContextMenuService
|
|
) {
|
|
super();
|
|
|
|
this._threadIsEmpty = CommentContextKeys.commentThreadIsEmpty.bindTo(this._contextKeyService);
|
|
this._threadIsEmpty.set(!_commentThread.comments || !_commentThread.comments.length);
|
|
|
|
this._commentMenus = this.commentService.getCommentMenus(this._owner);
|
|
|
|
this._header = new CommentThreadHeader<T>(
|
|
container,
|
|
{
|
|
collapse: this.collapse.bind(this)
|
|
},
|
|
this._commentMenus,
|
|
this._commentThread,
|
|
this._contextKeyService,
|
|
this._scopedInstatiationService,
|
|
contextMenuService
|
|
);
|
|
|
|
this._header.updateCommentThread(this._commentThread);
|
|
|
|
const bodyElement = <HTMLDivElement>dom.$('.body');
|
|
container.appendChild(bodyElement);
|
|
|
|
const tracker = this._register(dom.trackFocus(bodyElement));
|
|
this._register(registerNavigableContainer({
|
|
focusNotifiers: [tracker],
|
|
focusNextWidget: () => {
|
|
if (!this._commentReply?.isCommentEditorFocused()) {
|
|
this._commentReply?.expandReplyAreaAndFocusCommentEditor();
|
|
}
|
|
},
|
|
focusPreviousWidget: () => {
|
|
if (this._commentReply?.isCommentEditorFocused() && this._commentThread.comments?.length) {
|
|
this._body.focus();
|
|
}
|
|
}
|
|
}));
|
|
|
|
this._body = this._scopedInstatiationService.createInstance(
|
|
CommentThreadBody,
|
|
this._owner,
|
|
this._parentResourceUri,
|
|
bodyElement,
|
|
this._markdownOptions,
|
|
this._commentThread,
|
|
this._pendingEdits,
|
|
this._scopedInstatiationService,
|
|
this
|
|
) as unknown as CommentThreadBody<T>;
|
|
this._register(this._body);
|
|
|
|
this._styleElement = dom.createStyleSheet(this.container);
|
|
|
|
|
|
this._commentThreadContextValue = CommentContextKeys.commentThreadContext.bindTo(this._contextKeyService);
|
|
this._commentThreadContextValue.set(_commentThread.contextValue);
|
|
|
|
const commentControllerKey = CommentContextKeys.commentControllerContext.bindTo(this._contextKeyService);
|
|
const controller = this.commentService.getCommentController(this._owner);
|
|
|
|
if (controller?.contextValue) {
|
|
commentControllerKey.set(controller.contextValue);
|
|
}
|
|
|
|
this.currentThreadListeners();
|
|
}
|
|
|
|
private updateCurrentThread(hasMouse: boolean, hasFocus: boolean) {
|
|
if (hasMouse || hasFocus) {
|
|
this.commentService.setCurrentCommentThread(this.commentThread);
|
|
} else {
|
|
this.commentService.setCurrentCommentThread(undefined);
|
|
}
|
|
}
|
|
|
|
private currentThreadListeners() {
|
|
let hasMouse = false;
|
|
let hasFocus = false;
|
|
this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_ENTER, (e) => {
|
|
if ((<any>e).toElement === this.container) {
|
|
hasMouse = true;
|
|
this.updateCurrentThread(hasMouse, hasFocus);
|
|
}
|
|
}, true));
|
|
this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_LEAVE, (e) => {
|
|
if ((<any>e).fromElement === this.container) {
|
|
hasMouse = false;
|
|
this.updateCurrentThread(hasMouse, hasFocus);
|
|
}
|
|
}, true));
|
|
this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_IN, () => {
|
|
hasFocus = true;
|
|
this.updateCurrentThread(hasMouse, hasFocus);
|
|
}, true));
|
|
this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_OUT, () => {
|
|
hasFocus = false;
|
|
this.updateCurrentThread(hasMouse, hasFocus);
|
|
}, true));
|
|
}
|
|
|
|
updateCommentThread(commentThread: languages.CommentThread<T>) {
|
|
this._commentThread = commentThread;
|
|
dispose(this._commentThreadDisposables);
|
|
this._commentThreadDisposables = [];
|
|
this._bindCommentThreadListeners();
|
|
|
|
this._body.updateCommentThread(commentThread);
|
|
this._threadIsEmpty.set(!this._body.length);
|
|
this._header.updateCommentThread(commentThread);
|
|
this._commentReply?.updateCommentThread(commentThread);
|
|
|
|
if (this._commentThread.contextValue) {
|
|
this._commentThreadContextValue.set(this._commentThread.contextValue);
|
|
} else {
|
|
this._commentThreadContextValue.reset();
|
|
}
|
|
}
|
|
|
|
display(lineHeight: number) {
|
|
const headHeight = Math.ceil(lineHeight * 1.2);
|
|
this._header.updateHeight(headHeight);
|
|
|
|
this._body.display();
|
|
|
|
// create comment thread only when it supports reply
|
|
if (this._commentThread.canReply) {
|
|
this._createCommentForm();
|
|
}
|
|
this._createAdditionalActions();
|
|
|
|
this._register(this._body.onDidResize(dimension => {
|
|
this._refresh(dimension);
|
|
}));
|
|
|
|
// If there are no existing comments, place focus on the text area. This must be done after show, which also moves focus.
|
|
// if this._commentThread.comments is undefined, it doesn't finish initialization yet, so we don't focus the editor immediately.
|
|
if (this._commentThread.canReply && this._commentReply) {
|
|
this._commentReply.focusIfNeeded();
|
|
}
|
|
|
|
this._bindCommentThreadListeners();
|
|
}
|
|
|
|
private _refresh(dimension: dom.Dimension) {
|
|
this._body.layout();
|
|
this._onDidResize.fire(dimension);
|
|
}
|
|
|
|
override dispose() {
|
|
super.dispose();
|
|
this.updateCurrentThread(false, false);
|
|
}
|
|
|
|
private _bindCommentThreadListeners() {
|
|
this._commentThreadDisposables.push(this._commentThread.onDidChangeCanReply(() => {
|
|
if (this._commentReply) {
|
|
this._commentReply.updateCanReply();
|
|
} else {
|
|
if (this._commentThread.canReply) {
|
|
this._createCommentForm();
|
|
}
|
|
}
|
|
}));
|
|
|
|
this._commentThreadDisposables.push(this._commentThread.onDidChangeComments(async _ => {
|
|
await this.updateCommentThread(this._commentThread);
|
|
}));
|
|
|
|
this._commentThreadDisposables.push(this._commentThread.onDidChangeLabel(_ => {
|
|
this._header.createThreadLabel();
|
|
}));
|
|
}
|
|
|
|
private _createCommentForm() {
|
|
this._commentReply = this._scopedInstatiationService.createInstance(
|
|
CommentReply,
|
|
this._owner,
|
|
this._body.container,
|
|
this._commentThread,
|
|
this._scopedInstatiationService,
|
|
this._contextKeyService,
|
|
this._commentMenus,
|
|
this._commentOptions,
|
|
this._pendingComment,
|
|
this,
|
|
this._containerDelegate.actionRunner
|
|
);
|
|
|
|
this._register(this._commentReply);
|
|
}
|
|
|
|
private _createAdditionalActions() {
|
|
this._additionalActions = this._scopedInstatiationService.createInstance(
|
|
CommentThreadAdditionalActions,
|
|
this._body.container,
|
|
this._commentThread,
|
|
this._contextKeyService,
|
|
this._commentMenus,
|
|
this._containerDelegate.actionRunner,
|
|
);
|
|
|
|
this._register(this._additionalActions);
|
|
}
|
|
|
|
getCommentCoords(commentUniqueId: number) {
|
|
return this._body.getCommentCoords(commentUniqueId);
|
|
}
|
|
|
|
getPendingEdits(): { [key: number]: string } {
|
|
return this._body.getPendingEdits();
|
|
}
|
|
|
|
getPendingComment(): string | undefined {
|
|
if (this._commentReply) {
|
|
return this._commentReply.getPendingComment();
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
getDimensions() {
|
|
return this._body.getDimensions();
|
|
}
|
|
|
|
layout(widthInPixel?: number) {
|
|
this._body.layout();
|
|
|
|
if (widthInPixel !== undefined) {
|
|
this._commentReply?.layout(widthInPixel);
|
|
}
|
|
}
|
|
|
|
focusCommentEditor() {
|
|
this._commentReply?.focusCommentEditor();
|
|
}
|
|
|
|
focus() {
|
|
this._body.focus();
|
|
}
|
|
|
|
async submitComment() {
|
|
const activeComment = this._body.activeComment;
|
|
if (activeComment) {
|
|
return activeComment.submitComment();
|
|
} else if ((this._commentReply?.getPendingComment()?.length ?? 0) > 0) {
|
|
return this._commentReply?.submitComment();
|
|
}
|
|
}
|
|
|
|
collapse() {
|
|
this._containerDelegate.collapse();
|
|
}
|
|
|
|
applyTheme(theme: IColorTheme, fontInfo: FontInfo) {
|
|
const content: string[] = [];
|
|
|
|
content.push(`.monaco-editor .review-widget > .body { border-top: 1px solid var(${commentThreadStateColorVar}) }`);
|
|
content.push(`.monaco-editor .review-widget > .head { background-color: var(${commentThreadStateBackgroundColorVar}) }`);
|
|
|
|
const linkColor = theme.getColor(textLinkForeground);
|
|
if (linkColor) {
|
|
content.push(`.review-widget .body .comment-body a { color: ${linkColor} }`);
|
|
}
|
|
|
|
const linkActiveColor = theme.getColor(textLinkActiveForeground);
|
|
if (linkActiveColor) {
|
|
content.push(`.review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`);
|
|
}
|
|
|
|
const focusColor = theme.getColor(focusBorder);
|
|
if (focusColor) {
|
|
content.push(`.review-widget .body .comment-body a:focus { outline: 1px solid ${focusColor}; }`);
|
|
content.push(`.review-widget .body .monaco-editor.focused { outline: 1px solid ${focusColor}; }`);
|
|
}
|
|
|
|
const blockQuoteBackground = theme.getColor(textBlockQuoteBackground);
|
|
if (blockQuoteBackground) {
|
|
content.push(`.review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`);
|
|
}
|
|
|
|
const blockQuoteBOrder = theme.getColor(textBlockQuoteBorder);
|
|
if (blockQuoteBOrder) {
|
|
content.push(`.review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`);
|
|
}
|
|
|
|
const border = theme.getColor(PANEL_BORDER);
|
|
if (border) {
|
|
content.push(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border-color: ${border}; }`);
|
|
}
|
|
|
|
const hcBorder = theme.getColor(contrastBorder);
|
|
if (hcBorder) {
|
|
content.push(`.review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`);
|
|
content.push(`.review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`);
|
|
}
|
|
|
|
const errorBorder = theme.getColor(inputValidationErrorBorder);
|
|
if (errorBorder) {
|
|
content.push(`.review-widget .validation-error { border: 1px solid ${errorBorder}; }`);
|
|
}
|
|
|
|
const errorBackground = theme.getColor(inputValidationErrorBackground);
|
|
if (errorBackground) {
|
|
content.push(`.review-widget .validation-error { background: ${errorBackground}; }`);
|
|
}
|
|
|
|
const errorForeground = theme.getColor(inputValidationErrorForeground);
|
|
if (errorForeground) {
|
|
content.push(`.review-widget .body .comment-form .validation-error { color: ${errorForeground}; }`);
|
|
}
|
|
|
|
const fontFamilyVar = '--comment-thread-editor-font-family';
|
|
const fontSizeVar = '--comment-thread-editor-font-size';
|
|
const fontWeightVar = '--comment-thread-editor-font-weight';
|
|
this.container?.style.setProperty(fontFamilyVar, fontInfo.fontFamily);
|
|
this.container?.style.setProperty(fontSizeVar, `${fontInfo.fontSize}px`);
|
|
this.container?.style.setProperty(fontWeightVar, fontInfo.fontWeight);
|
|
|
|
content.push(`.review-widget .body code {
|
|
font-family: var(${fontFamilyVar});
|
|
font-weight: var(${fontWeightVar});
|
|
}`);
|
|
|
|
this._styleElement.textContent = content.join('\n');
|
|
this._commentReply?.setCommentEditorDecorations();
|
|
}
|
|
}
|