From aacc0eca67742ed6f927c66c9dd8e6d7c30c7f1b Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Tue, 30 Apr 2019 07:54:56 -0700 Subject: [PATCH] Merge from vscode 071a5cf16fc999727acc31c790d78f750fa4b166 (#5261) --- .../lib/tslint/noNlsInStandaloneEditorRule.js | 2 +- .../lib/tslint/noNlsInStandaloneEditorRule.ts | 2 +- src/vs/base/test/node/pfs/fixtures/site.css | 2 +- src/vs/editor/common/modes.ts | 14 +- .../contrib/snippet/snippetController2.ts | 21 +- .../editor/contrib/snippet/snippetSession.ts | 10 +- .../snippet/test/snippetController2.test.ts | 31 +-- .../snippet/test/snippetSession.test.ts | 2 +- src/vs/vscode.proposed.d.ts | 107 ++++++-- .../api/browser/mainThreadComments.ts | 32 ++- .../workbench/api/common/extHost.protocol.ts | 11 +- .../workbench/api/common/extHostComments.ts | 86 ++++++- src/vs/workbench/api/common/extHostTypes.ts | 56 +++++ src/vs/workbench/api/node/extHost.api.impl.ts | 1 + src/vs/workbench/common/theme.ts | 26 +- .../comments/browser/commentService.ts | 12 +- .../comments/browser/commentThreadWidget.ts | 228 ++++++++++-------- .../browser/commentsEditorContribution.ts | 81 +++++-- .../electron-browser/extensionsWidgets.ts | 6 +- .../contrib/files/browser/saveErrorHandler.ts | 4 +- .../workbench/contrib/files/common/files.ts | 17 +- .../test/common/fileOnDiskProvider.test.ts | 40 +++ .../dialogs/browser/remoteFileDialog.ts | 2 +- .../workbench/test/workbenchTestServices.ts | 9 + 24 files changed, 564 insertions(+), 238 deletions(-) create mode 100644 src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts diff --git a/build/lib/tslint/noNlsInStandaloneEditorRule.js b/build/lib/tslint/noNlsInStandaloneEditorRule.js index affd4cc837..28528d2e3e 100644 --- a/build/lib/tslint/noNlsInStandaloneEditorRule.js +++ b/build/lib/tslint/noNlsInStandaloneEditorRule.js @@ -1,7 +1,7 @@ "use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const ts = require("typescript"); diff --git a/build/lib/tslint/noNlsInStandaloneEditorRule.ts b/build/lib/tslint/noNlsInStandaloneEditorRule.ts index ae23d74d78..5fb7abac88 100644 --- a/build/lib/tslint/noNlsInStandaloneEditorRule.ts +++ b/build/lib/tslint/noNlsInStandaloneEditorRule.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as ts from 'typescript'; diff --git a/src/vs/base/test/node/pfs/fixtures/site.css b/src/vs/base/test/node/pfs/fixtures/site.css index f7b51e752b..b7e5283202 100644 --- a/src/vs/base/test/node/pfs/fixtures/site.css +++ b/src/vs/base/test/node/pfs/fixtures/site.css @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /*---------------------------------------------------------- diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 4e49af620f..1f407b0be3 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1211,6 +1211,16 @@ export interface Command { arguments?: any[]; } +/** + * @internal + */ +export interface CommentThreadTemplate { + label: string; + acceptInputCommand?: Command; + additionalCommands?: Command[]; + deleteCommand?: Command; +} + /** * @internal */ @@ -1220,6 +1230,7 @@ export interface CommentInfo { commentingRanges?: (IRange[] | CommentingRanges); reply?: Command; draftMode?: DraftMode; + template?: CommentThreadTemplate; } /** @@ -1298,8 +1309,7 @@ export interface CommentThread2 { export interface CommentingRanges { readonly resource: URI; ranges: IRange[]; - newCommentThreadCommand?: Command; - newCommentThreadCallback?: (uri: UriComponents, range: IRange) => Promise; + newCommentThreadCallback?: (uri: UriComponents, range: IRange) => Promise; } /** diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index f85af23293..69bb012890 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -19,7 +19,6 @@ import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from ' import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; import { SnippetSession } from './snippetSession'; -import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; export class SnippetController2 implements IEditorContribution { @@ -114,26 +113,10 @@ export class SnippetController2 implements IEditorContribution { this._updateState(); - // we listen on model and selection changes. usually - // both events come in together and this is to prevent - // that we don't call _updateState twice. - let state: EditorState; - let dedupedUpdateState = () => { - if (!state || !state.validate(this._editor)) { - this._updateState(); - state = new EditorState(this._editor, CodeEditorStateFlag.Selection | CodeEditorStateFlag.Value); - } - }; this._snippetListener = [ - this._editor.onDidChangeModelContent(e => { - if (e.isFlush) { - this.cancel(); - } else { - setTimeout(dedupedUpdateState, 0); - } - }), - this._editor.onDidChangeCursorSelection(dedupedUpdateState), + this._editor.onDidChangeModelContent(e => e.isFlush && this.cancel()), this._editor.onDidChangeModel(() => this.cancel()), + this._editor.onDidChangeCursorSelection(() => this._updateState()) ]; } diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index d195664be9..e8745ff99e 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -198,6 +198,10 @@ export class OneSnippet { let ranges: Range[] | undefined; for (const placeholder of placeholdersWithEqualIndex) { + if (placeholder.isFinalTabstop) { + // ignore those + break; + } if (!ranges) { ranges = []; @@ -571,12 +575,6 @@ export class SnippetSession { return false; } - if (allPossibleSelections.has(0)) { - // selection overlaps with a final tab stop which means - // we done - return false; - } - // add selections from 'this' snippet so that we know all // selections for this placeholder allPossibleSelections.forEach((array, index) => { diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts index 58be229805..6d792a9a58 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts @@ -11,7 +11,6 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { NullLogService } from 'vs/platform/log/common/log'; import { Handler } from 'vs/editor/common/editorCommon'; -import { timeout } from 'vs/base/common/async'; suite('SnippetController2', function () { @@ -134,11 +133,11 @@ suite('SnippetController2', function () { assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11)); editor.trigger('test', 'cut', {}); - assertContextKeys(contextKeys, false, false, false); + assertContextKeys(contextKeys, true, false, true); assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); editor.trigger('test', 'type', { text: 'abc' }); - assertContextKeys(contextKeys, false, false, false); + assertContextKeys(contextKeys, true, false, true); ctrl.next(); assertContextKeys(contextKeys, false, false, false); @@ -160,9 +159,9 @@ suite('SnippetController2', function () { assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); assertContextKeys(contextKeys, true, false, true); - // ctrl.next(); - // assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); - // assertContextKeys(contextKeys, true, true, true); + ctrl.next(); + assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + assertContextKeys(contextKeys, true, true, true); ctrl.next(); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); @@ -177,10 +176,10 @@ suite('SnippetController2', function () { ctrl.insert('farboo'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); - // assertContextKeys(contextKeys, true, false, true); + assertContextKeys(contextKeys, true, false, true); - // ctrl.next(); - // assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + ctrl.next(); + assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); assertContextKeys(contextKeys, false, false, false); }); @@ -403,22 +402,12 @@ suite('SnippetController2', function () { assertSelections(editor, new Selection(1, 22, 1, 22)); }); - test('A little confusing visual effect of highlighting for snippet tabstop #43270', async function () { + test('User defined snippet tab stops ignored #72862', function () { const ctrl = new SnippetController2(editor, logService, contextKeys); model.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - ctrl.insert('background-color: ${1:fff};$0'); - assertSelections(editor, new Selection(1, 19, 1, 22)); - - editor.setSelection(new Selection(1, 22, 1, 22)); + ctrl.insert('export default $1'); assertContextKeys(contextKeys, true, false, true); - editor.trigger('', 'deleteRight', null); - - assert.equal(model.getValue(), 'background-color: fff'); - - await timeout(0); // this depends on re-scheduling of events... - - assertContextKeys(contextKeys, false, false, false); }); }); diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index 4da4cfcbcd..0de028eb30 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -331,7 +331,7 @@ suite('SnippetSession', function () { // reset selection to placeholder session.next(); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.equal(session.isSelectionWithinPlaceholders(), true); assert.equal(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 13, 1, 13), new Selection(2, 17, 2, 17)); }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index e0ee7c7410..324a54ff3b 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -814,28 +814,35 @@ declare module 'vscode' { /** * A comment is displayed within the editor or the Comments Panel, depending on how it is provided. */ - export interface Comment { + export class Comment { /** * The id of the comment */ + readonly id: string; + + /** + * The id of the comment + * + * @deprecated Use Id instead + */ readonly commentId: string; /** - * The text of the comment + * The human-readable comment body */ readonly body: MarkdownString; + /** + * The display name of the user who created the comment + */ + readonly userName: string; + /** * Optional label describing the [Comment](#Comment) * Label will be rendered next to userName if exists. */ readonly label?: string; - /** - * The display name of the user who created the comment - */ - readonly userName: string; - /** * The icon path for the user who created the comment */ @@ -896,6 +903,13 @@ declare module 'vscode' { * Proposed Comment Reaction */ commentReactions?: CommentReaction[]; + + /** + * @param id The id of the comment + * @param body The human-readable comment body + * @param userName The display name of the user who created the comment + */ + constructor(id: string, body: MarkdownString, userName: string); } /** @@ -925,6 +939,7 @@ declare module 'vscode' { /** * Comment Reactions + * Stay in proposed. */ interface CommentReaction { readonly label?: string; @@ -1006,6 +1021,9 @@ declare module 'vscode' { value: string; } + /** + * Stay in proposed + */ export interface CommentReactionProvider { availableReactions: CommentReaction[]; toggleReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise; @@ -1016,12 +1034,50 @@ declare module 'vscode' { * Provide a list of ranges which allow new comment threads creation or null for a given document */ provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; + } + + export interface CommentThreadTemplate { + /** + * The human-readable label describing the [Comment Thread](#CommentThread) + */ + label: string; /** - * The method `createEmptyCommentThread` is called when users attempt to create new comment thread from the gutter or command palette. - * Extensions still need to call `createCommentThread` inside this call when appropriate. + * Optional accept input command + * + * `acceptInputCommand` is the default action rendered on Comment Widget, which is always placed rightmost. + * This command will be invoked when users the user accepts the value in the comment editor. + * This command will disabled when the comment editor is empty. */ - createEmptyCommentThread(document: TextDocument, range: Range): ProviderResult; + acceptInputCommand?: Command; + + /** + * Optional additonal commands. + * + * `additionalCommands` are the secondary actions rendered on Comment Widget. + */ + additionalCommands?: Command[]; + + /** + * The command to be executed when users try to delete the comment thread. Currently, this is only called + * when the user collapses a comment thread that has no comments in it. + */ + deleteCommand?: Command; + } + + export interface EmptyCommentThreadFactory { + template: CommentThreadTemplate; + /** + * When users attempt to create new comment thread from the gutter or command palette, `template` will be used first to create the Comment Thread Widget in the editor for users to start comment drafting. + * Then `createEmptyCommentThread` is called after that. Extensions should still call [`createCommentThread`](CommentController.createCommentThread) to create a real [`CommentThread`](#CommentThread) + * Extensions still need to call `createCommentThread` inside this call when appropriate. + * + * @param document The document in which users attempt to create a new comment thread + * @param range The range the comment threadill located within the document. + * + * @returns commentThread The [`CommentThread`](#CommentThread) created by extensions + */ + createEmptyCommentThread(document: TextDocument, range: Range): ProviderResult; } export interface CommentController { @@ -1036,23 +1092,44 @@ declare module 'vscode' { readonly label: string; /** - * The active (focused) [comment input box](#CommentInputBox). + * The active [comment input box](#CommentInputBox) or `undefined`. The active `inputBox` is the input box of + * the comment thread widget that currently has focus. It's `undefined` when the focus is not in any CommentInputBox. */ - readonly inputBox?: CommentInputBox; + readonly inputBox: CommentInputBox | undefined; /** - * Create a [CommentThread](#CommentThread) + * The active [comment thread](#CommentThread) or `undefined`. The `activeCommentThread` is the comment thread of + * the comment thread widget that currently has focus. It's `undefined` when the focus is not in any comment thread widget. + */ + readonly activeCommentThread: CommentThread | undefined; + + /** + * Create a [CommentThread](#CommentThread). The comment thread will be displayed in visible text editors (if the resource matches) + * and Comments Panel. + * @param id An `id` for the comment thread. + * @param resource The uri of the document the thread has been created on. + * @param range The range the comment thread is located within the document. + * @param comments The ordered comments of the thread. */ createCommentThread(id: string, resource: Uri, range: Range, comments: Comment[]): CommentThread; /** - * Optional commenting range provider. - * Provide a list [ranges](#Range) which support commenting to any given resource uri. + * Optional commenting range provider. Provide a list [ranges](#Range) which support commenting to any given resource uri. + * + * If not provided and `emptyCommentThreadFactory` exits, users can leave comments in any document opened in the editor. */ commentingRangeProvider?: CommentingRangeProvider; + /** + * Optional empty comment thread factory. It's necessary for supporting users to trigger Comment Thread creation from the editor or command palette. + * + * If not provided, users won't be able to trigger new comment thread creation from the editor gutter area or command palette. + */ + emptyCommentThreadFactory?: EmptyCommentThreadFactory; + /** * Optional reaction provider + * Stay in proposed. */ reactionProvider?: CommentReactionProvider; diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index adbc91086b..bf50a565f8 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -298,12 +298,18 @@ export class MainThreadCommentController { ); this._threads.set(commentThreadHandle, thread); - this._commentService.updateComments(this._uniqueId, { - added: [thread], - removed: [], - changed: [], - draftMode: modes.DraftMode.NotSupported - }); + + // As we create comment thread from template and then restore from the newly created maint thread comment thread, + // we postpone the update event to avoid duplication. + // This can be actually removed once we are on the new API. + setTimeout(() => { + this._commentService.updateComments(this._uniqueId, { + added: [thread], + removed: [], + changed: [], + draftMode: modes.DraftMode.NotSupported + }); + }, 0); return thread; } @@ -379,10 +385,17 @@ export class MainThreadCommentController { commentingRanges: commentingRanges ? { resource: resource, ranges: commentingRanges, newCommentThreadCallback: async (uri: UriComponents, range: IRange) => { - await this._proxy.$createNewCommentWidgetCallback(this.handle, uri, range, token); + let threadHandle = await this._proxy.$createNewCommentWidgetCallback(this.handle, uri, range, token); + + if (threadHandle !== undefined) { + return this.getKnownThread(threadHandle); + } + + return undefined; // {{SQL CARBON EDIT}} @anthonydresser revert back after strict-null-check } } : [], - draftMode: modes.DraftMode.NotSupported + draftMode: modes.DraftMode.NotSupported, + template: this._features.commentThreadTemplate }; } @@ -458,6 +471,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._proxy.$onCommentWidgetInputChange(controller.handle, this._input ? this._input.value : undefined); })); + await this._proxy.$onActiveCommentThreadChange(controller.handle, controller.activeCommentThread.commentThreadHandle); await this._proxy.$onCommentWidgetInputChange(controller.handle, this._input ? this._input.value : undefined); })); } @@ -510,8 +524,6 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - console.log('createCommentThread', commentThreadHandle); - return provider.createCommentThread(commentThreadHandle, threadId, resource, range); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8e17c06e15..2a04c56472 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -121,11 +121,19 @@ export interface MainThreadCommandsShape extends IDisposable { $getCommands(): Promise; } +export interface CommentThreadTemplate { + label: string; + acceptInputCommand?: modes.Command; + additionalCommands?: modes.Command[]; + deleteCommand?: modes.Command; +} + export interface CommentProviderFeatures { startDraftLabel?: string; deleteDraftLabel?: string; finishDraftLabel?: string; reactionGroup?: modes.CommentReaction[]; + commentThreadTemplate?: CommentThreadTemplate; } export interface MainThreadCommentsShape extends IDisposable { @@ -1206,10 +1214,11 @@ export interface ExtHostCommentsShape { $provideDocumentComments(handle: number, document: UriComponents): Promise; $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Promise; $onCommentWidgetInputChange(commentControllerHandle: number, input: string | undefined): Promise; + $onActiveCommentThreadChange(commentControllerHandle: number, threadHandle: number | undefined): Promise; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise; $provideReactionGroup(commentControllerHandle: number): Promise; $toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise; - $createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): Promise; + $createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): Promise; $replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Promise; $editComment(handle: number, document: UriComponents, comment: modes.Comment, text: string): Promise; $deleteComment(handle: number, document: UriComponents, comment: modes.Comment): Promise; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 4a77d2fe78..987dfa6b30 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -99,6 +99,17 @@ export class ExtHostComments implements ExtHostCommentsShape { return Promise.resolve(commentControllerHandle); } + $onActiveCommentThreadChange(commentControllerHandle: number, threadHandle: number): Promise { + const commentController = this._commentControllers.get(commentControllerHandle); + + if (!commentController) { + return Promise.resolve(undefined); + } + + commentController.$onActiveCommentThreadChange(threadHandle); + return Promise.resolve(threadHandle); + } + $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise { const commentController = this._commentControllers.get(commentControllerHandle); @@ -146,28 +157,34 @@ export class ExtHostComments implements ExtHostCommentsShape { }); } - $createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): Promise { + $createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): Promise { const commentController = this._commentControllers.get(commentControllerHandle); if (!commentController) { - return Promise.resolve(); + return Promise.resolve(undefined); } - if (!(commentController as any).emptyCommentThreadFactory && !(commentController.commentingRangeProvider && commentController.commentingRangeProvider.createEmptyCommentThread)) { - return Promise.resolve(); + if (!(commentController as any).emptyCommentThreadFactory && !(commentController.commentingRangeProvider && commentController.commentingRangeProvider.createEmptyCommentThread) && !(commentController.emptyCommentThreadFactory && commentController.emptyCommentThreadFactory.createEmptyCommentThread)) { + return Promise.resolve(undefined); } const document = this._documents.getDocument(URI.revive(uriComponents)); return asPromise(() => { - // TODO, remove this once GH PR stable deprecates `emptyCommentThreadFactory`. - if ((commentController as any).emptyCommentThreadFactory) { - return (commentController as any).emptyCommentThreadFactory!.createEmptyCommentThread(document, extHostTypeConverter.Range.to(range)); + if (commentController.emptyCommentThreadFactory) { + return commentController.emptyCommentThreadFactory!.createEmptyCommentThread(document, extHostTypeConverter.Range.to(range)); } if (commentController.commentingRangeProvider && commentController.commentingRangeProvider.createEmptyCommentThread) { return commentController.commentingRangeProvider.createEmptyCommentThread(document, extHostTypeConverter.Range.to(range)); } - }).then(() => Promise.resolve()); + + return undefined; // {{SQL CARBON EDIT}} @anthonydresser revert back after strict-null-check + }).then((commentThread: ExtHostCommentThread | undefined) => { + if (commentThread) { + return Promise.resolve(commentThread.handle); + } + return Promise.resolve(undefined); + }); } registerWorkspaceCommentProvider( @@ -451,6 +468,12 @@ export class ExtHostCommentThread implements vscode.CommentThread { private _localDisposables: types.Disposable[]; + private _isDiposed: boolean; + + public get isDisposed(): boolean { + return this._isDiposed; + } + constructor( private _proxy: MainThreadCommentsShape, private readonly _commandsConverter: CommandsConverter, @@ -469,6 +492,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { ); this._localDisposables = []; + this._isDiposed = false; this._localDisposables.push(this.onDidUpdateCommentThread(() => { this.eventuallyUpdateCommentThread(); @@ -519,6 +543,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { this._commentController.handle, this.handle ); + this._isDiposed = true; } } @@ -562,7 +587,17 @@ class ExtHostCommentController implements vscode.CommentController { return this._label; } - public inputBox?: ExtHostCommentInputBox; + public inputBox: ExtHostCommentInputBox | undefined; + private _activeCommentThread: ExtHostCommentThread | undefined; + + public get activeCommentThread(): ExtHostCommentThread | undefined { + if (this._activeCommentThread && this._activeCommentThread.isDisposed) { + this._activeCommentThread = undefined; + } + + return this._activeCommentThread; + } + public activeCommentingRange?: vscode.Range; public get handle(): number { @@ -570,7 +605,31 @@ class ExtHostCommentController implements vscode.CommentController { } private _threads: Map = new Map(); - commentingRangeProvider?: vscode.CommentingRangeProvider; + commentingRangeProvider?: vscode.CommentingRangeProvider & { createEmptyCommentThread: (document: vscode.TextDocument, range: types.Range) => Promise; }; + + private _emptyCommentThreadFactory: vscode.EmptyCommentThreadFactory | undefined; + get emptyCommentThreadFactory(): vscode.EmptyCommentThreadFactory | undefined { + return this._emptyCommentThreadFactory; + } + + set emptyCommentThreadFactory(newEmptyCommentThreadFactory: vscode.EmptyCommentThreadFactory | undefined) { + this._emptyCommentThreadFactory = newEmptyCommentThreadFactory; + + if (this._emptyCommentThreadFactory && this._emptyCommentThreadFactory.template) { + let template = this._emptyCommentThreadFactory.template; + const acceptInputCommand = template.acceptInputCommand ? this._commandsConverter.toInternal(template.acceptInputCommand) : undefined; + const additionalCommands = template.additionalCommands ? template.additionalCommands.map(x => this._commandsConverter.toInternal(x)) : []; + const deleteCommand = template.deleteCommand ? this._commandsConverter.toInternal(template.deleteCommand) : undefined; + this._proxy.$updateCommentControllerFeatures(this.handle, { + commentThreadTemplate: { + label: template.label, + acceptInputCommand, + additionalCommands, + deleteCommand + } + }); + } + } private _commentReactionProvider?: vscode.CommentReactionProvider; @@ -610,6 +669,10 @@ class ExtHostCommentController implements vscode.CommentController { } } + $onActiveCommentThreadChange(threadHandle: number) { + this._activeCommentThread = this.getCommentThread(threadHandle); + } + getCommentThread(handle: number) { return this._threads.get(handle); } @@ -664,6 +727,7 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { } return { + id: comment.commentId, commentId: comment.commentId, body: extHostTypeConverter.MarkdownString.to(comment.body), userName: comment.userName, @@ -685,7 +749,7 @@ function convertToModeComment(commentController: ExtHostCommentController, vscod const iconPath = vscodeComment.userIconPath ? vscodeComment.userIconPath.toString() : vscodeComment.gravatar; return { - commentId: vscodeComment.commentId, + commentId: vscodeComment.id || vscodeComment.commentId, body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), userName: vscodeComment.userName, userIconPath: iconPath, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 72e5528a19..485078c8ce 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2287,6 +2287,61 @@ export enum FoldingRangeKind { //#endregion +//#region Comment +@es5ClassCompat +export class Comment { + id: string; + body: MarkdownString; + userName: string; + label?: string; + userIconPath?: URI; + selectCommand?: vscode.Command; + editCommand?: vscode.Command; + deleteCommand?: vscode.Command; + + /** + * The id of the comment + * + * @deprecated Use Id instead + */ + commentId: string; + + /** + * @deprecated Use userIconPath instead. The avatar src of the user who created the comment + */ + gravatar?: string; + + /** + * @deprecated, use editCommand + */ + canEdit?: boolean; + + /** + * @deprecated, use deleteCommand + */ + canDelete?: boolean; + + /** + * @deprecated + */ + command?: vscode.Command; + + /** + * @deprecated + */ + isDraft?: boolean; + + /** + * Proposed Comment Reaction + */ + commentReactions?: vscode.CommentReaction[]; + + constructor(id: string, body: MarkdownString, userName: string) { + this.id = id; + this.body = body; + this.userName = userName; + } +} export enum CommentThreadCollapsibleState { /** @@ -2298,6 +2353,7 @@ export enum CommentThreadCollapsibleState { */ Expanded = 1 } +//#endregion @es5ClassCompat export class QuickInputButtons { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index b7be4d7cb2..cedd686248 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -780,6 +780,7 @@ export function createApiFactory( Color: extHostTypes.Color, ColorInformation: extHostTypes.ColorInformation, ColorPresentation: extHostTypes.ColorPresentation, + Comment: extHostTypes.Comment, CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 531fe4d7e1..8aec8ef9dd 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -309,17 +309,17 @@ export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusB hc: Color.black.transparent(0.3), }, nls.localize('statusBarProminentItemHoverBackground', "Status bar prominent items background color when hovering. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_HOST_NAME_BACKGROUND = registerColor('statusBarItem.hostBackground', { - dark: '#C40057', - light: '#C40057', - hc: '#C40057' -}, nls.localize('statusBarItemHostBackground', "Background color for the host indicator on the status bar.")); +export const STATUS_BAR_HOST_NAME_BACKGROUND = registerColor('statusBarItem.remoteBackground', { + dark: '#16825D', + light: '#16825D', + hc: '#FFFFFF00' +}, nls.localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); -export const STATUS_BAR_HOST_NAME_FOREGROUND = registerColor('statusBarItem.hostForeground', { +export const STATUS_BAR_HOST_NAME_FOREGROUND = registerColor('statusBarItem.remoteForeground', { dark: '#FFFFFF', light: '#FFFFFF', hc: '#FFFFFF' -}, nls.localize('statusBarItemHostForeground', "Foreground color for the host indicator on the status bar.")); +}, nls.localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); // < --- Activity Bar --- > @@ -366,6 +366,18 @@ export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.for hc: Color.white }, nls.localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', { + dark: ACTIVITY_BAR_BADGE_BACKGROUND, + light: ACTIVITY_BAR_BADGE_BACKGROUND, + hc: ACTIVITY_BAR_BADGE_BACKGROUND +}, nls.localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view")); + +export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', { + dark: ACTIVITY_BAR_BADGE_FOREGROUND, + light: ACTIVITY_BAR_BADGE_FOREGROUND, + hc: ACTIVITY_BAR_BADGE_FOREGROUND +}, nls.localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view")); + // < --- Side Bar --- > diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 8aa73ed758..b810444bcd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -322,15 +322,17 @@ export class CommentService extends Disposable implements ICommentService { } } - let commentControlResult: Promise[] = []; + let commentControlResult: Promise[] = []; this._commentControls.forEach(control => { - commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None)); + commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None) + .catch(e => { + console.log(e); + return null; + })); }); - let ret = [...await Promise.all(result), ...await Promise.all(commentControlResult)]; - - return ret; + return Promise.all([...result, ...commentControlResult]); } async getCommentingRanges(resource: URI): Promise { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 6ebd0c0878..1426739b29 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -212,6 +212,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } else { const deleteCommand = (this._commentThread as modes.CommentThread2).deleteCommand; if (deleteCommand) { + this.commentService.setActiveCommentThread(this._commentThread); return this.commandService.executeCommand(deleteCommand.id, ...(deleteCommand.arguments || [])); } } @@ -239,7 +240,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - async update(commentThread: modes.CommentThread | modes.CommentThread2) { + async update(commentThread: modes.CommentThread | modes.CommentThread2, replaceTemplate: boolean = false) { const oldCommentsLen = this._commentElements.length; const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; @@ -288,12 +289,20 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThread = commentThread; this._commentElements = newCommentNodeList; - this.createThreadLabel(); + this.createThreadLabel(replaceTemplate); + + if (replaceTemplate) { + this.createTextModelListener(); + } if (this._formActions && this._commentEditor.hasModel()) { dom.clearNode(this._formActions); const model = this._commentEditor.getModel(); this.createCommentWidgetActions2(this._formActions, model); + + if (replaceTemplate) { + this.createCommentWidgetActionsListener(this._formActions, model); + } } // Move comment glyph widget and show position if the line has changed. @@ -348,7 +357,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentEditor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); } - display(lineNumber: number) { + display(lineNumber: number, fromTemplate: boolean = false) { this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber); this._disposables.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); @@ -386,58 +395,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentEditor.setModel(model); this._disposables.push(this._commentEditor); this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => this.setCommentEditorDecorations())); - if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { - this._disposables.push(this._commentEditor.onDidFocusEditorWidget(() => { - let commentThread = this._commentThread as modes.CommentThread2; - commentThread.input = { - uri: this._commentEditor.getModel()!.uri, - value: this._commentEditor.getValue() - }; - this.commentService.setActiveCommentThread(this._commentThread); - })); - - this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => { - let modelContent = this._commentEditor.getValue(); - let thread = (this._commentThread as modes.CommentThread2); - if (thread.input && thread.input.uri === this._commentEditor.getModel()!.uri && thread.input.value !== modelContent) { - let newInput: modes.CommentInput = thread.input; - newInput.value = modelContent; - thread.input = newInput; - } - })); - - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeInput(input => { - let thread = (this._commentThread as modes.CommentThread2); - - if (thread.input && thread.input.uri !== this._commentEditor.getModel()!.uri) { - return; - } - if (!input) { - return; - } - - if (this._commentEditor.getValue() !== input.value) { - this._commentEditor.setValue(input.value); - - if (input.value === '') { - this._pendingComment = ''; - if (dom.hasClass(this._commentForm, 'expand')) { - dom.removeClass(this._commentForm, 'expand'); - } - this._commentEditor.getDomNode()!.style.outline = ''; - this._error.textContent = ''; - dom.addClass(this._error, 'hidden'); - } - } - })); - - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeComments(async _ => { - await this.update(this._commentThread); - })); - - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeLabel(_ => { - this.createThreadLabel(); - })); + if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined && !fromTemplate) { + this.createTextModelListener(); } this.setCommentEditorDecorations(); @@ -456,50 +415,9 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._formActions = dom.append(this._commentForm, dom.$('.form-actions')); if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { this.createCommentWidgetActions2(this._formActions, model); - - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeAcceptInputCommand(_ => { - if (this._formActions) { - dom.clearNode(this._formActions); - this.createCommentWidgetActions2(this._formActions, model); - } - })); - - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeAdditionalCommands(_ => { - if (this._formActions) { - dom.clearNode(this._formActions); - this.createCommentWidgetActions2(this._formActions, model); - } - })); - - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeRange(range => { - // Move comment glyph widget and show position if the line has changed. - const lineNumber = this._commentThread.range.startLineNumber; - let shouldMoveWidget = false; - if (this._commentGlyph) { - if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { - shouldMoveWidget = true; - this._commentGlyph.setLineNumber(lineNumber); - } - } - - if (shouldMoveWidget && this._isExpanded) { - this.show({ lineNumber, column: 1 }, 2); - } - })); - - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeCollasibleState(state => { - if (state === modes.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { - const lineNumber = this._commentThread.range.startLineNumber; - - this.show({ lineNumber, column: 1 }, 2); - return; - } - - if (state === modes.CommentThreadCollapsibleState.Collapsed && this._isExpanded) { - this.hide(); - return; - } - })); + if (!fromTemplate) { + this.createCommentWidgetActionsListener(this._formActions, model); + } } else { this.createCommentWidgetActions(this._formActions, model); } @@ -526,6 +444,106 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } + private createTextModelListener() { + this._disposables.push(this._commentEditor.onDidFocusEditorWidget(() => { + let commentThread = this._commentThread as modes.CommentThread2; + commentThread.input = { + uri: this._commentEditor.getModel()!.uri, + value: this._commentEditor.getValue() + }; + this.commentService.setActiveCommentThread(this._commentThread); + })); + + this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => { + let modelContent = this._commentEditor.getValue(); + let thread = (this._commentThread as modes.CommentThread2); + if (thread.input && thread.input.uri === this._commentEditor.getModel()!.uri && thread.input.value !== modelContent) { + let newInput: modes.CommentInput = thread.input; + newInput.value = modelContent; + thread.input = newInput; + } + })); + + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeInput(input => { + let thread = (this._commentThread as modes.CommentThread2); + + if (thread.input && thread.input.uri !== this._commentEditor.getModel()!.uri) { + return; + } + if (!input) { + return; + } + + if (this._commentEditor.getValue() !== input.value) { + this._commentEditor.setValue(input.value); + + if (input.value === '') { + this._pendingComment = ''; + if (dom.hasClass(this._commentForm, 'expand')) { + dom.removeClass(this._commentForm, 'expand'); + } + this._commentEditor.getDomNode()!.style.outline = ''; + this._error.textContent = ''; + dom.addClass(this._error, 'hidden'); + } + } + })); + + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeComments(async _ => { + await this.update(this._commentThread); + })); + + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeLabel(_ => { + this.createThreadLabel(); + })); + } + + private createCommentWidgetActionsListener(container: HTMLElement, model: ITextModel) { + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeAcceptInputCommand(_ => { + if (container) { + dom.clearNode(container); + this.createCommentWidgetActions2(container, model); + } + })); + + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeAdditionalCommands(_ => { + if (container) { + dom.clearNode(container); + this.createCommentWidgetActions2(container, model); + } + })); + + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeRange(range => { + // Move comment glyph widget and show position if the line has changed. + const lineNumber = this._commentThread.range.startLineNumber; + let shouldMoveWidget = false; + if (this._commentGlyph) { + if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { + shouldMoveWidget = true; + this._commentGlyph.setLineNumber(lineNumber); + } + } + + if (shouldMoveWidget && this._isExpanded) { + this.show({ lineNumber, column: 1 }, 2); + } + })); + + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeCollasibleState(state => { + if (state === modes.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { + const lineNumber = this._commentThread.range.startLineNumber; + + this.show({ lineNumber, column: 1 }, 2); + return; + } + + if (state === modes.CommentThreadCollapsibleState.Collapsed && this._isExpanded) { + this.hide(); + return; + } + })); + } + private handleError(e: Error) { this._error.textContent = e.message; this._commentEditor.getDomNode()!.style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`; @@ -798,13 +816,14 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - private createThreadLabel() { + private createThreadLabel(replaceTemplate: boolean = false) { let label: string | undefined; if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { label = (this._commentThread as modes.CommentThread2).label; } - if (label === undefined) { + if (label === undefined && !replaceTemplate) { + // if it's for replacing the comment thread template, the comment thread widget title can be undefined as extensions may set it later if (this._commentThread.comments && this._commentThread.comments.length) { const participantsList = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); label = nls.localize('commentThreadParticipants', "Participants: {0}", participantsList); @@ -813,8 +832,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - this._headingLabel.innerHTML = strings.escape(label); - this._headingLabel.setAttribute('aria-label', label); + if (label) { + this._headingLabel.innerHTML = strings.escape(label); + this._headingLabel.setAttribute('aria-label', label); + } + } private expandReplyArea() { diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 6393220adb..c2e1ecd496 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -30,7 +30,7 @@ import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/com import { overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/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 { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -64,7 +64,7 @@ class CommentingRangeDecoration { return this._decorationId; } - constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private commentingRangesInfo?: modes.CommentingRanges) { + constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private _template: modes.CommentThreadTemplate | undefined, private commentingRangesInfo?: modes.CommentingRanges) { const startLineNumber = _range.startLineNumber; const endLineNumber = _range.endLineNumber; let commentingRangeDecorations = [{ @@ -81,13 +81,14 @@ class CommentingRangeDecoration { } } - public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined } { + public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined } { return { extensionId: this._extensionId, label: this._label, replyCommand: this._reply, ownerId: this._ownerId, - commentingRangesInfo: this.commentingRangesInfo + commentingRangesInfo: this.commentingRangesInfo, + template: this._template }; } @@ -124,11 +125,11 @@ class CommentingRangeDecorator { for (const info of commentInfos) { if (Array.isArray(info.commentingRanges)) { info.commentingRanges.forEach(range => { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, info.reply, this.decorationOptions)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, info.reply, this.decorationOptions, info.template)); }); } else { (info.commentingRanges ? info.commentingRanges.ranges : []).forEach(range => { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, (info.commentingRanges as modes.CommentingRanges).newCommentThreadCommand, this.decorationOptions, info.commentingRanges as modes.CommentingRanges)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, undefined, this.decorationOptions, info.template, info.commentingRanges as modes.CommentingRanges)); }); } } @@ -178,7 +179,6 @@ export class ReviewController implements IEditorContribution { constructor( editor: ICodeEditor, @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, @@ -441,6 +441,11 @@ export class ReviewController implements IEditorContribution { } }); added.forEach(thread => { + let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + return; + } + const pendingCommentText = this._pendingCommentCache[e.owner] && this._pendingCommentCache[e.owner][thread.threadId]; this.displayCommentThread(e.owner, thread, pendingCommentText, draftMode); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); @@ -457,6 +462,30 @@ export class ReviewController implements IEditorContribution { this._commentWidgets.push(zoneWidget); } + private addCommentThreadFromTemplate(lineNumber: number, ownerId: string, extensionId: string | undefined, template: modes.CommentThreadTemplate): ReviewZoneWidget { + let templateReviewZoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, ownerId, { + commentThreadHandle: -1, + label: template!.label, + acceptInputCommand: template.acceptInputCommand, + additionalCommands: template.additionalCommands, + deleteCommand: template.deleteCommand, + extensionId: extensionId, + threadId: null, + resource: null, + comments: [], + range: { + startLineNumber: lineNumber, + startColumn: 0, + endLineNumber: lineNumber, + endColumn: 0 + }, + collapsibleState: modes.CommentThreadCollapsibleState.Expanded, + }, + '', modes.DraftMode.NotSupported); + + return templateReviewZoneWidget; + } + 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.`); @@ -612,16 +641,16 @@ export class ReviewController implements IEditorContribution { const commentInfos = newCommentInfos.filter(info => info.ownerId === pick.id); if (commentInfos.length) { - const { replyCommand, ownerId, extensionId, commentingRangesInfo } = commentInfos[0]; - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); + const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = commentInfos[0]; + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); } }).then(() => { this._addInProgress = false; }); } } else { - const { replyCommand, ownerId, extensionId, commentingRangesInfo } = newCommentInfos[0]!; - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); + const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = newCommentInfos[0]!; + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); } return Promise.resolve(); @@ -640,11 +669,11 @@ export class ReviewController implements IEditorContribution { return picks; } - private getContextMenuActions(commentInfos: { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] { + private getContextMenuActions(commentInfos: { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] { const actions: (IAction | ContextSubMenu)[] = []; commentInfos.forEach(commentInfo => { - const { replyCommand, ownerId, extensionId, label, commentingRangesInfo } = commentInfo; + const { replyCommand, ownerId, extensionId, label, commentingRangesInfo, template } = commentInfo; actions.push(new Action( 'addCommentThread', @@ -652,7 +681,7 @@ export class ReviewController implements IEditorContribution { undefined, true, () => { - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); return Promise.resolve(); } )); @@ -660,17 +689,25 @@ export class ReviewController implements IEditorContribution { return actions; } - public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined) { + public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined) { if (commentingRangesInfo) { let range = new Range(lineNumber, 1, lineNumber, 1); - if (commentingRangesInfo.newCommentThreadCommand) { - if (replyCommand) { - const commandId = replyCommand.id; - const args = replyCommand.arguments || []; + if (commentingRangesInfo.newCommentThreadCallback && template) { + // create comment widget through template + let commentThreadWidget = this.addCommentThreadFromTemplate(lineNumber, ownerId, extensionId, template); + commentThreadWidget.display(lineNumber, true); - this._commandService.executeCommand(commandId, ...args); - this._addInProgress = false; - } + return commentingRangesInfo.newCommentThreadCallback(this.editor.getModel()!.uri, range) + .then(commentThread => { + commentThreadWidget.update(commentThread!, true); + this._commentWidgets.push(commentThreadWidget); + this.processNextThreadToAdd(); + }) + .catch(e => { + this.notificationService.error(nls.localize('commentThreadAddFailure', "Adding a new comment thread failed: {0}.", e.message)); + commentThreadWidget.dispose(); + this.processNextThreadToAdd(); + }); } else if (commentingRangesInfo.newCommentThreadCallback) { return commentingRangesInfo.newCommentThreadCallback(this.editor.getModel()!.uri, range) .then(_ => { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts index 039975b901..90922667c2 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts @@ -13,7 +13,7 @@ import { IExtensionManagementServerService, IExtensionTipsService } from 'vs/pla import { ILabelService } from 'vs/platform/label/common/label'; import { extensionButtonProminentBackground, extensionButtonProminentForeground, DisabledLabelAction, ReloadAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; -import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; +import { EXTENSION_BADGE_REMOTE_BACKGROUND, EXTENSION_BADGE_REMOTE_FOREGROUND } from 'vs/workbench/common/theme'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Emitter, Event } from 'vs/base/common/event'; @@ -323,8 +323,8 @@ class RemoteBadge extends Disposable { if (!this.element) { return; } - const bgColor = this.themeService.getTheme().getColor(STATUS_BAR_HOST_NAME_BACKGROUND); - const fgColor = this.themeService.getTheme().getColor(STATUS_BAR_HOST_NAME_FOREGROUND); + const bgColor = this.themeService.getTheme().getColor(EXTENSION_BADGE_REMOTE_BACKGROUND); + const fgColor = this.themeService.getTheme().getColor(EXTENSION_BADGE_REMOTE_FOREGROUND); this.element.style.backgroundColor = bgColor ? bgColor.toString() : ''; this.element.style.color = fgColor ? fgColor.toString() : ''; }; diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts index 3deffe16ca..d609824caf 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts @@ -19,7 +19,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { FileOnDiskContentProvider } from 'vs/workbench/contrib/files/common/files'; +import { FileOnDiskContentProvider, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IModelService } from 'vs/editor/common/services/modelService'; import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; @@ -248,7 +248,7 @@ class ResolveSaveConflictAction extends Action { return this.editorService.openEditor( { - leftResource: resource.with({ scheme: CONFLICT_RESOLUTION_SCHEME }), + leftResource: resourceToFileOnDisk(CONFLICT_RESOLUTION_SCHEME, resource), rightResource: resource, label: editorLabel, options: { pinned: true } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 60a568c53f..4460a70b2b 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -23,8 +23,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { once } from 'vs/base/common/functional'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { toLocalResource } from 'vs/base/common/resources'; /** * Explorer viewlet id. @@ -133,6 +131,14 @@ export const SortOrderConfiguration = { export type SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified'; +export function resourceToFileOnDisk(scheme: string, resource: URI): URI { + return resource.with({ scheme, query: JSON.stringify({ scheme: resource.scheme }) }); +} + +export function fileOnDiskToResource(resource: URI): URI { + return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null }); +} + export class FileOnDiskContentProvider implements ITextModelContentProvider { private fileWatcherDisposable: IDisposable | undefined; @@ -140,13 +146,12 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { @ITextFileService private readonly textFileService: ITextFileService, @IFileService private readonly fileService: IFileService, @IModeService private readonly modeService: IModeService, - @IModelService private readonly modelService: IModelService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IModelService private readonly modelService: IModelService ) { } provideTextContent(resource: URI): Promise { - const savedFileResource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); + const savedFileResource = fileOnDiskToResource(resource); // Make sure our file from disk is resolved up to date return this.resolveEditorModel(resource).then(codeEditorModel => { @@ -174,7 +179,7 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { private resolveEditorModel(resource: URI, createAsNeeded?: true): Promise; private resolveEditorModel(resource: URI, createAsNeeded?: boolean): Promise; private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise { - const savedFileResource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); + const savedFileResource = fileOnDiskToResource(resource); return this.textFileService.readStream(savedFileResource).then(content => { let codeEditorModel = this.modelService.getModel(resource); diff --git a/src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts new file mode 100644 index 0000000000..f53616a234 --- /dev/null +++ b/src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { FileOnDiskContentProvider, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files'; +import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService } from 'vs/platform/files/common/files'; + +class ServiceAccessor { + constructor( + @IFileService public fileService: TestFileService + ) { + } +} + +suite('Files - FileOnDiskContentProvider', () => { + + let instantiationService: IInstantiationService; + let accessor: ServiceAccessor; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(ServiceAccessor); + }); + + test('provideTextContent', async () => { + const provider = instantiationService.createInstance(FileOnDiskContentProvider); + const uri = URI.parse('testFileOnDiskContentProvider://foo'); + + const content = await provider.provideTextContent(resourceToFileOnDisk('conflictResolution', uri)); + + assert.equal(snapshotToString(content.createSnapshot()), 'Hello Html'); + assert.equal(accessor.fileService.getLastReadFileUri().toString(), uri.toString()); + }); +}); diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index 1a2d60d96d..aaf81fcab6 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -469,7 +469,7 @@ export class RemoteFileDialog { this.autoCompletePathSegment = ''; return false; } - const itemBasename = quickPickItem.label; + const itemBasename = this.trimTrailingSlash(quickPickItem.label); // Either force the autocomplete, or the old value should be one smaller than the new value and match the new value. if (itemBasename === '..') { // Don't match on the up directory item ever. diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 752b31667a..7232145354 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -899,6 +899,7 @@ export class TestFileService implements IFileService { readonly onError: Event = Event.None; private content = 'Hello Html'; + private lastReadFileUri: URI; constructor() { this._onFileChanges = new Emitter(); @@ -913,6 +914,10 @@ export class TestFileService implements IFileService { return this.content; } + public getLastReadFileUri(): URI { + return this.lastReadFileUri; + } + public get onFileChanges(): Event { return this._onFileChanges.event; } @@ -952,6 +957,8 @@ export class TestFileService implements IFileService { } readFile(resource: URI, options?: IReadFileOptions | undefined): Promise { + this.lastReadFileUri = resource; + return Promise.resolve({ resource: resource, value: VSBuffer.fromString(this.content), @@ -964,6 +971,8 @@ export class TestFileService implements IFileService { } readFileStream(resource: URI, options?: IReadFileOptions | undefined): Promise { + this.lastReadFileUri = resource; + return Promise.resolve({ resource: resource, value: {