mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-30 16:50:30 -04:00
Merge from vscode 5b9869eb02fa4c96205a74d05cad9164dfd06d60 (#5607)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/vs/workbench/contrib/comments/browser/commentMenus.ts
Normal file
58
src/vs/workbench/contrib/comments/browser/commentMenus.ts
Normal 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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user