Merge from vscode 5b9869eb02fa4c96205a74d05cad9164dfd06d60 (#5607)

This commit is contained in:
Anthony Dresser
2019-05-24 12:20:30 -07:00
committed by GitHub
parent 361ada4963
commit bcc449b524
126 changed files with 3096 additions and 2255 deletions

View File

@@ -33,46 +33,47 @@ export class BackupRestorer implements IWorkbenchContribution {
this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.doRestoreBackups());
}
private doRestoreBackups(): Promise<URI[] | undefined> {
private async doRestoreBackups(): Promise<URI[] | undefined> {
// Find all files and untitled with backups
return this.backupFileService.getWorkspaceFileBackups().then(backups => {
const backups = await this.backupFileService.getWorkspaceFileBackups();
const unresolvedBackups = await this.doResolveOpenedBackups(backups);
// Resolve backups that are opened
return this.doResolveOpenedBackups(backups).then((unresolved): Promise<URI[] | undefined> | undefined => {
// Some failed to restore or were not opened at all so we open and resolve them manually
if (unresolvedBackups.length > 0) {
await this.doOpenEditors(unresolvedBackups);
// Some failed to restore or were not opened at all so we open and resolve them manually
if (unresolved.length > 0) {
return this.doOpenEditors(unresolved).then(() => this.doResolveOpenedBackups(unresolved));
}
return this.doResolveOpenedBackups(unresolvedBackups);
}
return undefined;
});
});
return undefined;
}
private doResolveOpenedBackups(backups: URI[]): Promise<URI[]> {
const restorePromises: Promise<unknown>[] = [];
const unresolved: URI[] = [];
private async doResolveOpenedBackups(backups: URI[]): Promise<URI[]> {
const unresolvedBackups: URI[] = [];
backups.forEach(backup => {
await Promise.all(backups.map(async backup => {
const openedEditor = this.editorService.getOpened({ resource: backup });
if (openedEditor) {
restorePromises.push(openedEditor.resolve().then(undefined, () => unresolved.push(backup)));
try {
await openedEditor.resolve(); // trigger load
} catch (error) {
unresolvedBackups.push(backup); // ignore error and remember as unresolved
}
} else {
unresolved.push(backup);
unresolvedBackups.push(backup);
}
});
}));
return Promise.all(restorePromises).then(() => unresolved, () => unresolved);
return unresolvedBackups;
}
private doOpenEditors(resources: URI[]): Promise<void> {
private async doOpenEditors(resources: URI[]): Promise<void> {
const hasOpenedEditors = this.editorService.visibleEditors.length > 0;
const inputs = resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors));
// Open all remaining backups as editors and resolve them to load their backups
return this.editorService.openEditors(inputs).then(() => undefined);
await this.editorService.openEditors(inputs);
}
private resolveInput(resource: URI, index: number, hasOpenedEditors: boolean): IResourceInput | IUntitledResourceInput {

View File

@@ -7,6 +7,7 @@ import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import * as cp from 'child_process';
import * as pfs from 'vs/base/node/pfs';
import * as extpath from 'vs/base/node/extpath';
import * as platform from 'vs/base/common/platform';
import { promisify } from 'util';
import { Action } from 'vs/base/common/actions';
@@ -91,7 +92,7 @@ class InstallAction extends Action {
private isInstalled(): Promise<boolean> {
return pfs.lstat(this.target)
.then(stat => stat.isSymbolicLink())
.then(() => pfs.readlink(this.target))
.then(() => extpath.realpath(this.target))
.then(link => link === getSource())
.then(undefined, ignore('ENOENT', false));
}

View File

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { IAction } from 'vs/base/common/actions';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { IMenu } from 'vs/platform/actions/common/actions';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export class CommentFormActions extends Disposable {
private _buttonElements: HTMLElement[] = [];
constructor(
private container: HTMLElement,
private actionHandler: (action: IAction) => void,
private themeService: IThemeService
) {
super();
}
setActions(menu: IMenu) {
dispose(this._toDispose);
this._buttonElements.forEach(b => DOM.removeNode(b));
const groups = menu.getActions({ shouldForwardArgs: true });
for (const group of groups) {
const [, actions] = group;
actions.forEach(action => {
const button = new Button(this.container);
this._buttonElements.push(button.element);
this._toDispose.push(button);
this._toDispose.push(attachButtonStyler(button, this.themeService));
this._toDispose.push(button.onDidClick(() => this.actionHandler(action)));
button.enabled = action.enabled;
button.label = action.label;
});
}
}
}

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
import { IAction } from 'vs/base/common/actions';
import { MainThreadCommentController } from 'vs/workbench/api/browser/mainThreadComments';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Comment, CommentThread2 } from 'vs/editor/common/modes';
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
export class CommentMenus implements IDisposable {
constructor(
controller: MainThreadCommentController,
@IContextKeyService private contextKeyService: IContextKeyService,
@IMenuService private readonly menuService: IMenuService,
@IContextMenuService private readonly contextMenuService: IContextMenuService
) {
const commentControllerKey = this.contextKeyService.createKey<string | undefined>('commentController', undefined);
commentControllerKey.set(controller.contextValue);
}
getCommentThreadTitleActions(commentThread: CommentThread2, contextKeyService: IContextKeyService): IMenu {
return this.getMenu(MenuId.CommentThreadTitle, contextKeyService);
}
getCommentThreadActions(commentThread: CommentThread2, contextKeyService: IContextKeyService): IMenu {
return this.getMenu(MenuId.CommentThreadActions, contextKeyService);
}
getCommentTitleActions(comment: Comment, contextKeyService: IContextKeyService): IMenu {
return this.getMenu(MenuId.CommentTitle, contextKeyService);
}
getCommentActions(comment: Comment, contextKeyService: IContextKeyService): IMenu {
return this.getMenu(MenuId.CommentActions, contextKeyService);
}
private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu {
const menu = this.menuService.createMenu(menuId, contextKeyService);
const primary: IAction[] = [];
const secondary: IAction[] = [];
const result = { primary, secondary };
fillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => true);
return menu;
}
dispose(): void {
}
}

View File

@@ -8,7 +8,7 @@ import * as dom from 'vs/base/browser/dom';
import * as modes from 'vs/editor/common/modes';
import { ActionsOrientation, ActionViewItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button } from 'vs/base/browser/ui/button/button';
import { Action, IActionRunner } from 'vs/base/common/actions';
import { Action, IActionRunner, IAction } from 'vs/base/common/actions';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
@@ -35,6 +35,11 @@ import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from '.
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
import { MenuItemAction } from 'vs/platform/actions/common/actions';
import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions';
const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment");
const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment...");
@@ -57,10 +62,13 @@ export class CommentNode extends Disposable {
private _updateCommentButton: Button;
private _errorEditingContainer: HTMLElement;
private _isPendingLabel: HTMLElement;
private _contextKeyService: IContextKeyService;
private _commentContextValue: IContextKey<string>;
private _deleteAction: Action;
protected actionRunner?: IActionRunner;
protected toolbar: ToolBar;
private _commentFormActions: CommentFormActions;
private _onDidDelete = new Emitter<CommentNode>();
@@ -85,12 +93,17 @@ export class CommentNode extends Disposable {
@IModelService private modelService: IModelService,
@IModeService private modeService: IModeService,
@IDialogService private dialogService: IDialogService,
@IKeybindingService private keybindingService: IKeybindingService,
@INotificationService private notificationService: INotificationService,
@IContextMenuService private contextMenuService: IContextMenuService
@IContextMenuService private contextMenuService: IContextMenuService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super();
this._domNode = dom.$('div.review-comment');
this._contextKeyService = contextKeyService.createScoped(this._domNode);
this._commentContextValue = this._contextKeyService.createKey('comment', comment.contextValue);
this._domNode.tabIndex = 0;
const avatar = dom.append(this._domNode, dom.$('div.avatar-container'));
if (comment.userIconPath) {
@@ -139,7 +152,7 @@ export class CommentNode extends Disposable {
}
private createActionsToolbar() {
const actions: Action[] = [];
const actions: IAction[] = [];
let reactionGroup = this.commentService.getReactionGroup(this.owner);
if (reactionGroup && reactionGroup.length) {
@@ -163,6 +176,17 @@ export class CommentNode extends Disposable {
actions.push(this._deleteAction);
}
let commentMenus = this.commentService.getCommentMenus(this.owner);
const menu = commentMenus.getCommentTitleActions(this.comment, this._contextKeyService);
this._toDispose.push(menu);
this._toDispose.push(menu.onDidChange(e => {
const contributedActions = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], <MenuItemAction[]>[]);
this.toolbar.setActions(contributedActions);
}));
const contributedActions = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], <MenuItemAction[]>[]);
actions.push(...contributedActions);
if (actions.length) {
this.toolbar = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {
actionViewItemProvider: action => {
@@ -185,6 +209,12 @@ export class CommentNode extends Disposable {
orientation: ActionsOrientation.HORIZONTAL
});
this.toolbar.context = {
thread: this.commentThread,
commentUniqueId: this.comment.uniqueIdInThread,
$mid: 9
};
this.registerActionBarListeners(this._actionsToolbarContainer);
this.toolbar.setActions(actions, [])();
this._toDispose.push(this.toolbar);
@@ -196,12 +226,15 @@ export class CommentNode extends Disposable {
if (action.id === 'comment.delete' || action.id === 'comment.edit' || action.id === ToggleReactionsAction.ID) {
options = { label: false, icon: true };
} else {
options = { label: true, icon: true };
options = { label: false, icon: true };
}
if (action.id === ReactionAction.ID) {
let item = new ReactionActionViewItem(action);
return item;
} else if (action instanceof MenuItemAction) {
let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
return item;
} else {
let item = new ActionViewItem({}, action, options);
return item;
@@ -391,14 +424,12 @@ export class CommentNode extends Disposable {
uri: this._commentEditor.getModel()!.uri,
value: this.comment.body.value
};
this.commentService.setActiveCommentThread(commentThread);
this._commentEditorDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => {
commentThread.input = {
uri: this._commentEditor!.getModel()!.uri,
value: this.comment.body.value
};
this.commentService.setActiveCommentThread(commentThread);
}));
this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => {
@@ -419,10 +450,15 @@ export class CommentNode extends Disposable {
private removeCommentEditor() {
this.isEditing = false;
this._editAction.enabled = true;
if (this._editAction) {
this._editAction.enabled = true;
}
this._body.classList.remove('hidden');
this._commentEditorModel.dispose();
if (this._commentEditorModel) {
this._commentEditorModel.dispose();
}
this._commentEditorDisposables.forEach(dispose => dispose.dispose());
this._commentEditorDisposables = [];
if (this._commentEditor) {
@@ -450,7 +486,6 @@ export class CommentNode extends Disposable {
uri: this._commentEditor.getModel()!.uri,
value: newBody
};
this.commentService.setActiveCommentThread(commentThread);
let commandId = this.comment.editCommand.id;
let args = this.comment.editCommand.arguments || [];
@@ -488,7 +523,6 @@ export class CommentNode extends Disposable {
if (result.confirmed) {
try {
if (this.comment.deleteCommand) {
this.commentService.setActiveCommentThread(this.commentThread as modes.CommentThread2);
let commandId = this.comment.deleteCommand.id;
let args = this.comment.deleteCommand.arguments || [];
@@ -512,41 +546,81 @@ export class CommentNode extends Disposable {
});
}
public switchToEditMode() {
if (this.isEditing) {
return;
}
this.isEditing = true;
this._body.classList.add('hidden');
this._commentEditContainer = dom.append(this._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 menus = this.commentService.getCommentMenus(this.owner);
const menu = menus.getCommentActions(this.comment, this._contextKeyService);
this._toDispose.push(menu);
this._toDispose.push(menu.onDidChange(() => {
this._commentFormActions.setActions(menu);
}));
this._commentFormActions = new CommentFormActions(formActions, (action: IAction): void => {
let text = this._commentEditor!.getValue();
action.run({
thread: this.commentThread,
commentUniqueId: this.comment.uniqueIdInThread,
text: text,
$mid: 10
});
this.removeCommentEditor();
}, this.themeService);
this._commentFormActions.setActions(menu);
}
private createEditAction(commentDetailsContainer: HTMLElement): Action {
return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => {
this.isEditing = true;
this._body.classList.add('hidden');
this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container'));
this.createCommentEditor();
this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden'));
const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions'));
const cancelEditButton = new Button(formActions);
cancelEditButton.label = nls.localize('label.cancel', "Cancel");
this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService));
this._toDispose.push(cancelEditButton.onDidClick(_ => {
this.removeCommentEditor();
}));
this._updateCommentButton = new Button(formActions);
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService));
this._toDispose.push(this._updateCommentButton.onDidClick(_ => {
this.editComment();
}));
this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => {
this._updateCommentButton.enabled = !!this._commentEditor!.getValue();
}));
this._editAction.enabled = false;
return Promise.resolve();
return this.editCommentAction(commentDetailsContainer);
});
}
private editCommentAction(commentDetailsContainer: HTMLElement) {
this.isEditing = true;
this._body.classList.add('hidden');
this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container'));
this.createCommentEditor();
this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden'));
const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions'));
const cancelEditButton = new Button(formActions);
cancelEditButton.label = nls.localize('label.cancel', "Cancel");
this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService));
this._toDispose.push(cancelEditButton.onDidClick(_ => {
this.removeCommentEditor();
}));
this._updateCommentButton = new Button(formActions);
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService));
this._toDispose.push(this._updateCommentButton.onDidClick(_ => {
this.editComment();
}));
this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => {
this._updateCommentButton.enabled = !!this._commentEditor!.getValue();
}));
this._editAction.enabled = false;
return Promise.resolve();
}
private registerActionBarListeners(actionsContainer: HTMLElement): void {
this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseenter', () => {
actionsContainer.classList.remove('hidden');
@@ -581,6 +655,14 @@ export class CommentNode extends Disposable {
this._body.appendChild(this._md);
}
if (newComment.mode !== undefined && newComment.mode !== this.comment.mode) {
if (newComment.mode === modes.CommentMode.Editing) {
this.switchToEditMode();
} else {
this.removeCommentEditor();
}
}
const shouldUpdateActions = newComment.editCommand !== this.comment.editCommand || newComment.deleteCommand !== this.comment.deleteCommand;
this.comment = newComment;
@@ -610,6 +692,12 @@ export class CommentNode extends Disposable {
if (this.comment.commentReactions && this.comment.commentReactions.length) {
this.createReactionsContainer(this._commentDetailsContainer);
}
if (this.comment.contextValue) {
this._commentContextValue.set(this.comment.contextValue);
} else {
this._commentContextValue.reset();
}
}
focus() {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread2 } from 'vs/editor/common/modes';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
@@ -14,6 +14,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { assign } from 'vs/base/common/objects';
import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { MainThreadCommentController } from 'vs/workbench/api/browser/mainThreadComments';
import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus';
export const ICommentService = createDecorator<ICommentService>('commentService');
@@ -37,9 +38,7 @@ export interface ICommentService {
readonly onDidSetResourceCommentInfos: Event<IResourceCommentThreadEvent>;
readonly onDidSetAllCommentThreads: Event<IWorkspaceCommentThreadsEvent>;
readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent>;
readonly onDidChangeActiveCommentThread: Event<CommentThread | null>;
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>;
readonly onDidChangeInput: Event<string>;
readonly onDidSetDataProvider: Event<void>;
readonly onDidDeleteDataProvider: Event<string>;
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void;
@@ -47,9 +46,13 @@ export interface ICommentService {
removeWorkspaceComments(owner: string): void;
registerCommentController(owner: string, commentControl: MainThreadCommentController): void;
unregisterCommentController(owner: string): void;
getCommentController(owner: string): MainThreadCommentController | undefined;
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void;
getCommentMenus(owner: string): CommentMenus;
registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void;
unregisterDataProvider(owner: string): void;
updateComments(ownerId: string, event: CommentThreadChangedEvent): void;
disposeCommentThread(ownerId: string, threadId: string): void;
createNewCommentThread(owner: string, resource: URI, range: Range, text: string): Promise<CommentThread | null>;
replyToCommentThread(owner: string, resource: URI, range: Range, thread: CommentThread, text: string): Promise<CommentThread | null>;
editComment(owner: string, resource: URI, comment: Comment, text: string): Promise<void>;
@@ -66,9 +69,6 @@ export interface ICommentService {
deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void>;
getReactionGroup(owner: string): CommentReaction[] | undefined;
toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise<void>;
getCommentThreadFromTemplate(owner: string, resource: URI, range: IRange, ): CommentThread2 | undefined;
setActiveCommentThread(commentThread: CommentThread | null): void;
setInput(input: string): void;
}
export class CommentService extends Disposable implements ICommentService {
@@ -89,11 +89,6 @@ export class CommentService extends Disposable implements ICommentService {
private readonly _onDidUpdateCommentThreads: Emitter<ICommentThreadChangedEvent> = this._register(new Emitter<ICommentThreadChangedEvent>());
readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent> = this._onDidUpdateCommentThreads.event;
private readonly _onDidChangeActiveCommentThread = this._register(new Emitter<CommentThread | null>());
readonly onDidChangeActiveCommentThread: Event<CommentThread | null> = this._onDidChangeActiveCommentThread.event;
private readonly _onDidChangeInput: Emitter<string> = this._register(new Emitter<string>());
readonly onDidChangeInput: Event<string> = this._onDidChangeInput.event;
private readonly _onDidChangeActiveCommentingRange: Emitter<{
range: Range, commentingRangesInfo:
CommentingRanges
@@ -106,19 +101,14 @@ export class CommentService extends Disposable implements ICommentService {
private _commentProviders = new Map<string, DocumentCommentProvider>();
private _commentControls = new Map<string, MainThreadCommentController>();
private _commentMenus = new Map<string, CommentMenus>();
constructor() {
constructor(
@IInstantiationService protected instantiationService: IInstantiationService
) {
super();
}
setActiveCommentThread(commentThread: CommentThread | null) {
this._onDidChangeActiveCommentThread.fire(commentThread);
}
setInput(input: string) {
this._onDidChangeInput.fire(input);
}
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void {
this._onDidSetResourceCommentInfos.fire({ resource, commentInfos });
}
@@ -141,6 +131,39 @@ export class CommentService extends Disposable implements ICommentService {
this._onDidDeleteDataProvider.fire(owner);
}
getCommentController(owner: string): MainThreadCommentController | undefined {
return this._commentControls.get(owner);
}
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void {
const commentController = this._commentControls.get(owner);
if (!commentController) {
return;
}
commentController.createCommentThreadTemplate(resource, range);
}
disposeCommentThread(owner: string, threadId: string) {
let controller = this.getCommentController(owner);
if (controller) {
controller.deleteCommentThreadMain(threadId);
}
}
getCommentMenus(owner: string): CommentMenus {
if (this._commentMenus.get(owner)) {
return this._commentMenus.get(owner)!;
}
let controller = this._commentControls.get(owner);
let menu = this.instantiationService.createInstance(CommentMenus, controller!);
this._commentMenus.set(owner, menu);
return menu;
}
registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void {
this._commentProviders.set(owner, commentProvider);
this._onDidSetDataProvider.fire();
@@ -256,16 +279,6 @@ export class CommentService extends Disposable implements ICommentService {
}
}
getCommentThreadFromTemplate(owner: string, resource: URI, range: IRange, ): CommentThread2 | undefined {
const commentController = this._commentControls.get(owner);
if (commentController) {
return commentController.getCommentThreadFromTemplate(resource, range);
}
return undefined;
}
getReactionGroup(owner: string): CommentReaction[] | undefined {
const commentProvider = this._commentControls.get(owner);

View File

@@ -5,9 +5,9 @@
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button } from 'vs/base/browser/ui/button/button';
import { Action } from 'vs/base/common/actions';
import { Action, IAction } from 'vs/base/common/actions';
import * as arrays from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
@@ -38,9 +38,18 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { generateUuid } from 'vs/base/common/uuid';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
import { withNullAsUndefined } from 'vs/base/common/types';
import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus';
import { MenuItemAction, IMenu } from 'vs/platform/actions/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions';
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-chevron-up';
const COMMENT_SCHEME = 'comment';
@@ -70,6 +79,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
private _styleElement: HTMLStyleElement;
private _formActions: HTMLElement | null;
private _error: HTMLElement;
private _contextKeyService: IContextKeyService;
private _threadIsEmpty: IContextKey<boolean>;
private _commentThreadContextValue: IContextKey<string>;
private _commentEditorIsEmpty: IContextKey<boolean>;
private _commentFormActions: CommentFormActions;
public get owner(): string {
return this._owner;
@@ -86,6 +100,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
return this._draftMode;
}
private _commentMenus: CommentMenus;
constructor(
editor: ICodeEditor,
private _owner: string,
@@ -98,15 +114,25 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
@IModelService private modelService: IModelService,
@IThemeService private themeService: IThemeService,
@ICommentService private commentService: ICommentService,
@IOpenerService private openerService: IOpenerService
@IOpenerService private openerService: IOpenerService,
@IKeybindingService private keybindingService: IKeybindingService,
@INotificationService private notificationService: INotificationService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(editor, { keepEditorSelection: true });
this._contextKeyService = contextKeyService.createScoped(this.domNode);
this._threadIsEmpty = CommentContextKeys.commentThreadIsEmpty.bindTo(this._contextKeyService);
this._threadIsEmpty.set(!_commentThread.comments || !_commentThread.comments.length);
this._commentThreadContextValue = contextKeyService.createKey('commentThread', _commentThread.contextValue);
this._resizeObserver = null;
this._isExpanded = _commentThread.collapsibleState ? _commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded : undefined;
this._globalToDispose = [];
this._commentThreadDisposables = [];
this._submitActionsDisposables = [];
this._formActions = null;
this._commentMenus = this.commentService.getCommentMenus(this._owner);
this.create();
this._styleElement = dom.createStyleSheet(this.domNode);
@@ -185,10 +211,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
this._bodyElement = <HTMLDivElement>dom.$('.body');
container.appendChild(this._bodyElement);
dom.addDisposableListener(this._bodyElement, dom.EventType.FOCUS_IN, e => {
this.commentService.setActiveCommentThread(this._commentThread);
});
}
protected _fillHead(container: HTMLElement): void {
@@ -198,12 +220,41 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
this.createThreadLabel();
const actionsContainer = dom.append(this._headElement, dom.$('.review-actions'));
this._actionbarWidget = new ActionBar(actionsContainer, {});
this._actionbarWidget = new ActionBar(actionsContainer, {
actionViewItemProvider: (action: IAction) => {
if (action instanceof MenuItemAction) {
let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
return item;
} else {
let item = new ActionViewItem({}, action, { label: false, icon: true });
return item;
}
}
});
this._disposables.push(this._actionbarWidget);
this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this.collapse());
this._actionbarWidget.push(this._collapseAction, { label: false, icon: true });
if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
const menu = this._commentMenus.getCommentThreadTitleActions(this._commentThread as modes.CommentThread2, this._contextKeyService);
this.setActionBarActions(menu);
this._disposables.push(menu);
this._disposables.push(menu.onDidChange(e => {
this.setActionBarActions(menu);
}));
} else {
this._actionbarWidget.push([this._collapseAction], { label: false, icon: true });
}
this._actionbarWidget.context = this._commentThread;
}
private setActionBarActions(menu: IMenu): void {
const groups = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], <MenuItemAction[]>[]);
this._actionbarWidget.clear();
this._actionbarWidget.push([...groups, this._collapseAction], { label: false, icon: true });
}
public collapse(): Promise<void> {
@@ -214,9 +265,9 @@ 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 || []));
} else if (this._commentEditor.getValue() === '') {
this.commentService.disposeCommentThread(this._owner, this._commentThread.threadId!);
this.dispose();
return Promise.resolve();
}
@@ -245,9 +296,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
}
}
async update(commentThread: modes.CommentThread | modes.CommentThread2, replaceTemplate: boolean = false) {
async update(commentThread: modes.CommentThread | modes.CommentThread2) {
const oldCommentsLen = this._commentElements.length;
const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0;
this._threadIsEmpty.set(!newCommentsLen);
let commentElementsToDel: CommentNode[] = [];
let commentElementsToDelIndex: number[] = [];
@@ -294,26 +346,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
this._commentThread = commentThread;
this._commentElements = newCommentNodeList;
this.createThreadLabel(replaceTemplate);
if (replaceTemplate) {
// since we are replacing the old comment thread, we need to rebind the listeners.
this._commentThreadDisposables.forEach(global => global.dispose());
this._commentThreadDisposables = [];
}
if (replaceTemplate) {
this.createTextModelListener();
}
this.createThreadLabel();
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.
@@ -346,6 +384,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
this.hide();
}
}
if (this._commentThread.contextValue) {
this._commentThreadContextValue.set(this._commentThread.contextValue);
} else {
this._commentThreadContextValue.reset();
}
}
updateDraftMode(draftMode: modes.DraftMode | undefined) {
@@ -368,7 +412,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, fromTemplate: boolean = false) {
display(lineNumber: number) {
this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber);
this._disposables.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
@@ -394,18 +438,30 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0;
this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form'));
this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this);
this._commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService);
this._commentEditorIsEmpty.set(!this._pendingComment);
const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID);
const params = JSON.stringify({
extensionId: this.extensionId,
commentThreadId: this.commentThread.threadId
});
const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md?${params}`);
let resource = URI.parse(`${COMMENT_SCHEME}://${this.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority.
let commentController = this.commentService.getCommentController(this.owner);
if (commentController) {
resource = resource.with({ authority: commentController.id });
}
const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
this._disposables.push(model);
this._commentEditor.setModel(model);
this._disposables.push(this._commentEditor);
this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => this.setCommentEditorDecorations()));
this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => {
this.setCommentEditorDecorations();
this._commentEditorIsEmpty.set(!this._commentEditor.getValue());
}));
if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
this.createTextModelListener();
}
@@ -426,9 +482,7 @@ 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);
if (!fromTemplate) {
this.createCommentWidgetActionsListener(this._formActions, model);
}
this.createCommentWidgetActionsListener(this._formActions, model);
} else {
this.createCommentWidgetActions(this._formActions, model);
}
@@ -462,7 +516,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
uri: this._commentEditor.getModel()!.uri,
value: this._commentEditor.getValue()
};
this.commentService.setActiveCommentThread(this._commentThread);
}));
this._commentThreadDisposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => {
@@ -674,7 +727,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
uri: this._commentEditor.getModel()!.uri,
value: this._commentEditor.getValue()
};
this.commentService.setActiveCommentThread(this._commentThread);
await this.commandService.executeCommand(acceptInputCommand.id, ...(acceptInputCommand.arguments || []));
}));
@@ -699,11 +751,29 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
uri: this._commentEditor.getModel()!.uri,
value: this._commentEditor.getValue()
};
this.commentService.setActiveCommentThread(this._commentThread);
await this.commandService.executeCommand(command.id, ...(command.arguments || []));
}));
});
}
const menu = this._commentMenus.getCommentThreadActions(commentThread, this._contextKeyService);
this._disposables.push(menu);
this._disposables.push(menu.onDidChange(() => {
this._commentFormActions.setActions(menu);
}));
this._commentFormActions = new CommentFormActions(container, (action: IAction) => {
action.run({
thread: this._commentThread,
text: this._commentEditor.getValue(),
$mid: 8
});
this.hideReplyArea();
}, this.themeService);
this._commentFormActions.setActions(menu);
}
private createNewCommentNode(comment: modes.Comment): CommentNode {
@@ -751,7 +821,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
uri: this._commentEditor.getModel()!.uri,
value: this._commentEditor.getValue()
};
this.commentService.setActiveCommentThread(this._commentThread);
let commandId = commentThread.acceptInputCommand.id;
let args = commentThread.acceptInputCommand.arguments || [];
@@ -827,14 +896,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
}
}
private createThreadLabel(replaceTemplate: boolean = false) {
private createThreadLabel() {
let label: string | undefined;
if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
label = (this._commentThread as modes.CommentThread2).label;
}
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 (label === undefined) {
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);
@@ -847,7 +915,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
this._headingLabel.innerHTML = strings.escape(label);
this._headingLabel.setAttribute('aria-label', label);
}
}
private expandReplyArea() {
@@ -857,6 +924,17 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
}
}
private hideReplyArea() {
this._commentEditor.setValue('');
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');
}
private createReplyButton() {
this._reviewThreadReplyButton = <HTMLButtonElement>dom.append(this._commentForm, dom.$('button.review-thread-reply-button'));
if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {

View File

@@ -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 _template: modes.CommentThreadTemplate | undefined, 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 commentingRangesInfo?: modes.CommentingRanges) {
const startLineNumber = _range.startLineNumber;
const endLineNumber = _range.endLineNumber;
let commentingRangeDecorations = [{
@@ -81,14 +81,13 @@ class CommentingRangeDecoration {
}
}
public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined } {
public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined } {
return {
extensionId: this._extensionId,
label: this._label,
replyCommand: this._reply,
ownerId: this._ownerId,
commentingRangesInfo: this.commentingRangesInfo,
template: this._template
commentingRangesInfo: this.commentingRangesInfo
};
}
@@ -125,11 +124,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, info.template));
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, info.reply, this.decorationOptions));
});
} else {
(info.commentingRanges ? info.commentingRanges.ranges : []).forEach(range => {
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, undefined, this.decorationOptions, info.template, info.commentingRanges as modes.CommentingRanges));
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, undefined, this.decorationOptions, info.commentingRanges as modes.CommentingRanges));
});
}
}
@@ -424,7 +423,7 @@ export class ReviewController implements IEditorContribution {
}
removed.forEach(thread => {
let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId);
let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== '');
if (matchedZones.length) {
let matchedZone = matchedZones[0];
let index = this._commentWidgets.indexOf(matchedZone);
@@ -449,7 +448,7 @@ export class ReviewController implements IEditorContribution {
let matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range));
if (matchedNewCommentThreadZones.length) {
matchedNewCommentThreadZones[0].update(thread, true);
matchedNewCommentThreadZones[0].update(thread);
return;
}
@@ -469,22 +468,6 @@ export class ReviewController implements IEditorContribution {
this._commentWidgets.push(zoneWidget);
}
private addCommentThreadFromTemplate(lineNumber: number, ownerId: string): ReviewZoneWidget {
let templateCommentThread = this.commentService.getCommentThreadFromTemplate(ownerId, this.editor.getModel()!.uri, {
startLineNumber: lineNumber,
startColumn: 1,
endLineNumber: lineNumber,
endColumn: 1
})!;
templateCommentThread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded;
templateCommentThread.comments = [];
let templateReviewZoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, ownerId, templateCommentThread, '', 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.`);
@@ -640,16 +623,16 @@ export class ReviewController implements IEditorContribution {
const commentInfos = newCommentInfos.filter(info => info.ownerId === pick.id);
if (commentInfos.length) {
const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = commentInfos[0];
this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template);
const { replyCommand, ownerId, extensionId, commentingRangesInfo } = commentInfos[0];
this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo);
}
}).then(() => {
this._addInProgress = false;
});
}
} else {
const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = newCommentInfos[0]!;
this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template);
const { replyCommand, ownerId, extensionId, commentingRangesInfo } = newCommentInfos[0]!;
this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo);
}
return Promise.resolve();
@@ -668,11 +651,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, template: modes.CommentThreadTemplate | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] {
private getContextMenuActions(commentInfos: { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] {
const actions: (IAction | ContextSubMenu)[] = [];
commentInfos.forEach(commentInfo => {
const { replyCommand, ownerId, extensionId, label, commentingRangesInfo, template } = commentInfo;
const { replyCommand, ownerId, extensionId, label, commentingRangesInfo } = commentInfo;
actions.push(new Action(
'addCommentThread',
@@ -680,7 +663,7 @@ export class ReviewController implements IEditorContribution {
undefined,
true,
() => {
this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template);
this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo);
return Promise.resolve();
}
));
@@ -688,23 +671,10 @@ export class ReviewController implements IEditorContribution {
return actions;
}
public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined) {
public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined) {
if (commentingRangesInfo) {
let range = new Range(lineNumber, 1, lineNumber, 1);
if (template) {
// create comment widget through template
let commentThreadWidget = this.addCommentThreadFromTemplate(lineNumber, ownerId);
commentThreadWidget.display(lineNumber, true);
this._commentWidgets.push(commentThreadWidget);
commentThreadWidget.onDidClose(() => {
this._commentWidgets = this._commentWidgets.filter(zoneWidget => !(
zoneWidget.owner === commentThreadWidget.owner &&
(zoneWidget.commentThread as any).commentThreadHandle === -1 &&
Range.equalsRange(zoneWidget.commentThread.range, commentThreadWidget.commentThread.range)
));
});
this.processNextThreadToAdd();
} else if (commentingRangesInfo.newCommentThreadCallback) {
if (commentingRangesInfo.newCommentThreadCallback) {
return commentingRangesInfo.newCommentThreadCallback(this.editor.getModel()!.uri, range)
.then(_ => {
this.processNextThreadToAdd();
@@ -713,6 +683,11 @@ export class ReviewController implements IEditorContribution {
this.notificationService.error(nls.localize('commentThreadAddFailure', "Adding a new comment thread failed: {0}.", e.message));
this.processNextThreadToAdd();
});
} else {
// latest api, no comments creation callback
this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range);
this.processNextThreadToAdd();
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
}
} else {
const commentInfo = this._commentInfos.filter(info => info.owner === ownerId);

View File

@@ -19,11 +19,12 @@ import { ReviewController } from 'vs/workbench/contrib/comments/browser/comments
import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService';
import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ResourceLabels } from 'vs/workbench/browser/labels';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
export const COMMENTS_PANEL_ID = 'workbench.panel.comments';
export const COMMENTS_PANEL_TITLE = 'Comments';
@@ -266,3 +267,14 @@ export class CommentsPanel extends Panel {
}
}
}
CommandsRegistry.registerCommand({
id: 'workbench.action.focusCommentsPanel',
handler: (accessor) => {
const panelService = accessor.get(IPanelService);
const panels = panelService.getPanels();
if (panels.some(panelIdentifier => panelIdentifier.id === COMMENTS_PANEL_ID)) {
panelService.openPanel(COMMENTS_PANEL_ID, true);
}
}
});

View File

@@ -198,6 +198,16 @@
background-image: url(./reaction-hc.svg);
}
.monaco-editor .review-widget .body .review-comment .comment-title .action-label {
display: block;
height: 18px;
line-height: 18px;
min-width: 28px;
background-size: 16px;
background-position: center center;
background-repeat: no-repeat;
}
.monaco-editor .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions {
background-image: url(./reaction.svg);
width: 18px;

View File

@@ -22,6 +22,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
export const ctxCommentEditorFocused = new RawContextKey<boolean>('commentEditorFocused', false);
@@ -30,6 +31,7 @@ export class SimpleCommentEditor extends CodeEditorWidget {
private _parentEditor: ICodeEditor;
private _parentThread: ICommentThreadWidget;
private _commentEditorFocused: IContextKey<boolean>;
private _commentEditorEmpty: IContextKey<boolean>;
constructor(
domElement: HTMLElement,
@@ -56,11 +58,15 @@ export class SimpleCommentEditor extends CodeEditorWidget {
super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService);
this._commentEditorFocused = ctxCommentEditorFocused.bindTo(this._contextKeyService);
this._commentEditorFocused = ctxCommentEditorFocused.bindTo(contextKeyService);
this._commentEditorEmpty = CommentContextKeys.commentIsEmpty.bindTo(contextKeyService);
this._commentEditorEmpty.set(!this.getValue());
this._parentEditor = parentEditor;
this._parentThread = parentThread;
this._register(this.onDidFocusEditorWidget(_ => this._commentEditorFocused.set(true)));
this._register(this.onDidChangeModelContent(e => this._commentEditorEmpty.set(!this.getValue())));
this._register(this.onDidBlurEditorWidget(_ => this._commentEditorFocused.reset()));
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export namespace CommentContextKeys {
/**
* A context key that is set when the comment thread has no comments.
*/
export const commentThreadIsEmpty = new RawContextKey<boolean>('commentThreadIsEmpty', false);
/**
* A context key that is set when the comment has no input.
*/
export const commentIsEmpty = new RawContextKey<boolean>('commentIsEmpty', false);
}

View File

@@ -161,7 +161,7 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer<IExpr
});
const styler = attachInputBoxStyler(inputBox, this.themeService);
inputBox.value = options.initialValue;
inputBox.value = replaceWhitespace(options.initialValue);
inputBox.focus();
inputBox.select();

View File

@@ -47,24 +47,19 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor {
);
}
private openInternal(input: EditorInput, options: EditorOptions): Promise<void> {
private async openInternal(input: EditorInput, options: EditorOptions): Promise<void> {
if (input instanceof FileEditorInput) {
input.setForceOpenAsText();
return this.editorService.openEditor(input, options, this.group).then(() => undefined);
await this.editorService.openEditor(input, options, this.group);
}
return Promise.resolve();
}
private openExternal(resource: URI): void {
this.windowsService.openExternal(resource.toString()).then(didOpen => {
if (!didOpen) {
return this.windowsService.showItemInFolder(resource);
}
return undefined;
});
private async openExternal(resource: URI): Promise<void> {
const didOpen = await this.windowsService.openExternal(resource.toString());
if (!didOpen) {
return this.windowsService.showItemInFolder(resource);
}
}
getTitle(): string | null {

View File

@@ -135,7 +135,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
const nonDirtyFileEditors = this.getOpenedFileEditors(false /* non-dirty only */);
nonDirtyFileEditors.forEach(editor => {
nonDirtyFileEditors.forEach(async editor => {
const resource = editor.getResource();
// Handle deletes in opened editors depending on:
@@ -170,20 +170,17 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
// file is really gone and not just a faulty file event.
// This only applies to external file events, so we need to check for the isExternal
// flag.
let checkExists: Promise<boolean>;
let exists = false;
if (isExternal) {
checkExists = timeout(100).then(() => this.fileService.exists(resource));
} else {
checkExists = Promise.resolve(false);
await timeout(100);
exists = await this.fileService.exists(resource);
}
checkExists.then(exists => {
if (!exists && !editor.isDisposed()) {
editor.dispose();
} else if (this.environmentService.verbose) {
console.warn(`File exists even though we received a delete event: ${resource.toString()}`);
}
});
if (!exists && !editor.isDisposed()) {
editor.dispose();
} else if (this.environmentService.verbose) {
console.warn(`File exists even though we received a delete event: ${resource.toString()}`);
}
}
});
}

View File

@@ -125,101 +125,103 @@ export class TextFileEditor extends BaseTextEditor {
}
}
setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
async setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
// Update/clear view settings if input changes
this.doSaveOrClearTextEditorViewState(this.input);
// Set input and resolve
return super.setInput(input, options, token).then(() => {
return input.resolve().then(resolvedModel => {
await super.setInput(input, options, token);
try {
const resolvedModel = await input.resolve();
// Check for cancellation
if (token.isCancellationRequested) {
return undefined;
}
// Check for cancellation
if (token.isCancellationRequested) {
return;
}
// There is a special case where the text editor has to handle binary file editor input: if a binary file
// has been resolved and cached before, it maybe an actual instance of BinaryEditorModel. In this case our text
// editor has to open this model using the binary editor. We return early in this case.
if (resolvedModel instanceof BinaryEditorModel) {
return this.openAsBinary(input, options);
}
// There is a special case where the text editor has to handle binary file editor input: if a binary file
// has been resolved and cached before, it maybe an actual instance of BinaryEditorModel. In this case our text
// editor has to open this model using the binary editor. We return early in this case.
if (resolvedModel instanceof BinaryEditorModel) {
return this.openAsBinary(input, options);
}
const textFileModel = <ITextFileEditorModel>resolvedModel;
const textFileModel = <ITextFileEditorModel>resolvedModel;
// Editor
const textEditor = this.getControl();
textEditor.setModel(textFileModel.textEditorModel);
// Editor
const textEditor = this.getControl();
textEditor.setModel(textFileModel.textEditorModel);
// Always restore View State if any associated
const editorViewState = this.loadTextEditorViewState(this.input.getResource());
if (editorViewState) {
textEditor.restoreViewState(editorViewState);
}
// Always restore View State if any associated
const editorViewState = this.loadTextEditorViewState(this.input.getResource());
if (editorViewState) {
textEditor.restoreViewState(editorViewState);
}
// TextOptions (avoiding instanceof here for a reason, do not change!)
if (options && types.isFunction((<TextEditorOptions>options).apply)) {
(<TextEditorOptions>options).apply(textEditor, ScrollType.Immediate);
}
// TextOptions (avoiding instanceof here for a reason, do not change!)
if (options && types.isFunction((<TextEditorOptions>options).apply)) {
(<TextEditorOptions>options).apply(textEditor, ScrollType.Immediate);
}
// Readonly flag
textEditor.updateOptions({ readOnly: textFileModel.isReadonly() });
}, error => {
// Readonly flag
textEditor.updateOptions({ readOnly: textFileModel.isReadonly() });
} catch (error) {
// In case we tried to open a file inside the text editor and the response
// indicates that this is not a text file, reopen the file through the binary
// editor.
if ((<TextFileOperationError>error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) {
return this.openAsBinary(input, options);
}
// In case we tried to open a file inside the text editor and the response
// indicates that this is not a text file, reopen the file through the binary
// editor.
if ((<TextFileOperationError>error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) {
return this.openAsBinary(input, options);
}
// Similar, handle case where we were asked to open a folder in the text editor.
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) {
this.openAsFolder(input);
// Similar, handle case where we were asked to open a folder in the text editor.
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) {
this.openAsFolder(input);
return Promise.reject(new Error(nls.localize('openFolderError', "File is a directory")));
}
throw new Error(nls.localize('openFolderError', "File is a directory"));
}
// Offer to create a file from the error if we have a file not found and the name is valid
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.getResource()))) {
return Promise.reject(createErrorWithActions(toErrorMessage(error), {
actions: [
new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, () => {
return this.textFileService.create(input.getResource()).then(() => this.editorService.openEditor({
resource: input.getResource(),
options: {
pinned: true // new file gets pinned by default
}
}));
})
]
}));
}
// Offer to create a file from the error if we have a file not found and the name is valid
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.getResource()))) {
throw createErrorWithActions(toErrorMessage(error), {
actions: [
new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, async () => {
await this.textFileService.create(input.getResource());
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_EXCEED_MEMORY_LIMIT) {
const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.configurationService.getValue<number>(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB);
return this.editorService.openEditor({
resource: input.getResource(),
options: {
pinned: true // new file gets pinned by default
}
});
})
]
});
}
return Promise.reject(createErrorWithActions(toErrorMessage(error), {
actions: [
new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => {
return this.windowsService.relaunch({
addArgs: [
`--max-memory=${memoryLimit}`
]
});
}),
new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => {
return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' });
})
]
}));
}
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_EXCEED_MEMORY_LIMIT) {
const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.configurationService.getValue<number>(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB);
// Otherwise make sure the error bubbles up
return Promise.reject(error);
});
});
throw createErrorWithActions(toErrorMessage(error), {
actions: [
new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => {
return this.windowsService.relaunch({
addArgs: [
`--max-memory=${memoryLimit}`
]
});
}),
new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => {
return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' });
})
]
});
}
// Otherwise make sure the error bubbles up
throw error;
}
}
private openAsBinary(input: FileEditorInput, options: EditorOptions): void {
@@ -227,21 +229,20 @@ export class TextFileEditor extends BaseTextEditor {
this.editorService.openEditor(input, options, this.group);
}
private openAsFolder(input: FileEditorInput): void {
private async openAsFolder(input: FileEditorInput): Promise<void> {
if (!this.group) {
return;
}
// Since we cannot open a folder, we have to restore the previous input if any and close the editor
this.group.closeEditor(this.input).then(() => {
await this.group.closeEditor(this.input);
// Best we can do is to reveal the folder in the explorer
if (this.contextService.isInsideWorkspace(input.getResource())) {
this.viewletService.openViewlet(VIEWLET_ID).then(() => {
this.explorerService.select(input.getResource(), true);
});
}
});
// Best we can do is to reveal the folder in the explorer
if (this.contextService.isInsideWorkspace(input.getResource())) {
await this.viewletService.openViewlet(VIEWLET_ID);
this.explorerService.select(input.getResource(), true);
}
}
protected getAriaLabel(): string {

View File

@@ -240,26 +240,26 @@ class ResolveSaveConflictAction extends Action {
super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare"));
}
run(): Promise<any> {
async run(): Promise<any> {
if (!this.model.isDisposed()) {
const resource = this.model.getResource();
const name = basename(resource);
const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong);
return TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }).then(() => {
if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
return; // return if this message is ignored
}
await TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true });
// Show additional help how to resolve the save conflict
const actions: INotificationActions = { primary: [], secondary: [] };
actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction));
actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction));
if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
return; // return if this message is ignored
}
const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions });
Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!));
pendingResolveSaveConflictMessages.push(handle);
});
// Show additional help how to resolve the save conflict
const actions: INotificationActions = { primary: [], secondary: [] };
actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction));
actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction));
const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions });
Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!));
pendingResolveSaveConflictMessages.push(handle);
}
return Promise.resolve(true);
@@ -316,31 +316,28 @@ export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource:
const editor = control.input;
const group = control.group;
resolverService.createModelReference(resource).then(reference => {
resolverService.createModelReference(resource).then(async reference => {
const model = reference.object as IResolvedTextFileEditorModel;
const localModelSnapshot = model.createSnapshot();
clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions
// Revert to be able to save
return model.revert().then(() => {
await model.revert();
// Restore user value (without loosing undo stack)
modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot));
// Restore user value (without loosing undo stack)
modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot));
// Trigger save
return model.save().then(() => {
// Trigger save
await model.save();
// Reopen file input
return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
// Reopen file input
await editorService.openEditor({ resource: model.getResource() }, group);
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
});
});
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
};
@@ -355,22 +352,20 @@ export const revertLocalChangesCommand = (accessor: ServicesAccessor, resource:
const editor = control.input;
const group = control.group;
resolverService.createModelReference(resource).then(reference => {
resolverService.createModelReference(resource).then(async reference => {
const model = reference.object as ITextFileEditorModel;
clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions
// Revert on model
return model.revert().then(() => {
await model.revert();
// Reopen file input
return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
// Reopen file input
await editorService.openEditor({ resource: model.getResource() }, group);
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
});
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
};

View File

@@ -273,16 +273,17 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
return this.doResolveAsText();
}
private doResolveAsText(): Promise<TextFileEditorModel | BinaryEditorModel> {
private async doResolveAsText(): Promise<TextFileEditorModel | BinaryEditorModel> {
// Resolve as text
return this.textFileService.models.loadOrCreate(this.resource, {
mode: this.preferredMode,
encoding: this.preferredEncoding,
reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model
allowBinary: this.forceOpenAsText,
reason: LoadReason.EDITOR
}).then(model => {
try {
await this.textFileService.models.loadOrCreate(this.resource, {
mode: this.preferredMode,
encoding: this.preferredEncoding,
reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model
allowBinary: this.forceOpenAsText,
reason: LoadReason.EDITOR
});
// This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary
// or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into
@@ -292,8 +293,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
this.textModelReference = this.textModelResolverService.createModelReference(this.resource);
}
return this.textModelReference.then(ref => ref.object as TextFileEditorModel);
}, error => {
const ref = await this.textModelReference;
return ref.object as TextFileEditorModel;
} catch (error) {
// In case of an error that indicates that the file is binary or too large, just return with the binary editor model
if (
@@ -304,12 +307,12 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
}
// Bubble any other error up
return Promise.reject(error);
});
throw error;
}
}
private doResolveAsBinary(): Promise<BinaryEditorModel> {
return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel);
private async doResolveAsBinary(): Promise<BinaryEditorModel> {
return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load();
}
isResolved(): boolean {

View File

@@ -143,15 +143,13 @@ export class TextFileContentProvider implements ITextModelContentProvider {
@IModelService private readonly modelService: IModelService
) { }
static open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise<void> {
return editorService.openEditor(
{
leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource),
rightResource: resource,
label,
options
}
).then();
static async open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise<void> {
await editorService.openEditor({
leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource),
rightResource: resource,
label,
options
});
}
private static resourceToTextFile(scheme: string, resource: URI): URI {
@@ -162,56 +160,55 @@ export class TextFileContentProvider implements ITextModelContentProvider {
return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null });
}
provideTextContent(resource: URI): Promise<ITextModel> {
async provideTextContent(resource: URI): Promise<ITextModel> {
const savedFileResource = TextFileContentProvider.textFileToResource(resource);
// Make sure our text file is resolved up to date
return this.resolveEditorModel(resource).then(codeEditorModel => {
const codeEditorModel = await this.resolveEditorModel(resource);
// Make sure to keep contents up to date when it changes
if (!this.fileWatcherDisposable) {
this.fileWatcherDisposable = this.fileService.onFileChanges(changes => {
if (changes.contains(savedFileResource, FileChangeType.UPDATED)) {
this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes
}
});
if (codeEditorModel) {
once(codeEditorModel.onWillDispose)(() => {
dispose(this.fileWatcherDisposable);
this.fileWatcherDisposable = undefined;
});
// Make sure to keep contents up to date when it changes
if (!this.fileWatcherDisposable) {
this.fileWatcherDisposable = this.fileService.onFileChanges(changes => {
if (changes.contains(savedFileResource, FileChangeType.UPDATED)) {
this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes
}
}
});
return codeEditorModel;
});
if (codeEditorModel) {
once(codeEditorModel.onWillDispose)(() => {
dispose(this.fileWatcherDisposable);
this.fileWatcherDisposable = undefined;
});
}
}
return codeEditorModel;
}
private resolveEditorModel(resource: URI, createAsNeeded?: true): Promise<ITextModel>;
private resolveEditorModel(resource: URI, createAsNeeded?: boolean): Promise<ITextModel | null>;
private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise<ITextModel | null> {
private async resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise<ITextModel | null> {
const savedFileResource = TextFileContentProvider.textFileToResource(resource);
return this.textFileService.readStream(savedFileResource).then(content => {
let codeEditorModel = this.modelService.getModel(resource);
if (codeEditorModel) {
this.modelService.updateModel(codeEditorModel, content.value);
} else if (createAsNeeded) {
const textFileModel = this.modelService.getModel(savedFileResource);
const content = await this.textFileService.readStream(savedFileResource);
let languageSelector: ILanguageSelection;
if (textFileModel) {
languageSelector = this.modeService.create(textFileModel.getModeId());
} else {
languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.path);
}
let codeEditorModel = this.modelService.getModel(resource);
if (codeEditorModel) {
this.modelService.updateModel(codeEditorModel, content.value);
} else if (createAsNeeded) {
const textFileModel = this.modelService.getModel(savedFileResource);
codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource);
let languageSelector: ILanguageSelection;
if (textFileModel) {
languageSelector = this.modeService.create(textFileModel.getModeId());
} else {
languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.path);
}
return codeEditorModel;
});
codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource);
}
return codeEditorModel;
}
dispose(): void {

View File

@@ -34,26 +34,25 @@ suite('Files - FileEditorTracker', () => {
accessor = instantiationService.createInstance(ServiceAccessor);
});
test('file change event updates model', function () {
test('file change event updates model', async function () {
const tracker = instantiationService.createInstance(FileEditorTracker);
const resource = toResource.call(this, '/path/index.txt');
return accessor.textFileService.models.loadOrCreate(resource).then((model: IResolvedTextFileEditorModel) => {
model.textEditorModel.setValue('Super Good');
assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good');
const model = await accessor.textFileService.models.loadOrCreate(resource) as IResolvedTextFileEditorModel;
return model.save().then(() => {
model.textEditorModel.setValue('Super Good');
assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good');
// change event (watcher)
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }]));
await model.save();
return timeout(0).then(() => { // due to event updating model async
assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html');
// change event (watcher)
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }]));
tracker.dispose();
});
});
});
await timeout(0); // due to event updating model async
assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html');
tracker.dispose();
});
});

View File

@@ -390,7 +390,7 @@ export class GotoSymbolHandler extends QuickOpenHandler {
this.rangeHighlightDecorationId = undefined;
}
getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel | null> {
async getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel | null> {
searchValue = searchValue.trim();
// Support to cancel pending outline requests
@@ -407,20 +407,19 @@ export class GotoSymbolHandler extends QuickOpenHandler {
}
// Resolve Outline Model
return this.getOutline().then(outline => {
if (!outline) {
return outline;
}
if (token.isCancellationRequested) {
return outline;
}
// Filter by search
outline.applyFilter(searchValue);
const outline = await this.getOutline();
if (!outline) {
return outline;
});
}
if (token.isCancellationRequested) {
return outline;
}
// Filter by search
outline.applyFilter(searchValue);
return outline;
}
getEmptyLabel(searchString: string): string {

View File

@@ -147,43 +147,45 @@ export class OpenFileHandler extends QuickOpenHandler {
return this.doFindResults(query, token, this.cacheState.cacheKey, maxSortedResults);
}
private doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): Promise<FileQuickOpenModel> {
private async doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): Promise<FileQuickOpenModel> {
const queryOptions = this.doResolveQueryOptions(query, cacheKey, maxSortedResults);
let iconClass: string;
let iconClass: string | undefined = undefined;
if (this.options && this.options.forceUseIcons && !this.themeService.getFileIconTheme()) {
iconClass = 'file'; // only use a generic file icon if we are forced to use an icon and have no icon theme set otherwise
}
return this.getAbsolutePathResult(query).then(result => {
if (token.isCancellationRequested) {
return Promise.resolve(<ISearchComplete>{ results: [] });
let complete: ISearchComplete | undefined = undefined;
const result = await this.getAbsolutePathResult(query);
if (token.isCancellationRequested) {
complete = <ISearchComplete>{ results: [] };
}
// If the original search value is an existing file on disk, return it immediately and bypass the search service
else if (result) {
complete = <ISearchComplete>{ results: [{ resource: result }] };
}
else {
complete = await this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token);
}
const results: QuickOpenEntry[] = [];
if (!token.isCancellationRequested) {
for (const fileMatch of complete.results) {
const label = basename(fileMatch.resource);
const description = this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true });
results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass));
}
}
// If the original search value is an existing file on disk, return it immediately and bypass the search service
if (result) {
return Promise.resolve(<ISearchComplete>{ results: [{ resource: result }] });
}
return this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token);
}).then(complete => {
const results: QuickOpenEntry[] = [];
if (!token.isCancellationRequested) {
for (const fileMatch of complete.results) {
const label = basename(fileMatch.resource);
const description = this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true });
results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass));
}
}
return new FileQuickOpenModel(results, <IFileSearchStats>complete.stats);
});
return new FileQuickOpenModel(results, <IFileSearchStats>complete.stats);
}
private getAbsolutePathResult(query: IPreparedQuery): Promise<URI | undefined> {
private async getAbsolutePathResult(query: IPreparedQuery): Promise<URI | undefined> {
const detildifiedQuery = untildify(query.original, this.environmentService.userHome);
if (isAbsolute(detildifiedQuery)) {
const workspaceFolders = this.contextService.getWorkspace().folders;
@@ -191,12 +193,16 @@ export class OpenFileHandler extends QuickOpenHandler {
workspaceFolders[0].uri.with({ path: detildifiedQuery }) :
URI.file(detildifiedQuery);
return this.fileService.resolve(resource).then(
stat => stat.isDirectory ? undefined : resource,
error => undefined);
try {
const stat = await this.fileService.resolve(resource);
return stat.isDirectory ? undefined : resource;
} catch (error) {
// ignore
}
}
return Promise.resolve(undefined);
return undefined;
}
private doResolveQueryOptions(query: IPreparedQuery, cacheKey?: string, maxSortedResults?: number): IFileQueryBuilderOptions {

View File

@@ -53,6 +53,7 @@ const ModulesToLookFor = [
// JS frameworks
'react',
'react-native',
'rnpm-plugin-windows',
'@angular/core',
'@ionic',
'vue',
@@ -268,6 +269,7 @@ export class WorkspaceStats implements IWorkbenchContribution {
"workspace.npm.hapi" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.npm.socket.io" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.npm.restify" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.npm.rnpm-plugin-windows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.npm.react" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.npm.@angular/core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.npm.vue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
@@ -288,6 +290,7 @@ export class WorkspaceStats implements IWorkbenchContribution {
"workspace.reactNative" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.ionic" : { "classification" : "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": "true" },
"workspace.nativeScript" : { "classification" : "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": "true" },
"workspace.java.pom" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.py.requirements" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.py.requirements.star" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"workspace.py.Pipfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
@@ -387,6 +390,8 @@ export class WorkspaceStats implements IWorkbenchContribution {
tags['workspace.npm'] = nameSet.has('package.json') || nameSet.has('node_modules');
tags['workspace.bower'] = nameSet.has('bower.json') || nameSet.has('bower_components');
tags['workspace.java.pom'] = nameSet.has('pom.xml');
tags['workspace.yeoman.code.ext'] = nameSet.has('vsc-extension-quickstart.md');
tags['workspace.py.requirements'] = nameSet.has('requirements.txt');
@@ -502,23 +507,24 @@ export class WorkspaceStats implements IWorkbenchContribution {
const packageJsonPromises = getFilePromises('package.json', this.fileService, this.textFileService, content => {
try {
const packageJsonContents = JSON.parse(content.value);
if (packageJsonContents['dependencies']) {
for (let module of ModulesToLookFor) {
if ('react-native' === module) {
if (packageJsonContents['dependencies'][module]) {
tags['workspace.reactNative'] = true;
}
} else if ('tns-core-modules' === module) {
if (packageJsonContents['dependencies'][module]) {
tags['workspace.nativescript'] = true;
}
} else {
if (packageJsonContents['dependencies'][module]) {
tags['workspace.npm.' + module] = true;
}
let dependencies = packageJsonContents['dependencies'];
let devDependencies = packageJsonContents['devDependencies'];
for (let module of ModulesToLookFor) {
if ('react-native' === module) {
if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) {
tags['workspace.reactNative'] = true;
}
} else if ('tns-core-modules' === module) {
if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) {
tags['workspace.nativescript'] = true;
}
} else {
if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) {
tags['workspace.npm.' + module] = true;
}
}
}
}
catch (e) {
// Ignore errors when resolving file or parsing file contents

View File

@@ -165,8 +165,7 @@ class WebviewPortMappingProvider extends Disposable {
session.onBeforeRequest(async (details) => {
const uri = URI.parse(details.url);
const allowedSchemes = ['http', 'https', 'ws', 'wss'];
if (allowedSchemes.indexOf(uri.scheme) === -1) {
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return undefined;
}