mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-30 16:50:30 -04:00
Merge from vscode 2b0b9136329c181a9e381463a1f7dc3a2d105a34 (#4880)
This commit is contained in:
@@ -49,7 +49,7 @@ export class BackupRestorer implements IWorkbenchContribution {
|
||||
}
|
||||
|
||||
private doResolveOpenedBackups(backups: URI[]): Promise<URI[]> {
|
||||
const restorePromises: Promise<any>[] = [];
|
||||
const restorePromises: Promise<unknown>[] = [];
|
||||
const unresolved: URI[] = [];
|
||||
|
||||
backups.forEach(backup => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as path from 'vs/base/common/path';
|
||||
import * as cp from 'child_process';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { nfcall } from 'vs/base/common/async';
|
||||
import { promisify } from 'util';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
@@ -105,7 +105,7 @@ class InstallAction extends Action {
|
||||
case 0 /* OK */:
|
||||
const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + this.target + '\'\\" with administrator privileges"';
|
||||
|
||||
nfcall(cp.exec, command, {})
|
||||
promisify(cp.exec)(command, {})
|
||||
.then(undefined, _ => Promise.reject(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'."))))
|
||||
.then(resolve, reject);
|
||||
break;
|
||||
@@ -172,7 +172,7 @@ class UninstallAction extends Action {
|
||||
case 0 /* OK */:
|
||||
const command = 'osascript -e "do shell script \\"rm \'' + this.target + '\'\\" with administrator privileges"';
|
||||
|
||||
nfcall(cp.exec, command, {})
|
||||
promisify(cp.exec)(command, {})
|
||||
.then(undefined, _ => Promise.reject(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", this.target))))
|
||||
.then(resolve, reject);
|
||||
break;
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface ICommentService {
|
||||
readonly onDidSetDataProvider: Event<void>;
|
||||
readonly onDidDeleteDataProvider: Event<string>;
|
||||
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void;
|
||||
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void;
|
||||
setWorkspaceComments(owner: string, commentsByResource: CommentThread[] | CommentThread2[]): void;
|
||||
removeWorkspaceComments(owner: string): void;
|
||||
registerCommentController(owner: string, commentControl: MainThreadCommentController): void;
|
||||
unregisterCommentController(owner: string): void;
|
||||
|
||||
@@ -60,7 +60,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
private _resizeObserver: any;
|
||||
private _onDidClose = new Emitter<ReviewZoneWidget | undefined>();
|
||||
private _onDidCreateThread = new Emitter<ReviewZoneWidget>();
|
||||
private _isCollapsed: boolean;
|
||||
private _isExpanded?: boolean;
|
||||
private _collapseAction: Action;
|
||||
private _commentGlyph?: CommentGlyphWidget;
|
||||
private _submitActionsDisposables: IDisposable[];
|
||||
@@ -74,7 +74,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
return this._owner;
|
||||
}
|
||||
public get commentThread(): modes.CommentThread {
|
||||
return this._commentThread;
|
||||
return this._commentThread as modes.CommentThread;
|
||||
}
|
||||
|
||||
public get extensionId(): string | undefined {
|
||||
@@ -101,7 +101,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
) {
|
||||
super(editor, { keepEditorSelection: true });
|
||||
this._resizeObserver = null;
|
||||
this._isCollapsed = _commentThread.collapsibleState !== modes.CommentThreadCollapsibleState.Expanded;
|
||||
this._isExpanded = _commentThread.collapsibleState ? _commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded : undefined;
|
||||
this._globalToDispose = [];
|
||||
this._submitActionsDisposables = [];
|
||||
this._formActions = null;
|
||||
@@ -144,7 +144,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
|
||||
public reveal(commentId?: string) {
|
||||
if (this._isCollapsed) {
|
||||
if (!this._isExpanded) {
|
||||
this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
|
||||
public collapse(): Promise<void> {
|
||||
if (this._commentThread.comments.length === 0) {
|
||||
if (this._commentThread.comments && this._commentThread.comments.length === 0) {
|
||||
if ((this._commentThread as modes.CommentThread2).commentThreadHandle === undefined) {
|
||||
this.dispose();
|
||||
return Promise.resolve();
|
||||
@@ -217,7 +217,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
}
|
||||
|
||||
this._isCollapsed = true;
|
||||
this.hide();
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -230,25 +229,25 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
|
||||
toggleExpand(lineNumber: number) {
|
||||
if (this._isCollapsed) {
|
||||
this.show({ lineNumber: lineNumber, column: 1 }, 2);
|
||||
} else {
|
||||
if (this._isExpanded) {
|
||||
this.hide();
|
||||
if (this._commentThread === null || this._commentThread.threadId === null) {
|
||||
this.dispose();
|
||||
}
|
||||
} else {
|
||||
this.show({ lineNumber: lineNumber, column: 1 }, 2);
|
||||
}
|
||||
}
|
||||
|
||||
async update(commentThread: modes.CommentThread | modes.CommentThread2) {
|
||||
const oldCommentsLen = this._commentElements.length;
|
||||
const newCommentsLen = commentThread.comments.length;
|
||||
const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0;
|
||||
|
||||
let commentElementsToDel: CommentNode[] = [];
|
||||
let commentElementsToDelIndex: number[] = [];
|
||||
for (let i = 0; i < oldCommentsLen; i++) {
|
||||
let comment = this._commentElements[i].comment;
|
||||
let newComment = commentThread.comments.filter(c => c.commentId === comment.commentId);
|
||||
let newComment = commentThread.comments ? commentThread.comments.filter(c => c.commentId === comment.commentId) : [];
|
||||
|
||||
if (newComment.length) {
|
||||
this._commentElements[i].update(newComment[0]);
|
||||
@@ -267,7 +266,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
let lastCommentElement: HTMLElement | null = null;
|
||||
let newCommentNodeList: CommentNode[] = [];
|
||||
for (let i = newCommentsLen - 1; i >= 0; i--) {
|
||||
let currentComment = commentThread.comments[i];
|
||||
let currentComment = commentThread.comments![i];
|
||||
let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.commentId === currentComment.commentId);
|
||||
if (oldCommentNode.length) {
|
||||
oldCommentNode[0].update(currentComment);
|
||||
@@ -291,6 +290,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
this._commentElements = newCommentNodeList;
|
||||
this.createThreadLabel();
|
||||
|
||||
if (this._formActions && this._commentEditor.hasModel()) {
|
||||
dom.clearNode(this._formActions);
|
||||
const model = this._commentEditor.getModel();
|
||||
this.createCommentWidgetActions2(this._formActions, model);
|
||||
}
|
||||
|
||||
// Move comment glyph widget and show position if the line has changed.
|
||||
const lineNumber = this._commentThread.range.startLineNumber;
|
||||
let shouldMoveWidget = false;
|
||||
@@ -305,9 +310,22 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
this.createReplyButton();
|
||||
}
|
||||
|
||||
if (shouldMoveWidget && !this._isCollapsed) {
|
||||
if (this._commentThread.comments && this._commentThread.comments.length === 0) {
|
||||
this.expandReplyArea();
|
||||
}
|
||||
|
||||
if (shouldMoveWidget && this._isExpanded) {
|
||||
this.show({ lineNumber, column: 1 }, 2);
|
||||
}
|
||||
|
||||
// The collapsible state is not initialized yet.
|
||||
if (this._isExpanded === undefined) {
|
||||
if (this._commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded) {
|
||||
this.show({ lineNumber, column: 1 }, 2);
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDraftMode(draftMode: modes.DraftMode | undefined) {
|
||||
@@ -344,14 +362,16 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
this._commentsElement.setAttribute('role', 'presentation');
|
||||
|
||||
this._commentElements = [];
|
||||
for (const comment of this._commentThread.comments) {
|
||||
const newCommentNode = this.createNewCommentNode(comment);
|
||||
if (this._commentThread.comments) {
|
||||
for (const comment of this._commentThread.comments) {
|
||||
const newCommentNode = this.createNewCommentNode(comment);
|
||||
|
||||
this._commentElements.push(newCommentNode);
|
||||
this._commentsElement.appendChild(newCommentNode.domNode);
|
||||
this._commentElements.push(newCommentNode);
|
||||
this._commentsElement.appendChild(newCommentNode.domNode);
|
||||
}
|
||||
}
|
||||
|
||||
const hasExistingComments = this._commentThread.comments.length > 0;
|
||||
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);
|
||||
|
||||
@@ -426,9 +446,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
if (hasExistingComments) {
|
||||
this.createReplyButton();
|
||||
} else {
|
||||
if (!dom.hasClass(this._commentForm, 'expand')) {
|
||||
dom.addClass(this._commentForm, 'expand');
|
||||
this._commentEditor.focus();
|
||||
if (this._commentThread.comments && this._commentThread.comments.length === 0) {
|
||||
this.expandReplyArea();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,20 +482,20 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldMoveWidget && !this._isCollapsed) {
|
||||
if (shouldMoveWidget && this._isExpanded) {
|
||||
this.show({ lineNumber, column: 1 }, 2);
|
||||
}
|
||||
}));
|
||||
|
||||
this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeCollasibleState(state => {
|
||||
if (state === modes.CommentThreadCollapsibleState.Expanded && this._isCollapsed) {
|
||||
if (state === modes.CommentThreadCollapsibleState.Expanded && !this._isExpanded) {
|
||||
const lineNumber = this._commentThread.range.startLineNumber;
|
||||
|
||||
this.show({ lineNumber, column: 1 }, 2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === modes.CommentThreadCollapsibleState.Collapsed && !this._isCollapsed) {
|
||||
if (state === modes.CommentThreadCollapsibleState.Collapsed && this._isExpanded) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
@@ -499,13 +518,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
|
||||
// If there are no existing comments, place focus on the text area. This must be done after show, which also moves focus.
|
||||
if ((this._commentThread as modes.CommentThread).reply && !this._commentThread.comments.length) {
|
||||
// if this._commentThread.comments is undefined, it doesn't finish initialization yet, so we don't focus the editor immediately.
|
||||
if ((this._commentThread as modes.CommentThread).reply && this._commentThread.comments && !this._commentThread.comments.length) {
|
||||
this._commentEditor.focus();
|
||||
} else if (this._commentEditor.getModel()!.getValueLength() > 0) {
|
||||
if (!dom.hasClass(this._commentForm, 'expand')) {
|
||||
dom.addClass(this._commentForm, 'expand');
|
||||
}
|
||||
this._commentEditor.focus();
|
||||
this.expandReplyArea();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,8 +633,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
*/
|
||||
private createCommentWidgetActions2(container: HTMLElement, model: ITextModel) {
|
||||
let commentThread = this._commentThread as modes.CommentThread2;
|
||||
const { acceptInputCommand, additionalCommands } = commentThread;
|
||||
|
||||
const { acceptInputCommand } = commentThread;
|
||||
if (acceptInputCommand) {
|
||||
const button = new Button(container);
|
||||
this._disposables.push(attachButtonStyler(button, this.themeService));
|
||||
@@ -642,20 +659,22 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}));
|
||||
}
|
||||
|
||||
commentThread.additionalCommands.reverse().forEach(command => {
|
||||
const button = new Button(container);
|
||||
this._disposables.push(attachButtonStyler(button, this.themeService));
|
||||
if (additionalCommands) {
|
||||
additionalCommands.reverse().forEach(command => {
|
||||
const button = new Button(container);
|
||||
this._disposables.push(attachButtonStyler(button, this.themeService));
|
||||
|
||||
button.label = command.title;
|
||||
this._disposables.push(button.onDidClick(async () => {
|
||||
commentThread.input = {
|
||||
uri: this._commentEditor.getModel()!.uri,
|
||||
value: this._commentEditor.getValue()
|
||||
};
|
||||
this.commentService.setActiveCommentThread(this._commentThread);
|
||||
await this.commandService.executeCommand(command.id, ...(command.arguments || []));
|
||||
}));
|
||||
});
|
||||
button.label = command.title;
|
||||
this._disposables.push(button.onDidClick(async () => {
|
||||
commentThread.input = {
|
||||
uri: this._commentEditor.getModel()!.uri,
|
||||
value: this._commentEditor.getValue()
|
||||
};
|
||||
this.commentService.setActiveCommentThread(this._commentThread);
|
||||
await this.commandService.executeCommand(command.id, ...(command.arguments || []));
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createNewCommentNode(comment: modes.Comment): CommentNode {
|
||||
@@ -676,15 +695,15 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
this._commentElements.splice(deletedElementIndex, 1);
|
||||
}
|
||||
|
||||
const deletedCommentIndex = arrays.firstIndex(this._commentThread.comments, comment => comment.commentId === deletedNodeId);
|
||||
const deletedCommentIndex = arrays.firstIndex(this._commentThread.comments!, comment => comment.commentId === deletedNodeId);
|
||||
if (deletedCommentIndex > -1) {
|
||||
this._commentThread.comments.splice(deletedCommentIndex, 1);
|
||||
this._commentThread.comments!.splice(deletedCommentIndex, 1);
|
||||
}
|
||||
|
||||
this._commentsElement.removeChild(deletedNode.domNode);
|
||||
deletedNode.dispose();
|
||||
|
||||
if (this._commentThread.comments.length === 0) {
|
||||
if (this._commentThread.comments!.length === 0) {
|
||||
this.dispose();
|
||||
}
|
||||
}));
|
||||
@@ -786,7 +805,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
|
||||
if (label === undefined) {
|
||||
if (this._commentThread.comments.length) {
|
||||
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);
|
||||
} else {
|
||||
@@ -825,7 +844,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
if (!this._isCollapsed && this._bodyElement) {
|
||||
if (this._isExpanded && this._bodyElement) {
|
||||
let dimensions = dom.getClientArea(this._bodyElement);
|
||||
const headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2);
|
||||
const lineHeight = this.editor.getConfiguration().lineHeight;
|
||||
@@ -841,7 +860,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
const model = this._commentEditor && this._commentEditor.getModel();
|
||||
if (model) {
|
||||
const valueLength = model.getValueLength();
|
||||
const hasExistingComments = this._commentThread.comments.length > 0;
|
||||
const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0;
|
||||
const placeholder = valueLength > 0
|
||||
? ''
|
||||
: hasExistingComments
|
||||
@@ -996,13 +1015,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
|
||||
}
|
||||
|
||||
show(rangeOrPos: IRange | IPosition, heightInLines: number): void {
|
||||
this._isCollapsed = false;
|
||||
this._isExpanded = true;
|
||||
super.show(rangeOrPos, heightInLines);
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this._isCollapsed = true;
|
||||
this._isExpanded = false;
|
||||
// Focus the container so that the comment editor will be blurred before it is hidden
|
||||
this.editor.focus();
|
||||
super.hide();
|
||||
|
||||
@@ -265,9 +265,15 @@ export class ReviewController implements IEditorContribution {
|
||||
if (commentThreadWidget.length === 1) {
|
||||
commentThreadWidget[0].reveal(commentId);
|
||||
} else if (fetchOnceIfNotExist) {
|
||||
this.beginCompute().then(_ => {
|
||||
this.revealCommentThread(threadId, commentId, false);
|
||||
});
|
||||
if (this._computePromise) {
|
||||
this._computePromise.then(_ => {
|
||||
this.revealCommentThread(threadId, commentId, false);
|
||||
});
|
||||
} else {
|
||||
this.beginCompute().then(_ => {
|
||||
this.revealCommentThread(threadId, commentId, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ export class ResourceWithCommentThreads {
|
||||
constructor(resource: URI, commentThreads: CommentThread[]) {
|
||||
this.id = resource.toString();
|
||||
this.resource = resource;
|
||||
this.commentThreads = commentThreads.filter(thread => thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(resource, thread));
|
||||
this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(resource, thread));
|
||||
}
|
||||
|
||||
public static createCommentNode(resource: URI, commentThread: CommentThread): CommentNode {
|
||||
const { threadId, comments, range } = commentThread;
|
||||
const commentNodes: CommentNode[] = comments.map(comment => new CommentNode(threadId!, resource, comment, range));
|
||||
const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(threadId!, resource, comment, range));
|
||||
if (commentNodes.length > 1) {
|
||||
commentNodes[0].replies = commentNodes.slice(1, commentNodes.length);
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export class CommentsModel {
|
||||
const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource);
|
||||
if (existingResource.length) {
|
||||
const resource = existingResource[0];
|
||||
if (thread.comments.length) {
|
||||
if (thread.comments && thread.comments.length) {
|
||||
resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(resource.resource, thread));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -558,6 +558,7 @@ export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolea
|
||||
preserveFocus,
|
||||
selection,
|
||||
revealIfVisible: true,
|
||||
revealIfOpened: true,
|
||||
revealInCenterIfOutsideViewport: true,
|
||||
pinned: !preserveFocus
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selec
|
||||
import { SelectActionItem, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IDebugService, IDebugSession } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IDebugService, IDebugSession, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
@@ -192,9 +192,10 @@ export class StartDebugActionItem implements IActionItem {
|
||||
export class FocusSessionActionItem extends SelectActionItem {
|
||||
constructor(
|
||||
action: IAction,
|
||||
@IDebugService protected debugService: IDebugService,
|
||||
@IDebugService protected readonly debugService: IDebugService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super(null, action, [], -1, contextViewService, { ariaLabel: nls.localize('debugSession', 'Debug Session') });
|
||||
|
||||
@@ -234,6 +235,9 @@ export class FocusSessionActionItem extends SelectActionItem {
|
||||
}
|
||||
|
||||
protected getSessions(): ReadonlyArray<IDebugSession> {
|
||||
return this.debugService.getModel().getSessions();
|
||||
const hideSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').hideSubSessions;
|
||||
const sessions = this.debugService.getModel().getSessions();
|
||||
|
||||
return hideSubSessions ? sessions.filter(s => !s.parentSession) : sessions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IListService } from 'vs/platform/list/browser/listService';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, REPL_ID } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, REPL_ID, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Expression, Variable, Breakpoint, FunctionBreakpoint, Thread } from 'vs/workbench/contrib/debug/common/debugModel';
|
||||
import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
@@ -30,6 +30,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
import { startDebugging } from 'vs/workbench/contrib/debug/common/debugUtils';
|
||||
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export const ADD_CONFIGURATION_ID = 'debug.addConfiguration';
|
||||
export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint';
|
||||
@@ -183,6 +184,12 @@ export function registerCommands(): void {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
if (!session || !session.getId) {
|
||||
session = debugService.getViewModel().focusedSession;
|
||||
const configurationService = accessor.get(IConfigurationService);
|
||||
const hideSubSessions = configurationService.getValue<IDebugConfiguration>('debug').hideSubSessions;
|
||||
// Stop should be sent to the root parent session
|
||||
while (hideSubSessions && session && session.parentSession) {
|
||||
session = session.parentSession;
|
||||
}
|
||||
}
|
||||
|
||||
debugService.stopSession(session).then(undefined, onUnexpectedError);
|
||||
|
||||
@@ -23,7 +23,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -67,7 +67,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution {
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@@ -89,7 +88,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution {
|
||||
orientation: ActionsOrientation.HORIZONTAL,
|
||||
actionItemProvider: (action: IAction) => {
|
||||
if (action.id === FocusSessionAction.ID) {
|
||||
return new FocusSessionActionItem(action, this.debugService, this.themeService, contextViewService);
|
||||
return this.instantiationService.createInstance(FocusSessionActionItem, action);
|
||||
}
|
||||
if (action instanceof MenuItemAction) {
|
||||
return new MenuItemActionItem(action, this.keybindingService, this.notificationService, contextMenuService);
|
||||
|
||||
@@ -136,7 +136,7 @@ export class DebugViewlet extends ViewContainerViewlet {
|
||||
return this.startDebugActionItem;
|
||||
}
|
||||
if (action.id === FocusSessionAction.ID) {
|
||||
return new FocusSessionActionItem(action, this.debugService, this.themeService, this.contextViewService);
|
||||
return new FocusSessionActionItem(action, this.debugService, this.themeService, this.contextViewService, this.configurationService);
|
||||
}
|
||||
if (action instanceof MenuItemAction) {
|
||||
return new MenuItemActionItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
|
||||
|
||||
@@ -138,7 +138,7 @@ export class LinkDetector {
|
||||
|
||||
private onLinkClick(event: IMouseEvent, resource: uri, line: number, column: number = 0): void {
|
||||
const selection = window.getSelection();
|
||||
if (selection.type === 'Range') {
|
||||
if (!selection || selection.type === 'Range') {
|
||||
return; // do not navigate when user is selecting
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { isWindows } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ltrim } from 'vs/base/common/strings';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { ResourceLabels, IResourceLabelProps, IResourceLabelOptions, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
|
||||
import { ResourceLabels, IResourceLabelProps, IResourceLabelOptions, IResourceLabel } from 'vs/workbench/browser/labels';
|
||||
import { FileKind } from 'vs/platform/files/common/files';
|
||||
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
|
||||
@@ -416,7 +416,7 @@ export class LoadedScriptsView extends ViewletPanel {
|
||||
|
||||
const root = new RootTreeItem(this.debugService.getModel(), this.environmentService, this.contextService, this.labelService);
|
||||
|
||||
this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
|
||||
this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
|
||||
this.disposables.push(this.treeLabels);
|
||||
|
||||
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new LoadedScriptsDelegate(),
|
||||
|
||||
@@ -158,9 +158,15 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.debug-viewlet .debug-call-stack .thread > .name,
|
||||
.debug-viewlet .debug-call-stack .session > .name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.debug-viewlet .debug-call-stack .thread > .state,
|
||||
.debug-viewlet .debug-call-stack .session > .state {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -424,6 +424,7 @@ export interface IDebugConfiguration {
|
||||
internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart';
|
||||
extensionHostDebugAdapter: boolean;
|
||||
enableAllHovers: boolean;
|
||||
hideSubSessions: boolean;
|
||||
console: {
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
|
||||
@@ -93,6 +93,7 @@ export class Source {
|
||||
preserveFocus,
|
||||
selection,
|
||||
revealIfVisible: true,
|
||||
revealIfOpened: true,
|
||||
revealInCenterIfOutsideViewport: true,
|
||||
pinned: pinned || (!preserveFocus && !this.inMemory)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class ViewModel implements IViewModel {
|
||||
this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false);
|
||||
this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false);
|
||||
this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false);
|
||||
const attach = !!session && session.configuration.request === 'attach' && !isExtensionHostDebugging(session.configuration);
|
||||
const attach = !!session && !session.parentSession && session.configuration.request === 'attach' && !isExtensionHostDebugging(session.configuration);
|
||||
this.focusedSessionIsAttach.set(attach);
|
||||
|
||||
if (shouldEmitForSession) {
|
||||
|
||||
@@ -33,9 +33,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
|
||||
import { IBroadcastService } from 'vs/workbench/services/broadcast/common/broadcast';
|
||||
import { IRemoteConsoleLog, parse, getFirstFrame } from 'vs/base/common/console';
|
||||
import { parse, getFirstFrame } from 'vs/base/common/console';
|
||||
import { TaskEvent, TaskEventKind, TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
@@ -47,6 +45,7 @@ import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_
|
||||
import { isExtensionHostDebugging } from 'vs/workbench/contrib/debug/common/debugUtils';
|
||||
import { isErrorWithActions, createErrorWithActions } from 'vs/base/common/errorsWithActions';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug';
|
||||
|
||||
const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint';
|
||||
const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated';
|
||||
@@ -98,7 +97,6 @@ export class DebugService implements IDebugService {
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@IBroadcastService private readonly broadcastService: IBroadcastService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@@ -109,6 +107,7 @@ export class DebugService implements IDebugService {
|
||||
@ITaskService private readonly taskService: ITaskService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService
|
||||
) {
|
||||
this.toDispose = [];
|
||||
|
||||
@@ -136,34 +135,31 @@ export class DebugService implements IDebugService {
|
||||
this.toDispose.push(this.storageService.onWillSaveState(this.saveState, this));
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
|
||||
this.toDispose.push(this.broadcastService.onBroadcast(broadcast => {
|
||||
const session = this.model.getSession(broadcast.payload.debugId, true);
|
||||
this.toDispose.push(this.extensionHostDebugService.onAttachSession(data => {
|
||||
const session = this.model.getSession(data.id, true);
|
||||
if (session) {
|
||||
switch (broadcast.channel) {
|
||||
|
||||
case EXTENSION_ATTACH_BROADCAST_CHANNEL:
|
||||
// EH was started in debug mode -> attach to it
|
||||
session.configuration.request = 'attach';
|
||||
session.configuration.port = broadcast.payload.port;
|
||||
this.launchOrAttachToSession(session);
|
||||
break;
|
||||
|
||||
case EXTENSION_TERMINATE_BROADCAST_CHANNEL:
|
||||
// EH was terminated
|
||||
session.disconnect();
|
||||
break;
|
||||
|
||||
case EXTENSION_LOG_BROADCAST_CHANNEL:
|
||||
// extension logged output -> show it in REPL
|
||||
const extensionOutput = <IRemoteConsoleLog>broadcast.payload.logEntry;
|
||||
const sev = extensionOutput.severity === 'warn' ? severity.Warning : extensionOutput.severity === 'error' ? severity.Error : severity.Info;
|
||||
const { args, stack } = parse(extensionOutput);
|
||||
const frame = !!stack ? getFirstFrame(stack) : undefined;
|
||||
session.logToRepl(sev, args, frame);
|
||||
break;
|
||||
}
|
||||
// EH was started in debug mode -> attach to it
|
||||
session.configuration.request = 'attach';
|
||||
session.configuration.port = data.port;
|
||||
this.launchOrAttachToSession(session).then(undefined, errors.onUnexpectedError);
|
||||
}
|
||||
}, this));
|
||||
}));
|
||||
this.toDispose.push(this.extensionHostDebugService.onTerminateSession(sessionId => {
|
||||
const session = this.model.getSession(sessionId);
|
||||
if (session) {
|
||||
session.disconnect().then(undefined, errors.onUnexpectedError);
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.extensionHostDebugService.onLogToSession(data => {
|
||||
const session = this.model.getSession(data.id, true);
|
||||
if (session) {
|
||||
// extension logged output -> show it in REPL
|
||||
const sev = data.log.severity === 'warn' ? severity.Warning : data.log.severity === 'error' ? severity.Error : severity.Info;
|
||||
const { args, stack } = parse(data.log);
|
||||
const frame = !!stack ? getFirstFrame(stack) : undefined;
|
||||
session.logToRepl(sev, args, frame);
|
||||
}
|
||||
}));
|
||||
|
||||
this.toDispose.push(this.viewModel.onDidFocusStackFrame(() => {
|
||||
this.onStateChange();
|
||||
@@ -443,8 +439,10 @@ export class DebugService implements IDebugService {
|
||||
}
|
||||
|
||||
this.viewModel.firstSessionStart = false;
|
||||
|
||||
if (this.model.getSessions().length > 1) {
|
||||
const hideSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').hideSubSessions;
|
||||
const sessions = this.model.getSessions();
|
||||
const shownSessions = hideSubSessions ? sessions.filter(s => !s.parentSession) : sessions;
|
||||
if (shownSessions.length > 1) {
|
||||
this.viewModel.setMultiSessionView(true);
|
||||
}
|
||||
|
||||
@@ -510,10 +508,7 @@ export class DebugService implements IDebugService {
|
||||
|
||||
// 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905
|
||||
if (isExtensionHostDebugging(session.configuration) && session.state === State.Running && session.configuration.noDebug) {
|
||||
this.broadcastService.broadcast({
|
||||
channel: EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL,
|
||||
payload: [session.root.uri.toString()]
|
||||
});
|
||||
this.extensionHostDebugService.close(session.root.uri);
|
||||
}
|
||||
|
||||
this.telemetryDebugSessionStop(session, adapterExitEvent);
|
||||
@@ -561,10 +556,7 @@ export class DebugService implements IDebugService {
|
||||
}
|
||||
|
||||
if (isExtensionHostDebugging(session.configuration) && session.root) {
|
||||
return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.broadcastService.broadcast({
|
||||
channel: EXTENSION_RELOAD_BROADCAST_CHANNEL,
|
||||
payload: [session.root.uri.toString()]
|
||||
}) : undefined);
|
||||
return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.extensionHostDebugService.reload(session.root.uri) : undefined);
|
||||
}
|
||||
|
||||
const shouldFocus = this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId();
|
||||
|
||||
@@ -5,19 +5,20 @@
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { WorkbenchTreeController, WorkbenchTree, IListService } from 'vs/platform/list/browser/listService';
|
||||
import { IListService, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
|
||||
export interface IExtensionTemplateData {
|
||||
icon: HTMLImageElement;
|
||||
@@ -39,49 +40,40 @@ export interface IExtensionData {
|
||||
parent: IExtensionData | null;
|
||||
}
|
||||
|
||||
export class DataSource implements IDataSource {
|
||||
export class AsyncDataSource implements IAsyncDataSource<IExtensionData, any> {
|
||||
|
||||
public getId(tree: ITree, { extension, parent }: IExtensionData): string {
|
||||
return parent ? this.getId(tree, parent) + '/' + extension.identifier.id : extension.identifier.id;
|
||||
}
|
||||
|
||||
public hasChildren(tree: ITree, { hasChildren }: IExtensionData): boolean {
|
||||
public hasChildren({ hasChildren }: IExtensionData): boolean {
|
||||
return hasChildren;
|
||||
}
|
||||
|
||||
public getChildren(tree: ITree, extensionData: IExtensionData): Promise<any> {
|
||||
public getChildren(extensionData: IExtensionData): Promise<any> {
|
||||
return extensionData.getChildren();
|
||||
}
|
||||
|
||||
public getParent(tree: ITree, { parent }: IExtensionData): Promise<any> {
|
||||
return Promise.resolve(parent);
|
||||
}
|
||||
|
||||
export class VirualDelegate implements IListVirtualDelegate<IExtensionData> {
|
||||
|
||||
public getHeight(element: IExtensionData): number {
|
||||
return 62;
|
||||
}
|
||||
public getTemplateId({ extension }: IExtensionData): string {
|
||||
return extension ? ExtensionRenderer.TEMPLATE_ID : UnknownExtensionRenderer.TEMPLATE_ID;
|
||||
}
|
||||
}
|
||||
|
||||
export class Renderer implements IRenderer {
|
||||
export class ExtensionRenderer implements IListRenderer<ITreeNode<IExtensionData>, IExtensionTemplateData> {
|
||||
|
||||
private static readonly EXTENSION_TEMPLATE_ID = 'extension-template';
|
||||
private static readonly UNKNOWN_EXTENSION_TEMPLATE_ID = 'unknown-extension-template';
|
||||
static readonly TEMPLATE_ID = 'extension-template';
|
||||
|
||||
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
|
||||
}
|
||||
|
||||
public getHeight(tree: ITree, element: IExtensionData): number {
|
||||
return 62;
|
||||
public get templateId(): string {
|
||||
return ExtensionRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
public getTemplateId(tree: ITree, { extension }: IExtensionData): string {
|
||||
return extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID;
|
||||
}
|
||||
|
||||
public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
|
||||
if (Renderer.EXTENSION_TEMPLATE_ID === templateId) {
|
||||
return this.renderExtensionTemplate(tree, container);
|
||||
}
|
||||
return this.renderUnknownExtensionTemplate(tree, container);
|
||||
}
|
||||
|
||||
private renderExtensionTemplate(tree: ITree, container: HTMLElement): IExtensionTemplateData {
|
||||
public renderTemplate(container: HTMLElement): IExtensionTemplateData {
|
||||
dom.addClass(container, 'extension');
|
||||
|
||||
const icon = dom.append(container, dom.$<HTMLImageElement>('img.icon'));
|
||||
@@ -91,8 +83,6 @@ export class Renderer implements IRenderer {
|
||||
const name = dom.append(header, dom.$('span.name'));
|
||||
const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction);
|
||||
const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => {
|
||||
tree.setFocus(openExtensionAction.extensionData);
|
||||
tree.setSelection([openExtensionAction.extensionData]);
|
||||
openExtensionAction.run(e.ctrlKey || e.metaKey);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -113,25 +103,8 @@ export class Renderer implements IRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
private renderUnknownExtensionTemplate(tree: ITree, container: HTMLElement): IUnknownExtensionTemplateData {
|
||||
const messageContainer = dom.append(container, dom.$('div.unknown-extension'));
|
||||
dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error");
|
||||
dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:");
|
||||
|
||||
const identifier = dom.append(messageContainer, dom.$('span.message'));
|
||||
return { identifier };
|
||||
}
|
||||
|
||||
public renderElement(tree: ITree, element: IExtensionData, templateId: string, templateData: any): void {
|
||||
if (templateId === Renderer.EXTENSION_TEMPLATE_ID) {
|
||||
this.renderExtension(tree, element, templateData);
|
||||
return;
|
||||
}
|
||||
this.renderUnknownExtension(tree, element, templateData);
|
||||
}
|
||||
|
||||
private renderExtension(tree: ITree, extensionData: IExtensionData, data: IExtensionTemplateData): void {
|
||||
const extension = extensionData.extension;
|
||||
public renderElement(node: ITreeNode<IExtensionData>, index: number, data: IExtensionTemplateData): void {
|
||||
const extension = node.element.extension;
|
||||
const onError = Event.once(domEvent(data.icon, 'error'));
|
||||
onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables);
|
||||
data.icon.src = extension.iconUrl;
|
||||
@@ -146,54 +119,36 @@ export class Renderer implements IRenderer {
|
||||
data.name.textContent = extension.displayName;
|
||||
data.identifier.textContent = extension.identifier.id;
|
||||
data.author.textContent = extension.publisherDisplayName;
|
||||
data.extensionData = extensionData;
|
||||
data.extensionData = node.element;
|
||||
}
|
||||
|
||||
private renderUnknownExtension(tree: ITree, { extension }: IExtensionData, data: IUnknownExtensionTemplateData): void {
|
||||
data.identifier.textContent = extension.identifier.id;
|
||||
}
|
||||
|
||||
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
|
||||
if (templateId === Renderer.EXTENSION_TEMPLATE_ID) {
|
||||
templateData.extensionDisposables = dispose((<IExtensionTemplateData>templateData).extensionDisposables);
|
||||
}
|
||||
public disposeTemplate(templateData: IExtensionTemplateData): void {
|
||||
templateData.extensionDisposables = dispose((<IExtensionTemplateData>templateData).extensionDisposables);
|
||||
}
|
||||
}
|
||||
|
||||
export class Controller extends WorkbenchTreeController {
|
||||
export class UnknownExtensionRenderer implements IListRenderer<ITreeNode<IExtensionData>, IUnknownExtensionTemplateData> {
|
||||
|
||||
constructor(
|
||||
@IExtensionsWorkbenchService private readonly extensionsWorkdbenchService: IExtensionsWorkbenchService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super({}, configurationService);
|
||||
static readonly TEMPLATE_ID = 'unknown-extension-template';
|
||||
|
||||
// TODO@Sandeep this should be a command
|
||||
this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true));
|
||||
public get templateId(): string {
|
||||
return UnknownExtensionRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: ITree, element: IExtensionData, event: IMouseEvent): boolean {
|
||||
let currentFocused = tree.getFocus();
|
||||
if (super.onLeftClick(tree, element, event)) {
|
||||
if (element.parent === null) {
|
||||
if (currentFocused) {
|
||||
tree.setFocus(currentFocused);
|
||||
} else {
|
||||
tree.focusFirst();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public renderTemplate(container: HTMLElement): IUnknownExtensionTemplateData {
|
||||
const messageContainer = dom.append(container, dom.$('div.unknown-extension'));
|
||||
dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error");
|
||||
dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:");
|
||||
|
||||
const identifier = dom.append(messageContainer, dom.$('span.message'));
|
||||
return { identifier };
|
||||
}
|
||||
|
||||
public openExtension(tree: ITree, sideByside: boolean): boolean {
|
||||
const element: IExtensionData = tree.getFocus();
|
||||
if (element.extension) {
|
||||
this.extensionsWorkdbenchService.open(element.extension, sideByside);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public renderElement(node: ITreeNode<IExtensionData>, index: number, data: IUnknownExtensionTemplateData): void {
|
||||
data.identifier.textContent = node.element.extension.identifier.id;
|
||||
}
|
||||
|
||||
public disposeTemplate(data: IUnknownExtensionTemplateData): void {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +173,7 @@ class OpenExtensionAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionsTree extends WorkbenchTree {
|
||||
export class ExtensionsTree extends WorkbenchAsyncDataTree<IExtensionData, any> {
|
||||
|
||||
constructor(
|
||||
input: IExtensionData,
|
||||
@@ -227,30 +182,37 @@ export class ExtensionsTree extends WorkbenchTree {
|
||||
@IListService listService: IListService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IAccessibilityService accessibilityService: IAccessibilityService,
|
||||
@IExtensionsWorkbenchService extensionsWorkdbenchService: IExtensionsWorkbenchService
|
||||
) {
|
||||
const renderer = instantiationService.createInstance(Renderer);
|
||||
const controller = instantiationService.createInstance(Controller);
|
||||
const delegate = new VirualDelegate();
|
||||
const dataSource = new AsyncDataSource();
|
||||
const renderers = [instantiationService.createInstance(ExtensionRenderer), instantiationService.createInstance(UnknownExtensionRenderer)];
|
||||
const identityProvider = {
|
||||
getId({ extension, parent }: IExtensionData): string {
|
||||
return parent ? this.getId(parent) + '/' + extension.identifier.id : extension.identifier.id;
|
||||
}
|
||||
};
|
||||
|
||||
super(
|
||||
container,
|
||||
delegate,
|
||||
renderers,
|
||||
dataSource,
|
||||
{
|
||||
dataSource: new DataSource(),
|
||||
renderer,
|
||||
controller
|
||||
}, {
|
||||
indentPixels: 40,
|
||||
twistiePixels: 20
|
||||
indent: 40,
|
||||
identityProvider,
|
||||
multipleSelectionSupport: false
|
||||
},
|
||||
contextKeyService, listService, themeService, instantiationService, configurationService
|
||||
contextKeyService, listService, themeService, configurationService, keybindingService, accessibilityService
|
||||
);
|
||||
|
||||
this.setInput(input);
|
||||
|
||||
this.disposables.push(this.onDidChangeSelection(event => {
|
||||
if (event && event.payload && event.payload.origin === 'keyboard') {
|
||||
controller.openExtension(this, false);
|
||||
}
|
||||
extensionsWorkdbenchService.open(event.elements[0], event.browserEvent instanceof MouseEvent && (event.browserEvent.ctrlKey || event.browserEvent.metaKey || event.browserEvent.altKey));
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export class ExtensionsInput extends EditorInput {
|
||||
return localize('extensionsInputName', "Extension: {0}", this.extension.displayName);
|
||||
}
|
||||
|
||||
matches(other: any): boolean {
|
||||
matches(other: unknown): boolean {
|
||||
if (!(other instanceof ExtensionsInput)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/we
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
|
||||
@@ -136,7 +135,7 @@ class NavBar {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.actionbar = dispose(this.actionbar);
|
||||
dispose(this.actionbar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +670,7 @@ export class ExtensionEditor extends BaseEditor {
|
||||
});
|
||||
}
|
||||
|
||||
private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): Tree {
|
||||
private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): ExtensionsTree {
|
||||
class ExtensionData implements IExtensionData {
|
||||
|
||||
private readonly extensionDependencies: IExtensionDependencies;
|
||||
@@ -720,7 +719,7 @@ export class ExtensionEditor extends BaseEditor {
|
||||
return Promise.resolve({ focus() { extensionsPackTree.domFocus(); } });
|
||||
}
|
||||
|
||||
private renderExtensionPack(container: HTMLElement, extension: IExtension): Tree {
|
||||
private renderExtensionPack(container: HTMLElement, extension: IExtension): ExtensionsTree {
|
||||
const extensionsWorkbenchService = this.extensionsWorkbenchService;
|
||||
class ExtensionData implements IExtensionData {
|
||||
|
||||
|
||||
@@ -10,17 +10,17 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { tmpdir } from 'os';
|
||||
import * as os from 'os';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { writeFile } from 'vs/base/node/pfs';
|
||||
import { IExtensionHostProfileService, ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
|
||||
import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
|
||||
|
||||
export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
@@ -30,11 +30,11 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
|
||||
constructor(
|
||||
@IExtensionService private readonly _extensionService: IExtensionService,
|
||||
@IExtensionHostProfileService private readonly _extensionProfileService: IExtensionHostProfileService,
|
||||
@IExtensionsWorkbenchService private readonly _anotherExtensionService: IExtensionsWorkbenchService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this._register(_extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this));
|
||||
@@ -132,16 +132,12 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
|
||||
return;
|
||||
}
|
||||
|
||||
// add to running extensions view
|
||||
this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile);
|
||||
|
||||
// print message to log
|
||||
const path = join(tmpdir(), `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`);
|
||||
const path = join(os.tmpdir(), `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`);
|
||||
await writeFile(path, JSON.stringify(profile.data));
|
||||
this._logService.warn(`UNRESPONSIVE extension host, '${top.id}' took ${top!.percentage}% of ${duration / 1e3}ms, saved PROFILE here: '${path}'`, data);
|
||||
|
||||
// send telemetry
|
||||
const id = generateUuid();
|
||||
|
||||
/* __GDPR__
|
||||
"exthostunresponsive" : {
|
||||
@@ -151,24 +147,22 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
|
||||
}
|
||||
*/
|
||||
this._telemetryService.publicLog('exthostunresponsive', {
|
||||
id,
|
||||
duration,
|
||||
data,
|
||||
});
|
||||
|
||||
// add to running extensions view
|
||||
this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile);
|
||||
|
||||
// prompt: when really slow/greedy
|
||||
if (!(top.percentage >= 99 && top.total >= 5e6)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prompt: only when you can file an issue
|
||||
const reportAction = new ReportExtensionIssueAction({
|
||||
marketplaceInfo: this._anotherExtensionService.local.filter(value => ExtensionIdentifier.equals(value.identifier.id, extension.identifier))[0],
|
||||
description: extension,
|
||||
unresponsiveProfile: profile,
|
||||
status: undefined,
|
||||
});
|
||||
if (!reportAction.enabled) {
|
||||
const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, extension, profile);
|
||||
|
||||
if (!action) {
|
||||
// cannot report issues against this extension...
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,18 +184,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
|
||||
label: localize('show', 'Show Extensions'),
|
||||
run: () => this._editorService.openEditor(new RuntimeExtensionsInput())
|
||||
},
|
||||
{
|
||||
label: localize('report', "Report Issue"),
|
||||
run: () => {
|
||||
/* __GDPR__
|
||||
"exthostunresponsive/report" : {
|
||||
"id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
|
||||
}
|
||||
*/
|
||||
this._telemetryService.publicLog('exthostunresponsive/report', { id });
|
||||
return reportAction.run();
|
||||
}
|
||||
}],
|
||||
action
|
||||
],
|
||||
{ silent: true }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as os from 'os';
|
||||
import pkg from 'vs/platform/product/node/package';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IRequestService } from 'vs/platform/request/node/request';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { asText } from 'vs/base/node/request';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
|
||||
abstract class RepoInfo {
|
||||
readonly base: string;
|
||||
readonly owner: string;
|
||||
readonly repo: string;
|
||||
|
||||
static fromExtension(desc: IExtensionDescription): RepoInfo | undefined {
|
||||
|
||||
let result: RepoInfo | undefined;
|
||||
|
||||
// scheme:auth/OWNER/REPO/issues/
|
||||
if (desc.bugs && typeof desc.bugs.url === 'string') {
|
||||
const base = URI.parse(desc.bugs.url);
|
||||
const match = /\/([^/]+)\/([^/]+)\/issues\/?$/.exec(desc.bugs.url);
|
||||
if (match) {
|
||||
result = {
|
||||
base: base.with({ path: null, fragment: null, query: null }).toString(true),
|
||||
owner: match[1],
|
||||
repo: match[2]
|
||||
};
|
||||
}
|
||||
}
|
||||
// scheme:auth/OWNER/REPO.git
|
||||
if (!result && desc.repository && typeof desc.repository.url === 'string') {
|
||||
const base = URI.parse(desc.repository.url);
|
||||
const match = /\/([^/]+)\/([^/]+)(\.git)?$/.exec(desc.repository.url);
|
||||
if (match) {
|
||||
result = {
|
||||
base: base.with({ path: null, fragment: null, query: null }).toString(true),
|
||||
owner: match[1],
|
||||
repo: match[2]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// for now only GH is supported
|
||||
if (result && result.base.indexOf('github') === -1) {
|
||||
result = undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class SlowExtensionAction extends Action {
|
||||
|
||||
constructor(
|
||||
readonly extension: IExtensionDescription,
|
||||
readonly profile: IExtensionHostProfile,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super('report.slow', localize('cmd.reportOrShow', "Performance Issue"), 'extension-action report-issue');
|
||||
this.enabled = Boolean(RepoInfo.fromExtension(extension));
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, this.extension, this.profile);
|
||||
if (action) {
|
||||
await action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSlowExtensionAction(
|
||||
accessor: ServicesAccessor,
|
||||
extension: IExtensionDescription,
|
||||
profile: IExtensionHostProfile
|
||||
): Promise<Action | undefined> {
|
||||
|
||||
const info = RepoInfo.fromExtension(extension);
|
||||
if (!info) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const requestService = accessor.get(IRequestService);
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const url = `https://api.github.com/search/issues?q=is:issue+state:open+in:title+repo:${info.owner}/${info.repo}+%22Extension+causes+high+cpu+load%22`;
|
||||
const res = await requestService.request({ url }, CancellationToken.None);
|
||||
const rawText = await asText(res);
|
||||
if (!rawText) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = <{ total_count: number; }>JSON.parse(rawText);
|
||||
if (!data || typeof data.total_count !== 'number') {
|
||||
return undefined;
|
||||
} else if (data.total_count === 0) {
|
||||
return instaService.createInstance(ReportExtensionSlowAction, extension, info, profile);
|
||||
} else {
|
||||
return instaService.createInstance(ShowExtensionSlowAction, extension, info, profile);
|
||||
}
|
||||
}
|
||||
|
||||
class ReportExtensionSlowAction extends Action {
|
||||
|
||||
constructor(
|
||||
readonly extension: IExtensionDescription,
|
||||
readonly repoInfo: RepoInfo,
|
||||
readonly profile: IExtensionHostProfile,
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
) {
|
||||
super('report.slow', localize('cmd.report', "Report Issue"));
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
|
||||
// rewrite pii (paths) and store on disk
|
||||
const profiler = await import('v8-inspect-profiler');
|
||||
const data = profiler.rewriteAbsolutePaths({ profile: <any>this.profile.data }, 'pii_removed');
|
||||
const path = join(os.homedir(), `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`);
|
||||
await profiler.writeProfile(data, path).then(undefined, onUnexpectedError);
|
||||
|
||||
// build issue
|
||||
const title = encodeURIComponent('Extension causes high cpu load');
|
||||
const osVersion = `${os.type()} ${os.arch()} ${os.release()}`;
|
||||
const message = `:warning: Make sure to **attach** this file from your *home*-directory:\n:warning:\`${path}\`\n\nFind more details here: https://github.com/Microsoft/vscode/wiki/Explain:-extension-causes-high-cpu-load`;
|
||||
const body = encodeURIComponent(`- Issue Type: \`Performance\`
|
||||
- Extension Name: \`${this.extension.name}\`
|
||||
- Extension Version: \`${this.extension.version}\`
|
||||
- OS Version: \`${osVersion}\`
|
||||
- VSCode version: \`${pkg.version}\`\n\n${message}`);
|
||||
|
||||
const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues/new/?body=${body}&title=${title}`;
|
||||
window.open(url);
|
||||
|
||||
this._dialogService.show(
|
||||
Severity.Info,
|
||||
localize('attach.title', "Did you attach the CPU-Profile?"),
|
||||
[localize('ok', 'OK')],
|
||||
{ detail: localize('attach.msg', "This is a reminder to make sure that you have not forgotten to attach '{0}' to the issue you have just created.", path) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShowExtensionSlowAction extends Action {
|
||||
|
||||
constructor(
|
||||
readonly extension: IExtensionDescription,
|
||||
readonly repoInfo: RepoInfo,
|
||||
readonly profile: IExtensionHostProfile,
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
) {
|
||||
super('show.slow', localize('cmd.show', "Show Issues"));
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
|
||||
// rewrite pii (paths) and store on disk
|
||||
const profiler = await import('v8-inspect-profiler');
|
||||
const data = profiler.rewriteAbsolutePaths({ profile: <any>this.profile.data }, 'pii_removed');
|
||||
const path = join(os.homedir(), `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`);
|
||||
await profiler.writeProfile(data, path).then(undefined, onUnexpectedError);
|
||||
|
||||
// show issues
|
||||
const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues?utf8=✓&q=is%3Aissue+state%3Aopen+%22Extension+causes+high+cpu+load%22`;
|
||||
window.open(url);
|
||||
|
||||
this._dialogService.show(
|
||||
Severity.Info,
|
||||
localize('attach.title', "Did you attach the CPU-Profile?"),
|
||||
[localize('ok', 'OK')],
|
||||
{ detail: localize('attach.msg2', "This is a reminder to make sure that you have not forgotten to attach '{0}' to an existing performance issue.", path) }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -300,11 +300,11 @@
|
||||
border-color: rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .content .unknown-extension {
|
||||
.extension-editor .subcontent .monaco-list-row .content .unknown-extension {
|
||||
line-height: 62px;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .content .unknown-extension > .error-marker {
|
||||
.extension-editor .subcontent .monaco-list-row .content .unknown-extension > .error-marker {
|
||||
background-color: #BE1100;
|
||||
padding: 2px 4px;
|
||||
font-weight: bold;
|
||||
@@ -312,46 +312,46 @@
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .unknown-extension > .message {
|
||||
.extension-editor .subcontent .monaco-list-row .unknown-extension > .message {
|
||||
padding-left: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension {
|
||||
.extension-editor .subcontent .monaco-list-row .extension {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .details {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .details {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .details > .header {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .details > .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
line-height: 19px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .icon {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .details > .header > .name {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name:hover {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .details > .header > .name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .identifier {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .details > .header > .identifier {
|
||||
font-size: 90%;
|
||||
opacity: 0.6;
|
||||
margin-left: 10px;
|
||||
@@ -360,14 +360,14 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .details > .footer {
|
||||
display: flex;
|
||||
height: 19px;
|
||||
line-height: 19px;
|
||||
overflow: hidden;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer > .author {
|
||||
.extension-editor .subcontent .monaco-list-row .extension > .details > .footer > .author {
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
opacity: 0.6;
|
||||
|
||||
@@ -40,10 +40,9 @@ import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/cont
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
|
||||
|
||||
export const IExtensionHostProfileService = createDecorator<IExtensionHostProfileService>('extensionHostProfileService');
|
||||
export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey<string>('profileSessionState', 'none');
|
||||
@@ -308,7 +307,10 @@ export class RuntimeExtensionsEditor extends BaseEditor {
|
||||
data.activationTime.textContent = activationTimes.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`;
|
||||
|
||||
data.actionbar.clear();
|
||||
if (element.unresponsiveProfile || isNonEmptyArray(element.status.runtimeErrors)) {
|
||||
if (element.unresponsiveProfile) {
|
||||
data.actionbar.push(this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile), { icon: true, label: true });
|
||||
}
|
||||
if (isNonEmptyArray(element.status.runtimeErrors)) {
|
||||
data.actionbar.push(new ReportExtensionIssueAction(element), { icon: true, label: true });
|
||||
}
|
||||
|
||||
@@ -471,7 +473,6 @@ export class ReportExtensionIssueAction extends Action {
|
||||
private static _label = nls.localize('reportExtensionIssue', "Report Issue");
|
||||
|
||||
private readonly _url: string;
|
||||
private readonly _task?: () => Promise<any>;
|
||||
|
||||
constructor(extension: {
|
||||
description: IExtensionDescription;
|
||||
@@ -484,15 +485,10 @@ export class ReportExtensionIssueAction extends Action {
|
||||
&& extension.marketplaceInfo.type === ExtensionType.User
|
||||
&& !!extension.description.repository && !!extension.description.repository.url;
|
||||
|
||||
const { url, task } = ReportExtensionIssueAction._generateNewIssueUrl(extension);
|
||||
this._url = url;
|
||||
this._task = task;
|
||||
this._url = ReportExtensionIssueAction._generateNewIssueUrl(extension);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
if (this._task) {
|
||||
await this._task();
|
||||
}
|
||||
window.open(this._url);
|
||||
}
|
||||
|
||||
@@ -501,9 +497,9 @@ export class ReportExtensionIssueAction extends Action {
|
||||
marketplaceInfo: IExtension;
|
||||
status?: IExtensionsStatus;
|
||||
unresponsiveProfile?: IExtensionHostProfile
|
||||
}): { url: string, task?: () => Promise<any> } {
|
||||
}): string {
|
||||
|
||||
|
||||
let task: (() => Promise<any>) | undefined;
|
||||
let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined;
|
||||
if (!!baseUrl) {
|
||||
baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`;
|
||||
@@ -511,28 +507,10 @@ export class ReportExtensionIssueAction extends Action {
|
||||
baseUrl = product.reportIssueUrl;
|
||||
}
|
||||
|
||||
let title: string;
|
||||
let message: string;
|
||||
let reason: string;
|
||||
if (extension.unresponsiveProfile) {
|
||||
// unresponsive extension host caused
|
||||
reason = 'Performance';
|
||||
title = 'Extension causes high cpu load';
|
||||
let path = join(os.homedir(), `${extension.description.identifier.value}-unresponsive.cpuprofile.txt`);
|
||||
task = async () => {
|
||||
const profiler = await import('v8-inspect-profiler');
|
||||
const data = profiler.rewriteAbsolutePaths({ profile: <any>extension.unresponsiveProfile!.data }, 'pii_removed');
|
||||
profiler.writeProfile(data, path).then(undefined, onUnexpectedError);
|
||||
};
|
||||
message = `:warning: Make sure to **attach** this file from your *home*-directory:\n:warning:\`${path}\`\n\nFind more details here: https://github.com/Microsoft/vscode/wiki/Explain:-extension-causes-high-cpu-load`;
|
||||
|
||||
} else {
|
||||
// generic
|
||||
reason = 'Bug';
|
||||
title = 'Extension issue';
|
||||
message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:';
|
||||
clipboard.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
|
||||
}
|
||||
let reason = 'Bug';
|
||||
let title = 'Extension issue';
|
||||
let message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:';
|
||||
clipboard.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
|
||||
|
||||
const osVersion = `${os.type()} ${os.arch()} ${os.release()}`;
|
||||
const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&';
|
||||
@@ -544,10 +522,7 @@ export class ReportExtensionIssueAction extends Action {
|
||||
- VSCode version: \`${pkg.version}\`\n\n${message}`
|
||||
);
|
||||
|
||||
return {
|
||||
url: `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`,
|
||||
task
|
||||
};
|
||||
return `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export class RuntimeExtensionsInput extends EditorInput {
|
||||
return nls.localize('extensionsInputName', "Running Extensions");
|
||||
}
|
||||
|
||||
matches(other: any): boolean {
|
||||
matches(other: unknown): boolean {
|
||||
if (!(other instanceof RuntimeExtensionsInput)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as path from 'vs/base/common/path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs';
|
||||
import {
|
||||
IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService,
|
||||
IExtensionEnablementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier
|
||||
@@ -25,9 +26,7 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { FileService } from 'vs/workbench/services/files/node/fileService';
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IPager } from 'vs/base/common/paging';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
|
||||
@@ -99,7 +99,7 @@ export class FeedbackDropdown extends Dropdown {
|
||||
y: position.top - 9, // above status bar
|
||||
width: position.width,
|
||||
height: position.height
|
||||
} as IAnchor;
|
||||
};
|
||||
}
|
||||
|
||||
protected renderContents(container: HTMLElement): IDisposable {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
import { FeedbackDropdown, IFeedback, IFeedbackDelegate, FEEDBACK_VISIBLE_CONFIG, IFeedbackDropdownOptions } from 'vs/workbench/contrib/feedback/electron-browser/feedback';
|
||||
import { FeedbackDropdown, IFeedback, IFeedbackDelegate, FEEDBACK_VISIBLE_CONFIG } from 'vs/workbench/contrib/feedback/electron-browser/feedback';
|
||||
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
@@ -125,14 +125,14 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem {
|
||||
this.dropdown = this._register(this.instantiationService.createInstance(FeedbackDropdown, this.container, {
|
||||
contextViewProvider: this.contextViewService,
|
||||
feedbackService: this.instantiationService.createInstance(TwitterFeedbackService),
|
||||
onFeedbackVisibilityChange: visible => {
|
||||
onFeedbackVisibilityChange: (visible: boolean) => {
|
||||
if (visible) {
|
||||
addClass(this.container, 'has-beak');
|
||||
} else {
|
||||
removeClass(this.container, 'has-beak');
|
||||
}
|
||||
}
|
||||
} as IFeedbackDropdownOptions));
|
||||
}));
|
||||
|
||||
this.updateStyles();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/te
|
||||
import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
|
||||
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { distinct, coalesce } from 'vs/base/common/arrays';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -34,10 +34,9 @@ import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
|
||||
export class FileEditorTracker extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
protected closeOnFileDelete: boolean;
|
||||
|
||||
private modelLoadQueue: ResourceQueue;
|
||||
private activeOutOfWorkspaceWatchers: ResourceMap<URI>;
|
||||
private closeOnFileDelete: boolean;
|
||||
private modelLoadQueue = new ResourceQueue();
|
||||
private activeOutOfWorkspaceWatchers = new ResourceMap<IDisposable>();
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@@ -52,9 +51,6 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
|
||||
) {
|
||||
super();
|
||||
|
||||
this.modelLoadQueue = new ResourceQueue();
|
||||
this.activeOutOfWorkspaceWatchers = new ResourceMap<URI>();
|
||||
|
||||
this.onConfigurationUpdated(configurationService.getValue<IWorkbenchEditorConfiguration>());
|
||||
|
||||
this.registerListeners();
|
||||
@@ -358,9 +354,9 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
|
||||
});
|
||||
|
||||
// Handle no longer visible out of workspace resources
|
||||
this.activeOutOfWorkspaceWatchers.forEach(resource => {
|
||||
this.activeOutOfWorkspaceWatchers.keys().forEach(resource => {
|
||||
if (!visibleOutOfWorkspacePaths.get(resource)) {
|
||||
this.fileService.unwatch(resource);
|
||||
dispose(this.activeOutOfWorkspaceWatchers.get(resource));
|
||||
this.activeOutOfWorkspaceWatchers.delete(resource);
|
||||
}
|
||||
});
|
||||
@@ -368,8 +364,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
|
||||
// Handle newly visible out of workspace resources
|
||||
visibleOutOfWorkspacePaths.forEach(resource => {
|
||||
if (!this.activeOutOfWorkspaceWatchers.get(resource)) {
|
||||
this.fileService.watch(resource);
|
||||
this.activeOutOfWorkspaceWatchers.set(resource, resource);
|
||||
const disposable = this.fileService.watch(resource);
|
||||
this.activeOutOfWorkspaceWatchers.set(resource, disposable);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -377,8 +373,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
// Dispose watchers if any
|
||||
this.activeOutOfWorkspaceWatchers.forEach(resource => this.fileService.unwatch(resource));
|
||||
// Dispose remaining watchers if any
|
||||
this.activeOutOfWorkspaceWatchers.forEach(disposable => dispose(disposable));
|
||||
this.activeOutOfWorkspaceWatchers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export class TextFileEditor extends BaseTextEditor {
|
||||
// React to editors closing to preserve or clear view state. This needs to happen
|
||||
// in the onWillCloseEditor because at that time the editor has not yet
|
||||
// been disposed and we can safely persist the view state still as needed.
|
||||
this.groupListener = dispose(this.groupListener);
|
||||
dispose(this.groupListener);
|
||||
this.groupListener = ((group as IEditorGroupView).onWillCloseEditor(e => this.onWillCloseEditorInGroup(e)));
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export class TextFileEditor extends BaseTextEditor {
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) {
|
||||
this.openAsFolder(input);
|
||||
|
||||
return Promise.reject<any>(new Error(nls.localize('openFolderError', "File is a directory")));
|
||||
return Promise.reject(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
|
||||
@@ -296,7 +296,7 @@ export class TextFileEditor extends BaseTextEditor {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.groupListener = dispose(this.groupListener);
|
||||
dispose(this.groupListener);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import 'vs/css!./media/explorerviewlet';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, OpenEditorsVisibleCondition, VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files';
|
||||
import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
import { ExplorerView } from 'vs/workbench/contrib/files/browser/views/explorerView';
|
||||
@@ -103,7 +103,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor
|
||||
name: OpenEditorsView.NAME,
|
||||
ctorDescriptor: { ctor: OpenEditorsView },
|
||||
order: 0,
|
||||
when: OpenEditorsVisibleCondition,
|
||||
when: OpenEditorsVisibleContext,
|
||||
canToggleVisibility: true,
|
||||
focusCommand: {
|
||||
id: 'workbench.files.action.focusOpenEditorsView',
|
||||
|
||||
@@ -15,7 +15,7 @@ import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/c
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService } from 'vs/workbench/contrib/files/common/files';
|
||||
import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService, ExplorerResourceMoveableToTrash } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands';
|
||||
import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
|
||||
import { AutoSaveContext } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
@@ -63,7 +63,7 @@ const MOVE_FILE_TO_TRASH_ID = 'moveFileToTrash';
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: MOVE_FILE_TO_TRASH_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.has('config.files.enableTrash')),
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash),
|
||||
primary: KeyCode.Delete,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Backspace
|
||||
@@ -75,7 +75,7 @@ const DELETE_FILE_ID = 'deleteFile';
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: DELETE_FILE_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.has('config.files.enableTrash')),
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext),
|
||||
primary: KeyMod.Shift | KeyCode.Delete,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace
|
||||
@@ -86,7 +86,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: DELETE_FILE_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.not('config.files.enableTrash')),
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash.toNegated()),
|
||||
primary: KeyCode.Delete,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Backspace
|
||||
@@ -517,7 +517,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
title: nls.localize('deleteFile', "Delete Permanently"),
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ContextKeyExpr.has('config.files.enableTrash'))
|
||||
when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
@@ -528,7 +528,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
title: nls.localize('deleteFile', "Delete Permanently"),
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ContextKeyExpr.not('config.files.enableTrash'))
|
||||
when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash.toNegated())
|
||||
});
|
||||
|
||||
// Empty Editor Group Context Menu
|
||||
|
||||
@@ -17,12 +17,12 @@ import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IFileService, AutoSaveConfiguration } from 'vs/platform/files/common/files';
|
||||
import { toResource, IUntitledResourceInput, ITextEditor } from 'vs/workbench/common/editor';
|
||||
import { toResource, ITextEditor } from 'vs/workbench/common/editor';
|
||||
import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { IInstantiationService, ServicesAccessor, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands';
|
||||
@@ -68,36 +68,14 @@ export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste");
|
||||
|
||||
export const FileCopiedContext = new RawContextKey<boolean>('fileCopied', false);
|
||||
|
||||
export class BaseErrorReportingAction extends Action {
|
||||
const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete';
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private _notificationService: INotificationService
|
||||
) {
|
||||
super(id, label);
|
||||
function onError(notificationService: INotificationService, error: any): void {
|
||||
if (error.message === 'string') {
|
||||
error = error.message;
|
||||
}
|
||||
|
||||
public get notificationService() {
|
||||
return this._notificationService;
|
||||
}
|
||||
|
||||
protected onError(error: any): void {
|
||||
if (error.message === 'string') {
|
||||
error = error.message;
|
||||
}
|
||||
|
||||
this._notificationService.error(toErrorMessage(error, false));
|
||||
}
|
||||
|
||||
protected onErrorWithRetry(error: any, retry: () => Promise<any>): void {
|
||||
this._notificationService.prompt(Severity.Error, toErrorMessage(error, false),
|
||||
[{
|
||||
label: nls.localize('retry', "Retry"),
|
||||
run: () => retry()
|
||||
}]
|
||||
);
|
||||
}
|
||||
notificationService.error(toErrorMessage(error, false));
|
||||
}
|
||||
|
||||
function refreshIfSeparator(value: string, explorerService: IExplorerService): void {
|
||||
@@ -108,66 +86,26 @@ function refreshIfSeparator(value: string, explorerService: IExplorerService): v
|
||||
}
|
||||
|
||||
/* New File */
|
||||
export class NewFileAction extends BaseErrorReportingAction {
|
||||
export class NewFileAction extends Action {
|
||||
static readonly ID = 'workbench.files.action.createFileFromExplorer';
|
||||
static readonly LABEL = nls.localize('createNewFile', "New File");
|
||||
|
||||
private toDispose: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private getElement: () => ExplorerItem,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IExplorerService private explorerService: IExplorerService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IEditorService private editorService: IEditorService
|
||||
@IExplorerService explorerService: IExplorerService,
|
||||
@ICommandService private commandService: ICommandService
|
||||
) {
|
||||
super('explorer.newFile', NEW_FILE_LABEL, notificationService);
|
||||
super('explorer.newFile', NEW_FILE_LABEL);
|
||||
this.class = 'explorer-action new-file';
|
||||
this.toDispose.push(this.explorerService.onDidChangeEditable(e => {
|
||||
const elementIsBeingEdited = this.explorerService.isEditable(e);
|
||||
this.toDispose.push(explorerService.onDidChangeEditable(e => {
|
||||
const elementIsBeingEdited = explorerService.isEditable(e);
|
||||
this.enabled = !elementIsBeingEdited;
|
||||
}));
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
let folder: ExplorerItem;
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
folder = element.isDirectory ? element : element.parent!;
|
||||
} else {
|
||||
folder = this.explorerService.roots[0];
|
||||
}
|
||||
|
||||
if (folder.isReadonly) {
|
||||
return Promise.reject(new Error('Parent folder is readonly.'));
|
||||
}
|
||||
|
||||
const stat = new NewExplorerItem(folder, false);
|
||||
return folder.fetchChildren(this.fileService, this.explorerService).then(() => {
|
||||
folder.addChild(stat);
|
||||
|
||||
const onSuccess = (value: string) => {
|
||||
return this.fileService.createFile(resources.joinPath(folder.resource, value)).then(stat => {
|
||||
refreshIfSeparator(value, this.explorerService);
|
||||
return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
|
||||
}, (error) => {
|
||||
this.onErrorWithRetry(error, () => onSuccess(value));
|
||||
});
|
||||
};
|
||||
|
||||
this.explorerService.setEditable(stat, {
|
||||
validationMessage: value => validateFileName(stat, value),
|
||||
onFinish: (value, success) => {
|
||||
folder.removeChild(stat);
|
||||
this.explorerService.setEditable(stat, null);
|
||||
if (success) {
|
||||
onSuccess(value);
|
||||
} else {
|
||||
this.explorerService.select(folder.resource).then(undefined, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return this.commandService.executeCommand(NEW_FILE_COMMAND_ID);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -177,65 +115,26 @@ export class NewFileAction extends BaseErrorReportingAction {
|
||||
}
|
||||
|
||||
/* New Folder */
|
||||
export class NewFolderAction extends BaseErrorReportingAction {
|
||||
export class NewFolderAction extends Action {
|
||||
static readonly ID = 'workbench.files.action.createFolderFromExplorer';
|
||||
static readonly LABEL = nls.localize('createNewFolder', "New Folder");
|
||||
|
||||
private toDispose: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private getElement: () => ExplorerItem,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IExplorerService private explorerService: IExplorerService
|
||||
@IExplorerService explorerService: IExplorerService,
|
||||
@ICommandService private commandService: ICommandService
|
||||
) {
|
||||
super('explorer.newFolder', NEW_FOLDER_LABEL, notificationService);
|
||||
super('explorer.newFolder', NEW_FOLDER_LABEL);
|
||||
this.class = 'explorer-action new-folder';
|
||||
this.toDispose.push(this.explorerService.onDidChangeEditable(e => {
|
||||
const elementIsBeingEdited = this.explorerService.isEditable(e);
|
||||
this.toDispose.push(explorerService.onDidChangeEditable(e => {
|
||||
const elementIsBeingEdited = explorerService.isEditable(e);
|
||||
this.enabled = !elementIsBeingEdited;
|
||||
}));
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
let folder: ExplorerItem;
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
folder = element.isDirectory ? element : element.parent!;
|
||||
} else {
|
||||
folder = this.explorerService.roots[0];
|
||||
}
|
||||
|
||||
if (folder.isReadonly) {
|
||||
return Promise.reject(new Error('Parent folder is readonly.'));
|
||||
}
|
||||
|
||||
const stat = new NewExplorerItem(folder, true);
|
||||
return folder.fetchChildren(this.fileService, this.explorerService).then(() => {
|
||||
folder.addChild(stat);
|
||||
|
||||
const onSuccess = (value: string) => {
|
||||
return this.fileService.createFolder(resources.joinPath(folder.resource, value)).then(stat => {
|
||||
refreshIfSeparator(value, this.explorerService);
|
||||
return this.explorerService.select(stat.resource, true);
|
||||
}, (error) => {
|
||||
this.onErrorWithRetry(error, () => onSuccess(value));
|
||||
});
|
||||
};
|
||||
|
||||
this.explorerService.setEditable(stat, {
|
||||
validationMessage: value => validateFileName(stat, value),
|
||||
onFinish: (value, success) => {
|
||||
folder.removeChild(stat);
|
||||
this.explorerService.setEditable(stat, null);
|
||||
if (success) {
|
||||
onSuccess(value);
|
||||
} else {
|
||||
this.explorerService.select(folder.resource).then(undefined, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return this.commandService.executeCommand(NEW_FOLDER_COMMAND_ID);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -267,229 +166,210 @@ export class GlobalNewUntitledFileAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
class BaseDeleteFileAction extends BaseErrorReportingAction {
|
||||
|
||||
private static readonly CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete';
|
||||
|
||||
private skipConfirm: boolean;
|
||||
|
||||
constructor(
|
||||
private elements: ExplorerItem[],
|
||||
private useTrash: boolean,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super('moveFileToTrash', MOVE_FILE_TO_TRASH_LABEL, notificationService);
|
||||
|
||||
this.useTrash = useTrash && elements.every(e => !extpath.isUNC(e.resource.fsPath)); // on UNC shares there is no trash
|
||||
this.enabled = this.elements && this.elements.every(e => !e.isReadonly);
|
||||
function deleteFiles(serviceAccesor: ServicesAccessor, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise<void> {
|
||||
let primaryButton: string;
|
||||
if (useTrash) {
|
||||
primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash");
|
||||
} else {
|
||||
primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete");
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
const distinctElements = resources.distinctParents(elements, e => e.resource);
|
||||
const textFileService = serviceAccesor.get(ITextFileService);
|
||||
const dialogService = serviceAccesor.get(IDialogService);
|
||||
const configurationService = serviceAccesor.get(IConfigurationService);
|
||||
const fileService = serviceAccesor.get(IFileService);
|
||||
|
||||
let primaryButton: string;
|
||||
if (this.useTrash) {
|
||||
primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash");
|
||||
} else {
|
||||
primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete");
|
||||
}
|
||||
|
||||
const distinctElements = resources.distinctParents(this.elements, e => e.resource);
|
||||
|
||||
// Handle dirty
|
||||
let confirmDirtyPromise: Promise<boolean> = Promise.resolve(true);
|
||||
const dirty = this.textFileService.getDirty().filter(d => distinctElements.some(e => resources.isEqualOrParent(d, e.resource, !isLinux /* ignorecase */)));
|
||||
if (dirty.length) {
|
||||
let message: string;
|
||||
if (distinctElements.length > 1) {
|
||||
message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?");
|
||||
} else if (distinctElements[0].isDirectory) {
|
||||
if (dirty.length === 1) {
|
||||
message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder with unsaved changes in 1 file. Do you want to continue?");
|
||||
} else {
|
||||
message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length);
|
||||
}
|
||||
// Handle dirty
|
||||
let confirmDirtyPromise: Promise<boolean> = Promise.resolve(true);
|
||||
const dirty = textFileService.getDirty().filter(d => distinctElements.some(e => resources.isEqualOrParent(d, e.resource, !isLinux /* ignorecase */)));
|
||||
if (dirty.length) {
|
||||
let message: string;
|
||||
if (distinctElements.length > 1) {
|
||||
message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?");
|
||||
} else if (distinctElements[0].isDirectory) {
|
||||
if (dirty.length === 1) {
|
||||
message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder with unsaved changes in 1 file. Do you want to continue?");
|
||||
} else {
|
||||
message = nls.localize('dirtyMessageFileDelete', "You are deleting a file with unsaved changes. Do you want to continue?");
|
||||
message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length);
|
||||
}
|
||||
|
||||
confirmDirtyPromise = this.dialogService.confirm({
|
||||
message,
|
||||
type: 'warning',
|
||||
detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),
|
||||
primaryButton
|
||||
}).then(res => {
|
||||
if (!res.confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.skipConfirm = true; // since we already asked for confirmation
|
||||
return this.textFileService.revertAll(dirty).then(() => true);
|
||||
});
|
||||
} else {
|
||||
message = nls.localize('dirtyMessageFileDelete', "You are deleting a file with unsaved changes. Do you want to continue?");
|
||||
}
|
||||
|
||||
// Check if file is dirty in editor and save it to avoid data loss
|
||||
return confirmDirtyPromise.then(confirmed => {
|
||||
if (!confirmed) {
|
||||
return null;
|
||||
confirmDirtyPromise = dialogService.confirm({
|
||||
message,
|
||||
type: 'warning',
|
||||
detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),
|
||||
primaryButton
|
||||
}).then(res => {
|
||||
if (!res.confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let confirmDeletePromise: Promise<IConfirmationResult>;
|
||||
|
||||
// Check if we need to ask for confirmation at all
|
||||
if (this.skipConfirm || (this.useTrash && this.configurationService.getValue<boolean>(BaseDeleteFileAction.CONFIRM_DELETE_SETTING_KEY) === false)) {
|
||||
confirmDeletePromise = Promise.resolve({ confirmed: true } as IConfirmationResult);
|
||||
}
|
||||
|
||||
// Confirm for moving to trash
|
||||
else if (this.useTrash) {
|
||||
const message = this.getMoveToTrashMessage(distinctElements);
|
||||
|
||||
confirmDeletePromise = this.dialogService.confirm({
|
||||
message,
|
||||
detail: isWindows ? nls.localize('undoBin', "You can restore from the Recycle Bin.") : nls.localize('undoTrash', "You can restore from the Trash."),
|
||||
primaryButton,
|
||||
checkbox: {
|
||||
label: nls.localize('doNotAskAgain', "Do not ask me again")
|
||||
},
|
||||
type: 'question'
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm for deleting permanently
|
||||
else {
|
||||
const message = this.getDeleteMessage(distinctElements);
|
||||
confirmDeletePromise = this.dialogService.confirm({
|
||||
message,
|
||||
detail: nls.localize('irreversible', "This action is irreversible!"),
|
||||
primaryButton,
|
||||
type: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
return confirmDeletePromise.then(confirmation => {
|
||||
|
||||
// Check for confirmation checkbox
|
||||
let updateConfirmSettingsPromise: Promise<void> = Promise.resolve(undefined);
|
||||
if (confirmation.confirmed && confirmation.checkboxChecked === true) {
|
||||
updateConfirmSettingsPromise = this.configurationService.updateValue(BaseDeleteFileAction.CONFIRM_DELETE_SETTING_KEY, false, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
return updateConfirmSettingsPromise.then(() => {
|
||||
|
||||
// Check for confirmation
|
||||
if (!confirmation.confirmed) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Call function
|
||||
const servicePromise = Promise.all(distinctElements.map(e => this.fileService.del(e.resource, { useTrash: this.useTrash, recursive: true })))
|
||||
.then(undefined, (error: any) => {
|
||||
// Handle error to delete file(s) from a modal confirmation dialog
|
||||
let errorMessage: string;
|
||||
let detailMessage: string | undefined;
|
||||
let primaryButton: string;
|
||||
if (this.useTrash) {
|
||||
errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?");
|
||||
detailMessage = nls.localize('irreversible', "This action is irreversible!");
|
||||
primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently");
|
||||
} else {
|
||||
errorMessage = toErrorMessage(error, false);
|
||||
primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry");
|
||||
}
|
||||
|
||||
return this.dialogService.confirm({
|
||||
message: errorMessage,
|
||||
detail: detailMessage,
|
||||
type: 'warning',
|
||||
primaryButton
|
||||
}).then(res => {
|
||||
|
||||
if (res.confirmed) {
|
||||
if (this.useTrash) {
|
||||
this.useTrash = false; // Delete Permanently
|
||||
}
|
||||
|
||||
this.skipConfirm = true;
|
||||
|
||||
return this.run();
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
return servicePromise;
|
||||
});
|
||||
});
|
||||
skipConfirm = true; // since we already asked for confirmation
|
||||
return textFileService.revertAll(dirty).then(() => true);
|
||||
});
|
||||
}
|
||||
|
||||
private getMoveToTrashMessage(distinctElements: ExplorerItem[]): string {
|
||||
if (this.containsBothDirectoryAndFile(distinctElements)) {
|
||||
return getConfirmMessage(nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
// Check if file is dirty in editor and save it to avoid data loss
|
||||
return confirmDirtyPromise.then(confirmed => {
|
||||
if (!confirmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (distinctElements.length > 1) {
|
||||
if (distinctElements[0].isDirectory) {
|
||||
return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
let confirmDeletePromise: Promise<IConfirmationResult>;
|
||||
|
||||
// Check if we need to ask for confirmation at all
|
||||
if (skipConfirm || (useTrash && configurationService.getValue<boolean>(CONFIRM_DELETE_SETTING_KEY) === false)) {
|
||||
confirmDeletePromise = Promise.resolve({ confirmed: true });
|
||||
}
|
||||
|
||||
// Confirm for moving to trash
|
||||
else if (useTrash) {
|
||||
const message = getMoveToTrashMessage(distinctElements);
|
||||
|
||||
confirmDeletePromise = dialogService.confirm({
|
||||
message,
|
||||
detail: isWindows ? nls.localize('undoBin', "You can restore from the Recycle Bin.") : nls.localize('undoTrash', "You can restore from the Trash."),
|
||||
primaryButton,
|
||||
checkbox: {
|
||||
label: nls.localize('doNotAskAgain', "Do not ask me again")
|
||||
},
|
||||
type: 'question'
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm for deleting permanently
|
||||
else {
|
||||
const message = getDeleteMessage(distinctElements);
|
||||
confirmDeletePromise = dialogService.confirm({
|
||||
message,
|
||||
detail: nls.localize('irreversible', "This action is irreversible!"),
|
||||
primaryButton,
|
||||
type: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
return confirmDeletePromise.then(confirmation => {
|
||||
|
||||
// Check for confirmation checkbox
|
||||
let updateConfirmSettingsPromise: Promise<void> = Promise.resolve(undefined);
|
||||
if (confirmation.confirmed && confirmation.checkboxChecked === true) {
|
||||
updateConfirmSettingsPromise = configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
return updateConfirmSettingsPromise.then(() => {
|
||||
|
||||
// Check for confirmation
|
||||
if (!confirmation.confirmed) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Call function
|
||||
const servicePromise = Promise.all(distinctElements.map(e => fileService.del(e.resource, { useTrash: useTrash, recursive: true })))
|
||||
.then(undefined, (error: any) => {
|
||||
// Handle error to delete file(s) from a modal confirmation dialog
|
||||
let errorMessage: string;
|
||||
let detailMessage: string | undefined;
|
||||
let primaryButton: string;
|
||||
if (useTrash) {
|
||||
errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?");
|
||||
detailMessage = nls.localize('irreversible', "This action is irreversible!");
|
||||
primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently");
|
||||
} else {
|
||||
errorMessage = toErrorMessage(error, false);
|
||||
primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry");
|
||||
}
|
||||
|
||||
return dialogService.confirm({
|
||||
message: errorMessage,
|
||||
detail: detailMessage,
|
||||
type: 'warning',
|
||||
primaryButton
|
||||
}).then(res => {
|
||||
|
||||
if (res.confirmed) {
|
||||
if (useTrash) {
|
||||
useTrash = false; // Delete Permanently
|
||||
}
|
||||
|
||||
skipConfirm = true;
|
||||
|
||||
return deleteFiles(serviceAccesor, elements, useTrash, skipConfirm);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return servicePromise;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getMoveToTrashMessage(distinctElements: ExplorerItem[]): string {
|
||||
if (containsBothDirectoryAndFile(distinctElements)) {
|
||||
return getConfirmMessage(nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
|
||||
if (distinctElements.length > 1) {
|
||||
if (distinctElements[0].isDirectory) {
|
||||
return nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name);
|
||||
return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
|
||||
return nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name);
|
||||
return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
|
||||
private getDeleteMessage(distinctElements: ExplorerItem[]): string {
|
||||
if (this.containsBothDirectoryAndFile(distinctElements)) {
|
||||
return getConfirmMessage(nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
if (distinctElements[0].isDirectory) {
|
||||
return nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name);
|
||||
}
|
||||
|
||||
if (distinctElements.length > 1) {
|
||||
if (distinctElements[0].isDirectory) {
|
||||
return getConfirmMessage(nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
return nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name);
|
||||
}
|
||||
|
||||
return getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
function getDeleteMessage(distinctElements: ExplorerItem[]): string {
|
||||
if (containsBothDirectoryAndFile(distinctElements)) {
|
||||
return getConfirmMessage(nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
|
||||
if (distinctElements.length > 1) {
|
||||
if (distinctElements[0].isDirectory) {
|
||||
return nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name);
|
||||
return getConfirmMessage(nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
|
||||
return nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name);
|
||||
return getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource));
|
||||
}
|
||||
|
||||
private containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean {
|
||||
const directories = distinctElements.filter(element => element.isDirectory);
|
||||
const files = distinctElements.filter(element => !element.isDirectory);
|
||||
|
||||
return directories.length > 0 && files.length > 0;
|
||||
if (distinctElements[0].isDirectory) {
|
||||
return nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name);
|
||||
}
|
||||
|
||||
return nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name);
|
||||
}
|
||||
|
||||
function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean {
|
||||
const directories = distinctElements.filter(element => element.isDirectory);
|
||||
const files = distinctElements.filter(element => !element.isDirectory);
|
||||
|
||||
return directories.length > 0 && files.length > 0;
|
||||
}
|
||||
|
||||
let pasteShouldMove = false;
|
||||
// Paste File/Folder
|
||||
class PasteFileAction extends BaseErrorReportingAction {
|
||||
class PasteFileAction extends Action {
|
||||
|
||||
public static readonly ID = 'filesExplorer.paste';
|
||||
|
||||
constructor(
|
||||
private element: ExplorerItem,
|
||||
@IFileService private fileService: IFileService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService
|
||||
) {
|
||||
super(PasteFileAction.ID, PASTE_FILE_LABEL, notificationService);
|
||||
super(PasteFileAction.ID, PASTE_FILE_LABEL);
|
||||
|
||||
if (!this.element) {
|
||||
this.element = this.explorerService.roots[0];
|
||||
@@ -528,9 +408,9 @@ class PasteFileAction extends BaseErrorReportingAction {
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, e => this.onError(e));
|
||||
}, e => onError(this.notificationService, e));
|
||||
}, error => {
|
||||
this.onError(new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile")));
|
||||
onError(this.notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile")));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -701,7 +581,7 @@ export class ToggleAutoSaveAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseSaveAllAction extends BaseErrorReportingAction {
|
||||
export abstract class BaseSaveAllAction extends Action {
|
||||
private toDispose: IDisposable[];
|
||||
private lastIsDirty: boolean;
|
||||
|
||||
@@ -711,9 +591,9 @@ export abstract class BaseSaveAllAction extends BaseErrorReportingAction {
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
|
||||
@ICommandService protected commandService: ICommandService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
) {
|
||||
super(id, label, notificationService);
|
||||
super(id, label);
|
||||
|
||||
this.toDispose = [];
|
||||
this.lastIsDirty = this.textFileService.isDirty();
|
||||
@@ -747,7 +627,7 @@ export abstract class BaseSaveAllAction extends BaseErrorReportingAction {
|
||||
|
||||
public run(context?: any): Promise<boolean> {
|
||||
return this.doRun(context).then(() => true, error => {
|
||||
this.onError(error);
|
||||
onError(this.notificationService, error);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -918,7 +798,7 @@ export class ShowOpenedFileInNewWindow extends Action {
|
||||
const fileResource = toResource(this.editorService.activeEditor, { supportSideBySide: true });
|
||||
if (fileResource) {
|
||||
if (this.fileService.canHandleResource(fileResource)) {
|
||||
this.windowService.openWindow([{ uri: fileResource, typeHint: 'file' }], { forceNewWindow: true, forceOpenWorkspaceAsFile: true });
|
||||
this.windowService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true });
|
||||
} else {
|
||||
this.notificationService.info(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource."));
|
||||
}
|
||||
@@ -1002,7 +882,7 @@ export class CompareWithClipboardAction extends Action {
|
||||
|
||||
private static readonly SCHEME = 'clipboardCompare';
|
||||
|
||||
private registrationDisposal: IDisposable;
|
||||
private registrationDisposal: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
@@ -1029,7 +909,8 @@ export class CompareWithClipboardAction extends Action {
|
||||
const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name);
|
||||
|
||||
return this.editorService.openEditor({ leftResource: resource.with({ scheme: CompareWithClipboardAction.SCHEME }), rightResource: resource, label: editorLabel }).finally(() => {
|
||||
this.registrationDisposal = dispose(this.registrationDisposal);
|
||||
dispose(this.registrationDisposal);
|
||||
this.registrationDisposal = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1039,7 +920,8 @@ export class CompareWithClipboardAction extends Action {
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.registrationDisposal = dispose(this.registrationDisposal);
|
||||
dispose(this.registrationDisposal);
|
||||
this.registrationDisposal = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,43 +955,82 @@ function getContext(listWidget: ListWidget): IExplorerContext {
|
||||
return { stat, selection: selection && typeof stat !== 'undefined' && selection.indexOf(stat) >= 0 ? selection : [] };
|
||||
}
|
||||
|
||||
// TODO@isidor these commands are calling into actions due to the complex inheritance action structure.
|
||||
// It should be the other way around, that actions call into commands.
|
||||
function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature1<() => ExplorerItem, Action>): Promise<any> {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
function onErrorWithRetry(notificationService: INotificationService, error: any, retry: () => Promise<any>): void {
|
||||
notificationService.prompt(Severity.Error, toErrorMessage(error, false),
|
||||
[{
|
||||
label: nls.localize('retry', "Retry"),
|
||||
run: () => retry()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {
|
||||
const listService = accessor.get(IListService);
|
||||
const explorerService = accessor.get(IExplorerService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const viewletService = accessor.get(IViewletService);
|
||||
const activeViewlet = viewletService.getActiveViewlet();
|
||||
let explorerPromise = Promise.resolve(activeViewlet);
|
||||
if (!activeViewlet || activeViewlet.getId() !== VIEWLET_ID) {
|
||||
explorerPromise = viewletService.openViewlet(VIEWLET_ID, true);
|
||||
if (!activeViewlet || activeViewlet.getId() !== VIEWLET_ID || !listService.lastFocusedList) {
|
||||
await viewletService.openViewlet(VIEWLET_ID, true);
|
||||
}
|
||||
|
||||
return explorerPromise.then((explorer: ExplorerViewlet) => {
|
||||
const explorerView = explorer.getExplorerView();
|
||||
if (explorerView && explorerView.isBodyVisible() && listService.lastFocusedList) {
|
||||
explorerView.focus();
|
||||
const { stat } = getContext(listService.lastFocusedList);
|
||||
const action = instantiationService.createInstance(constructor, () => stat);
|
||||
|
||||
return action.run();
|
||||
const list = listService.lastFocusedList;
|
||||
if (list) {
|
||||
const { stat } = getContext(list);
|
||||
let folder: ExplorerItem;
|
||||
if (stat) {
|
||||
folder = stat.isDirectory ? stat : stat.parent!;
|
||||
} else {
|
||||
folder = explorerService.roots[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
if (folder.isReadonly) {
|
||||
throw new Error('Parent folder is readonly.');
|
||||
}
|
||||
|
||||
const newStat = new NewExplorerItem(folder, isFolder);
|
||||
await folder.fetchChildren(fileService, explorerService);
|
||||
|
||||
folder.addChild(newStat);
|
||||
|
||||
const onSuccess = async (value: string) => {
|
||||
const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : fileService.createFile(resources.joinPath(folder.resource, value));
|
||||
return createPromise.then(created => {
|
||||
refreshIfSeparator(value, explorerService);
|
||||
return isFolder ? explorerService.select(created.resource, true)
|
||||
: editorService.openEditor({ resource: created.resource, options: { pinned: true } }).then(() => undefined);
|
||||
}, (error) => {
|
||||
onErrorWithRetry(accessor.get(INotificationService), error, () => onSuccess(value));
|
||||
});
|
||||
};
|
||||
|
||||
explorerService.setEditable(newStat, {
|
||||
validationMessage: value => validateFileName(newStat, value),
|
||||
onFinish: (value, success) => {
|
||||
folder.removeChild(newStat);
|
||||
explorerService.setEditable(newStat, null);
|
||||
if (success) {
|
||||
onSuccess(value);
|
||||
} else {
|
||||
explorerService.select(folder.resource).then(undefined, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: NEW_FILE_COMMAND_ID,
|
||||
handler: (accessor) => {
|
||||
return openExplorerAndRunAction(accessor, NewFileAction);
|
||||
openExplorerAndCreate(accessor, false).then(undefined, onUnexpectedError);
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: NEW_FOLDER_COMMAND_ID,
|
||||
handler: (accessor) => {
|
||||
return openExplorerAndRunAction(accessor, NewFolderAction);
|
||||
openExplorerAndCreate(accessor, true).then(undefined, onUnexpectedError);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1140,29 +1061,25 @@ export const renameHandler = (accessor: ServicesAccessor) => {
|
||||
};
|
||||
|
||||
export const moveFileToTrashHandler = (accessor: ServicesAccessor) => {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
const listService = accessor.get(IListService);
|
||||
if (!listService.lastFocusedList) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const explorerContext = getContext(listService.lastFocusedList);
|
||||
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];
|
||||
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!];
|
||||
|
||||
const moveFileToTrashAction = instantiationService.createInstance(BaseDeleteFileAction, stats, true);
|
||||
return moveFileToTrashAction.run();
|
||||
return deleteFiles(accessor, stats, true);
|
||||
};
|
||||
|
||||
export const deleteFileHandler = (accessor: ServicesAccessor) => {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
const listService = accessor.get(IListService);
|
||||
if (!listService.lastFocusedList) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const explorerContext = getContext(listService.lastFocusedList);
|
||||
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];
|
||||
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!];
|
||||
|
||||
const deleteFileAction = instantiationService.createInstance(BaseDeleteFileAction, stats, false);
|
||||
return deleteFileAction.run();
|
||||
return deleteFiles(accessor, stats, false);
|
||||
};
|
||||
|
||||
export const copyFileHandler = (accessor: ServicesAccessor) => {
|
||||
|
||||
@@ -315,11 +315,6 @@ configurationRegistry.registerConfiguration({
|
||||
],
|
||||
'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE)
|
||||
},
|
||||
'files.useExperimentalFileWatcher': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': nls.localize('useExperimentalFileWatcher', "Use the new experimental file watcher.")
|
||||
},
|
||||
'files.defaultLanguage': {
|
||||
'type': 'string',
|
||||
'description': nls.localize('defaultLanguage', "The default language mode that is assigned to new files.")
|
||||
|
||||
@@ -14,14 +14,14 @@ import { coalesce } from 'vs/base/common/arrays';
|
||||
|
||||
// Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding
|
||||
// To cover all these cases we need to properly compute the resource on which the command is being executed
|
||||
export function getResourceForCommand(resource: URI | object, listService: IListService, editorService: IEditorService): URI | null {
|
||||
export function getResourceForCommand(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): URI | null {
|
||||
if (URI.isUri(resource)) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
let list = listService.lastFocusedList;
|
||||
if (list && list.getHTMLElement() === document.activeElement) {
|
||||
let focus: any;
|
||||
let focus: unknown;
|
||||
if (list instanceof List) {
|
||||
const focused = list.getFocusedElements();
|
||||
if (focused.length) {
|
||||
@@ -44,7 +44,7 @@ export function getResourceForCommand(resource: URI | object, listService: IList
|
||||
return editorService.activeEditor ? toResource(editorService.activeEditor, { supportSideBySide: true }) : null;
|
||||
}
|
||||
|
||||
export function getMultiSelectedResources(resource: URI | object, listService: IListService, editorService: IEditorService): Array<URI> {
|
||||
export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): Array<URI> {
|
||||
const list = listService.lastFocusedList;
|
||||
if (list && list.getHTMLElement() === document.activeElement) {
|
||||
// Explorer
|
||||
|
||||
@@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import * as perf from 'vs/base/common/performance';
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut } from 'vs/workbench/contrib/files/common/files';
|
||||
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut, ExplorerResourceMoveableToTrash } from 'vs/workbench/contrib/files/common/files';
|
||||
import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView } from 'vs/workbench/contrib/files/browser/fileActions';
|
||||
import { toResource } from 'vs/workbench/common/editor';
|
||||
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
|
||||
@@ -39,7 +39,7 @@ import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemAc
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
|
||||
import { ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
import { createFileIconThemableTreeContainerScope } from 'vs/workbench/browser/parts/views/views';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree';
|
||||
@@ -49,6 +49,7 @@ import { isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { first } from 'vs/base/common/arrays';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
|
||||
export class ExplorerView extends ViewletPanel {
|
||||
static readonly ID: string = 'workbench.explorer.fileView';
|
||||
@@ -61,6 +62,7 @@ export class ExplorerView extends ViewletPanel {
|
||||
private folderContext: IContextKey<boolean>;
|
||||
private readonlyContext: IContextKey<boolean>;
|
||||
private rootContext: IContextKey<boolean>;
|
||||
private resourceMoveableToTrash: IContextKey<boolean>;
|
||||
|
||||
// Refresh is needed on the initial explorer open
|
||||
private shouldRefresh = true;
|
||||
@@ -85,7 +87,8 @@ export class ExplorerView extends ViewletPanel {
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IClipboardService private clipboardService: IClipboardService
|
||||
@IClipboardService private clipboardService: IClipboardService,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService);
|
||||
|
||||
@@ -94,6 +97,7 @@ export class ExplorerView extends ViewletPanel {
|
||||
this.folderContext = ExplorerFolderContext.bindTo(contextKeyService);
|
||||
this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService);
|
||||
this.rootContext = ExplorerRootContext.bindTo(contextKeyService);
|
||||
this.resourceMoveableToTrash = ExplorerResourceMoveableToTrash.bindTo(contextKeyService);
|
||||
|
||||
const decorationProvider = new ExplorerDecorationsProvider(this.explorerService, contextService);
|
||||
decorationService.registerDecorationsProvider(decorationProvider);
|
||||
@@ -217,12 +221,8 @@ export class ExplorerView extends ViewletPanel {
|
||||
getActions(): IAction[] {
|
||||
const actions: Action[] = [];
|
||||
|
||||
const getFocus = () => {
|
||||
const focus = this.tree.getFocus();
|
||||
return focus.length > 0 ? focus[0] : undefined;
|
||||
};
|
||||
actions.push(this.instantiationService.createInstance(NewFileAction, getFocus));
|
||||
actions.push(this.instantiationService.createInstance(NewFolderAction, getFocus));
|
||||
actions.push(this.instantiationService.createInstance(NewFileAction));
|
||||
actions.push(this.instantiationService.createInstance(NewFolderAction));
|
||||
actions.push(this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL));
|
||||
actions.push(this.instantiationService.createInstance(CollapseAction, this.tree, true, 'explorer-action collapse-explorer'));
|
||||
|
||||
@@ -267,7 +267,7 @@ export class ExplorerView extends ViewletPanel {
|
||||
private createTree(container: HTMLElement): void {
|
||||
this.filter = this.instantiationService.createInstance(FilesFilter);
|
||||
this.disposables.push(this.filter);
|
||||
const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
|
||||
const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
|
||||
this.disposables.push(explorerLabels);
|
||||
|
||||
const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat);
|
||||
@@ -405,6 +405,14 @@ export class ExplorerView extends ViewletPanel {
|
||||
this.folderContext.set((isSingleFolder && !stat) || !!stat && stat.isDirectory);
|
||||
this.readonlyContext.set(!!stat && stat.isReadonly);
|
||||
this.rootContext.set(!stat || (stat && stat.isRoot));
|
||||
|
||||
if (stat) {
|
||||
const enableTrash = this.configurationService.getValue<IFilesConfiguration>().files.enableTrash;
|
||||
const hasCapability = this.fileService.hasCapability(stat.resource, FileSystemProviderCapabilities.Trash);
|
||||
this.resourceMoveableToTrash.set(enableTrash && hasCapability);
|
||||
} else {
|
||||
this.resourceMoveableToTrash.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// General methods
|
||||
@@ -462,7 +470,7 @@ export class ExplorerView extends ViewletPanel {
|
||||
} else {
|
||||
const rawViewState = this.storageService.get(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
if (rawViewState) {
|
||||
viewState = JSON.parse(rawViewState) as IAsyncDataTreeViewState;
|
||||
viewState = JSON.parse(rawViewState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
|
||||
|
||||
const excludesConfigCopy = deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods
|
||||
|
||||
this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression);
|
||||
this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
|
||||
});
|
||||
|
||||
return needsRefresh;
|
||||
@@ -334,7 +334,7 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.workspaceFolderChangeListener = dispose(this.workspaceFolderChangeListener);
|
||||
dispose(this.workspaceFolderChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +735,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
|
||||
primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
|
||||
});
|
||||
} else {
|
||||
confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult);
|
||||
confirmPromise = Promise.resolve({ confirmed: true });
|
||||
}
|
||||
|
||||
return confirmPromise.then(res => {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { WorkbenchList } from 'vs/platform/list/browser/listService';
|
||||
import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list';
|
||||
import { ResourceLabels, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
|
||||
import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
@@ -212,7 +212,7 @@ export class OpenEditorsView extends ViewletPanel {
|
||||
if (this.listLabels) {
|
||||
this.listLabels.clear();
|
||||
}
|
||||
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
|
||||
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
|
||||
this.list = this.instantiationService.createInstance(WorkbenchList, container, delegate, [
|
||||
new EditorGroupRenderer(this.keybindingService, this.instantiationService),
|
||||
new OpenEditorRenderer(this.listLabels, this.instantiationService, this.keybindingService, this.configurationService)
|
||||
|
||||
@@ -296,7 +296,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
matches(otherInput: any): boolean {
|
||||
matches(otherInput: unknown): boolean {
|
||||
if (super.matches(otherInput) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export class ExplorerModel implements IDisposable {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._listener = dispose(this._listener);
|
||||
dispose(this._listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ export class ExplorerItem {
|
||||
}
|
||||
|
||||
fetchChildren(fileService: IFileService, explorerService: IExplorerService): Promise<ExplorerItem[]> {
|
||||
let promise: Promise<any> = Promise.resolve(undefined);
|
||||
let promise: Promise<unknown> = Promise.resolve(undefined);
|
||||
if (!this._isDirectoryResolved) {
|
||||
// Resolve metadata only when the mtime is needed since this can be expensive
|
||||
// Mtime is only used when the sort order is 'modified'
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Schemas } from 'vs/base/common/network';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
/**
|
||||
* Explorer viewlet id.
|
||||
@@ -67,30 +68,20 @@ export const IExplorerService = createDecorator<IExplorerService>('explorerServi
|
||||
/**
|
||||
* Context Keys to use with keybindings for the Explorer and Open Editors view
|
||||
*/
|
||||
const explorerViewletVisibleId = 'explorerViewletVisible';
|
||||
const filesExplorerFocusId = 'filesExplorerFocus';
|
||||
const openEditorsVisibleId = 'openEditorsVisible';
|
||||
const openEditorsFocusId = 'openEditorsFocus';
|
||||
const explorerViewletFocusId = 'explorerViewletFocus';
|
||||
const explorerResourceIsFolderId = 'explorerResourceIsFolder';
|
||||
const explorerResourceReadonly = 'explorerResourceReadonly';
|
||||
const explorerResourceIsRootId = 'explorerResourceIsRoot';
|
||||
const explorerResourceCutId = 'explorerResourceCut';
|
||||
|
||||
export const ExplorerViewletVisibleContext = new RawContextKey<boolean>(explorerViewletVisibleId, true);
|
||||
export const ExplorerFolderContext = new RawContextKey<boolean>(explorerResourceIsFolderId, false);
|
||||
export const ExplorerResourceReadonlyContext = new RawContextKey<boolean>(explorerResourceReadonly, false);
|
||||
export const ExplorerViewletVisibleContext = new RawContextKey<boolean>('explorerViewletVisible', true);
|
||||
export const ExplorerFolderContext = new RawContextKey<boolean>('explorerResourceIsFolder', false);
|
||||
export const ExplorerResourceReadonlyContext = new RawContextKey<boolean>('explorerResourceReadonly', false);
|
||||
export const ExplorerResourceNotReadonlyContext = ExplorerResourceReadonlyContext.toNegated();
|
||||
export const ExplorerRootContext = new RawContextKey<boolean>(explorerResourceIsRootId, false);
|
||||
export const ExplorerResourceCut = new RawContextKey<boolean>(explorerResourceCutId, false);
|
||||
export const FilesExplorerFocusedContext = new RawContextKey<boolean>(filesExplorerFocusId, true);
|
||||
export const OpenEditorsVisibleContext = new RawContextKey<boolean>(openEditorsVisibleId, false);
|
||||
export const OpenEditorsFocusedContext = new RawContextKey<boolean>(openEditorsFocusId, true);
|
||||
export const ExplorerFocusedContext = new RawContextKey<boolean>(explorerViewletFocusId, true);
|
||||
export const ExplorerRootContext = new RawContextKey<boolean>('explorerResourceIsRoot', false);
|
||||
export const ExplorerResourceCut = new RawContextKey<boolean>('explorerResourceCut', false);
|
||||
export const ExplorerResourceMoveableToTrash = new RawContextKey<boolean>('explorerResourceMoveableToTrash', false);
|
||||
export const FilesExplorerFocusedContext = new RawContextKey<boolean>('filesExplorerFocus', true);
|
||||
export const OpenEditorsVisibleContext = new RawContextKey<boolean>('openEditorsVisible', false);
|
||||
export const OpenEditorsFocusedContext = new RawContextKey<boolean>('openEditorsFocus', true);
|
||||
export const ExplorerFocusedContext = new RawContextKey<boolean>('explorerViewletFocus', true);
|
||||
|
||||
export const OpenEditorsVisibleCondition = ContextKeyExpr.has(openEditorsVisibleId);
|
||||
export const FilesExplorerFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(explorerViewletVisibleId), ContextKeyExpr.has(filesExplorerFocusId), ContextKeyExpr.not(InputFocusedContextKey));
|
||||
export const ExplorerFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(explorerViewletVisibleId), ContextKeyExpr.has(explorerViewletFocusId), ContextKeyExpr.not(InputFocusedContextKey));
|
||||
export const FilesExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, FilesExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey));
|
||||
export const ExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, ExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey));
|
||||
|
||||
/**
|
||||
* Text file editor id.
|
||||
@@ -141,7 +132,7 @@ export const SortOrderConfiguration = {
|
||||
export type SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified';
|
||||
|
||||
export class FileOnDiskContentProvider implements ITextModelContentProvider {
|
||||
private fileWatcher: IDisposable;
|
||||
private fileWatcherDisposable: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@@ -158,17 +149,17 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider {
|
||||
return this.resolveEditorModel(resource).then(codeEditorModel => {
|
||||
|
||||
// Make sure to keep contents on disk up to date when it changes
|
||||
if (!this.fileWatcher) {
|
||||
this.fileWatcher = this.fileService.onFileChanges(changes => {
|
||||
if (!this.fileWatcherDisposable) {
|
||||
this.fileWatcherDisposable = this.fileService.onFileChanges(changes => {
|
||||
if (changes.contains(fileOnDiskResource, FileChangeType.UPDATED)) {
|
||||
this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes
|
||||
}
|
||||
});
|
||||
|
||||
if (codeEditorModel) {
|
||||
const disposeListener = codeEditorModel.onWillDispose(() => {
|
||||
disposeListener.dispose();
|
||||
this.fileWatcher = dispose(this.fileWatcher);
|
||||
once(codeEditorModel.onWillDispose)(() => {
|
||||
dispose(this.fileWatcherDisposable);
|
||||
this.fileWatcherDisposable = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -204,7 +195,8 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.fileWatcher = dispose(this.fileWatcher);
|
||||
dispose(this.fileWatcherDisposable);
|
||||
this.fileWatcherDisposable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
@@ -28,6 +28,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
|
||||
type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider;
|
||||
|
||||
@@ -40,6 +41,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@IExtensionService private readonly _extensionService: IExtensionService,
|
||||
@IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService,
|
||||
@IConfigurationService private readonly _configService: IConfigurationService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IQuickInputService private readonly _quickInputService: IQuickInputService,
|
||||
@@ -83,18 +85,17 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution {
|
||||
if (defaultFormatter) {
|
||||
// formatter available
|
||||
return defaultFormatter;
|
||||
}
|
||||
|
||||
} else {
|
||||
// formatter gone
|
||||
const extension = await this._extensionService.getExtension(defaultFormatterId);
|
||||
// bad -> formatter gone
|
||||
const extension = await this._extensionService.getExtension(defaultFormatterId);
|
||||
if (extension && this._extensionEnablementService.isEnabled(toExtension(extension))) {
|
||||
// formatter does not target this file
|
||||
const label = this._labelService.getUriLabel(document.uri, { relative: true });
|
||||
const message = extension
|
||||
? nls.localize('miss', "Extension '{0}' cannot format '{1}'", extension.displayName || extension.name, label)
|
||||
: nls.localize('gone', "Extension '{0}' is configured as formatter but not available", defaultFormatterId);
|
||||
const message = nls.localize('miss', "Extension '{0}' cannot format '{1}'", extension.displayName || extension.name, label);
|
||||
this._statusbarService.setStatusMessage(message, 4000);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
} else if (formatter.length === 1) {
|
||||
// ok -> nothing configured but only one formatter available
|
||||
return formatter[0];
|
||||
@@ -102,7 +103,9 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId();
|
||||
const silent = mode === FormattingMode.Silent;
|
||||
const message = nls.localize('config.needed', "There are multiple formatters for {0}-files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName));
|
||||
const message = !defaultFormatterId
|
||||
? nls.localize('config.needed', "There are multiple formatters for {0}-files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName))
|
||||
: nls.localize('config.bad', "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", defaultFormatterId);
|
||||
|
||||
return new Promise<T | undefined>((resolve, reject) => {
|
||||
this._notificationService.prompt(
|
||||
|
||||
@@ -7,7 +7,7 @@ import { basename } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { IMarker, MarkerSeverity, IRelatedInformation, IMarkerData } from 'vs/platform/markers/common/markers';
|
||||
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
|
||||
import { isFalsyOrEmpty, mergeSort } from 'vs/base/common/arrays';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
@@ -146,7 +146,7 @@ export class MarkersModel {
|
||||
if (isFalsyOrEmpty(rawMarkers)) {
|
||||
this.resourcesByUri.delete(resource.toString());
|
||||
} else {
|
||||
const markers = rawMarkers.map(rawMarker => {
|
||||
const markers = mergeSort(rawMarkers.map(rawMarker => {
|
||||
let relatedInformation: RelatedInformation[] | undefined = undefined;
|
||||
|
||||
if (rawMarker.relatedInformation) {
|
||||
@@ -154,9 +154,7 @@ export class MarkersModel {
|
||||
}
|
||||
|
||||
return new Marker(rawMarker, relatedInformation);
|
||||
});
|
||||
|
||||
markers.sort(compareMarkers);
|
||||
}), compareMarkers);
|
||||
|
||||
this.resourcesByUri.set(resource.toString(), new ResourceMarkers(resource, markers));
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import { domEvent } from 'vs/base/browser/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
import { IMarker } from 'vs/platform/markers/common/markers';
|
||||
import { withUndefinedAsNull } from 'vs/base/common/types';
|
||||
|
||||
function createModelIterator(model: MarkersModel): Iterator<ITreeElement<TreeElement>> {
|
||||
const resourcesIt = Iterator.fromArray(model.resourceMarkers);
|
||||
@@ -459,7 +460,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController {
|
||||
|
||||
private setCurrentActiveEditor(): void {
|
||||
const activeEditor = this.editorService.activeEditor;
|
||||
this.currentActiveResource = activeEditor ? activeEditor.getResource() : null;
|
||||
this.currentActiveResource = activeEditor ? withUndefinedAsNull(activeEditor.getResource()) : null;
|
||||
}
|
||||
|
||||
private onSelected(): void {
|
||||
|
||||
@@ -601,7 +601,7 @@ export class MarkerViewModel extends Disposable {
|
||||
return Promise.resolve(model);
|
||||
}
|
||||
if (waitForModel) {
|
||||
if (this.modelPromise === null) {
|
||||
if (!this.modelPromise) {
|
||||
this.modelPromise = createCancelablePromise(cancellationToken => {
|
||||
return new Promise((c) => {
|
||||
this._register(this.modelService.onModelAdded(model => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export class OutputLinkProvider {
|
||||
|
||||
private worker?: MonacoWebWorker<OutputLinkComputer>;
|
||||
private disposeWorkerScheduler: RunOnceScheduler;
|
||||
private linkProviderRegistration: IDisposable;
|
||||
private linkProviderRegistration: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@@ -48,7 +48,8 @@ export class OutputLinkProvider {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.linkProviderRegistration = dispose(this.linkProviderRegistration);
|
||||
dispose(this.linkProviderRegistration);
|
||||
this.linkProviderRegistration = undefined;
|
||||
}
|
||||
|
||||
// Dispose worker to recreate with folders on next provideLinks request
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { dirname, join, basename } from 'vs/base/common/path';
|
||||
import { del, exists, readdir, readFile } from 'vs/base/node/pfs';
|
||||
import { exists, readdir, readFile, rimraf } from 'vs/base/node/pfs';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
@@ -50,7 +50,7 @@ export class StartupProfiler implements IWorkbenchContribution {
|
||||
|
||||
const removeArgs: string[] = ['--prof-startup'];
|
||||
const markerFile = readFile(profileFilenamePrefix).then(value => removeArgs.push(...value.toString().split('|')))
|
||||
.then(() => del(profileFilenamePrefix)) // (1) delete the file to tell the main process to stop profiling
|
||||
.then(() => rimraf(profileFilenamePrefix)) // (1) delete the file to tell the main process to stop profiling
|
||||
.then(() => new Promise(resolve => { // (2) wait for main that recreates the fail to signal profiling has stopped
|
||||
const check = () => {
|
||||
exists(profileFilenamePrefix).then(exists => {
|
||||
@@ -63,7 +63,7 @@ export class StartupProfiler implements IWorkbenchContribution {
|
||||
};
|
||||
check();
|
||||
}))
|
||||
.then(() => del(profileFilenamePrefix)); // (3) finally delete the file again
|
||||
.then(() => rimraf(profileFilenamePrefix)); // (3) finally delete the file again
|
||||
|
||||
markerFile.then(() => {
|
||||
return readdir(dir).then(files => files.filter(value => value.indexOf(prefix) === 0));
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { appendFile } from 'fs';
|
||||
import { nfcall, timeout } from 'vs/base/common/async';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { promisify } from 'util';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -59,7 +60,7 @@ export class StartupTimings implements IWorkbenchContribution {
|
||||
}
|
||||
|
||||
private async _appendStartupTimes(isStandardStartup: boolean) {
|
||||
let appendTo = this._envService.args['prof-append-timers'];
|
||||
const appendTo = this._envService.args['prof-append-timers'];
|
||||
if (!appendTo) {
|
||||
// nothing to do
|
||||
return;
|
||||
@@ -71,7 +72,7 @@ export class StartupTimings implements IWorkbenchContribution {
|
||||
this._timerService.startupMetrics,
|
||||
timeout(15000), // wait: cached data creation, telemetry sending
|
||||
]).then(([startupMetrics]) => {
|
||||
return nfcall(appendFile, appendTo, `${startupMetrics.ellapsed}\t${product.nameShort}\t${(product.commit || '').slice(0, 10) || '0000000000'}\t${sessionId}\t${isStandardStartup ? 'standard_start' : 'NO_standard_start'}\n`);
|
||||
return promisify(appendFile)(appendTo, `${startupMetrics.ellapsed}\t${product.nameShort}\t${(product.commit || '').slice(0, 10) || '0000000000'}\t${sessionId}\t${isStandardStartup ? 'standard_start' : 'NO_standard_start'}\n`);
|
||||
}).then(() => {
|
||||
this._windowsService.quit();
|
||||
}).catch(err => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { SearchWidget, SearchOptions } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
export interface KeybindingsSearchOptions extends SearchOptions {
|
||||
recordEnter?: boolean;
|
||||
@@ -257,7 +258,7 @@ export class DefineKeybindingWidget extends Widget {
|
||||
this._chordPart = chordPart;
|
||||
dom.clearNode(this._outputNode);
|
||||
dom.clearNode(this._showExistingKeybindingsNode);
|
||||
new KeybindingLabel(this._outputNode, OS).set(this._firstPart);
|
||||
new KeybindingLabel(this._outputNode, OS).set(withNullAsUndefined(this._firstPart));
|
||||
if (this._chordPart) {
|
||||
this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to")));
|
||||
new KeybindingLabel(this._outputNode, OS).set(this._chordPart);
|
||||
|
||||
@@ -330,6 +330,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor
|
||||
this._register(this.searchWidget.onDidChange(searchValue => {
|
||||
clearInputAction.enabled = !!searchValue;
|
||||
this.delayedFiltering.trigger(() => this.filterKeybindings());
|
||||
this.updateSearchOptions();
|
||||
}));
|
||||
this._register(this.searchWidget.onEscape(() => this.recordKeysAction.checked = false));
|
||||
|
||||
@@ -344,6 +345,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor
|
||||
if (e.checked !== undefined) {
|
||||
this.renderKeybindingsEntries(false);
|
||||
}
|
||||
this.updateSearchOptions();
|
||||
}));
|
||||
|
||||
const recordKeysActionKeybinding = this.keybindingsService.lookupKeybinding(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS);
|
||||
@@ -364,6 +366,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor
|
||||
this.searchWidget.stopRecordingKeys();
|
||||
this.searchWidget.focus();
|
||||
}
|
||||
this.updateSearchOptions();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -383,6 +386,17 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor
|
||||
this.actionBar.push([this.recordKeysAction, this.sortByPrecedenceAction, clearInputAction], { label: false, icon: true });
|
||||
}
|
||||
|
||||
private updateSearchOptions(): void {
|
||||
const keybindingsEditorInput = this.input as KeybindingsEditorInput;
|
||||
if (keybindingsEditorInput) {
|
||||
keybindingsEditorInput.searchOptions = {
|
||||
searchValue: this.searchWidget.getValue(),
|
||||
recordKeybindings: !!this.recordKeysAction.checked,
|
||||
sortByPrecedence: !!this.sortByPrecedenceAction.checked
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private createRecordingBadge(container: HTMLElement): HTMLElement {
|
||||
const recordingBadge = DOM.append(container, DOM.$('.recording-badge.disabled'));
|
||||
recordingBadge.textContent = localize('recording', "Recording Keys");
|
||||
@@ -482,6 +496,13 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor
|
||||
}, {});
|
||||
await this.keybindingsEditorModel.resolve(editorActionsLabels);
|
||||
this.renderKeybindingsEntries(false, preserveFocus);
|
||||
if (input.searchOptions) {
|
||||
this.recordKeysAction.checked = input.searchOptions.recordKeybindings;
|
||||
this.sortByPrecedenceAction.checked = input.searchOptions.sortByPrecedence;
|
||||
this.searchWidget.setValue(input.searchOptions.searchValue);
|
||||
} else {
|
||||
this.updateSearchOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,7 +897,7 @@ class ActionsColumn extends Column {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.actionBar = dispose(this.actionBar);
|
||||
dispose(this.actionBar);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ export class PreferencesEditor extends BaseEditor {
|
||||
this.sideBySidePreferencesWidget.layout(new DOM.Dimension(dimension.width, dimension.height - headerHeight));
|
||||
}
|
||||
|
||||
getControl(): IEditorControl | null {
|
||||
getControl(): IEditorControl | undefined {
|
||||
return this.sideBySidePreferencesWidget.getControl();
|
||||
}
|
||||
|
||||
@@ -890,8 +890,8 @@ class SideBySidePreferencesWidget extends Widget {
|
||||
}
|
||||
}
|
||||
|
||||
getControl(): IEditorControl | null {
|
||||
return this.editablePreferencesEditor ? this.editablePreferencesEditor.getControl() : null;
|
||||
getControl(): IEditorControl | undefined {
|
||||
return this.editablePreferencesEditor ? this.editablePreferencesEditor.getControl() : undefined;
|
||||
}
|
||||
|
||||
clearInput(): void {
|
||||
|
||||
@@ -53,7 +53,7 @@ export class PreferencesContribution implements IWorkbenchContribution {
|
||||
private handleSettingsEditorOverride(): void {
|
||||
|
||||
// dispose any old listener we had
|
||||
this.editorOpeningListener = dispose(this.editorOpeningListener);
|
||||
dispose(this.editorOpeningListener);
|
||||
|
||||
// install editor opening listener unless user has disabled this
|
||||
if (!!this.configurationService.getValue(USE_SPLIT_JSON_SETTING)) {
|
||||
@@ -144,7 +144,7 @@ export class PreferencesContribution implements IWorkbenchContribution {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.editorOpeningListener = dispose(this.editorOpeningListener);
|
||||
this.settingsListener = dispose(this.settingsListener);
|
||||
dispose(this.editorOpeningListener);
|
||||
dispose(this.settingsListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
|
||||
import { registerEditorAction, EditorAction, IEditorCommandMenuOptions } from 'vs/editor/browser/editorExtensions';
|
||||
import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { LRUCache } from 'vs/base/common/map';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -145,7 +145,7 @@ export class ShowAllCommandsAction extends Action {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(context?: any): Promise<void> {
|
||||
run(): Promise<void> {
|
||||
const config = <IWorkbenchQuickOpenConfiguration>this.configurationService.getValue();
|
||||
const restoreInput = config.workbench && config.workbench.commandPalette && config.workbench.commandPalette.preserveInput === true;
|
||||
|
||||
@@ -174,7 +174,7 @@ export class ClearCommandHistoryAction extends Action {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(context?: any): Promise<void> {
|
||||
run(): Promise<void> {
|
||||
const commandHistoryLength = resolveCommandHistory(this.configurationService);
|
||||
if (commandHistoryLength > 0) {
|
||||
commandHistory = new LRUCache<string, number>(commandHistoryLength);
|
||||
@@ -196,7 +196,7 @@ class CommandPaletteEditorAction extends EditorAction {
|
||||
menuOpts: {
|
||||
group: 'z_commands',
|
||||
order: 1
|
||||
} as IEditorCommandMenuOptions
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,12 @@ import { isEqual } from 'vs/base/common/resources';
|
||||
import { isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
|
||||
interface IConfiguration extends IWindowsConfiguration {
|
||||
update: { mode: string; };
|
||||
telemetry: { enableCrashReporter: boolean };
|
||||
keyboard: { touchbar: { enabled: boolean } };
|
||||
workbench: { list: { horizontalScrolling: boolean }, useExperimentalGridLayout: boolean };
|
||||
files: { useExperimentalFileWatcher: boolean, watcherExclude: object };
|
||||
}
|
||||
|
||||
export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution {
|
||||
@@ -38,8 +36,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
|
||||
private enableCrashReporter: boolean;
|
||||
private touchbarEnabled: boolean;
|
||||
private treeHorizontalScrolling: boolean;
|
||||
private experimentalFileWatcher: boolean;
|
||||
private fileWatcherExclude: object;
|
||||
private useGridLayout: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -47,8 +43,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
|
||||
@IWindowService private readonly windowService: IWindowService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IEnvironmentService private readonly envService: IEnvironmentService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IDialogService private readonly dialogService: IDialogService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -96,20 +91,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Experimental File Watcher
|
||||
if (config.files && typeof config.files.useExperimentalFileWatcher === 'boolean' && config.files.useExperimentalFileWatcher !== this.experimentalFileWatcher) {
|
||||
this.experimentalFileWatcher = config.files.useExperimentalFileWatcher;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// File Watcher Excludes (only if in folder workspace mode)
|
||||
if (!this.experimentalFileWatcher && this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
|
||||
if (config.files && typeof config.files.watcherExclude === 'object' && !equals(config.files.watcherExclude, this.fileWatcherExclude)) {
|
||||
this.fileWatcherExclude = config.files.watcherExclude;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// macOS: Touchbar config
|
||||
if (isMacintosh && config.keyboard && config.keyboard.touchbar && typeof config.keyboard.touchbar.enabled === 'boolean' && config.keyboard.touchbar.enabled !== this.touchbarEnabled) {
|
||||
this.touchbarEnabled = config.keyboard.touchbar.enabled;
|
||||
@@ -164,7 +145,7 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor
|
||||
private firstFolderResource?: URI;
|
||||
private extensionHostRestarter: RunOnceScheduler;
|
||||
|
||||
private onDidChangeWorkspaceFoldersUnbind: IDisposable;
|
||||
private onDidChangeWorkspaceFoldersUnbind: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@@ -216,7 +197,8 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor
|
||||
|
||||
// Ignore the workspace folder changes in EMPTY or FOLDER state
|
||||
else {
|
||||
this.onDidChangeWorkspaceFoldersUnbind = dispose(this.onDidChangeWorkspaceFoldersUnbind);
|
||||
dispose(this.onDidChangeWorkspaceFoldersUnbind);
|
||||
this.onDidChangeWorkspaceFoldersUnbind = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .scm-provider > .monaco-action-bar {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListEvent, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { VIEWLET_ID, ISCMService, ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType, VIEW_CONTAINER } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { ResourceLabels, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
|
||||
import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels';
|
||||
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -829,7 +829,7 @@ export class RepositoryPanel extends ViewletPanel {
|
||||
|
||||
const actionItemProvider = (action: IAction) => this.getActionItem(action);
|
||||
|
||||
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
|
||||
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
|
||||
this.disposables.push(this.listLabels);
|
||||
|
||||
const renderers = [
|
||||
|
||||
@@ -361,7 +361,7 @@ const searchInFolderCommand: ICommandHandler = (accessor, resource?: URI) => {
|
||||
const panelService = accessor.get(IPanelService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const configurationService = accessor.get(IConfigurationService);
|
||||
const resources = resource && getMultiSelectedResources(resource, listService, accessor.get(IEditorService));
|
||||
const resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService));
|
||||
|
||||
return openSearchView(viewletService, panelService, configurationService, true).then(searchView => {
|
||||
if (resources && resources.length && searchView) {
|
||||
|
||||
@@ -40,7 +40,7 @@ import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, edi
|
||||
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions';
|
||||
import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
|
||||
import { ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
import { IEditor } from 'vs/workbench/common/editor';
|
||||
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
|
||||
import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction } from 'vs/workbench/contrib/search/browser/searchActions';
|
||||
@@ -625,7 +625,7 @@ export class SearchView extends ViewletPanel {
|
||||
}
|
||||
};
|
||||
|
||||
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer));
|
||||
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }));
|
||||
this.tree = this._register(<WorkbenchObjectTree<RenderableMatch, any>>this.instantiationService.createInstance(WorkbenchObjectTree,
|
||||
this.resultsElement,
|
||||
delegate,
|
||||
|
||||
@@ -114,20 +114,16 @@ namespace snippetExt {
|
||||
}
|
||||
|
||||
function watch(service: IFileService, resource: URI, callback: (type: FileChangeType, resource: URI) => any): IDisposable {
|
||||
let listener = service.onFileChanges(e => {
|
||||
for (const change of e.changes) {
|
||||
if (resources.isEqualOrParent(change.resource, resource)) {
|
||||
callback(change.type, change.resource);
|
||||
return combinedDisposable([
|
||||
service.watch(resource),
|
||||
service.onFileChanges(e => {
|
||||
for (const change of e.changes) {
|
||||
if (resources.isEqualOrParent(change.resource, resource)) {
|
||||
callback(change.type, change.resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
service.watch(resource);
|
||||
return {
|
||||
dispose() {
|
||||
listener.dispose();
|
||||
service.unwatch(resource);
|
||||
}
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
class SnippetsService implements ISnippetsService {
|
||||
|
||||
@@ -558,7 +558,7 @@ export class WorkspaceStats implements IWorkbenchContribution {
|
||||
|
||||
this.notificationService.prompt(Severity.Info, localize('workspaceFound', "This folder contains a workspace file '{0}'. Do you want to open it? [Learn more]({1}) about workspace files.", workspaceFile, 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{
|
||||
label: localize('openWorkspace', "Open Workspace"),
|
||||
run: () => this.windowService.openWindow([{ uri: joinPath(folder, workspaceFile), typeHint: 'file' }])
|
||||
run: () => this.windowService.openWindow([{ workspaceUri: joinPath(folder, workspaceFile) }])
|
||||
}, doNotShowAgain]);
|
||||
}
|
||||
|
||||
@@ -571,7 +571,7 @@ export class WorkspaceStats implements IWorkbenchContribution {
|
||||
workspaces.map(workspace => ({ label: workspace } as IQuickPickItem)),
|
||||
{ placeHolder: localize('selectToOpen', "Select a workspace to open") }).then(pick => {
|
||||
if (pick) {
|
||||
this.windowService.openWindow([{ uri: joinPath(folder, pick.label), typeHint: 'file' }]);
|
||||
this.windowService.openWindow([{ workspaceUri: joinPath(folder, pick.label) }]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ interface MetaData<T, U> {
|
||||
}
|
||||
|
||||
|
||||
function _isEmpty<T>(this: void, value: T, properties: MetaData<T, any>[] | undefined): boolean {
|
||||
function _isEmpty<T>(this: void, value: T | undefined, properties: MetaData<T, any>[] | undefined): boolean {
|
||||
if (value === undefined || value === null || properties === undefined) {
|
||||
return true;
|
||||
}
|
||||
@@ -551,11 +551,11 @@ function _isEmpty<T>(this: void, value: T, properties: MetaData<T, any>[] | unde
|
||||
return true;
|
||||
}
|
||||
|
||||
function _assignProperties<T>(this: void, target: T, source: T, properties: MetaData<T, any>[]): T {
|
||||
if (_isEmpty(source, properties)) {
|
||||
function _assignProperties<T>(this: void, target: T | undefined, source: T | undefined, properties: MetaData<T, any>[]): T | undefined {
|
||||
if (!source || _isEmpty(source, properties)) {
|
||||
return target;
|
||||
}
|
||||
if (_isEmpty(target, properties)) {
|
||||
if (!target || _isEmpty(target, properties)) {
|
||||
return source;
|
||||
}
|
||||
for (let meta of properties) {
|
||||
@@ -573,11 +573,11 @@ function _assignProperties<T>(this: void, target: T, source: T, properties: Meta
|
||||
return target;
|
||||
}
|
||||
|
||||
function _fillProperties<T>(this: void, target: T, source: T, properties: MetaData<T, any>[] | undefined): T {
|
||||
if (_isEmpty(source, properties)) {
|
||||
function _fillProperties<T>(this: void, target: T | undefined, source: T | undefined, properties: MetaData<T, any>[] | undefined): T | undefined {
|
||||
if (!source || _isEmpty(source, properties)) {
|
||||
return target;
|
||||
}
|
||||
if (_isEmpty(target, properties)) {
|
||||
if (!target || _isEmpty(target, properties)) {
|
||||
return source;
|
||||
}
|
||||
for (let meta of properties!) {
|
||||
@@ -595,11 +595,11 @@ function _fillProperties<T>(this: void, target: T, source: T, properties: MetaDa
|
||||
return target;
|
||||
}
|
||||
|
||||
function _fillDefaults<T>(this: void, target: T, defaults: T, properties: MetaData<T, any>[], context: ParseContext): T | undefined {
|
||||
function _fillDefaults<T>(this: void, target: T | undefined, defaults: T | undefined, properties: MetaData<T, any>[], context: ParseContext): T | undefined {
|
||||
if (target && Object.isFrozen(target)) {
|
||||
return target;
|
||||
}
|
||||
if (target === undefined || target === null) {
|
||||
if (target === undefined || target === null || defaults === undefined || defaults === null) {
|
||||
if (defaults !== undefined && defaults !== null) {
|
||||
return Objects.deepClone(defaults);
|
||||
} else {
|
||||
@@ -715,7 +715,7 @@ namespace ShellConfiguration {
|
||||
return _assignProperties(target, source, properties);
|
||||
}
|
||||
|
||||
export function fillProperties(this: void, target: Tasks.ShellConfiguration, source: Tasks.ShellConfiguration): Tasks.ShellConfiguration {
|
||||
export function fillProperties(this: void, target: Tasks.ShellConfiguration, source: Tasks.ShellConfiguration): Tasks.ShellConfiguration | undefined {
|
||||
return _fillProperties(target, source, properties);
|
||||
}
|
||||
|
||||
@@ -1016,7 +1016,7 @@ namespace CommandConfiguration {
|
||||
return target;
|
||||
}
|
||||
|
||||
export function fillProperties(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration): Tasks.CommandConfiguration {
|
||||
export function fillProperties(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration): Tasks.CommandConfiguration | undefined {
|
||||
return _fillProperties(target, source, properties);
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,11 @@ configurationRegistry.registerConfiguration({
|
||||
description: nls.localize('terminal.integrated.windowsEnableConpty', "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.experimentalRefreshOnResume': {
|
||||
description: nls.localize('terminal.integrated.experimentalRefreshOnResume', "An experimental setting that will refresh the terminal renderer when the system is resumed."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -765,6 +765,14 @@ export class TerminalInstance implements ITerminalInstance {
|
||||
}
|
||||
|
||||
public forceRedraw(): void {
|
||||
if (this._configHelper.config.experimentalRefreshOnResume) {
|
||||
if (this._xterm.getOption('rendererType') !== 'dom') {
|
||||
this._xterm.setOption('rendererType', 'dom');
|
||||
// Do this asynchronously to clear our the texture atlas as all terminals will not
|
||||
// be using canvas
|
||||
setTimeout(() => this._xterm.setOption('rendererType', 'canvas'), 0);
|
||||
}
|
||||
}
|
||||
this._xterm.refresh(0, this._xterm.rows - 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface ITerminalConfiguration {
|
||||
experimentalBufferImpl: 'JsArray' | 'TypedArray';
|
||||
splitCwd: 'workspaceRoot' | 'initial' | 'inherited';
|
||||
windowsEnableConpty: boolean;
|
||||
experimentalRefreshOnResume: boolean;
|
||||
}
|
||||
|
||||
export interface ITerminalConfigHelper {
|
||||
|
||||
@@ -20,9 +20,9 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IRequestService } from 'vs/platform/request/node/request';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils';
|
||||
import { IWebviewEditorService } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorService';
|
||||
import { IWebviewEditorService } from 'vs/workbench/contrib/webview/browser/webviewEditorService';
|
||||
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput';
|
||||
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
|
||||
import { KeybindingParser } from 'vs/base/common/keybindingParser';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
@@ -3,56 +3,104 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// @ts-check
|
||||
(function () {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
// @ts-ignore
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
/**
|
||||
* Use polling to track focus of main webview and iframes within the webview
|
||||
*
|
||||
* @param {Object} handlers
|
||||
* @param {() => void} handlers.onFocus
|
||||
* @param {() => void} handlers.onBlur
|
||||
*/
|
||||
const trackFocus = ({ onFocus, onBlur }) => {
|
||||
const interval = 50;
|
||||
let isFocused = document.hasFocus();
|
||||
setInterval(() => {
|
||||
const isCurrentlyFocused = document.hasFocus();
|
||||
if (isCurrentlyFocused === isFocused) {
|
||||
return;
|
||||
}
|
||||
isFocused = isCurrentlyFocused;
|
||||
if (isCurrentlyFocused) {
|
||||
onFocus();
|
||||
} else {
|
||||
onBlur();
|
||||
}
|
||||
}, interval);
|
||||
};
|
||||
|
||||
const registerVscodeResourceScheme = (function () {
|
||||
let hasRegistered = false;
|
||||
return () => {
|
||||
if (hasRegistered) {
|
||||
return;
|
||||
}
|
||||
const getActiveFrame = () => {
|
||||
return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame'));
|
||||
};
|
||||
|
||||
hasRegistered = true;
|
||||
const getPendingFrame = () => {
|
||||
return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame'));
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
require('electron').webFrame.registerURLSchemeAsPrivileged('vscode-resource', {
|
||||
secure: true,
|
||||
bypassCSP: false,
|
||||
allowServiceWorkers: false,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true
|
||||
});
|
||||
};
|
||||
}());
|
||||
const defaultCssRules = `
|
||||
body {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-weight: var(--vscode-editor-font-weight);
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use polling to track focus of main webview and iframes within the webview
|
||||
*
|
||||
* @param {Object} handlers
|
||||
* @param {() => void} handlers.onFocus
|
||||
* @param {() => void} handlers.onBlur
|
||||
*/
|
||||
const trackFocus = ({ onFocus, onBlur }) => {
|
||||
const interval = 50;
|
||||
let isFocused = document.hasFocus();
|
||||
setInterval(() => {
|
||||
const isCurrentlyFocused = document.hasFocus();
|
||||
if (isCurrentlyFocused === isFocused) {
|
||||
return;
|
||||
}
|
||||
isFocused = isCurrentlyFocused;
|
||||
if (isCurrentlyFocused) {
|
||||
onFocus();
|
||||
} else {
|
||||
onBlur();
|
||||
}
|
||||
}, interval);
|
||||
};
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
a:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--vscode-textPreformat-foreground);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-color: var(--vscode-textBlockQuote-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}`;
|
||||
|
||||
/**
|
||||
* @typedef {{ postMessage: (channel: string, data?: any) => void, onMessage: (channel: string, handler: any) => void }} HostCommunications
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {HostCommunications} host
|
||||
*/
|
||||
module.exports = function createWebviewManager(host) {
|
||||
// state
|
||||
let firstLoad = true;
|
||||
let loadTimeout;
|
||||
@@ -82,14 +130,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const getActiveFrame = () => {
|
||||
return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame'));
|
||||
};
|
||||
|
||||
const getPendingFrame = () => {
|
||||
return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame'));
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
@@ -111,7 +151,7 @@
|
||||
scrollTarget.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
ipcRenderer.sendToHost('did-click-link', node.href);
|
||||
host.postMessage('did-click-link', node.href);
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
@@ -124,7 +164,7 @@
|
||||
* @param {KeyboardEvent} e
|
||||
*/
|
||||
const handleInnerKeydown = (e) => {
|
||||
ipcRenderer.sendToHost('did-keydown', {
|
||||
host.postMessage('did-keydown', {
|
||||
key: e.key,
|
||||
keyCode: e.keyCode,
|
||||
code: e.code,
|
||||
@@ -137,7 +177,7 @@
|
||||
};
|
||||
|
||||
const onMessage = (message) => {
|
||||
ipcRenderer.sendToHost(message.data.command, message.data.data);
|
||||
host.postMessage(message.data.command, message.data.data);
|
||||
};
|
||||
|
||||
let isHandlingScroll = false;
|
||||
@@ -154,7 +194,7 @@
|
||||
isHandlingScroll = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
try {
|
||||
ipcRenderer.sendToHost('did-scroll', progress);
|
||||
host.postMessage('did-scroll', progress);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
@@ -163,11 +203,7 @@
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ipcRenderer.on('baseUrl', (_event, value) => {
|
||||
initData.baseUrl = value;
|
||||
});
|
||||
|
||||
ipcRenderer.on('styles', (_event, variables, activeTheme) => {
|
||||
host.onMessage('styles', (_event, variables, activeTheme) => {
|
||||
initData.styles = variables;
|
||||
initData.activeTheme = activeTheme;
|
||||
|
||||
@@ -180,7 +216,7 @@
|
||||
});
|
||||
|
||||
// propagate focus
|
||||
ipcRenderer.on('focus', () => {
|
||||
host.onMessage('focus', () => {
|
||||
const target = getActiveFrame();
|
||||
if (target) {
|
||||
target.contentWindow.focus();
|
||||
@@ -188,11 +224,9 @@
|
||||
});
|
||||
|
||||
// update iframe-contents
|
||||
ipcRenderer.on('content', (_event, data) => {
|
||||
host.onMessage('content', (_event, data) => {
|
||||
const options = data.options;
|
||||
|
||||
registerVscodeResourceScheme();
|
||||
|
||||
const text = data.contents;
|
||||
const newDocument = new DOMParser().parseFromString(text, 'text/html');
|
||||
|
||||
@@ -202,13 +236,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// set base-url if applicable
|
||||
if (initData.baseUrl && newDocument.head.getElementsByTagName('base').length === 0) {
|
||||
const baseElement = newDocument.createElement('base');
|
||||
baseElement.href = initData.baseUrl;
|
||||
newDocument.head.appendChild(baseElement);
|
||||
}
|
||||
|
||||
// apply default script
|
||||
if (options.allowScripts) {
|
||||
const defaultScript = newDocument.createElement('script');
|
||||
@@ -299,7 +326,7 @@
|
||||
newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown);
|
||||
newFrame.contentWindow.onbeforeunload = () => {
|
||||
if (isInDevelopmentMode) { // Allow reloads while developing a webview
|
||||
ipcRenderer.sendToHost('do-reload');
|
||||
host.postMessage('do-reload');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -361,11 +388,11 @@
|
||||
newFrame.contentDocument.write(newDocument.documentElement.innerHTML);
|
||||
newFrame.contentDocument.close();
|
||||
|
||||
ipcRenderer.sendToHost('did-set-content');
|
||||
host.postMessage('did-set-content', undefined);
|
||||
});
|
||||
|
||||
// Forward message to the embedded iframe
|
||||
ipcRenderer.on('message', (_event, data) => {
|
||||
host.onMessage('message', (_event, data) => {
|
||||
const pending = getPendingFrame();
|
||||
if (!pending) {
|
||||
const target = getActiveFrame();
|
||||
@@ -377,79 +404,23 @@
|
||||
pendingMessages.push(data);
|
||||
});
|
||||
|
||||
ipcRenderer.on('initial-scroll-position', (_event, progress) => {
|
||||
host.onMessage('initial-scroll-position', (_event, progress) => {
|
||||
initData.initialScrollProgress = progress;
|
||||
});
|
||||
|
||||
ipcRenderer.on('devtools-opened', () => {
|
||||
host.onMessage('devtools-opened', () => {
|
||||
isInDevelopmentMode = true;
|
||||
});
|
||||
|
||||
trackFocus({
|
||||
onFocus: () => { ipcRenderer.sendToHost('did-focus'); },
|
||||
onBlur: () => { ipcRenderer.sendToHost('did-blur'); }
|
||||
onFocus: () => host.postMessage('did-focus'),
|
||||
onBlur: () => host.postMessage('did-blur')
|
||||
});
|
||||
|
||||
// Forward messages from the embedded iframe
|
||||
window.onmessage = onMessage;
|
||||
|
||||
// signal ready
|
||||
ipcRenderer.sendToHost('webview-ready', process.pid);
|
||||
host.postMessage('webview-ready', process.pid);
|
||||
});
|
||||
|
||||
const defaultCssRules = `
|
||||
body {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-weight: var(--vscode-editor-font-weight);
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
a:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--vscode-textPreformat-foreground);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-color: var(--vscode-textBlockQuote-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}`;
|
||||
}());
|
||||
};
|
||||
138
src/vs/workbench/contrib/webview/browser/webview.contribution.ts
Normal file
138
src/vs/workbench/contrib/webview/browser/webview.contribution.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { localize } from 'vs/nls';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
|
||||
import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory';
|
||||
import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/common/webview';
|
||||
import { CopyWebviewEditorCommand, CutWebviewEditorCommand, HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, PasteWebviewEditorCommand, RedoWebviewEditorCommand, ReloadWebviewAction, SelectAllWebviewEditorCommand, ShowWebViewEditorFindWidgetCommand, UndoWebviewEditorCommand } from '../browser/webviewCommands';
|
||||
import { WebviewEditor } from '../browser/webviewEditor';
|
||||
import { WebviewEditorInput } from '../browser/webviewEditorInput';
|
||||
import { IWebviewEditorService, WebviewEditorService } from '../browser/webviewEditorService';
|
||||
|
||||
(Registry.as<IEditorRegistry>(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(
|
||||
WebviewEditor,
|
||||
WebviewEditor.ID,
|
||||
localize('webview.editor.label', "webview editor")),
|
||||
[new SyncDescriptor(WebviewEditorInput)]);
|
||||
|
||||
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
|
||||
WebviewEditorInputFactory.ID,
|
||||
WebviewEditorInputFactory);
|
||||
|
||||
registerSingleton(IWebviewEditorService, WebviewEditorService, true);
|
||||
|
||||
|
||||
const webviewDeveloperCategory = localize('developer', "Developer");
|
||||
|
||||
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
|
||||
export function registerWebViewCommands(editorId: string): void {
|
||||
const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */);
|
||||
|
||||
const showNextFindWidgetCommand = new ShowWebViewEditorFindWidgetCommand({
|
||||
id: ShowWebViewEditorFindWidgetCommand.ID,
|
||||
precondition: contextKeyExpr,
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_F,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
showNextFindWidgetCommand.register();
|
||||
|
||||
(new HideWebViewEditorFindCommand({
|
||||
id: HideWebViewEditorFindCommand.ID,
|
||||
precondition: ContextKeyExpr.and(
|
||||
contextKeyExpr,
|
||||
KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE),
|
||||
kbOpts: {
|
||||
primary: KeyCode.Escape,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new SelectAllWebviewEditorCommand({
|
||||
id: SelectAllWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
// These commands are only needed on MacOS where we have to disable the menu bar commands
|
||||
if (isMacintosh) {
|
||||
(new CopyWebviewEditorCommand({
|
||||
id: CopyWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new PasteWebviewEditorCommand({
|
||||
id: PasteWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
|
||||
(new CutWebviewEditorCommand({
|
||||
id: CutWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_X,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new UndoWebviewEditorCommand({
|
||||
id: UndoWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_Z,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new RedoWebviewEditorCommand({
|
||||
id: RedoWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_Y,
|
||||
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z],
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
}
|
||||
}
|
||||
|
||||
registerWebViewCommands(WebviewEditor.ID);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL),
|
||||
'Webview Tools',
|
||||
webviewDeveloperCategory);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL),
|
||||
'Reload Webview',
|
||||
webviewDeveloperCategory);
|
||||
@@ -8,7 +8,7 @@ import { Command } from 'vs/editor/browser/editorExtensions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { WebviewEditor } from 'vs/workbench/contrib/webview/electron-browser/webviewEditor';
|
||||
import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor';
|
||||
|
||||
export class ShowWebViewEditorFindWidgetCommand extends Command {
|
||||
public static readonly ID = 'editor.action.webvieweditor.showFind';
|
||||
@@ -8,8 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
@@ -17,19 +16,15 @@ import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput';
|
||||
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
|
||||
import { IWebviewService, Webview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/common/webview';
|
||||
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { WebviewElement } from './webviewElement';
|
||||
|
||||
/** A context key that is set when the find widget in a webview is visible. */
|
||||
export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey<boolean>('webviewFindWidgetVisible', false);
|
||||
|
||||
|
||||
export class WebviewEditor extends BaseEditor {
|
||||
|
||||
protected _webview: WebviewElement | undefined;
|
||||
protected _webview: Webview | undefined;
|
||||
protected findWidgetVisible: IContextKey<boolean>;
|
||||
|
||||
public static readonly ID = 'WebviewEditor';
|
||||
@@ -50,9 +45,8 @@ export class WebviewEditor extends BaseEditor {
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextKeyService private _contextKeyService: IContextKeyService,
|
||||
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
|
||||
@IWebviewService private readonly _webviewService: IWebviewService,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@IStorageService storageService: IStorageService
|
||||
@@ -130,11 +124,11 @@ export class WebviewEditor extends BaseEditor {
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.withWebviewElement(webview => webview.reload());
|
||||
this.withWebview(webview => webview.reload());
|
||||
}
|
||||
|
||||
public layout(_dimension: DOM.Dimension): void {
|
||||
this.withWebviewElement(webview => {
|
||||
this.withWebview(webview => {
|
||||
this.doUpdateContainer();
|
||||
webview.layout();
|
||||
});
|
||||
@@ -151,34 +145,34 @@ export class WebviewEditor extends BaseEditor {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.withWebviewElement(webview => webview.focus());
|
||||
this.withWebview(webview => webview.focus());
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
this.withWebviewElement(webview => webview.selectAll());
|
||||
this.withWebview(webview => webview.selectAll());
|
||||
}
|
||||
|
||||
public copy(): void {
|
||||
this.withWebviewElement(webview => webview.copy());
|
||||
this.withWebview(webview => webview.copy());
|
||||
}
|
||||
|
||||
public paste(): void {
|
||||
this.withWebviewElement(webview => webview.paste());
|
||||
this.withWebview(webview => webview.paste());
|
||||
}
|
||||
|
||||
public cut(): void {
|
||||
this.withWebviewElement(webview => webview.cut());
|
||||
this.withWebview(webview => webview.cut());
|
||||
}
|
||||
|
||||
public undo(): void {
|
||||
this.withWebviewElement(webview => webview.undo());
|
||||
this.withWebview(webview => webview.undo());
|
||||
}
|
||||
|
||||
public redo(): void {
|
||||
this.withWebviewElement(webview => webview.redo());
|
||||
this.withWebview(webview => webview.redo());
|
||||
}
|
||||
|
||||
private withWebviewElement(f: (element: WebviewElement) => void): void {
|
||||
private withWebview(f: (element: Webview) => void): void {
|
||||
if (this._webview) {
|
||||
f(this._webview);
|
||||
}
|
||||
@@ -264,7 +258,7 @@ export class WebviewEditor extends BaseEditor {
|
||||
return rootPaths;
|
||||
}
|
||||
|
||||
private getWebview(input: WebviewEditorInput): WebviewElement {
|
||||
private getWebview(input: WebviewEditorInput): Webview {
|
||||
if (this._webview) {
|
||||
return this._webview;
|
||||
}
|
||||
@@ -279,14 +273,12 @@ export class WebviewEditor extends BaseEditor {
|
||||
this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService);
|
||||
}
|
||||
|
||||
this._webview = this._instantiationService.createInstance(WebviewElement,
|
||||
this._layoutService.getContainer(Parts.EDITOR_PART),
|
||||
this._webview = this._webviewService.createWebview(
|
||||
{
|
||||
allowSvgs: true,
|
||||
extension: input.extension,
|
||||
enableFindWidget: input.options.enableFindWidget
|
||||
},
|
||||
{});
|
||||
}, {});
|
||||
this._webview.mountTo(this._webviewContent);
|
||||
input.webview = this._webview;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { EditorInput, EditorModel, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { WebviewEvents, WebviewInputOptions } from './webviewEditorService';
|
||||
import { WebviewElement, WebviewOptions } from './webviewElement';
|
||||
import { Webview, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview';
|
||||
|
||||
export class WebviewEditorInput extends EditorInput {
|
||||
private static handlePool = 0;
|
||||
@@ -57,7 +57,7 @@ export class WebviewEditorInput extends EditorInput {
|
||||
private _currentWebviewHtml: string = '';
|
||||
public _events: WebviewEvents | undefined;
|
||||
private _container?: HTMLElement;
|
||||
private _webview: WebviewElement | undefined;
|
||||
private _webview?: Webview;
|
||||
private _webviewOwner: any;
|
||||
private _webviewDisposables: IDisposable[] = [];
|
||||
private _group?: GroupIdentifier;
|
||||
@@ -72,7 +72,6 @@ export class WebviewEditorInput extends EditorInput {
|
||||
|
||||
constructor(
|
||||
public readonly viewType: string,
|
||||
id: number | undefined,
|
||||
name: string,
|
||||
options: WebviewInputOptions,
|
||||
state: any,
|
||||
@@ -85,12 +84,7 @@ export class WebviewEditorInput extends EditorInput {
|
||||
) {
|
||||
super();
|
||||
|
||||
if (typeof id === 'number') {
|
||||
this._id = id;
|
||||
WebviewEditorInput.handlePool = Math.max(id, WebviewEditorInput.handlePool) + 1;
|
||||
} else {
|
||||
this._id = WebviewEditorInput.handlePool++;
|
||||
}
|
||||
this._id = WebviewEditorInput.handlePool++;
|
||||
|
||||
this._name = name;
|
||||
this._options = options;
|
||||
@@ -103,10 +97,6 @@ export class WebviewEditorInput extends EditorInput {
|
||||
return WebviewEditorInput.typeId;
|
||||
}
|
||||
|
||||
public getId(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeIcon = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeIcon = this._onDidChangeIcon.event;
|
||||
|
||||
@@ -161,7 +151,7 @@ export class WebviewEditorInput extends EditorInput {
|
||||
}
|
||||
|
||||
public matches(other: IEditorInput): boolean {
|
||||
return other === this || (other instanceof WebviewEditorInput && other._id === this._id);
|
||||
return other === this;
|
||||
}
|
||||
|
||||
public get group(): GroupIdentifier | undefined {
|
||||
@@ -234,11 +224,11 @@ export class WebviewEditorInput extends EditorInput {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
public get webview(): WebviewElement | undefined {
|
||||
public get webview(): Webview | undefined {
|
||||
return this._webview;
|
||||
}
|
||||
|
||||
public set webview(value: WebviewElement | undefined) {
|
||||
public set webview(value: Webview | undefined) {
|
||||
this._webviewDisposables = dispose(this._webviewDisposables);
|
||||
|
||||
this._webview = value;
|
||||
@@ -272,6 +262,7 @@ export class WebviewEditorInput extends EditorInput {
|
||||
}
|
||||
|
||||
public claimWebview(owner: any) {
|
||||
|
||||
this._webviewOwner = owner;
|
||||
}
|
||||
|
||||
@@ -315,7 +306,6 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput {
|
||||
|
||||
constructor(
|
||||
viewType: string,
|
||||
id: number | undefined,
|
||||
name: string,
|
||||
options: WebviewInputOptions,
|
||||
state: any,
|
||||
@@ -324,10 +314,10 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput {
|
||||
readonly location: URI;
|
||||
readonly id: ExtensionIdentifier
|
||||
},
|
||||
public readonly reviver: (input: WebviewEditorInput) => Promise<void>,
|
||||
private readonly reviver: (input: WebviewEditorInput) => Promise<void>,
|
||||
@IWorkbenchLayoutService partService: IWorkbenchLayoutService,
|
||||
) {
|
||||
super(viewType, id, name, options, state, events, extension, partService);
|
||||
super(viewType, name, options, state, events, extension, partService);
|
||||
}
|
||||
|
||||
public async resolve(): Promise<IEditorModel> {
|
||||
@@ -17,7 +17,6 @@ interface SerializedIconPath {
|
||||
|
||||
interface SerializedWebview {
|
||||
readonly viewType: string;
|
||||
readonly id: number;
|
||||
readonly title: string;
|
||||
readonly options: WebviewInputOptions;
|
||||
readonly extensionLocation: string | UriComponents | undefined;
|
||||
@@ -44,7 +43,6 @@ export class WebviewEditorInputFactory implements IEditorInputFactory {
|
||||
|
||||
const data: SerializedWebview = {
|
||||
viewType: input.viewType,
|
||||
id: input.getId(),
|
||||
title: input.getName(),
|
||||
options: input.options,
|
||||
extensionLocation: input.extension ? input.extension.location : undefined,
|
||||
@@ -69,7 +67,7 @@ export class WebviewEditorInputFactory implements IEditorInputFactory {
|
||||
const extensionLocation = reviveUri(data.extensionLocation);
|
||||
const extensionId = data.extensionId ? new ExtensionIdentifier(data.extensionId) : undefined;
|
||||
const iconPath = reviveIconPath(data.iconPath);
|
||||
return this._webviewService.reviveWebview(data.viewType, data.id, data.title, iconPath, data.state, data.options, extensionLocation ? {
|
||||
return this._webviewService.reviveWebview(data.viewType, data.title, iconPath, data.state, data.options, extensionLocation ? {
|
||||
location: extensionLocation,
|
||||
id: extensionId
|
||||
} : undefined, data.group);
|
||||
@@ -39,7 +39,6 @@ export interface IWebviewEditorService {
|
||||
|
||||
reviveWebview(
|
||||
viewType: string,
|
||||
id: number,
|
||||
title: string,
|
||||
iconPath: { light: URI, dark: URI } | undefined,
|
||||
state: any,
|
||||
@@ -143,7 +142,7 @@ export class WebviewEditorService implements IWebviewEditorService {
|
||||
},
|
||||
events: WebviewEvents
|
||||
): WebviewEditorInput {
|
||||
const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extension);
|
||||
const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, {}, events, extension);
|
||||
this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus }, showOptions.group);
|
||||
return webviewInput;
|
||||
}
|
||||
@@ -165,7 +164,6 @@ export class WebviewEditorService implements IWebviewEditorService {
|
||||
|
||||
public reviveWebview(
|
||||
viewType: string,
|
||||
id: number,
|
||||
title: string,
|
||||
iconPath: { light: URI, dark: URI } | undefined,
|
||||
state: any,
|
||||
@@ -176,7 +174,7 @@ export class WebviewEditorService implements IWebviewEditorService {
|
||||
},
|
||||
group: number | undefined,
|
||||
): WebviewEditorInput {
|
||||
const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, id, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise<void> => {
|
||||
const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise<void> => {
|
||||
const didRevive = await this.tryRevive(webview);
|
||||
if (didRevive) {
|
||||
return Promise.resolve(undefined);
|
||||
@@ -220,7 +218,7 @@ export class WebviewEditorService implements IWebviewEditorService {
|
||||
|
||||
// Revived webviews may not have an actively registered reviver but we still want to presist them
|
||||
// since a reviver should exist when it is actually needed.
|
||||
return !(webview instanceof RevivedWebviewEditorInput);
|
||||
return webview instanceof RevivedWebviewEditorInput;
|
||||
}
|
||||
|
||||
private async tryRevive(
|
||||
@@ -4,52 +4,45 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { WebviewElement } from './webviewElement';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
|
||||
export interface WebviewFindDelegate {
|
||||
find(value: string, previous: boolean): void;
|
||||
startFind(value: string): void;
|
||||
stopFind(keepSelection?: boolean): void;
|
||||
focus(): void;
|
||||
}
|
||||
|
||||
export class WebviewFindWidget extends SimpleFindWidget {
|
||||
|
||||
constructor(
|
||||
private _webview: WebviewElement | undefined,
|
||||
private readonly _delegate: WebviewFindDelegate,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(contextViewService, contextKeyService);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._webview = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public find(previous: boolean) {
|
||||
if (!this._webview) {
|
||||
return;
|
||||
}
|
||||
const val = this.inputValue;
|
||||
if (val) {
|
||||
this._webview.find(val, { findNext: true, forward: !previous });
|
||||
this._delegate.find(val, previous);
|
||||
}
|
||||
}
|
||||
|
||||
public hide() {
|
||||
super.hide();
|
||||
if (this._webview) {
|
||||
this._webview.stopFind(true);
|
||||
this._webview.focus();
|
||||
}
|
||||
this._delegate.stopFind(true);
|
||||
this._delegate.focus();
|
||||
}
|
||||
|
||||
public onInputChanged() {
|
||||
if (!this._webview) {
|
||||
return;
|
||||
}
|
||||
const val = this.inputValue;
|
||||
if (val) {
|
||||
this._webview.startFind(val);
|
||||
this._delegate.startFind(val);
|
||||
} else {
|
||||
this._webview.stopFind(false);
|
||||
this._delegate.stopFind(false);
|
||||
}
|
||||
}
|
||||
|
||||
88
src/vs/workbench/contrib/webview/common/webview.ts
Normal file
88
src/vs/workbench/contrib/webview/common/webview.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
/**
|
||||
* Set when the find widget in a webview is visible.
|
||||
*/
|
||||
export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey<boolean>('webviewFindWidgetVisible', false);
|
||||
|
||||
export const IWebviewService = createDecorator<IWebviewService>('webviewService');
|
||||
|
||||
/**
|
||||
* Handles the creation of webview elements.
|
||||
*/
|
||||
export interface IWebviewService {
|
||||
_serviceBrand: any;
|
||||
|
||||
createWebview(
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
): Webview;
|
||||
}
|
||||
|
||||
export interface WebviewPortMapping {
|
||||
readonly port: number;
|
||||
readonly resolvedPort: number;
|
||||
}
|
||||
|
||||
export interface WebviewOptions {
|
||||
readonly allowSvgs?: boolean;
|
||||
readonly extension?: {
|
||||
readonly location: URI;
|
||||
readonly id?: ExtensionIdentifier;
|
||||
};
|
||||
readonly enableFindWidget?: boolean;
|
||||
}
|
||||
|
||||
export interface WebviewContentOptions {
|
||||
readonly allowScripts?: boolean;
|
||||
readonly svgWhiteList?: string[];
|
||||
readonly localResourceRoots?: ReadonlyArray<URI>;
|
||||
readonly portMappings?: ReadonlyArray<WebviewPortMapping>;
|
||||
}
|
||||
|
||||
export interface Webview {
|
||||
|
||||
contents: string;
|
||||
options: WebviewContentOptions;
|
||||
initialScrollProgress: number;
|
||||
state: string | undefined;
|
||||
|
||||
readonly onDidFocus: Event<void>;
|
||||
readonly onDidClickLink: Event<URI>;
|
||||
readonly onDidScroll: Event<{ scrollYPercentage: number }>;
|
||||
readonly onDidUpdateState: Event<string | undefined>;
|
||||
readonly onMessage: Event<any>;
|
||||
|
||||
sendMessage(data: any): void;
|
||||
update(
|
||||
value: string,
|
||||
options: WebviewContentOptions,
|
||||
retainContextWhenHidden: boolean
|
||||
): void;
|
||||
|
||||
layout(): void;
|
||||
mountTo(parent: HTMLElement): void;
|
||||
focus(): void;
|
||||
dispose(): void;
|
||||
|
||||
|
||||
reload(): void;
|
||||
selectAll(): void;
|
||||
copy(): void;
|
||||
paste(): void;
|
||||
cut(): void;
|
||||
undo(): void;
|
||||
redo(): void;
|
||||
|
||||
showFind(): void;
|
||||
hideFind(): void;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// @ts-check
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const registerVscodeResourceScheme = (function () {
|
||||
let hasRegistered = false;
|
||||
return () => {
|
||||
if (hasRegistered) {
|
||||
return;
|
||||
}
|
||||
hasRegistered = true;
|
||||
|
||||
// @ts-ignore
|
||||
require('electron').webFrame.registerURLSchemeAsPrivileged('vscode-resource', {
|
||||
secure: true,
|
||||
bypassCSP: false,
|
||||
allowServiceWorkers: false,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true
|
||||
});
|
||||
};
|
||||
}());
|
||||
|
||||
// @ts-ignore
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
require('../../browser/pre/main')({
|
||||
postMessage: (channel, data) => {
|
||||
ipcRenderer.sendToHost(channel, data);
|
||||
},
|
||||
onMessage: (channel, handler) => {
|
||||
ipcRenderer.on(channel, handler);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
registerVscodeResourceScheme();
|
||||
});
|
||||
}());
|
||||
@@ -3,135 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
|
||||
import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory';
|
||||
import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, SelectAllWebviewEditorCommand, CopyWebviewEditorCommand, PasteWebviewEditorCommand, CutWebviewEditorCommand, UndoWebviewEditorCommand, RedoWebviewEditorCommand } from './webviewCommands';
|
||||
import { WebviewEditor, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './webviewEditor';
|
||||
import { WebviewEditorInput } from './webviewEditorInput';
|
||||
import { IWebviewEditorService, WebviewEditorService } from './webviewEditorService';
|
||||
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { IWebviewService } from 'vs/workbench/contrib/webview/common/webview';
|
||||
import { WebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService';
|
||||
|
||||
(Registry.as<IEditorRegistry>(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(
|
||||
WebviewEditor,
|
||||
WebviewEditor.ID,
|
||||
localize('webview.editor.label', "webview editor")),
|
||||
[new SyncDescriptor(WebviewEditorInput)]);
|
||||
|
||||
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
|
||||
WebviewEditorInputFactory.ID,
|
||||
WebviewEditorInputFactory);
|
||||
|
||||
registerSingleton(IWebviewEditorService, WebviewEditorService, true);
|
||||
|
||||
|
||||
const webviewDeveloperCategory = localize('developer', "Developer");
|
||||
|
||||
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
|
||||
export function registerWebViewCommands(editorId: string): void {
|
||||
const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */);
|
||||
|
||||
const showNextFindWidgetCommand = new ShowWebViewEditorFindWidgetCommand({
|
||||
id: ShowWebViewEditorFindWidgetCommand.ID,
|
||||
precondition: contextKeyExpr,
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_F,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
showNextFindWidgetCommand.register();
|
||||
|
||||
(new HideWebViewEditorFindCommand({
|
||||
id: HideWebViewEditorFindCommand.ID,
|
||||
precondition: ContextKeyExpr.and(
|
||||
contextKeyExpr,
|
||||
KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE),
|
||||
kbOpts: {
|
||||
primary: KeyCode.Escape,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new SelectAllWebviewEditorCommand({
|
||||
id: SelectAllWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
// These commands are only needed on MacOS where we have to disable the menu bar commands
|
||||
if (isMacintosh) {
|
||||
(new CopyWebviewEditorCommand({
|
||||
id: CopyWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new PasteWebviewEditorCommand({
|
||||
id: PasteWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
|
||||
(new CutWebviewEditorCommand({
|
||||
id: CutWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_X,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new UndoWebviewEditorCommand({
|
||||
id: UndoWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_Z,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
|
||||
(new RedoWebviewEditorCommand({
|
||||
id: RedoWebviewEditorCommand.ID,
|
||||
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_Y,
|
||||
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z],
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})).register();
|
||||
}
|
||||
}
|
||||
|
||||
registerWebViewCommands(WebviewEditor.ID);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL),
|
||||
'Webview Tools',
|
||||
webviewDeveloperCategory);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL),
|
||||
'Reload Webview',
|
||||
webviewDeveloperCategory);
|
||||
registerSingleton(IWebviewService, WebviewService, true);
|
||||
|
||||
@@ -17,36 +17,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
|
||||
import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService';
|
||||
import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols';
|
||||
import { areWebviewInputOptionsEqual } from './webviewEditorService';
|
||||
import { WebviewFindWidget } from './webviewFindWidget';
|
||||
import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService';
|
||||
import { WebviewFindWidget } from '../browser/webviewFindWidget';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { WebviewContentOptions, WebviewPortMapping, WebviewOptions, Webview } from 'vs/workbench/contrib/webview/common/webview';
|
||||
|
||||
export interface WebviewPortMapping {
|
||||
readonly port: number;
|
||||
readonly resolvedPort: number;
|
||||
}
|
||||
|
||||
export interface WebviewPortMapping {
|
||||
readonly port: number;
|
||||
readonly resolvedPort: number;
|
||||
}
|
||||
|
||||
export interface WebviewOptions {
|
||||
readonly allowSvgs?: boolean;
|
||||
readonly extension?: {
|
||||
readonly location: URI;
|
||||
readonly id?: ExtensionIdentifier;
|
||||
};
|
||||
readonly enableFindWidget?: boolean;
|
||||
}
|
||||
|
||||
export interface WebviewContentOptions {
|
||||
readonly allowScripts?: boolean;
|
||||
readonly svgWhiteList?: string[];
|
||||
readonly localResourceRoots?: ReadonlyArray<URI>;
|
||||
readonly portMappings?: ReadonlyArray<WebviewPortMapping>;
|
||||
}
|
||||
|
||||
interface IKeydownEvent {
|
||||
key: string;
|
||||
@@ -316,7 +292,7 @@ class WebviewKeyboardHandler extends Disposable {
|
||||
}
|
||||
|
||||
|
||||
export class WebviewElement extends Disposable {
|
||||
export class WebviewElement extends Disposable implements Webview {
|
||||
private _webview: Electron.WebviewTag;
|
||||
private _ready: Promise<void>;
|
||||
|
||||
@@ -350,7 +326,7 @@ export class WebviewElement extends Disposable {
|
||||
this._webview.style.height = '0';
|
||||
this._webview.style.outline = '0';
|
||||
|
||||
this._webview.preload = require.toUrl('./webview-pre.js');
|
||||
this._webview.preload = require.toUrl('./pre/electron-index.js');
|
||||
this._webview.src = 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E';
|
||||
|
||||
this._ready = new Promise(resolve => {
|
||||
@@ -539,10 +515,6 @@ export class WebviewElement extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
public set baseUrl(value: string) {
|
||||
this._send('baseUrl', value);
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this._webview.focus();
|
||||
this._send('focus');
|
||||
@@ -641,12 +613,13 @@ export class WebviewElement extends Disposable {
|
||||
*
|
||||
* @param value The string to search for. Empty strings are ignored.
|
||||
*/
|
||||
public find(value: string, options?: Electron.FindInPageOptions): void {
|
||||
public find(value: string, previous: boolean): void {
|
||||
// Searching with an empty value will throw an exception
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { findNext: true, forward: !previous };
|
||||
if (!this._findStarted) {
|
||||
this.startFind(value, options);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
|
||||
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IWebviewService, WebviewOptions, WebviewContentOptions, Webview } from 'vs/workbench/contrib/webview/common/webview';
|
||||
|
||||
export class WebviewService implements IWebviewService {
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) { }
|
||||
|
||||
createWebview(
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions
|
||||
): Webview {
|
||||
const element = this._instantiationService.createInstance(WebviewElement,
|
||||
this._layoutService.getContainer(Parts.EDITOR_PART),
|
||||
options,
|
||||
contentOptions);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { onUnexpectedError, isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { IWindowService, URIType } from 'vs/platform/windows/common/windows';
|
||||
import { IWindowService, IURIToOpen } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { localize } from 'vs/nls';
|
||||
@@ -344,16 +344,13 @@ class WelcomePage {
|
||||
private createListEntries(recents: (IRecentWorkspace | IRecentFolder)[]) {
|
||||
return recents.map(recent => {
|
||||
let fullPath: string;
|
||||
let resource: URI;
|
||||
let typeHint: URIType | undefined;
|
||||
let uriToOpen: IURIToOpen;
|
||||
if (isRecentFolder(recent)) {
|
||||
resource = recent.folderUri;
|
||||
uriToOpen = { folderUri: recent.folderUri };
|
||||
fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.folderUri, { verbose: true });
|
||||
typeHint = 'folder';
|
||||
} else {
|
||||
fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true });
|
||||
resource = recent.workspace.configPath;
|
||||
typeHint = 'file';
|
||||
uriToOpen = { workspaceUri: recent.workspace.configPath };
|
||||
}
|
||||
|
||||
const { name, parentPath } = splitName(fullPath);
|
||||
@@ -376,7 +373,7 @@ class WelcomePage {
|
||||
id: 'openRecentFolder',
|
||||
from: telemetryFrom
|
||||
});
|
||||
this.windowService.openWindow([{ uri: resource, typeHint }], { forceNewWindow: e.ctrlKey || e.metaKey });
|
||||
this.windowService.openWindow([uriToOpen], { forceNewWindow: e.ctrlKey || e.metaKey });
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@ export class WalkThroughInput extends EditorInput {
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
matches(otherInput: any): boolean {
|
||||
matches(otherInput: unknown): boolean {
|
||||
if (super.matches(otherInput) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user