Vscode merge (#4582)
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { BackupModelTracker } from 'vs/workbench/contrib/backup/common/backupModelTracker';
|
||||
import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
|
||||
// Register Backup Model Tracker
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupModelTracker, LifecyclePhase.Starting);
|
||||
|
||||
// Register Backup Restorer
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting);
|
||||
94
src/vs/workbench/contrib/backup/common/backupModelTracker.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI as Uri } from 'vs/base/common/uri';
|
||||
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ITextFileService, TextFileModelChangeEvent, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IFilesConfiguration, AutoSaveConfiguration, CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files';
|
||||
|
||||
const AUTO_SAVE_AFTER_DELAY_DISABLED_TIME = CONTENT_CHANGE_EVENT_BUFFER_DELAY + 500;
|
||||
|
||||
export class BackupModelTracker extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private configuredAutoSaveAfterDelay: boolean;
|
||||
|
||||
constructor(
|
||||
@IBackupFileService private readonly backupFileService: IBackupFileService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
|
||||
// Listen for text file model changes
|
||||
this._register(this.textFileService.models.onModelContentChanged((e) => this.onTextFileModelChanged(e)));
|
||||
this._register(this.textFileService.models.onModelSaved((e) => this.discardBackup(e.resource)));
|
||||
this._register(this.textFileService.models.onModelDisposed((e) => this.discardBackup(e)));
|
||||
|
||||
// Listen for untitled model changes
|
||||
this._register(this.untitledEditorService.onDidChangeContent((e) => this.onUntitledModelChanged(e)));
|
||||
this._register(this.untitledEditorService.onDidDisposeModel((e) => this.discardBackup(e)));
|
||||
|
||||
// Listen to config changes
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(this.configurationService.getValue<IFilesConfiguration>())));
|
||||
}
|
||||
|
||||
private onConfigurationChange(configuration: IFilesConfiguration): void {
|
||||
if (!configuration || !configuration.files) {
|
||||
this.configuredAutoSaveAfterDelay = false;
|
||||
return;
|
||||
}
|
||||
this.configuredAutoSaveAfterDelay =
|
||||
(configuration.files.autoSave === AutoSaveConfiguration.AFTER_DELAY &&
|
||||
configuration.files.autoSaveDelay <= AUTO_SAVE_AFTER_DELAY_DISABLED_TIME);
|
||||
}
|
||||
|
||||
private onTextFileModelChanged(event: TextFileModelChangeEvent): void {
|
||||
if (event.kind === StateChange.REVERTED) {
|
||||
// This must proceed even if auto save after delay is configured in order to clean up
|
||||
// any backups made before the config change
|
||||
this.discardBackup(event.resource);
|
||||
} else if (event.kind === StateChange.CONTENT_CHANGE) {
|
||||
// Do not backup when auto save after delay is configured
|
||||
if (!this.configuredAutoSaveAfterDelay) {
|
||||
const model = this.textFileService.models.get(event.resource);
|
||||
if (model) {
|
||||
const snapshot = model.createSnapshot();
|
||||
if (snapshot) {
|
||||
this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onUntitledModelChanged(resource: Uri): void {
|
||||
if (this.untitledEditorService.isDirty(resource)) {
|
||||
this.untitledEditorService.loadOrCreate({ resource }).then(model => {
|
||||
const snapshot = model.createSnapshot();
|
||||
if (snapshot) {
|
||||
this.backupFileService.backupResource(resource, snapshot, model.getVersionId());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.discardBackup(resource);
|
||||
}
|
||||
}
|
||||
|
||||
private discardBackup(resource: Uri): void {
|
||||
this.backupFileService.discardResourceBackup(resource);
|
||||
}
|
||||
}
|
||||
87
src/vs/workbench/contrib/backup/common/backupRestorer.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IResourceInput } from 'vs/platform/editor/common/editor';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IUntitledResourceInput } from 'vs/workbench/common/editor';
|
||||
|
||||
export class BackupRestorer implements IWorkbenchContribution {
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
private static readonly SQLQUERY_REGEX = /SQLQuery\d+/;
|
||||
private static readonly UNTITLED_REGEX = /Untitled-\d+/;
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IBackupFileService private readonly backupFileService: IBackupFileService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService
|
||||
) {
|
||||
this.restoreBackups();
|
||||
}
|
||||
|
||||
private restoreBackups(): void {
|
||||
this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.doRestoreBackups());
|
||||
}
|
||||
|
||||
private doRestoreBackups(): Promise<URI[] | undefined> {
|
||||
|
||||
// Find all files and untitled with backups
|
||||
return this.backupFileService.getWorkspaceFileBackups().then(backups => {
|
||||
|
||||
// Resolve backups that are opened
|
||||
return this.doResolveOpenedBackups(backups).then((unresolved): Promise<URI[] | undefined> | undefined => {
|
||||
|
||||
// Some failed to restore or were not opened at all so we open and resolve them manually
|
||||
if (unresolved.length > 0) {
|
||||
return this.doOpenEditors(unresolved).then(() => this.doResolveOpenedBackups(unresolved));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doResolveOpenedBackups(backups: URI[]): Promise<URI[]> {
|
||||
const restorePromises: Promise<any>[] = [];
|
||||
const unresolved: URI[] = [];
|
||||
|
||||
backups.forEach(backup => {
|
||||
const openedEditor = this.editorService.getOpened({ resource: backup });
|
||||
if (openedEditor) {
|
||||
restorePromises.push(openedEditor.resolve().then(undefined, () => unresolved.push(backup)));
|
||||
} else {
|
||||
unresolved.push(backup);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(restorePromises).then(() => unresolved, () => unresolved);
|
||||
}
|
||||
|
||||
private doOpenEditors(resources: URI[]): Promise<void> {
|
||||
const hasOpenedEditors = this.editorService.visibleEditors.length > 0;
|
||||
const inputs = resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors));
|
||||
|
||||
// Open all remaining backups as editors and resolve them to load their backups
|
||||
return this.editorService.openEditors(inputs).then(() => undefined);
|
||||
}
|
||||
|
||||
private resolveInput(resource: URI, index: number, hasOpenedEditors: boolean): IResourceInput | IUntitledResourceInput {
|
||||
const options = { pinned: true, preserveFocus: true, inactive: index > 0 || hasOpenedEditors };
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
if (resource.scheme === Schemas.untitled
|
||||
&& !BackupRestorer.UNTITLED_REGEX.test(resource.fsPath)
|
||||
&& !BackupRestorer.SQLQUERY_REGEX.test(resource.fsPath)) {
|
||||
return { filePath: resource.fsPath, options };
|
||||
}
|
||||
|
||||
return { resource, options };
|
||||
}
|
||||
}
|
||||
194
src/vs/workbench/contrib/cli/node/cli.contribution.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as cp from 'child_process';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { nfcall } from 'vs/base/common/async';
|
||||
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';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
function ignore<T>(code: string, value: T): (err: any) => Promise<T> {
|
||||
return err => err.code === code ? Promise.resolve<T>(value) : Promise.reject<T>(err);
|
||||
}
|
||||
|
||||
let _source: string | null = null;
|
||||
function getSource(): string {
|
||||
if (!_source) {
|
||||
const root = getPathFromAmdModule(require, '');
|
||||
_source = path.resolve(root, '..', 'bin', 'code');
|
||||
}
|
||||
return _source;
|
||||
}
|
||||
|
||||
function isAvailable(): Promise<boolean> {
|
||||
return Promise.resolve(pfs.exists(getSource()));
|
||||
}
|
||||
|
||||
class InstallAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.installCommandLine';
|
||||
static LABEL = nls.localize('install', "Install '{0}' command in PATH", product.applicationName);
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
private get target(): string {
|
||||
return `/usr/local/bin/${product.applicationName}`;
|
||||
}
|
||||
|
||||
run(): Promise<void> {
|
||||
return isAvailable().then(isAvailable => {
|
||||
if (!isAvailable) {
|
||||
const message = nls.localize('not available', "This command is not available");
|
||||
this.notificationService.info(message);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.isInstalled()
|
||||
.then(isInstalled => {
|
||||
if (!isAvailable || isInstalled) {
|
||||
return Promise.resolve(null);
|
||||
} else {
|
||||
return pfs.unlink(this.target)
|
||||
.then(undefined, ignore('ENOENT', null))
|
||||
.then(() => pfs.symlink(getSource(), this.target))
|
||||
.then(undefined, err => {
|
||||
if (err.code === 'EACCES' || err.code === 'ENOENT') {
|
||||
return this.createBinFolderAndSymlinkAsAdmin();
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.logService.trace('cli#install', this.target);
|
||||
this.notificationService.info(nls.localize('successIn', "Shell command '{0}' successfully installed in PATH.", product.applicationName));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private isInstalled(): Promise<boolean> {
|
||||
return pfs.lstat(this.target)
|
||||
.then(stat => stat.isSymbolicLink())
|
||||
.then(() => pfs.readlink(this.target))
|
||||
.then(link => link === getSource())
|
||||
.then(undefined, ignore('ENOENT', false));
|
||||
}
|
||||
|
||||
private createBinFolderAndSymlinkAsAdmin(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")];
|
||||
|
||||
this.dialogService.show(Severity.Info, nls.localize('warnEscalation', "Code will now prompt with 'osascript' for Administrator privileges to install the shell command."), buttons, { cancelId: 1 }).then(choice => {
|
||||
switch (choice) {
|
||||
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, {})
|
||||
.then(undefined, _ => Promise.reject(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'."))))
|
||||
.then(resolve, reject);
|
||||
break;
|
||||
case 1 /* Cancel */:
|
||||
reject(new Error(nls.localize('aborted', "Aborted")));
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class UninstallAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.uninstallCommandLine';
|
||||
static LABEL = nls.localize('uninstall', "Uninstall '{0}' command from PATH", product.applicationName);
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IDialogService private readonly dialogService: IDialogService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
private get target(): string {
|
||||
return `/usr/local/bin/${product.applicationName}`;
|
||||
}
|
||||
|
||||
run(): Promise<void> {
|
||||
return isAvailable().then(isAvailable => {
|
||||
if (!isAvailable) {
|
||||
const message = nls.localize('not available', "This command is not available");
|
||||
this.notificationService.info(message);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uninstall = () => {
|
||||
return pfs.unlink(this.target)
|
||||
.then(undefined, ignore('ENOENT', null));
|
||||
};
|
||||
|
||||
return uninstall().then(undefined, err => {
|
||||
if (err.code === 'EACCES') {
|
||||
return this.deleteSymlinkAsAdmin();
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
}).then(() => {
|
||||
this.logService.trace('cli#uninstall', this.target);
|
||||
this.notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", product.applicationName));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private deleteSymlinkAsAdmin(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")];
|
||||
|
||||
this.dialogService.show(Severity.Info, nls.localize('warnEscalationUninstall', "Code will now prompt with 'osascript' for Administrator privileges to uninstall the shell command."), buttons, { cancelId: 1 }).then(choice => {
|
||||
switch (choice) {
|
||||
case 0 /* OK */:
|
||||
const command = 'osascript -e "do shell script \\"rm \'' + this.target + '\'\\" with administrator privileges"';
|
||||
|
||||
nfcall(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;
|
||||
case 1 /* Cancel */:
|
||||
reject(new Error(nls.localize('aborted', "Aborted")));
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
const category = nls.localize('shellCommand', "Shell Command");
|
||||
|
||||
const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(InstallAction, InstallAction.ID, InstallAction.LABEL), 'Shell Command: Install \'code\' command in PATH', category);
|
||||
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(UninstallAction, UninstallAction.ID, UninstallAction.LABEL), 'Shell Command: Uninstall \'code\' command from PATH', category);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .accessibilityHelpWidget {
|
||||
padding: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./accessibility';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { renderFormattedText } from 'vs/base/browser/htmlContentRenderer';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import * as editorOptions from 'vs/editor/common/config/editorOptions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { contrastBorder, editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
|
||||
|
||||
const CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE = new RawContextKey<boolean>('accessibilityHelpWidgetVisible', false);
|
||||
|
||||
class AccessibilityHelpController extends Disposable implements IEditorContribution {
|
||||
|
||||
private static readonly ID = 'editor.contrib.accessibilityHelpController';
|
||||
|
||||
public static get(editor: ICodeEditor): AccessibilityHelpController {
|
||||
return editor.getContribution<AccessibilityHelpController>(AccessibilityHelpController.ID);
|
||||
}
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _widget: AccessibilityHelpWidget;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IInstantiationService instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editor = editor;
|
||||
this._widget = this._register(instantiationService.createInstance(AccessibilityHelpWidget, this._editor));
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return AccessibilityHelpController.ID;
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this._widget.show();
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this._widget.hide();
|
||||
}
|
||||
}
|
||||
|
||||
class AccessibilityHelpWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
private static readonly ID = 'editor.contrib.accessibilityHelpWidget';
|
||||
private static readonly WIDTH = 500;
|
||||
private static readonly HEIGHT = 300;
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _domNode: FastDomNode<HTMLElement>;
|
||||
private _contentDomNode: FastDomNode<HTMLElement>;
|
||||
private _isVisible: boolean;
|
||||
private _isVisibleKey: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editor = editor;
|
||||
this._isVisibleKey = CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE.bindTo(this._contextKeyService);
|
||||
|
||||
this._domNode = createFastDomNode(document.createElement('div'));
|
||||
this._domNode.setClassName('accessibilityHelpWidget');
|
||||
this._domNode.setWidth(AccessibilityHelpWidget.WIDTH);
|
||||
this._domNode.setHeight(AccessibilityHelpWidget.HEIGHT);
|
||||
this._domNode.setDisplay('none');
|
||||
this._domNode.setAttribute('role', 'dialog');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this._contentDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._contentDomNode.setAttribute('role', 'document');
|
||||
this._domNode.appendChild(this._contentDomNode);
|
||||
|
||||
this._isVisible = false;
|
||||
|
||||
this._register(this._editor.onDidLayoutChange(() => {
|
||||
if (this._isVisible) {
|
||||
this._layout();
|
||||
}
|
||||
}));
|
||||
|
||||
// Intentionally not configurable!
|
||||
this._register(dom.addStandardDisposableListener(this._contentDomNode.domNode, 'keydown', (e) => {
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_E)) {
|
||||
alert(nls.localize('emergencyConfOn', "Now changing the setting `editor.accessibilitySupport` to 'on'."));
|
||||
|
||||
this._configurationService.updateValue('editor.accessibilitySupport', 'on', ConfigurationTarget.USER);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_H)) {
|
||||
alert(nls.localize('openingDocs', "Now opening the VS Code Accessibility documentation page."));
|
||||
|
||||
this._openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=851010'));
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
this.onblur(this._contentDomNode.domNode, () => {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
this._editor.addOverlayWidget(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.removeOverlayWidget(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return AccessibilityHelpWidget.ID;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode.domNode;
|
||||
}
|
||||
|
||||
public getPosition(): IOverlayWidgetPosition {
|
||||
return {
|
||||
preference: null
|
||||
};
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
if (this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = true;
|
||||
this._isVisibleKey.set(true);
|
||||
this._layout();
|
||||
this._domNode.setDisplay('block');
|
||||
this._domNode.setAttribute('aria-hidden', 'false');
|
||||
this._contentDomNode.domNode.tabIndex = 0;
|
||||
this._buildContent();
|
||||
this._contentDomNode.domNode.focus();
|
||||
}
|
||||
|
||||
private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string {
|
||||
let kb = this._keybindingService.lookupKeybinding(commandId);
|
||||
if (kb) {
|
||||
return strings.format(msg, kb.getAriaLabel());
|
||||
}
|
||||
return strings.format(noKbMsg, commandId);
|
||||
}
|
||||
|
||||
private _buildContent() {
|
||||
let opts = this._editor.getConfiguration();
|
||||
let text = nls.localize('introMsg', "Thank you for trying out VS Code's accessibility options.");
|
||||
|
||||
text += '\n\n' + nls.localize('status', "Status:");
|
||||
|
||||
const configuredValue = this._configurationService.getValue<editorOptions.IEditorOptions>('editor').accessibilitySupport;
|
||||
const actualValue = opts.accessibilitySupport;
|
||||
|
||||
const emergencyTurnOnMessage = (
|
||||
platform.isMacintosh
|
||||
? nls.localize('changeConfigToOnMac', "To configure the editor to be permanently optimized for usage with a Screen Reader press Command+E now.")
|
||||
: nls.localize('changeConfigToOnWinLinux', "To configure the editor to be permanently optimized for usage with a Screen Reader press Control+E now.")
|
||||
);
|
||||
|
||||
switch (configuredValue) {
|
||||
case 'auto':
|
||||
switch (actualValue) {
|
||||
case AccessibilitySupport.Unknown:
|
||||
// Should never happen in VS Code
|
||||
text += '\n\n - ' + nls.localize('auto_unknown', "The editor is configured to use platform APIs to detect when a Screen Reader is attached, but the current runtime does not support this.");
|
||||
break;
|
||||
case AccessibilitySupport.Enabled:
|
||||
text += '\n\n - ' + nls.localize('auto_on', "The editor has automatically detected a Screen Reader is attached.");
|
||||
break;
|
||||
case AccessibilitySupport.Disabled:
|
||||
text += '\n\n - ' + nls.localize('auto_off', "The editor is configured to automatically detect when a Screen Reader is attached, which is not the case at this time.");
|
||||
text += ' ' + emergencyTurnOnMessage;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'on':
|
||||
text += '\n\n - ' + nls.localize('configuredOn', "The editor is configured to be permanently optimized for usage with a Screen Reader - you can change this by editing the setting `editor.accessibilitySupport`.");
|
||||
break;
|
||||
case 'off':
|
||||
text += '\n\n - ' + nls.localize('configuredOff', "The editor is configured to never be optimized for usage with a Screen Reader.");
|
||||
text += ' ' + emergencyTurnOnMessage;
|
||||
break;
|
||||
}
|
||||
|
||||
const NLS_TAB_FOCUS_MODE_ON = nls.localize('tabFocusModeOnMsg', "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}.");
|
||||
const NLS_TAB_FOCUS_MODE_ON_NO_KB = nls.localize('tabFocusModeOnMsgNoKb', "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding.");
|
||||
const NLS_TAB_FOCUS_MODE_OFF = nls.localize('tabFocusModeOffMsg', "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}.");
|
||||
const NLS_TAB_FOCUS_MODE_OFF_NO_KB = nls.localize('tabFocusModeOffMsgNoKb', "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding.");
|
||||
|
||||
if (opts.tabFocusMode) {
|
||||
text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, NLS_TAB_FOCUS_MODE_ON, NLS_TAB_FOCUS_MODE_ON_NO_KB);
|
||||
} else {
|
||||
text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, NLS_TAB_FOCUS_MODE_OFF, NLS_TAB_FOCUS_MODE_OFF_NO_KB);
|
||||
}
|
||||
|
||||
const openDocMessage = (
|
||||
platform.isMacintosh
|
||||
? nls.localize('openDocMac', "Press Command+H now to open a browser window with more VS Code information related to Accessibility.")
|
||||
: nls.localize('openDocWinLinux', "Press Control+H now to open a browser window with more VS Code information related to Accessibility.")
|
||||
);
|
||||
|
||||
text += '\n\n' + openDocMessage;
|
||||
|
||||
text += '\n\n' + nls.localize('outroMsg', "You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape.");
|
||||
|
||||
this._contentDomNode.domNode.appendChild(renderFormattedText(text));
|
||||
// Per https://www.w3.org/TR/wai-aria/roles#document, Authors SHOULD provide a title or label for documents
|
||||
this._contentDomNode.domNode.setAttribute('aria-label', text);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = false;
|
||||
this._isVisibleKey.reset();
|
||||
this._domNode.setDisplay('none');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
this._contentDomNode.domNode.tabIndex = -1;
|
||||
dom.clearNode(this._contentDomNode.domNode);
|
||||
|
||||
this._editor.focus();
|
||||
}
|
||||
|
||||
private _layout(): void {
|
||||
let editorLayout = this._editor.getLayoutInfo();
|
||||
|
||||
let top = Math.round((editorLayout.height - AccessibilityHelpWidget.HEIGHT) / 2);
|
||||
this._domNode.setTop(top);
|
||||
|
||||
let left = Math.round((editorLayout.width - AccessibilityHelpWidget.WIDTH) / 2);
|
||||
this._domNode.setLeft(left);
|
||||
}
|
||||
}
|
||||
|
||||
class ShowAccessibilityHelpAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.showAccessibilityHelp',
|
||||
label: nls.localize('ShowAccessibilityHelpAction', "Show Accessibility Help"),
|
||||
alias: 'Show Accessibility Help',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.Alt | KeyCode.F1,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
let controller = AccessibilityHelpController.get(editor);
|
||||
if (controller) {
|
||||
controller.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(AccessibilityHelpController);
|
||||
registerEditorAction(ShowAccessibilityHelpAction);
|
||||
|
||||
const AccessibilityHelpCommand = EditorCommand.bindToContribution<AccessibilityHelpController>(AccessibilityHelpController.get);
|
||||
|
||||
registerEditorCommand(new AccessibilityHelpCommand({
|
||||
id: 'closeAccessibilityHelp',
|
||||
precondition: CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE,
|
||||
handler: x => x.hide(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 100,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape]
|
||||
}
|
||||
}));
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const widgetBackground = theme.getColor(editorWidgetBackground);
|
||||
if (widgetBackground) {
|
||||
collector.addRule(`.monaco-editor .accessibilityHelpWidget { background-color: ${widgetBackground}; }`);
|
||||
}
|
||||
|
||||
const widgetShadowColor = theme.getColor(widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
collector.addRule(`.monaco-editor .accessibilityHelpWidget { box-shadow: 0 2px 8px ${widgetShadowColor}; }`);
|
||||
}
|
||||
|
||||
const hcBorder = theme.getColor(contrastBorder);
|
||||
if (hcBorder) {
|
||||
collector.addRule(`.monaco-editor .accessibilityHelpWidget { border: 2px solid ${hcBorder}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import './menuPreventer';
|
||||
import './accessibility/accessibility';
|
||||
import './inspectKeybindings';
|
||||
import './largeFileOptimizations';
|
||||
import './selectionClipboard';
|
||||
import './inspectTMScopes/inspectTMScopes';
|
||||
import './toggleMinimap';
|
||||
import './toggleMultiCursorModifier';
|
||||
import './toggleRenderControlCharacter';
|
||||
import './toggleRenderWhitespace';
|
||||
import './toggleWordWrap';
|
||||
import './workbenchReferenceSearch';
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IUntitledResourceInput } from 'vs/workbench/common/editor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
class InspectKeyMap extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.inspectKeyMappings',
|
||||
label: nls.localize('workbench.action.inspectKeyMap', "Developer: Inspect Key Mappings"),
|
||||
alias: 'Developer: Inspect Key Mappings',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
const keybindingService = accessor.get(IKeybindingService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
|
||||
editorService.openEditor({ contents: keybindingService._dumpDebugInfo(), options: { pinned: true } } as IUntitledResourceInput);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(InspectKeyMap);
|
||||
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.tm-inspect-widget {
|
||||
z-index: 50;
|
||||
user-select: text;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tm-token {
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
|
||||
.tm-metadata-separator {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.tm-token-length {
|
||||
font-weight: normal;
|
||||
font-size: 60%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.tm-metadata-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tm-metadata-value {
|
||||
font-family: var(--monaco-monospace-font);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tm-theme-selector {
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./inspectTMScopes';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { escape } from 'vs/base/common/strings';
|
||||
import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { FontStyle, LanguageIdentifier, StandardTokenType, TokenMetadata, TokenizationRegistry } from 'vs/editor/common/modes';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { HIGH_CONTRAST, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { findMatchingThemeRule } from 'vs/workbench/services/textMate/common/TMHelper';
|
||||
import { ITextMateService, IGrammar, IToken, StackElement } from 'vs/workbench/services/textMate/common/textMateService';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
class InspectTMScopesController extends Disposable implements IEditorContribution {
|
||||
|
||||
private static readonly ID = 'editor.contrib.inspectTMScopes';
|
||||
|
||||
public static get(editor: ICodeEditor): InspectTMScopesController {
|
||||
return editor.getContribution<InspectTMScopesController>(InspectTMScopesController.ID);
|
||||
}
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _textMateService: ITextMateService;
|
||||
private _themeService: IWorkbenchThemeService;
|
||||
private _modeService: IModeService;
|
||||
private _notificationService: INotificationService;
|
||||
private _widget: InspectTMScopesWidget | null;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@ITextMateService textMateService: ITextMateService,
|
||||
@IModeService modeService: IModeService,
|
||||
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._textMateService = textMateService;
|
||||
this._themeService = themeService;
|
||||
this._modeService = modeService;
|
||||
this._notificationService = notificationService;
|
||||
this._widget = null;
|
||||
|
||||
this._register(this._editor.onDidChangeModel((e) => this.stop()));
|
||||
this._register(this._editor.onDidChangeModelLanguage((e) => this.stop()));
|
||||
this._register(this._editor.onKeyUp((e) => e.keyCode === KeyCode.Escape && this.stop()));
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return InspectTMScopesController.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public launch(): void {
|
||||
if (this._widget) {
|
||||
return;
|
||||
}
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
this._widget = new InspectTMScopesWidget(this._editor, this._textMateService, this._modeService, this._themeService, this._notificationService);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this._widget) {
|
||||
this._widget.dispose();
|
||||
this._widget = null;
|
||||
}
|
||||
}
|
||||
|
||||
public toggle(): void {
|
||||
if (!this._widget) {
|
||||
this.launch();
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InspectTMScopes extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.inspectTMScopes',
|
||||
label: nls.localize('inspectTMScopes', "Developer: Inspect TM Scopes"),
|
||||
alias: 'Developer: Inspect TM Scopes',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
let controller = InspectTMScopesController.get(editor);
|
||||
if (controller) {
|
||||
controller.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ICompleteLineTokenization {
|
||||
startState: StackElement | null;
|
||||
tokens1: IToken[];
|
||||
tokens2: Uint32Array;
|
||||
endState: StackElement;
|
||||
}
|
||||
|
||||
interface IDecodedMetadata {
|
||||
languageIdentifier: LanguageIdentifier;
|
||||
tokenType: StandardTokenType;
|
||||
fontStyle: FontStyle;
|
||||
foreground: Color;
|
||||
background: Color;
|
||||
}
|
||||
|
||||
function renderTokenText(tokenText: string): string {
|
||||
if (tokenText.length > 40) {
|
||||
tokenText = tokenText.substr(0, 20) + '…' + tokenText.substr(tokenText.length - 20);
|
||||
}
|
||||
let result: string = '';
|
||||
for (let charIndex = 0, len = tokenText.length; charIndex < len; charIndex++) {
|
||||
let charCode = tokenText.charCodeAt(charIndex);
|
||||
switch (charCode) {
|
||||
case CharCode.Tab:
|
||||
result += '→';
|
||||
break;
|
||||
|
||||
case CharCode.Space:
|
||||
result += '·';
|
||||
break;
|
||||
|
||||
case CharCode.LessThan:
|
||||
result += '<';
|
||||
break;
|
||||
|
||||
case CharCode.GreaterThan:
|
||||
result += '>';
|
||||
break;
|
||||
|
||||
case CharCode.Ampersand:
|
||||
result += '&';
|
||||
break;
|
||||
|
||||
default:
|
||||
result += String.fromCharCode(charCode);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class InspectTMScopesWidget extends Disposable implements IContentWidget {
|
||||
|
||||
private static readonly _ID = 'editor.contrib.inspectTMScopesWidget';
|
||||
|
||||
// Editor.IContentWidget.allowEditorOverflow
|
||||
public readonly allowEditorOverflow = true;
|
||||
|
||||
private _isDisposed: boolean;
|
||||
private readonly _editor: IActiveCodeEditor;
|
||||
private readonly _modeService: IModeService;
|
||||
private readonly _themeService: IWorkbenchThemeService;
|
||||
private readonly _notificationService: INotificationService;
|
||||
private readonly _model: ITextModel;
|
||||
private readonly _domNode: HTMLElement;
|
||||
private readonly _grammar: Promise<IGrammar>;
|
||||
|
||||
constructor(
|
||||
editor: IActiveCodeEditor,
|
||||
textMateService: ITextMateService,
|
||||
modeService: IModeService,
|
||||
themeService: IWorkbenchThemeService,
|
||||
notificationService: INotificationService
|
||||
) {
|
||||
super();
|
||||
this._isDisposed = false;
|
||||
this._editor = editor;
|
||||
this._modeService = modeService;
|
||||
this._themeService = themeService;
|
||||
this._notificationService = notificationService;
|
||||
this._model = this._editor.getModel();
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'tm-inspect-widget';
|
||||
this._grammar = textMateService.createGrammar(this._model.getLanguageIdentifier().language);
|
||||
this._beginCompute(this._editor.getPosition());
|
||||
this._register(this._editor.onDidChangeCursorPosition((e) => this._beginCompute(this._editor.getPosition())));
|
||||
this._editor.addContentWidget(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._editor.removeContentWidget(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return InspectTMScopesWidget._ID;
|
||||
}
|
||||
|
||||
private _beginCompute(position: Position): void {
|
||||
dom.clearNode(this._domNode);
|
||||
this._domNode.appendChild(document.createTextNode(nls.localize('inspectTMScopesWidget.loading', "Loading...")));
|
||||
this._grammar.then(
|
||||
(grammar) => this._compute(grammar, position),
|
||||
(err) => {
|
||||
this._notificationService.warn(err);
|
||||
setTimeout(() => {
|
||||
InspectTMScopesController.get(this._editor).stop();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _compute(grammar: IGrammar, position: Position): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
let data = this._getTokensAtLine(grammar, position.lineNumber);
|
||||
|
||||
let token1Index = 0;
|
||||
for (let i = data.tokens1.length - 1; i >= 0; i--) {
|
||||
let t = data.tokens1[i];
|
||||
if (position.column - 1 >= t.startIndex) {
|
||||
token1Index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let token2Index = 0;
|
||||
for (let i = (data.tokens2.length >>> 1); i >= 0; i--) {
|
||||
if (position.column - 1 >= data.tokens2[(i << 1)]) {
|
||||
token2Index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
let tokenStartIndex = data.tokens1[token1Index].startIndex;
|
||||
let tokenEndIndex = data.tokens1[token1Index].endIndex;
|
||||
let tokenText = this._model.getLineContent(position.lineNumber).substring(tokenStartIndex, tokenEndIndex);
|
||||
result += `<h2 class="tm-token">${renderTokenText(tokenText)}<span class="tm-token-length">(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})</span></h2>`;
|
||||
|
||||
result += `<hr class="tm-metadata-separator" style="clear:both"/>`;
|
||||
|
||||
let metadata = this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]);
|
||||
result += `<table class="tm-metadata-table"><tbody>`;
|
||||
result += `<tr><td class="tm-metadata-key">language</td><td class="tm-metadata-value">${escape(metadata.languageIdentifier.language)}</td></tr>`;
|
||||
result += `<tr><td class="tm-metadata-key">token type</td><td class="tm-metadata-value">${this._tokenTypeToString(metadata.tokenType)}</td></tr>`;
|
||||
result += `<tr><td class="tm-metadata-key">font style</td><td class="tm-metadata-value">${this._fontStyleToString(metadata.fontStyle)}</td></tr>`;
|
||||
result += `<tr><td class="tm-metadata-key">foreground</td><td class="tm-metadata-value">${Color.Format.CSS.formatHexA(metadata.foreground)}</td></tr>`;
|
||||
result += `<tr><td class="tm-metadata-key">background</td><td class="tm-metadata-value">${Color.Format.CSS.formatHexA(metadata.background)}</td></tr>`;
|
||||
if (metadata.background.isOpaque() && metadata.foreground.isOpaque()) {
|
||||
result += `<tr><td class="tm-metadata-key">contrast ratio</td><td class="tm-metadata-value">${metadata.background.getContrastRatio(metadata.foreground).toFixed(2)}</td></tr>`;
|
||||
} else {
|
||||
result += '<tr><td class="tm-metadata-key">Contrast ratio cannot be precise for colors that use transparency</td><td class="tm-metadata-value"></td></tr>';
|
||||
}
|
||||
result += `</tbody></table>`;
|
||||
|
||||
let theme = this._themeService.getColorTheme();
|
||||
result += `<hr class="tm-metadata-separator"/>`;
|
||||
let matchingRule = findMatchingThemeRule(theme, data.tokens1[token1Index].scopes, false);
|
||||
if (matchingRule) {
|
||||
result += `<code class="tm-theme-selector">${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}</code>`;
|
||||
} else {
|
||||
result += `<span class="tm-theme-selector">No theme selector.</span>`;
|
||||
}
|
||||
|
||||
result += `<hr class="tm-metadata-separator"/>`;
|
||||
|
||||
result += `<ul>`;
|
||||
for (let i = data.tokens1[token1Index].scopes.length - 1; i >= 0; i--) {
|
||||
result += `<li>${escape(data.tokens1[token1Index].scopes[i])}</li>`;
|
||||
}
|
||||
result += `</ul>`;
|
||||
|
||||
|
||||
this._domNode.innerHTML = result;
|
||||
this._editor.layoutContentWidget(this);
|
||||
}
|
||||
|
||||
private _decodeMetadata(metadata: number): IDecodedMetadata {
|
||||
let colorMap = TokenizationRegistry.getColorMap()!;
|
||||
let languageId = TokenMetadata.getLanguageId(metadata);
|
||||
let tokenType = TokenMetadata.getTokenType(metadata);
|
||||
let fontStyle = TokenMetadata.getFontStyle(metadata);
|
||||
let foreground = TokenMetadata.getForeground(metadata);
|
||||
let background = TokenMetadata.getBackground(metadata);
|
||||
return {
|
||||
languageIdentifier: this._modeService.getLanguageIdentifier(languageId)!,
|
||||
tokenType: tokenType,
|
||||
fontStyle: fontStyle,
|
||||
foreground: colorMap[foreground],
|
||||
background: colorMap[background]
|
||||
};
|
||||
}
|
||||
|
||||
private _tokenTypeToString(tokenType: StandardTokenType): string {
|
||||
switch (tokenType) {
|
||||
case StandardTokenType.Other: return 'Other';
|
||||
case StandardTokenType.Comment: return 'Comment';
|
||||
case StandardTokenType.String: return 'String';
|
||||
case StandardTokenType.RegEx: return 'RegEx';
|
||||
}
|
||||
return '??';
|
||||
}
|
||||
|
||||
private _fontStyleToString(fontStyle: FontStyle): string {
|
||||
let r = '';
|
||||
if (fontStyle & FontStyle.Italic) {
|
||||
r += 'italic ';
|
||||
}
|
||||
if (fontStyle & FontStyle.Bold) {
|
||||
r += 'bold ';
|
||||
}
|
||||
if (fontStyle & FontStyle.Underline) {
|
||||
r += 'underline ';
|
||||
}
|
||||
if (r.length === 0) {
|
||||
r = '---';
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private _getTokensAtLine(grammar: IGrammar, lineNumber: number): ICompleteLineTokenization {
|
||||
let stateBeforeLine = this._getStateBeforeLine(grammar, lineNumber);
|
||||
|
||||
let tokenizationResult1 = grammar.tokenizeLine(this._model.getLineContent(lineNumber), stateBeforeLine);
|
||||
let tokenizationResult2 = grammar.tokenizeLine2(this._model.getLineContent(lineNumber), stateBeforeLine);
|
||||
|
||||
return {
|
||||
startState: stateBeforeLine,
|
||||
tokens1: tokenizationResult1.tokens,
|
||||
tokens2: tokenizationResult2.tokens,
|
||||
endState: tokenizationResult1.ruleStack
|
||||
};
|
||||
}
|
||||
|
||||
private _getStateBeforeLine(grammar: IGrammar, lineNumber: number): StackElement | null {
|
||||
let state: StackElement | null = null;
|
||||
|
||||
for (let i = 1; i < lineNumber; i++) {
|
||||
let tokenizationResult = grammar.tokenizeLine(this._model.getLineContent(i), state);
|
||||
state = tokenizationResult.ruleStack;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public getPosition(): IContentWidgetPosition {
|
||||
return {
|
||||
position: this._editor.getPosition(),
|
||||
preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(InspectTMScopesController);
|
||||
registerEditorAction(InspectTMScopes);
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const border = theme.getColor(editorHoverBorder);
|
||||
if (border) {
|
||||
let borderWidth = theme.type === HIGH_CONTRAST ? 2 : 1;
|
||||
collector.addRule(`.monaco-editor .tm-inspect-widget { border: ${borderWidth}px solid ${border}; }`);
|
||||
collector.addRule(`.monaco-editor .tm-inspect-widget .tm-metadata-separator { background-color: ${border}; }`);
|
||||
}
|
||||
const background = theme.getColor(editorHoverBackground);
|
||||
if (background) {
|
||||
collector.addRule(`.monaco-editor .tm-inspect-widget { background-color: ${background}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,603 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ParseError, parse } from 'vs/base/common/json';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { CharacterPair, CommentRule, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService';
|
||||
|
||||
interface IRegExp {
|
||||
pattern: string;
|
||||
flags?: string;
|
||||
}
|
||||
|
||||
interface IIndentationRules {
|
||||
decreaseIndentPattern: string | IRegExp;
|
||||
increaseIndentPattern: string | IRegExp;
|
||||
indentNextLinePattern?: string | IRegExp;
|
||||
unIndentedLinePattern?: string | IRegExp;
|
||||
}
|
||||
|
||||
interface ILanguageConfiguration {
|
||||
comments?: CommentRule;
|
||||
brackets?: CharacterPair[];
|
||||
autoClosingPairs?: Array<CharacterPair | IAutoClosingPairConditional>;
|
||||
surroundingPairs?: Array<CharacterPair | IAutoClosingPair>;
|
||||
wordPattern?: string | IRegExp;
|
||||
indentationRules?: IIndentationRules;
|
||||
folding?: FoldingRules;
|
||||
autoCloseBefore?: string;
|
||||
}
|
||||
|
||||
function isStringArr(something: string[] | null): something is string[] {
|
||||
if (!Array.isArray(something)) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0, len = something.length; i < len; i++) {
|
||||
if (typeof something[i] !== 'string') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
function isCharacterPair(something: CharacterPair | null): boolean {
|
||||
return (
|
||||
isStringArr(something)
|
||||
&& something.length === 2
|
||||
);
|
||||
}
|
||||
|
||||
export class LanguageConfigurationFileHandler {
|
||||
|
||||
private _done: boolean[];
|
||||
|
||||
constructor(
|
||||
@ITextMateService textMateService: ITextMateService,
|
||||
@IModeService private readonly _modeService: IModeService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IExtensionService private readonly _extensionService: IExtensionService
|
||||
) {
|
||||
this._done = [];
|
||||
|
||||
// Listen for hints that a language configuration is needed/usefull and then load it once
|
||||
this._modeService.onDidCreateMode((mode) => {
|
||||
const languageIdentifier = mode.getLanguageIdentifier();
|
||||
// Modes can be instantiated before the extension points have finished registering
|
||||
this._extensionService.whenInstalledExtensionsRegistered().then(() => {
|
||||
this._loadConfigurationsForMode(languageIdentifier);
|
||||
});
|
||||
});
|
||||
textMateService.onDidEncounterLanguage((languageId) => {
|
||||
this._loadConfigurationsForMode(this._modeService.getLanguageIdentifier(languageId)!);
|
||||
});
|
||||
}
|
||||
|
||||
private _loadConfigurationsForMode(languageIdentifier: LanguageIdentifier): void {
|
||||
if (this._done[languageIdentifier.id]) {
|
||||
return;
|
||||
}
|
||||
this._done[languageIdentifier.id] = true;
|
||||
|
||||
let configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language);
|
||||
configurationFiles.forEach((configFileLocation) => this._handleConfigFile(languageIdentifier, configFileLocation));
|
||||
}
|
||||
|
||||
private _handleConfigFile(languageIdentifier: LanguageIdentifier, configFileLocation: URI): void {
|
||||
this._fileService.resolveContent(configFileLocation, { encoding: 'utf8' }).then((contents) => {
|
||||
const errors: ParseError[] = [];
|
||||
const configuration = <ILanguageConfiguration>parse(contents.value.toString(), errors);
|
||||
if (errors.length) {
|
||||
console.error(nls.localize('parseErrors', "Errors parsing {0}: {1}", configFileLocation.toString(), errors.join('\n')));
|
||||
}
|
||||
this._handleConfig(languageIdentifier, configuration);
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
private _extractValidCommentRule(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): CommentRule | null {
|
||||
const source = configuration.comments;
|
||||
if (typeof source === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
if (!types.isObject(source)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`comments\` to be an object.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let result: CommentRule | null = null;
|
||||
if (typeof source.lineComment !== 'undefined') {
|
||||
if (typeof source.lineComment !== 'string') {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`comments.lineComment\` to be a string.`);
|
||||
} else {
|
||||
result = result || {};
|
||||
result.lineComment = source.lineComment;
|
||||
}
|
||||
}
|
||||
if (typeof source.blockComment !== 'undefined') {
|
||||
if (!isCharacterPair(source.blockComment)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`comments.blockComment\` to be an array of two strings.`);
|
||||
} else {
|
||||
result = result || {};
|
||||
result.blockComment = source.blockComment;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _extractValidBrackets(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): CharacterPair[] | null {
|
||||
const source = configuration.brackets;
|
||||
if (typeof source === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(source)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`brackets\` to be an array.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let result: CharacterPair[] | null = null;
|
||||
for (let i = 0, len = source.length; i < len; i++) {
|
||||
const pair = source[i];
|
||||
if (!isCharacterPair(pair)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`brackets[${i}]\` to be an array of two strings.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
result = result || [];
|
||||
result.push(pair);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _extractValidAutoClosingPairs(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): IAutoClosingPairConditional[] | null {
|
||||
const source = configuration.autoClosingPairs;
|
||||
if (typeof source === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(source)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`autoClosingPairs\` to be an array.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let result: IAutoClosingPairConditional[] | null = null;
|
||||
for (let i = 0, len = source.length; i < len; i++) {
|
||||
const pair = source[i];
|
||||
if (Array.isArray(pair)) {
|
||||
if (!isCharacterPair(pair)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`autoClosingPairs[${i}]\` to be an array of two strings or an object.`);
|
||||
continue;
|
||||
}
|
||||
result = result || [];
|
||||
result.push({ open: pair[0], close: pair[1] });
|
||||
} else {
|
||||
if (!types.isObject(pair)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`autoClosingPairs[${i}]\` to be an array of two strings or an object.`);
|
||||
continue;
|
||||
}
|
||||
if (typeof pair.open !== 'string') {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`autoClosingPairs[${i}].open\` to be a string.`);
|
||||
continue;
|
||||
}
|
||||
if (typeof pair.close !== 'string') {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`autoClosingPairs[${i}].close\` to be a string.`);
|
||||
continue;
|
||||
}
|
||||
if (typeof pair.notIn !== 'undefined') {
|
||||
if (!isStringArr(pair.notIn)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`autoClosingPairs[${i}].notIn\` to be a string array.`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result = result || [];
|
||||
result.push({ open: pair.open, close: pair.close, notIn: pair.notIn });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _extractValidSurroundingPairs(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): IAutoClosingPair[] | null {
|
||||
const source = configuration.surroundingPairs;
|
||||
if (typeof source === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(source)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`surroundingPairs\` to be an array.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let result: IAutoClosingPair[] | null = null;
|
||||
for (let i = 0, len = source.length; i < len; i++) {
|
||||
const pair = source[i];
|
||||
if (Array.isArray(pair)) {
|
||||
if (!isCharacterPair(pair)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`surroundingPairs[${i}]\` to be an array of two strings or an object.`);
|
||||
continue;
|
||||
}
|
||||
result = result || [];
|
||||
result.push({ open: pair[0], close: pair[1] });
|
||||
} else {
|
||||
if (!types.isObject(pair)) {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`surroundingPairs[${i}]\` to be an array of two strings or an object.`);
|
||||
continue;
|
||||
}
|
||||
if (typeof pair.open !== 'string') {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`surroundingPairs[${i}].open\` to be a string.`);
|
||||
continue;
|
||||
}
|
||||
if (typeof pair.close !== 'string') {
|
||||
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`surroundingPairs[${i}].close\` to be a string.`);
|
||||
continue;
|
||||
}
|
||||
result = result || [];
|
||||
result.push({ open: pair.open, close: pair.close });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// private _mapCharacterPairs(pairs: Array<CharacterPair | IAutoClosingPairConditional>): IAutoClosingPairConditional[] {
|
||||
// return pairs.map(pair => {
|
||||
// if (Array.isArray(pair)) {
|
||||
// return { open: pair[0], close: pair[1] };
|
||||
// }
|
||||
// return <IAutoClosingPairConditional>pair;
|
||||
// });
|
||||
// }
|
||||
|
||||
private _handleConfig(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): void {
|
||||
|
||||
let richEditConfig: LanguageConfiguration = {};
|
||||
|
||||
const comments = this._extractValidCommentRule(languageIdentifier, configuration);
|
||||
if (comments) {
|
||||
richEditConfig.comments = comments;
|
||||
}
|
||||
|
||||
const brackets = this._extractValidBrackets(languageIdentifier, configuration);
|
||||
if (brackets) {
|
||||
richEditConfig.brackets = brackets;
|
||||
}
|
||||
|
||||
const autoClosingPairs = this._extractValidAutoClosingPairs(languageIdentifier, configuration);
|
||||
if (autoClosingPairs) {
|
||||
richEditConfig.autoClosingPairs = autoClosingPairs;
|
||||
}
|
||||
|
||||
const surroundingPairs = this._extractValidSurroundingPairs(languageIdentifier, configuration);
|
||||
if (surroundingPairs) {
|
||||
richEditConfig.surroundingPairs = surroundingPairs;
|
||||
}
|
||||
|
||||
const autoCloseBefore = configuration.autoCloseBefore;
|
||||
if (typeof autoCloseBefore === 'string') {
|
||||
richEditConfig.autoCloseBefore = autoCloseBefore;
|
||||
}
|
||||
|
||||
if (configuration.wordPattern) {
|
||||
try {
|
||||
let wordPattern = this._parseRegex(configuration.wordPattern);
|
||||
if (wordPattern) {
|
||||
richEditConfig.wordPattern = wordPattern;
|
||||
}
|
||||
} catch (error) {
|
||||
// Malformed regexes are ignored
|
||||
}
|
||||
}
|
||||
|
||||
if (configuration.indentationRules) {
|
||||
let indentationRules = this._mapIndentationRules(configuration.indentationRules);
|
||||
if (indentationRules) {
|
||||
richEditConfig.indentationRules = indentationRules;
|
||||
}
|
||||
}
|
||||
|
||||
if (configuration.folding) {
|
||||
let markers = configuration.folding.markers;
|
||||
|
||||
richEditConfig.folding = {
|
||||
offSide: configuration.folding.offSide,
|
||||
markers: markers ? { start: new RegExp(markers.start), end: new RegExp(markers.end) } : undefined
|
||||
};
|
||||
}
|
||||
|
||||
LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig);
|
||||
}
|
||||
|
||||
private _parseRegex(value: string | IRegExp) {
|
||||
if (typeof value === 'string') {
|
||||
return new RegExp(value, '');
|
||||
} else if (typeof value === 'object') {
|
||||
return new RegExp(value.pattern, value.flags);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private _mapIndentationRules(indentationRules: IIndentationRules): IndentationRule | null {
|
||||
try {
|
||||
let increaseIndentPattern = this._parseRegex(indentationRules.increaseIndentPattern);
|
||||
let decreaseIndentPattern = this._parseRegex(indentationRules.decreaseIndentPattern);
|
||||
|
||||
if (increaseIndentPattern && decreaseIndentPattern) {
|
||||
let result: IndentationRule = {
|
||||
increaseIndentPattern: increaseIndentPattern,
|
||||
decreaseIndentPattern: decreaseIndentPattern
|
||||
};
|
||||
|
||||
if (indentationRules.indentNextLinePattern) {
|
||||
result.indentNextLinePattern = this._parseRegex(indentationRules.indentNextLinePattern);
|
||||
}
|
||||
if (indentationRules.unIndentedLinePattern) {
|
||||
result.unIndentedLinePattern = this._parseRegex(indentationRules.unIndentedLinePattern);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
// Malformed regexes are ignored
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const schemaId = 'vscode://schemas/language-configuration';
|
||||
const schema: IJSONSchema = {
|
||||
allowComments: true,
|
||||
default: {
|
||||
comments: {
|
||||
blockComment: ['/*', '*/'],
|
||||
lineComment: '//'
|
||||
},
|
||||
brackets: [['(', ')'], ['[', ']'], ['{', '}']],
|
||||
autoClosingPairs: [['(', ')'], ['[', ']'], ['{', '}']],
|
||||
surroundingPairs: [['(', ')'], ['[', ']'], ['{', '}']]
|
||||
},
|
||||
definitions: {
|
||||
openBracket: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.openBracket', 'The opening bracket character or string sequence.')
|
||||
},
|
||||
closeBracket: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.closeBracket', 'The closing bracket character or string sequence.')
|
||||
},
|
||||
bracketPair: {
|
||||
type: 'array',
|
||||
items: [{
|
||||
$ref: '#definitions/openBracket'
|
||||
}, {
|
||||
$ref: '#definitions/closeBracket'
|
||||
}]
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
comments: {
|
||||
default: {
|
||||
blockComment: ['/*', '*/'],
|
||||
lineComment: '//'
|
||||
},
|
||||
description: nls.localize('schema.comments', 'Defines the comment symbols'),
|
||||
type: 'object',
|
||||
properties: {
|
||||
blockComment: {
|
||||
type: 'array',
|
||||
description: nls.localize('schema.blockComments', 'Defines how block comments are marked.'),
|
||||
items: [{
|
||||
type: 'string',
|
||||
description: nls.localize('schema.blockComment.begin', 'The character sequence that starts a block comment.')
|
||||
}, {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.blockComment.end', 'The character sequence that ends a block comment.')
|
||||
}]
|
||||
},
|
||||
lineComment: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.lineComment', 'The character sequence that starts a line comment.')
|
||||
}
|
||||
}
|
||||
},
|
||||
brackets: {
|
||||
default: [['(', ')'], ['[', ']'], ['{', '}']],
|
||||
description: nls.localize('schema.brackets', 'Defines the bracket symbols that increase or decrease the indentation.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#definitions/bracketPair'
|
||||
}
|
||||
},
|
||||
autoClosingPairs: {
|
||||
default: [['(', ')'], ['[', ']'], ['{', '}']],
|
||||
description: nls.localize('schema.autoClosingPairs', 'Defines the bracket pairs. When a opening bracket is entered, the closing bracket is inserted automatically.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: [{
|
||||
$ref: '#definitions/bracketPair'
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
open: {
|
||||
$ref: '#definitions/openBracket'
|
||||
},
|
||||
close: {
|
||||
$ref: '#definitions/closeBracket'
|
||||
},
|
||||
notIn: {
|
||||
type: 'array',
|
||||
description: nls.localize('schema.autoClosingPairs.notIn', 'Defines a list of scopes where the auto pairs are disabled.'),
|
||||
items: {
|
||||
enum: ['string', 'comment']
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
autoCloseBefore: {
|
||||
default: ';:.,=}])> \n\t',
|
||||
description: nls.localize('schema.autoCloseBefore', 'Defines what characters must be after the cursor in order for bracket or quote autoclosing to occur when using the \'languageDefined\' autoclosing setting. This is typically the set of characters which can not start an expression.'),
|
||||
type: 'string',
|
||||
},
|
||||
surroundingPairs: {
|
||||
default: [['(', ')'], ['[', ']'], ['{', '}']],
|
||||
description: nls.localize('schema.surroundingPairs', 'Defines the bracket pairs that can be used to surround a selected string.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: [{
|
||||
$ref: '#definitions/bracketPair'
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
open: {
|
||||
$ref: '#definitions/openBracket'
|
||||
},
|
||||
close: {
|
||||
$ref: '#definitions/closeBracket'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
wordPattern: {
|
||||
default: '',
|
||||
description: nls.localize('schema.wordPattern', 'Defines what is considered to be a word in the programming language.'),
|
||||
type: ['string', 'object'],
|
||||
properties: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.wordPattern.pattern', 'The RegExp pattern used to match words.'),
|
||||
default: '',
|
||||
},
|
||||
flags: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.wordPattern.flags', 'The RegExp flags used to match words.'),
|
||||
default: 'g',
|
||||
pattern: '^([gimuy]+)$',
|
||||
patternErrorMessage: nls.localize('schema.wordPattern.flags.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
|
||||
}
|
||||
}
|
||||
},
|
||||
indentationRules: {
|
||||
default: {
|
||||
increaseIndentPattern: '',
|
||||
decreaseIndentPattern: ''
|
||||
},
|
||||
description: nls.localize('schema.indentationRules', 'The language\'s indentation settings.'),
|
||||
type: 'object',
|
||||
properties: {
|
||||
increaseIndentPattern: {
|
||||
type: ['string', 'object'],
|
||||
description: nls.localize('schema.indentationRules.increaseIndentPattern', 'If a line matches this pattern, then all the lines after it should be indented once (until another rule matches).'),
|
||||
properties: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.increaseIndentPattern.pattern', 'The RegExp pattern for increaseIndentPattern.'),
|
||||
default: '',
|
||||
},
|
||||
flags: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.increaseIndentPattern.flags', 'The RegExp flags for increaseIndentPattern.'),
|
||||
default: '',
|
||||
pattern: '^([gimuy]+)$',
|
||||
patternErrorMessage: nls.localize('schema.indentationRules.increaseIndentPattern.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
|
||||
}
|
||||
}
|
||||
},
|
||||
decreaseIndentPattern: {
|
||||
type: ['string', 'object'],
|
||||
description: nls.localize('schema.indentationRules.decreaseIndentPattern', 'If a line matches this pattern, then all the lines after it should be unindented once (until another rule matches).'),
|
||||
properties: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.decreaseIndentPattern.pattern', 'The RegExp pattern for decreaseIndentPattern.'),
|
||||
default: '',
|
||||
},
|
||||
flags: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.decreaseIndentPattern.flags', 'The RegExp flags for decreaseIndentPattern.'),
|
||||
default: '',
|
||||
pattern: '^([gimuy]+)$',
|
||||
patternErrorMessage: nls.localize('schema.indentationRules.decreaseIndentPattern.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
|
||||
}
|
||||
}
|
||||
},
|
||||
indentNextLinePattern: {
|
||||
type: ['string', 'object'],
|
||||
description: nls.localize('schema.indentationRules.indentNextLinePattern', 'If a line matches this pattern, then **only the next line** after it should be indented once.'),
|
||||
properties: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.indentNextLinePattern.pattern', 'The RegExp pattern for indentNextLinePattern.'),
|
||||
default: '',
|
||||
},
|
||||
flags: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.indentNextLinePattern.flags', 'The RegExp flags for indentNextLinePattern.'),
|
||||
default: '',
|
||||
pattern: '^([gimuy]+)$',
|
||||
patternErrorMessage: nls.localize('schema.indentationRules.indentNextLinePattern.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
|
||||
}
|
||||
}
|
||||
},
|
||||
unIndentedLinePattern: {
|
||||
type: ['string', 'object'],
|
||||
description: nls.localize('schema.indentationRules.unIndentedLinePattern', 'If a line matches this pattern, then its indentation should not be changed and it should not be evaluated against the other rules.'),
|
||||
properties: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.unIndentedLinePattern.pattern', 'The RegExp pattern for unIndentedLinePattern.'),
|
||||
default: '',
|
||||
},
|
||||
flags: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.indentationRules.unIndentedLinePattern.flags', 'The RegExp flags for unIndentedLinePattern.'),
|
||||
default: '',
|
||||
pattern: '^([gimuy]+)$',
|
||||
patternErrorMessage: nls.localize('schema.indentationRules.unIndentedLinePattern.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
folding: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.folding', 'The language\'s folding settings.'),
|
||||
properties: {
|
||||
offSide: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('schema.folding.offSide', 'A language adheres to the off-side rule if blocks in that language are expressed by their indentation. If set, empty lines belong to the subsequent block.'),
|
||||
},
|
||||
markers: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.folding.markers', 'Language specific folding markers such as \'#region\' and \'#endregion\'. The start and end regexes will be tested against the contents of all lines and must be designed efficiently'),
|
||||
properties: {
|
||||
start: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.folding.markers.start', 'The RegExp pattern for the start marker. The regexp must start with \'^\'.')
|
||||
},
|
||||
end: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.folding.markers.end', 'The RegExp pattern for the end marker. The regexp must start with \'^\'.')
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
let schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
|
||||
schemaRegistry.registerSchema(schemaId, schema);
|
||||
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
|
||||
/**
|
||||
* Shows a message when opening a large file which has been memory optimized (and features disabled).
|
||||
*/
|
||||
export class LargeFileOptimizationsWarner extends Disposable implements IEditorContribution {
|
||||
|
||||
private static readonly ID = 'editor.contrib.largeFileOptimizationsWarner';
|
||||
|
||||
private _isDisabled: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._isDisabled = Boolean(this._storageService.getBoolean('editor.neverPromptForLargeFiles', StorageScope.GLOBAL, false));
|
||||
|
||||
this._register(this._editor.onDidChangeModel((e) => {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (this._isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.isTooLargeForTokenization()) {
|
||||
const message = nls.localize(
|
||||
{
|
||||
key: 'largeFile',
|
||||
comment: [
|
||||
'Variable 0 will be a file name.'
|
||||
]
|
||||
},
|
||||
"{0}: tokenization, wrapping and folding have been turned off for this large file in order to reduce memory usage and avoid freezing or crashing.",
|
||||
path.basename(model.uri.path)
|
||||
);
|
||||
|
||||
this._notificationService.prompt(Severity.Info, message, [
|
||||
{
|
||||
label: nls.localize('dontShowAgain', "Don't Show Again"),
|
||||
run: () => {
|
||||
this._isDisabled = true;
|
||||
this._storageService.store('editor.neverPromptForLargeFiles', true, StorageScope.GLOBAL);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: nls.localize('removeOptimizations', "Forcefully enable features"),
|
||||
run: () => {
|
||||
this._configurationService.updateValue(`editor.largeFileOptimizations`, false).then(() => {
|
||||
this._notificationService.info(nls.localize('reopenFilePrompt', "Please reopen file in order for this setting to take effect."));
|
||||
}, (err) => {
|
||||
this._notificationService.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return LargeFileOptimizationsWarner.ID;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(LargeFileOptimizationsWarner);
|
||||
64
src/vs/workbench/contrib/codeEditor/browser/menuPreventer.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
|
||||
/**
|
||||
* Prevents the top-level menu from showing up when doing Alt + Click in the editor
|
||||
*/
|
||||
export class MenuPreventer extends Disposable implements IEditorContribution {
|
||||
|
||||
private static readonly ID = 'editor.contrib.menuPreventer';
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _altListeningMouse: boolean;
|
||||
private _altMouseTriggered: boolean;
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._altListeningMouse = false;
|
||||
this._altMouseTriggered = false;
|
||||
|
||||
// A global crossover handler to prevent menu bar from showing up
|
||||
// When <alt> is hold, we will listen to mouse events and prevent
|
||||
// the release event up <alt> if the mouse is triggered.
|
||||
|
||||
this._register(this._editor.onMouseDown((e) => {
|
||||
if (this._altListeningMouse) {
|
||||
this._altMouseTriggered = true;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._editor.onKeyDown((e) => {
|
||||
if (e.equals(KeyMod.Alt)) {
|
||||
if (!this._altListeningMouse) {
|
||||
this._altMouseTriggered = false;
|
||||
}
|
||||
this._altListeningMouse = true;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._editor.onKeyUp((e) => {
|
||||
if (e.equals(KeyMod.Alt)) {
|
||||
if (this._altMouseTriggered) {
|
||||
e.preventDefault();
|
||||
}
|
||||
this._altListeningMouse = false;
|
||||
this._altMouseTriggered = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return MenuPreventer.ID;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(MenuPreventer);
|
||||
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as process from 'vs/base/common/process';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
|
||||
import { ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EndOfLinePreference } from 'vs/editor/common/model';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
|
||||
export class SelectionClipboard extends Disposable implements IEditorContribution {
|
||||
private static SELECTION_LENGTH_LIMIT = 65536;
|
||||
private static readonly ID = 'editor.contrib.selectionClipboard';
|
||||
|
||||
constructor(editor: ICodeEditor, @IClipboardService clipboardService: IClipboardService) {
|
||||
super();
|
||||
|
||||
if (platform.isLinux) {
|
||||
let isEnabled = editor.getConfiguration().contribInfo.selectionClipboard;
|
||||
|
||||
this._register(editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
|
||||
if (e.contribInfo) {
|
||||
isEnabled = editor.getConfiguration().contribInfo.selectionClipboard;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(editor.onMouseDown((e: IEditorMouseEvent) => {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
if (e.event.middleButton) {
|
||||
e.event.preventDefault();
|
||||
editor.focus();
|
||||
|
||||
if (e.target.position) {
|
||||
editor.setPosition(e.target.position);
|
||||
}
|
||||
|
||||
if (e.target.type === MouseTargetType.SCROLLBAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.nextTick(() => {
|
||||
// TODO@Alex: electron weirdness: calling clipboard.readText('selection') generates a paste event, so no need to execute paste ourselves
|
||||
clipboardService.readText('selection');
|
||||
// keybindingService.executeCommand(Handler.Paste, {
|
||||
// text: clipboard.readText('selection'),
|
||||
// pasteOnNewLine: false
|
||||
// });
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
let setSelectionToClipboard = this._register(new RunOnceScheduler(() => {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
let model = editor.getModel();
|
||||
let selections = editor.getSelections();
|
||||
selections = selections.slice(0);
|
||||
selections.sort(Range.compareRangesUsingStarts);
|
||||
|
||||
let resultLength = 0;
|
||||
for (const sel of selections) {
|
||||
if (sel.isEmpty()) {
|
||||
// Only write if all cursors have selection
|
||||
return;
|
||||
}
|
||||
resultLength += model.getValueLengthInRange(sel);
|
||||
}
|
||||
|
||||
if (resultLength > SelectionClipboard.SELECTION_LENGTH_LIMIT) {
|
||||
// This is a large selection!
|
||||
// => do not write it to the selection clipboard
|
||||
return;
|
||||
}
|
||||
|
||||
let result: string[] = [];
|
||||
for (const sel of selections) {
|
||||
result.push(model.getValueInRange(sel, EndOfLinePreference.TextDefined));
|
||||
}
|
||||
|
||||
let textToCopy = result.join(model.getEOL());
|
||||
clipboardService.writeText(textToCopy, 'selection');
|
||||
}, 100));
|
||||
|
||||
this._register(editor.onDidChangeCursorSelection((e: ICursorSelectionChangedEvent) => {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
setSelectionToClipboard.schedule();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return SelectionClipboard.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(SelectionClipboard);
|
||||
@@ -0,0 +1,52 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
|
||||
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
|
||||
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
|
||||
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
|
||||
import { SelectionClipboard } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
|
||||
import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion';
|
||||
|
||||
export function getSimpleEditorOptions(): IEditorOptions {
|
||||
return {
|
||||
wordWrap: 'on',
|
||||
overviewRulerLanes: 0,
|
||||
glyphMargin: false,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
selectOnLineNumbers: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
selectionHighlight: false,
|
||||
scrollbar: {
|
||||
horizontal: 'hidden'
|
||||
},
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
scrollBeyondLastLine: false,
|
||||
renderLineHighlight: 'none',
|
||||
fixedOverflowWidgets: true,
|
||||
acceptSuggestionOnEnter: 'smart',
|
||||
minimap: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions {
|
||||
return {
|
||||
isSimpleWidget: true,
|
||||
contributions: [
|
||||
MenuPreventer,
|
||||
SelectionClipboard,
|
||||
ContextMenuController,
|
||||
SuggestController,
|
||||
SnippetController2,
|
||||
TabCompletionController,
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M16 3H9v.445c-.034-.07-.062-.145-.1-.21a2.318 2.318 0 0 0-.759-.793C7.81 2.229 7 2.117 7 2.109V0H4v2.438c-.078-.042-.15-.092-.233-.125a2.685 2.685 0 0 0-1.013-.188c-.184 0-.392.016-.619.055a5.422 5.422 0 0 0-.506.115c-.149.045-.337.096-.493.162-.173.077-.39.151-.533.25L0 3.006v3.17c0 .297.045.56.136.821.1.291.255.525.454.736.207.224.462.351.75.467.065.027.137.026.205.039a2.827 2.827 0 0 0-.651.532 3.064 3.064 0 0 0-.6 1.061 3.81 3.81 0 0 0-.188 1.21c0 .402.062.784.185 1.137.132.377.323.709.562.978a2.748 2.748 0 0 0 2.131.942c.333 0 .595-.042.876-.13a2.61 2.61 0 0 0 .678-.333L5 13.225v-1.811L8.586 15h4.828l-2-2H16V3zM5 10.586V8.523l-.556-.404c-.081-.043-.152-.083-.228-.119h1.96c.196 0 .409.266.634.266.187 0 .366-.007.538-.029L5 10.586z"/></g><g id="icon_x5F_bg"><path class="st2" d="M10 4v2h3v4H9l2-2H9l-3 3 3 3h2l-2-2h6V4zM3.869 3.568a1.21 1.21 0 0 0-.473-.329c-.274-.111-.623-.15-1.055-.076a3.5 3.5 0 0 0-.711.208 1.501 1.501 0 0 0-.234.125l-.043.03v1.056l.168-.139c.149-.124.326-.225.527-.303.196-.074.399-.113.604-.113.188 0 .33.051.431.157.087.095.137.248.147.456l-.962.144c-.219.03-.41.086-.57.166a1.245 1.245 0 0 0-.398.311 1.234 1.234 0 0 0-.229.426 1.714 1.714 0 0 0 .011 1.008 1.096 1.096 0 0 0 .638.67c.155.063.328.093.528.093a1.25 1.25 0 0 0 .978-.441v.345h1.007V4.769c0-.255-.03-.484-.089-.681a1.423 1.423 0 0 0-.275-.52zm-.636 1.896V5.7c0 .119-.018.231-.055.341a.745.745 0 0 1-.377.447.694.694 0 0 1-.512.027.454.454 0 0 1-.156-.094.389.389 0 0 1-.094-.139.474.474 0 0 1-.035-.186c0-.077.009-.147.024-.212a.33.33 0 0 1 .078-.141.436.436 0 0 1 .161-.109 1.3 1.3 0 0 1 .305-.073l.661-.097zM8.284 4.397a2.253 2.253 0 0 0-.244-.656 1.354 1.354 0 0 0-.436-.459 1.165 1.165 0 0 0-.642-.173 1.136 1.136 0 0 0-.69.223 1.312 1.312 0 0 0-.264.266V1.119H5.09v6.224h.918v-.281a1.023 1.023 0 0 0 .472.328c.098.032.208.047.33.047.255 0 .483-.06.677-.177.192-.115.355-.278.486-.486a2.29 2.29 0 0 0 .293-.718 3.87 3.87 0 0 0 .096-.886 3.76 3.76 0 0 0-.078-.773zm-.861.758c0 .232-.02.439-.059.613-.036.172-.09.315-.159.424a.639.639 0 0 1-.233.237.582.582 0 0 1-.565.014.683.683 0 0 1-.211-.183.925.925 0 0 1-.141-.283 1.187 1.187 0 0 1-.054-.358v-.517c0-.164.02-.314.059-.447.037-.132.088-.242.157-.336a.668.668 0 0 1 .228-.208.584.584 0 0 1 .289-.071.554.554 0 0 1 .497.279c.063.099.108.214.143.354.031.143.049.306.049.482zM2.409 10.019a.913.913 0 0 1 .316-.239c.218-.1.547-.105.766-.018.104.042.204.1.32.184l.329.26V9.064l-.096-.062a1.932 1.932 0 0 0-.905-.215c-.308 0-.593.057-.846.168-.25.11-.467.27-.647.475a2.072 2.072 0 0 0-.403.717c-.09.272-.137.57-.137.895 0 .289.043.561.129.808.087.249.212.471.374.652.161.185.361.333.597.441.232.104.493.155.778.155.233 0 .434-.028.613-.084a1.85 1.85 0 0 0 .466-.217l.078-.061v-.889l-.2.095a.402.402 0 0 1-.076.026c-.05.017-.099.035-.128.049-.036.023-.227.09-.227.09-.06.024-.14.043-.218.059a.977.977 0 0 1-.599-.057.827.827 0 0 1-.306-.225 1.088 1.088 0 0 1-.205-.376 1.728 1.728 0 0 1-.076-.529c0-.21.028-.399.083-.56.054-.158.129-.294.22-.4z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.suggest-input-container {
|
||||
padding: 3px 4px 5px;
|
||||
}
|
||||
|
||||
.suggest-input-container .monaco-editor-background,
|
||||
.suggest-input-container .monaco-editor,
|
||||
.suggest-input-container .mtk1 {
|
||||
/* allow the embedded monaco to be styled from the outer context */
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.suggest-input-container .suggest-input-placeholder {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
margin-top: 2px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./suggestEnabledInput';
|
||||
import { $, Dimension, addClass, append, removeClass } from 'vs/base/browser/dom';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { URI as uri } from 'vs/base/common/uri';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
|
||||
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
|
||||
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
|
||||
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ColorIdentifier, editorSelectionBackground, inputBackground, inputBorder, inputForeground, inputPlaceholderForeground, selectionBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IStyleOverrides, IThemable, attachStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
|
||||
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
|
||||
import { SelectionClipboard } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
|
||||
|
||||
interface SuggestResultsProvider {
|
||||
/**
|
||||
* Provider function for suggestion results.
|
||||
*
|
||||
* @param query the full text of the input.
|
||||
*/
|
||||
provideResults: (query: string) => string[];
|
||||
|
||||
/**
|
||||
* Trigger characters for this input. Suggestions will appear when one of these is typed,
|
||||
* or upon `ctrl+space` triggering at a word boundary.
|
||||
*
|
||||
* Defaults to the empty array.
|
||||
*/
|
||||
triggerCharacters?: string[];
|
||||
|
||||
/**
|
||||
* Defines the sorting function used when showing results.
|
||||
*
|
||||
* Defaults to the identity function.
|
||||
*/
|
||||
sortKey?: (result: string) => string;
|
||||
}
|
||||
|
||||
interface SuggestEnabledInputOptions {
|
||||
/**
|
||||
* The text to show when no input is present.
|
||||
*
|
||||
* Defaults to the empty string.
|
||||
*/
|
||||
placeholderText?: string;
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* Context key tracking the focus state of this element
|
||||
*/
|
||||
focusContextKey?: IContextKey<boolean>;
|
||||
}
|
||||
|
||||
export interface ISuggestEnabledInputStyleOverrides extends IStyleOverrides {
|
||||
inputBackground?: ColorIdentifier;
|
||||
inputForeground?: ColorIdentifier;
|
||||
inputBorder?: ColorIdentifier;
|
||||
inputPlaceholderForeground?: ColorIdentifier;
|
||||
}
|
||||
|
||||
type ISuggestEnabledInputStyles = {
|
||||
[P in keyof ISuggestEnabledInputStyleOverrides]: Color;
|
||||
};
|
||||
|
||||
export function attachSuggestEnabledInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: ISuggestEnabledInputStyleOverrides): IDisposable {
|
||||
return attachStyler(themeService, {
|
||||
inputBackground: (style && style.inputBackground) || inputBackground,
|
||||
inputForeground: (style && style.inputForeground) || inputForeground,
|
||||
inputBorder: (style && style.inputBorder) || inputBorder,
|
||||
inputPlaceholderForeground: (style && style.inputPlaceholderForeground) || inputPlaceholderForeground,
|
||||
} as ISuggestEnabledInputStyleOverrides, widget);
|
||||
}
|
||||
|
||||
export class SuggestEnabledInput extends Widget implements IThemable {
|
||||
|
||||
private _onShouldFocusResults = new Emitter<void>();
|
||||
readonly onShouldFocusResults: Event<void> = this._onShouldFocusResults.event;
|
||||
|
||||
private _onEnter = new Emitter<void>();
|
||||
readonly onEnter: Event<void> = this._onEnter.event;
|
||||
|
||||
private _onInputDidChange = new Emitter<string | undefined>();
|
||||
readonly onInputDidChange: Event<string | undefined> = this._onInputDidChange.event;
|
||||
|
||||
private disposables: IDisposable[] = [];
|
||||
private inputWidget: CodeEditorWidget;
|
||||
private stylingContainer: HTMLDivElement;
|
||||
private placeholderText: HTMLDivElement;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
parent: HTMLElement,
|
||||
suggestionProvider: SuggestResultsProvider,
|
||||
ariaLabel: string,
|
||||
resourceHandle: string,
|
||||
options: SuggestEnabledInputOptions,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IModelService modelService: IModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.stylingContainer = append(parent, $('.suggest-input-container'));
|
||||
this.placeholderText = append(this.stylingContainer, $('.suggest-input-placeholder', undefined, options.placeholderText || ''));
|
||||
|
||||
const editorOptions: IEditorOptions = mixin(
|
||||
getSimpleEditorOptions(),
|
||||
getSuggestEnabledInputOptions(ariaLabel));
|
||||
|
||||
this.inputWidget = instantiationService.createInstance(CodeEditorWidget, this.stylingContainer,
|
||||
editorOptions,
|
||||
{
|
||||
contributions: [SuggestController, SnippetController2, ContextMenuController, MenuPreventer, SelectionClipboard],
|
||||
isSimpleWidget: true,
|
||||
});
|
||||
this.disposables.push(this.inputWidget);
|
||||
|
||||
let scopeHandle = uri.parse(resourceHandle);
|
||||
this.inputWidget.setModel(modelService.createModel('', null, scopeHandle, true));
|
||||
|
||||
this.disposables.push(this.inputWidget.onDidPaste(() => this.setValue(this.getValue()))); // setter cleanses
|
||||
|
||||
this.disposables.push((this.inputWidget.onDidFocusEditorText(() => {
|
||||
if (options.focusContextKey) { options.focusContextKey.set(true); }
|
||||
addClass(this.stylingContainer, 'synthetic-focus');
|
||||
})));
|
||||
this.disposables.push((this.inputWidget.onDidBlurEditorText(() => {
|
||||
if (options.focusContextKey) { options.focusContextKey.set(false); }
|
||||
removeClass(this.stylingContainer, 'synthetic-focus');
|
||||
})));
|
||||
|
||||
const onKeyDownMonaco = Event.chain(this.inputWidget.onKeyDown);
|
||||
onKeyDownMonaco.filter(e => e.keyCode === KeyCode.Enter).on(e => { e.preventDefault(); this._onEnter.fire(); }, this, this.disposables);
|
||||
onKeyDownMonaco.filter(e => e.keyCode === KeyCode.DownArrow && (isMacintosh ? e.metaKey : e.ctrlKey)).on(() => this._onShouldFocusResults.fire(), this, this.disposables);
|
||||
|
||||
let preexistingContent = this.getValue();
|
||||
const inputWidgetModel = this.inputWidget.getModel();
|
||||
if (inputWidgetModel) {
|
||||
this.disposables.push(inputWidgetModel.onDidChangeContent(() => {
|
||||
let content = this.getValue();
|
||||
this.placeholderText.style.visibility = content ? 'hidden' : 'visible';
|
||||
if (preexistingContent.trim() === content.trim()) { return; }
|
||||
this._onInputDidChange.fire(undefined);
|
||||
preexistingContent = content;
|
||||
}));
|
||||
}
|
||||
|
||||
let validatedSuggestProvider = {
|
||||
provideResults: suggestionProvider.provideResults,
|
||||
sortKey: suggestionProvider.sortKey || (a => a),
|
||||
triggerCharacters: suggestionProvider.triggerCharacters || []
|
||||
};
|
||||
|
||||
this.setValue(options.value || '');
|
||||
|
||||
this.disposables.push(modes.CompletionProviderRegistry.register({ scheme: scopeHandle.scheme, pattern: '**/' + scopeHandle.path, hasAccessToAllModels: true }, {
|
||||
triggerCharacters: validatedSuggestProvider.triggerCharacters,
|
||||
provideCompletionItems: (model: ITextModel, position: Position, _context: modes.CompletionContext) => {
|
||||
let query = model.getValue();
|
||||
|
||||
let wordStart = query.lastIndexOf(' ', position.column - 1) + 1;
|
||||
let alreadyTypedCount = position.column - wordStart - 1;
|
||||
|
||||
// dont show suggestions if the user has typed something, but hasn't used the trigger character
|
||||
if (alreadyTypedCount > 0 && (validatedSuggestProvider.triggerCharacters).indexOf(query[wordStart]) === -1) { return { suggestions: [] }; }
|
||||
|
||||
return {
|
||||
suggestions: suggestionProvider.provideResults(query).map(result => {
|
||||
return <modes.CompletionItem>{
|
||||
label: result,
|
||||
insertText: result,
|
||||
range: Range.fromPositions(position.delta(0, -alreadyTypedCount), position),
|
||||
sortText: validatedSuggestProvider.sortKey(result),
|
||||
kind: modes.CompletionItemKind.Keyword
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public get onFocus(): Event<void> { return this.inputWidget.onDidFocusEditorText; }
|
||||
|
||||
public setValue(val: string) {
|
||||
val = val.replace(/\s/g, ' ');
|
||||
const fullRange = new Range(1, 1, 1, this.getValue().length + 1);
|
||||
this.inputWidget.executeEdits('suggestEnabledInput.setValue', [EditOperation.replace(fullRange, val)]);
|
||||
this.inputWidget.setScrollTop(0);
|
||||
this.inputWidget.setPosition(new Position(1, val.length + 1));
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputWidget.getValue();
|
||||
}
|
||||
|
||||
|
||||
public style(colors: ISuggestEnabledInputStyles): void {
|
||||
this.stylingContainer.style.backgroundColor = colors.inputBackground ? colors.inputBackground.toString() : null;
|
||||
this.stylingContainer.style.color = colors.inputForeground ? colors.inputForeground.toString() : null;
|
||||
this.placeholderText.style.color = colors.inputPlaceholderForeground ? colors.inputPlaceholderForeground.toString() : null;
|
||||
|
||||
this.stylingContainer.style.borderWidth = '1px';
|
||||
this.stylingContainer.style.borderStyle = 'solid';
|
||||
this.stylingContainer.style.borderColor = colors.inputBorder ?
|
||||
colors.inputBorder.toString() :
|
||||
'transparent';
|
||||
|
||||
const cursor = this.stylingContainer.getElementsByClassName('cursor')[0] as HTMLDivElement;
|
||||
if (cursor) {
|
||||
cursor.style.backgroundColor = colors.inputForeground ? colors.inputForeground.toString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
public focus(selectAll?: boolean): void {
|
||||
this.inputWidget.focus();
|
||||
|
||||
if (selectAll && this.inputWidget.getValue()) {
|
||||
this.selectAll();
|
||||
}
|
||||
}
|
||||
|
||||
public layout(dimension: Dimension): void {
|
||||
this.inputWidget.layout(dimension);
|
||||
this.placeholderText.style.width = `${dimension.width}px`;
|
||||
}
|
||||
|
||||
private selectAll(): void {
|
||||
this.inputWidget.setSelection(new Range(1, 1, 1, this.getValue().length + 1));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Override styles in selections.ts
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let selectionColor = theme.getColor(selectionBackground);
|
||||
if (selectionColor) {
|
||||
selectionColor = selectionColor.transparent(0.4);
|
||||
} else {
|
||||
selectionColor = theme.getColor(editorSelectionBackground);
|
||||
}
|
||||
|
||||
if (selectionColor) {
|
||||
collector.addRule(`.suggest-input-container .monaco-editor .focused .selected-text { background-color: ${selectionColor}; }`);
|
||||
}
|
||||
|
||||
// Override inactive selection bg
|
||||
const inputBackgroundColor = theme.getColor(inputBackground);
|
||||
if (inputBackgroundColor) {
|
||||
collector.addRule(`.suggest-input-container .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`);
|
||||
}
|
||||
|
||||
// Override selected fg
|
||||
const inputForegroundColor = theme.getColor(inputForeground);
|
||||
if (inputForegroundColor) {
|
||||
collector.addRule(`.suggest-input-container .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function getSuggestEnabledInputOptions(ariaLabel?: string): IEditorOptions {
|
||||
return {
|
||||
fontSize: 13,
|
||||
lineHeight: 20,
|
||||
wordWrap: 'off',
|
||||
scrollbar: { vertical: 'hidden', },
|
||||
roundedSelection: false,
|
||||
renderIndentGuides: false,
|
||||
cursorWidth: 1,
|
||||
fontFamily: ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif',
|
||||
ariaLabel: ariaLabel || '',
|
||||
|
||||
snippetSuggestions: 'none',
|
||||
suggest: { filterGraceful: false, showIcons: false }
|
||||
};
|
||||
}
|
||||
45
src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
|
||||
export class ToggleMinimapAction extends Action {
|
||||
public static readonly ID = 'editor.action.toggleMinimap';
|
||||
public static readonly LABEL = nls.localize('toggleMinimap', "View: Toggle Minimap");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
const newValue = !this._configurationService.getValue<boolean>('editor.minimap.enabled');
|
||||
return this._configurationService.updateValue('editor.minimap.enabled', newValue, ConfigurationTarget.USER);
|
||||
}
|
||||
}
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMinimapAction, ToggleMinimapAction.ID, ToggleMinimapAction.LABEL), 'View: Toggle Minimap');
|
||||
|
||||
// {{SQL CARBON EDIT}} - Disable unused menu item
|
||||
// MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
|
||||
// group: '5_editor',
|
||||
// command: {
|
||||
// id: ToggleMinimapAction.ID,
|
||||
// title: nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap"),
|
||||
// toggled: ContextKeyExpr.equals('config.editor.minimap.enabled', true)
|
||||
// },
|
||||
// order: 2
|
||||
// });
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
@@ -0,0 +1,91 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
|
||||
|
||||
export class ToggleMultiCursorModifierAction extends Action {
|
||||
|
||||
public static readonly ID = 'workbench.action.toggleMultiCursorModifier';
|
||||
public static readonly LABEL = nls.localize('toggleLocation', "Toggle Multi-Cursor Modifier");
|
||||
|
||||
private static readonly multiCursorModifierConfigurationKey = 'editor.multiCursorModifier';
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
const editorConf = this.configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
|
||||
const newValue: 'ctrlCmd' | 'alt' = (editorConf.multiCursorModifier === 'ctrlCmd' ? 'alt' : 'ctrlCmd');
|
||||
|
||||
return this.configurationService.updateValue(ToggleMultiCursorModifierAction.multiCursorModifierConfigurationKey, newValue, ConfigurationTarget.USER);
|
||||
}
|
||||
}
|
||||
|
||||
const multiCursorModifier = new RawContextKey<string>('multiCursorModifier', 'altKey');
|
||||
|
||||
class MultiCursorModifierContextKeyController implements IWorkbenchContribution {
|
||||
|
||||
private readonly _multiCursorModifier: IContextKey<string>;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
this._multiCursorModifier = multiCursorModifier.bindTo(contextKeyService);
|
||||
configurationService.onDidChangeConfiguration((e) => {
|
||||
if (e.affectsConfiguration('editor.multiCursorModifier')) {
|
||||
this._update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
const editorConf = this.configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
|
||||
const value = (editorConf.multiCursorModifier === 'ctrlCmd' ? 'ctrlCmd' : 'altKey');
|
||||
this._multiCursorModifier.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(MultiCursorModifierContextKeyController, LifecyclePhase.Restored);
|
||||
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMultiCursorModifierAction, ToggleMultiCursorModifierAction.ID, ToggleMultiCursorModifierAction.LABEL), 'Toggle Multi-Cursor Modifier');
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, {
|
||||
group: '3_multi',
|
||||
command: {
|
||||
id: ToggleMultiCursorModifierAction.ID,
|
||||
title: nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor")
|
||||
},
|
||||
when: multiCursorModifier.isEqualTo('ctrlCmd'),
|
||||
order: 1
|
||||
});
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, {
|
||||
group: '3_multi',
|
||||
command: {
|
||||
id: ToggleMultiCursorModifierAction.ID,
|
||||
title: (
|
||||
platform.isMacintosh
|
||||
? nls.localize('miMultiCursorCmd', "Switch to Cmd+Click for Multi-Cursor")
|
||||
: nls.localize('miMultiCursorCtrl', "Switch to Ctrl+Click for Multi-Cursor")
|
||||
)
|
||||
},
|
||||
when: multiCursorModifier.isEqualTo('altKey'),
|
||||
order: 1
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
|
||||
export class ToggleRenderControlCharacterAction extends Action {
|
||||
|
||||
public static readonly ID = 'editor.action.toggleRenderControlCharacter';
|
||||
public static readonly LABEL = nls.localize('toggleRenderControlCharacters', "View: Toggle Control Characters");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
let newRenderControlCharacters = !this._configurationService.getValue<boolean>('editor.renderControlCharacters');
|
||||
return this._configurationService.updateValue('editor.renderControlCharacters', newRenderControlCharacters, ConfigurationTarget.USER);
|
||||
}
|
||||
}
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRenderControlCharacterAction, ToggleRenderControlCharacterAction.ID, ToggleRenderControlCharacterAction.LABEL), 'View: Toggle Control Characters');
|
||||
|
||||
// {{SQL CARBON EDIT}} - Disable unused menu item
|
||||
// MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
|
||||
// group: '5_editor',
|
||||
// command: {
|
||||
// id: ToggleRenderControlCharacterAction.ID,
|
||||
// title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters"),
|
||||
// toggled: ContextKeyExpr.equals('config.editor.renderControlCharacters', true)
|
||||
// },
|
||||
// order: 4
|
||||
// });
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
@@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
|
||||
export class ToggleRenderWhitespaceAction extends Action {
|
||||
|
||||
public static readonly ID = 'editor.action.toggleRenderWhitespace';
|
||||
public static readonly LABEL = nls.localize('toggleRenderWhitespace', "View: Toggle Render Whitespace");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
const renderWhitespace = this._configurationService.getValue<string>('editor.renderWhitespace');
|
||||
|
||||
let newRenderWhitespace: string;
|
||||
if (renderWhitespace === 'none') {
|
||||
newRenderWhitespace = 'all';
|
||||
} else {
|
||||
newRenderWhitespace = 'none';
|
||||
}
|
||||
|
||||
return this._configurationService.updateValue('editor.renderWhitespace', newRenderWhitespace, ConfigurationTarget.USER);
|
||||
}
|
||||
}
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRenderWhitespaceAction, ToggleRenderWhitespaceAction.ID, ToggleRenderWhitespaceAction.LABEL), 'View: Toggle Render Whitespace');
|
||||
|
||||
// {{SQL CARBON EDIT}} - Disable unused menu item
|
||||
// MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
|
||||
// group: '5_editor',
|
||||
// command: {
|
||||
// id: ToggleRenderWhitespaceAction.ID,
|
||||
// title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace"),
|
||||
// toggled: ContextKeyExpr.notEquals('config.editor.renderWhitespace', 'none')
|
||||
// },
|
||||
// order: 3
|
||||
// });
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
318
src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { EDITOR_DEFAULTS, InternalEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
const transientWordWrapState = 'transientWordWrapState';
|
||||
const isWordWrapMinifiedKey = 'isWordWrapMinified';
|
||||
const isDominatedByLongLinesKey = 'isDominatedByLongLines';
|
||||
const inDiffEditorKey = 'inDiffEditor';
|
||||
|
||||
/**
|
||||
* State written/read by the toggle word wrap action and associated with a particular model.
|
||||
*/
|
||||
interface IWordWrapTransientState {
|
||||
readonly forceWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded';
|
||||
readonly forceWordWrapMinified: boolean;
|
||||
}
|
||||
|
||||
interface IWordWrapState {
|
||||
readonly configuredWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded' | undefined;
|
||||
readonly configuredWordWrapMinified: boolean;
|
||||
readonly transientState: IWordWrapTransientState | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store (in memory) the word wrap state for a particular model.
|
||||
*/
|
||||
export function writeTransientState(model: ITextModel, state: IWordWrapTransientState | null, codeEditorService: ICodeEditorService): void {
|
||||
codeEditorService.setTransientModelProperty(model, transientWordWrapState, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read (in memory) the word wrap state for a particular model.
|
||||
*/
|
||||
function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState {
|
||||
return codeEditorService.getTransientModelProperty(model, transientWordWrapState);
|
||||
}
|
||||
|
||||
function readWordWrapState(model: ITextModel, configurationService: ITextResourceConfigurationService, codeEditorService: ICodeEditorService): IWordWrapState {
|
||||
const editorConfig = configurationService.getValue(model.uri, 'editor') as { wordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded'; wordWrapMinified: boolean };
|
||||
let _configuredWordWrap = editorConfig && (typeof editorConfig.wordWrap === 'string' || typeof editorConfig.wordWrap === 'boolean') ? editorConfig.wordWrap : undefined;
|
||||
|
||||
// Compatibility with old true or false values
|
||||
if (<any>_configuredWordWrap === true) {
|
||||
_configuredWordWrap = 'on';
|
||||
} else if (<any>_configuredWordWrap === false) {
|
||||
_configuredWordWrap = 'off';
|
||||
}
|
||||
|
||||
const _configuredWordWrapMinified = editorConfig && typeof editorConfig.wordWrapMinified === 'boolean' ? editorConfig.wordWrapMinified : undefined;
|
||||
const _transientState = readTransientState(model, codeEditorService);
|
||||
return {
|
||||
configuredWordWrap: _configuredWordWrap,
|
||||
configuredWordWrapMinified: (typeof _configuredWordWrapMinified === 'boolean' ? _configuredWordWrapMinified : EDITOR_DEFAULTS.wordWrapMinified),
|
||||
transientState: _transientState
|
||||
};
|
||||
}
|
||||
|
||||
function toggleWordWrap(editor: ICodeEditor, state: IWordWrapState): IWordWrapState {
|
||||
if (state.transientState) {
|
||||
// toggle off => go to null
|
||||
return {
|
||||
configuredWordWrap: state.configuredWordWrap,
|
||||
configuredWordWrapMinified: state.configuredWordWrapMinified,
|
||||
transientState: null
|
||||
};
|
||||
}
|
||||
|
||||
const config = editor.getConfiguration();
|
||||
let transientState: IWordWrapTransientState;
|
||||
|
||||
const actualWrappingInfo = config.wrappingInfo;
|
||||
if (actualWrappingInfo.isWordWrapMinified) {
|
||||
// => wrapping due to minified file
|
||||
transientState = {
|
||||
forceWordWrap: 'off',
|
||||
forceWordWrapMinified: false
|
||||
};
|
||||
} else if (state.configuredWordWrap !== 'off') {
|
||||
// => wrapping is configured to be on (or some variant)
|
||||
transientState = {
|
||||
forceWordWrap: 'off',
|
||||
forceWordWrapMinified: false
|
||||
};
|
||||
} else {
|
||||
// => wrapping is configured to be off
|
||||
transientState = {
|
||||
forceWordWrap: 'on',
|
||||
forceWordWrapMinified: state.configuredWordWrapMinified
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
configuredWordWrap: state.configuredWordWrap,
|
||||
configuredWordWrapMinified: state.configuredWordWrapMinified,
|
||||
transientState: transientState
|
||||
};
|
||||
}
|
||||
|
||||
const TOGGLE_WORD_WRAP_ID = 'editor.action.toggleWordWrap';
|
||||
class ToggleWordWrapAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: TOGGLE_WORD_WRAP_ID,
|
||||
label: nls.localize('toggle.wordwrap', "View: Toggle Word Wrap"),
|
||||
alias: 'View: Toggle Word Wrap',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: null,
|
||||
primary: KeyMod.Alt | KeyCode.KEY_Z,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
const editorConfiguration = editor.getConfiguration();
|
||||
if (editorConfiguration.wrappingInfo.inDiffEditor) {
|
||||
// Cannot change wrapping settings inside the diff editor
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
notificationService.info(nls.localize('wordWrap.notInDiffEditor', "Cannot toggle word wrap in a diff editor."));
|
||||
return;
|
||||
}
|
||||
|
||||
const textResourceConfigurationService = accessor.get(ITextResourceConfigurationService);
|
||||
const codeEditorService = accessor.get(ICodeEditorService);
|
||||
const model = editor.getModel();
|
||||
|
||||
if (!canToggleWordWrap(model.uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the current state
|
||||
const currentState = readWordWrapState(model, textResourceConfigurationService, codeEditorService);
|
||||
// Compute the new state
|
||||
const newState = toggleWordWrap(editor, currentState);
|
||||
// Write the new state
|
||||
// (this will cause an event and the controller will apply the state)
|
||||
writeTransientState(model, newState.transientState, codeEditorService);
|
||||
}
|
||||
}
|
||||
|
||||
class ToggleWordWrapController extends Disposable implements IEditorContribution {
|
||||
|
||||
private static readonly _ID = 'editor.contrib.toggleWordWrapController';
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@IContextKeyService readonly contextKeyService: IContextKeyService,
|
||||
@ITextResourceConfigurationService readonly configurationService: ITextResourceConfigurationService,
|
||||
@ICodeEditorService readonly codeEditorService: ICodeEditorService
|
||||
) {
|
||||
super();
|
||||
|
||||
const configuration = this.editor.getConfiguration();
|
||||
const isWordWrapMinified = this.contextKeyService.createKey(isWordWrapMinifiedKey, this._isWordWrapMinified(configuration));
|
||||
const isDominatedByLongLines = this.contextKeyService.createKey(isDominatedByLongLinesKey, this._isDominatedByLongLines(configuration));
|
||||
const inDiffEditor = this.contextKeyService.createKey(inDiffEditorKey, this._inDiffEditor(configuration));
|
||||
let currentlyApplyingEditorConfig = false;
|
||||
|
||||
this._register(editor.onDidChangeConfiguration((e) => {
|
||||
if (!e.wrappingInfo) {
|
||||
return;
|
||||
}
|
||||
const configuration = this.editor.getConfiguration();
|
||||
isWordWrapMinified.set(this._isWordWrapMinified(configuration));
|
||||
isDominatedByLongLines.set(this._isDominatedByLongLines(configuration));
|
||||
inDiffEditor.set(this._inDiffEditor(configuration));
|
||||
if (!currentlyApplyingEditorConfig) {
|
||||
// I am not the cause of the word wrap getting changed
|
||||
ensureWordWrapSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(editor.onDidChangeModel((e) => {
|
||||
ensureWordWrapSettings();
|
||||
}));
|
||||
|
||||
this._register(codeEditorService.onDidChangeTransientModelProperty(() => {
|
||||
ensureWordWrapSettings();
|
||||
}));
|
||||
|
||||
const ensureWordWrapSettings = () => {
|
||||
// Ensure correct word wrap settings
|
||||
const newModel = this.editor.getModel();
|
||||
if (!newModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configuration = this.editor.getConfiguration();
|
||||
if (this._inDiffEditor(configuration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canToggleWordWrap(newModel.uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read current configured values and toggle state
|
||||
const desiredState = readWordWrapState(newModel, this.configurationService, this.codeEditorService);
|
||||
|
||||
// Apply the state
|
||||
try {
|
||||
currentlyApplyingEditorConfig = true;
|
||||
this._applyWordWrapState(desiredState);
|
||||
} finally {
|
||||
currentlyApplyingEditorConfig = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _applyWordWrapState(state: IWordWrapState): void {
|
||||
if (state.transientState) {
|
||||
// toggle is on
|
||||
this.editor.updateOptions({
|
||||
wordWrap: state.transientState.forceWordWrap,
|
||||
wordWrapMinified: state.transientState.forceWordWrapMinified
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// toggle is off
|
||||
this.editor.updateOptions({
|
||||
wordWrap: state.configuredWordWrap,
|
||||
wordWrapMinified: state.configuredWordWrapMinified
|
||||
});
|
||||
}
|
||||
|
||||
private _isWordWrapMinified(config: InternalEditorOptions): boolean {
|
||||
return config.wrappingInfo.isWordWrapMinified;
|
||||
}
|
||||
|
||||
private _isDominatedByLongLines(config: InternalEditorOptions): boolean {
|
||||
return config.wrappingInfo.isDominatedByLongLines;
|
||||
}
|
||||
|
||||
private _inDiffEditor(config: InternalEditorOptions): boolean {
|
||||
return config.wrappingInfo.inDiffEditor;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return ToggleWordWrapController._ID;
|
||||
}
|
||||
}
|
||||
|
||||
function canToggleWordWrap(uri: URI): boolean {
|
||||
if (!uri) {
|
||||
return false;
|
||||
}
|
||||
return (uri.scheme !== 'output' && uri.scheme !== 'vscode');
|
||||
}
|
||||
|
||||
|
||||
registerEditorContribution(ToggleWordWrapController);
|
||||
|
||||
registerEditorAction(ToggleWordWrapAction);
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
|
||||
command: {
|
||||
id: TOGGLE_WORD_WRAP_ID,
|
||||
title: nls.localize('unwrapMinified', "Disable wrapping for this file"),
|
||||
iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/contrib/codeEditor/electron-browser/media/WordWrap_16x.svg')) }
|
||||
},
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(
|
||||
ContextKeyExpr.not(inDiffEditorKey),
|
||||
ContextKeyExpr.has(isDominatedByLongLinesKey),
|
||||
ContextKeyExpr.has(isWordWrapMinifiedKey)
|
||||
)
|
||||
});
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
|
||||
command: {
|
||||
id: TOGGLE_WORD_WRAP_ID,
|
||||
title: nls.localize('wrapMinified', "Enable wrapping for this file"),
|
||||
iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/contrib/codeEditor/electron-browser/media/WordWrap_16x.svg')) }
|
||||
},
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(
|
||||
ContextKeyExpr.not(inDiffEditorKey),
|
||||
ContextKeyExpr.has(isDominatedByLongLinesKey),
|
||||
ContextKeyExpr.not(isWordWrapMinifiedKey)
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
// View menu
|
||||
// {{SQL CARBON EDIT}} - Disable unused menu item
|
||||
// MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
|
||||
// group: '5_editor',
|
||||
// command: {
|
||||
// id: TOGGLE_WORD_WRAP_ID,
|
||||
// title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap")
|
||||
// },
|
||||
// order: 1
|
||||
// });
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { ReferencesController } from 'vs/editor/contrib/referenceSearch/referencesController';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export class WorkbenchReferencesController extends ReferencesController {
|
||||
|
||||
public constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICodeEditorService editorService: ICodeEditorService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
) {
|
||||
super(
|
||||
false,
|
||||
editor,
|
||||
contextKeyService,
|
||||
editorService,
|
||||
notificationService,
|
||||
instantiationService,
|
||||
storageService,
|
||||
configurationService
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(WorkbenchReferencesController);
|
||||
@@ -0,0 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import './sleepResumeRepaintMinimap';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
|
||||
class SleepResumeRepaintMinimap implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService
|
||||
) {
|
||||
ipc.on('vscode:osResume', () => {
|
||||
codeEditorService.listCodeEditors().forEach(editor => editor.render(true));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SleepResumeRepaintMinimap, LifecyclePhase.Eventually);
|
||||
70
src/vs/workbench/contrib/codeinset/common/codeInset.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry';
|
||||
import { ProviderResult } from 'vs/editor/common/modes';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
|
||||
export interface ICodeInsetSymbol {
|
||||
id: string;
|
||||
range: IRange;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface CodeInsetProvider {
|
||||
onDidChange?: Event<this>;
|
||||
provideCodeInsets(model: ITextModel, token: CancellationToken): ProviderResult<ICodeInsetSymbol[]>;
|
||||
resolveCodeInset(model: ITextModel, codeInset: ICodeInsetSymbol, token: CancellationToken): ProviderResult<ICodeInsetSymbol>;
|
||||
}
|
||||
|
||||
export const CodeInsetProviderRegistry = new LanguageFeatureRegistry<CodeInsetProvider>();
|
||||
|
||||
export interface ICodeInsetData {
|
||||
symbol: ICodeInsetSymbol;
|
||||
provider: CodeInsetProvider;
|
||||
resolved?: boolean;
|
||||
}
|
||||
|
||||
export function getCodeInsetData(model: ITextModel, token: CancellationToken): Promise<ICodeInsetData[]> {
|
||||
|
||||
const symbols: ICodeInsetData[] = [];
|
||||
const providers = CodeInsetProviderRegistry.ordered(model);
|
||||
|
||||
const promises = providers.map(provider =>
|
||||
Promise.resolve(provider.provideCodeInsets(model, token)).then(result => {
|
||||
if (Array.isArray(result)) {
|
||||
for (let symbol of result) {
|
||||
symbols.push({ symbol, provider });
|
||||
}
|
||||
}
|
||||
}).catch(onUnexpectedExternalError));
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
|
||||
return mergeSort(symbols, (a, b) => {
|
||||
// sort by lineNumber, provider-rank, and column
|
||||
if (a.symbol.range.startLineNumber < b.symbol.range.startLineNumber) {
|
||||
return -1;
|
||||
} else if (a.symbol.range.startLineNumber > b.symbol.range.startLineNumber) {
|
||||
return 1;
|
||||
} else if (providers.indexOf(a.provider) < providers.indexOf(b.provider)) {
|
||||
return -1;
|
||||
} else if (providers.indexOf(a.provider) > providers.indexOf(b.provider)) {
|
||||
return 1;
|
||||
} else if (a.symbol.range.startColumn < b.symbol.range.startColumn) {
|
||||
return -1;
|
||||
} else if (a.symbol.range.startColumn > b.symbol.range.startColumn) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { StableEditorScrollState } from 'vs/editor/browser/core/editorState';
|
||||
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
|
||||
import { CodeInsetProviderRegistry, getCodeInsetData, ICodeInsetData } from '../common/codeInset';
|
||||
import { CodeInsetWidget, CodeInsetHelper } from './codeInsetWidget';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
|
||||
// import { localize } from 'vs/nls';
|
||||
|
||||
export class CodeInsetController implements editorCommon.IEditorContribution {
|
||||
|
||||
static get(editor: editorBrowser.ICodeEditor): CodeInsetController {
|
||||
return editor.getContribution(CodeInsetController.ID);
|
||||
}
|
||||
|
||||
private static readonly ID: string = 'css.editor.codeInset';
|
||||
|
||||
private _isEnabled: boolean;
|
||||
|
||||
private _globalToDispose: IDisposable[];
|
||||
private _localToDispose: IDisposable[];
|
||||
private _insetWidgets: CodeInsetWidget[];
|
||||
private _pendingWebviews = new Map<string, (element: WebviewElement) => any>();
|
||||
private _currentFindCodeInsetSymbolsPromise: CancelablePromise<ICodeInsetData[]> | null;
|
||||
private _modelChangeCounter: number;
|
||||
private _currentResolveCodeInsetSymbolsPromise: CancelablePromise<any> | null;
|
||||
private _detectVisibleInsets: RunOnceScheduler;
|
||||
|
||||
constructor(
|
||||
private _editor: editorBrowser.ICodeEditor,
|
||||
@IConfigurationService private readonly _configService: IConfigurationService,
|
||||
) {
|
||||
this._isEnabled = this._configService.getValue<boolean>('editor.codeInsets');
|
||||
|
||||
this._globalToDispose = [];
|
||||
this._localToDispose = [];
|
||||
this._insetWidgets = [];
|
||||
this._currentFindCodeInsetSymbolsPromise = null;
|
||||
this._modelChangeCounter = 0;
|
||||
|
||||
this._globalToDispose.push(this._editor.onDidChangeModel(() => this._onModelChange()));
|
||||
this._globalToDispose.push(this._editor.onDidChangeModelLanguage(() => this._onModelChange()));
|
||||
this._globalToDispose.push(this._configService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('editor.codeInsets')) {
|
||||
let prevIsEnabled = this._isEnabled;
|
||||
this._isEnabled = this._configService.getValue<boolean>('editor.codeInsets');
|
||||
if (prevIsEnabled !== this._isEnabled) {
|
||||
this._onModelChange();
|
||||
}
|
||||
}
|
||||
}));
|
||||
this._globalToDispose.push(CodeInsetProviderRegistry.onDidChange(this._onModelChange, this));
|
||||
this._onModelChange();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._localDispose();
|
||||
this._globalToDispose = dispose(this._globalToDispose);
|
||||
}
|
||||
|
||||
acceptWebview(symbolId: string, webviewElement: WebviewElement): boolean {
|
||||
const pendingWebview = this._pendingWebviews.get(symbolId);
|
||||
if (pendingWebview) {
|
||||
pendingWebview(webviewElement);
|
||||
this._pendingWebviews.delete(symbolId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _localDispose(): void {
|
||||
if (this._currentFindCodeInsetSymbolsPromise) {
|
||||
this._currentFindCodeInsetSymbolsPromise.cancel();
|
||||
this._currentFindCodeInsetSymbolsPromise = null;
|
||||
this._modelChangeCounter++;
|
||||
}
|
||||
if (this._currentResolveCodeInsetSymbolsPromise) {
|
||||
this._currentResolveCodeInsetSymbolsPromise.cancel();
|
||||
this._currentResolveCodeInsetSymbolsPromise = null;
|
||||
}
|
||||
this._localToDispose = dispose(this._localToDispose);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return CodeInsetController.ID;
|
||||
}
|
||||
|
||||
private _onModelChange(): void {
|
||||
this._localDispose();
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model || !this._isEnabled || !CodeInsetProviderRegistry.has(model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const provider of CodeInsetProviderRegistry.all(model)) {
|
||||
if (typeof provider.onDidChange === 'function') {
|
||||
let registration = provider.onDidChange(() => scheduler.schedule());
|
||||
this._localToDispose.push(registration);
|
||||
}
|
||||
}
|
||||
|
||||
this._detectVisibleInsets = new RunOnceScheduler(() => {
|
||||
this._onViewportChanged();
|
||||
}, 500);
|
||||
|
||||
const scheduler = new RunOnceScheduler(() => {
|
||||
const counterValue = ++this._modelChangeCounter;
|
||||
if (this._currentFindCodeInsetSymbolsPromise) {
|
||||
this._currentFindCodeInsetSymbolsPromise.cancel();
|
||||
}
|
||||
|
||||
this._currentFindCodeInsetSymbolsPromise = createCancelablePromise(token => getCodeInsetData(model, token));
|
||||
|
||||
this._currentFindCodeInsetSymbolsPromise.then(codeInsetData => {
|
||||
if (counterValue === this._modelChangeCounter) { // only the last one wins
|
||||
this._renderCodeInsetSymbols(codeInsetData);
|
||||
this._detectVisibleInsets.schedule();
|
||||
}
|
||||
}, onUnexpectedError);
|
||||
}, 250);
|
||||
|
||||
this._localToDispose.push(scheduler);
|
||||
|
||||
this._localToDispose.push(this._detectVisibleInsets);
|
||||
|
||||
this._localToDispose.push(this._editor.onDidChangeModelContent(() => {
|
||||
this._editor.changeDecorations(changeAccessor => {
|
||||
this._editor.changeViewZones(viewAccessor => {
|
||||
let toDispose: CodeInsetWidget[] = [];
|
||||
let lastInsetLineNumber: number = -1;
|
||||
this._insetWidgets.forEach(inset => {
|
||||
if (!inset.isValid() || lastInsetLineNumber === inset.getLineNumber()) {
|
||||
// invalid -> Inset collapsed, attach range doesn't exist anymore
|
||||
// line_number -> insets should never be on the same line
|
||||
toDispose.push(inset);
|
||||
}
|
||||
else {
|
||||
inset.reposition(viewAccessor);
|
||||
lastInsetLineNumber = inset.getLineNumber();
|
||||
}
|
||||
});
|
||||
let helper = new CodeInsetHelper();
|
||||
toDispose.forEach((l) => {
|
||||
l.dispose(helper, viewAccessor);
|
||||
this._insetWidgets.splice(this._insetWidgets.indexOf(l), 1);
|
||||
});
|
||||
helper.commit(changeAccessor);
|
||||
});
|
||||
});
|
||||
// Compute new `visible` code insets
|
||||
this._detectVisibleInsets.schedule();
|
||||
// Ask for all references again
|
||||
scheduler.schedule();
|
||||
}));
|
||||
|
||||
this._localToDispose.push(this._editor.onDidScrollChange(e => {
|
||||
if (e.scrollTopChanged && this._insetWidgets.length > 0) {
|
||||
this._detectVisibleInsets.schedule();
|
||||
}
|
||||
}));
|
||||
|
||||
this._localToDispose.push(this._editor.onDidLayoutChange(() => {
|
||||
this._detectVisibleInsets.schedule();
|
||||
}));
|
||||
|
||||
this._localToDispose.push(toDisposable(() => {
|
||||
if (this._editor.getModel()) {
|
||||
const scrollState = StableEditorScrollState.capture(this._editor);
|
||||
this._editor.changeDecorations((changeAccessor) => {
|
||||
this._editor.changeViewZones((accessor) => {
|
||||
this._disposeAllInsets(changeAccessor, accessor);
|
||||
});
|
||||
});
|
||||
scrollState.restore(this._editor);
|
||||
} else {
|
||||
// No accessors available
|
||||
this._disposeAllInsets(null, null);
|
||||
}
|
||||
}));
|
||||
|
||||
scheduler.schedule();
|
||||
}
|
||||
|
||||
private _disposeAllInsets(decChangeAccessor: IModelDecorationsChangeAccessor | null, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor | null): void {
|
||||
let helper = new CodeInsetHelper();
|
||||
this._insetWidgets.forEach((Inset) => Inset.dispose(helper, viewZoneChangeAccessor));
|
||||
if (decChangeAccessor) {
|
||||
helper.commit(decChangeAccessor);
|
||||
}
|
||||
this._insetWidgets = [];
|
||||
}
|
||||
|
||||
private _renderCodeInsetSymbols(symbols: ICodeInsetData[]): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let maxLineNumber = this._editor.getModel().getLineCount();
|
||||
let groups: ICodeInsetData[][] = [];
|
||||
let lastGroup: ICodeInsetData[] | undefined;
|
||||
|
||||
for (let symbol of symbols) {
|
||||
let line = symbol.symbol.range.startLineNumber;
|
||||
if (line < 1 || line > maxLineNumber) {
|
||||
// invalid code Inset
|
||||
continue;
|
||||
} else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) {
|
||||
// on same line as previous
|
||||
lastGroup.push(symbol);
|
||||
} else {
|
||||
// on later line as previous
|
||||
lastGroup = [symbol];
|
||||
groups.push(lastGroup);
|
||||
}
|
||||
}
|
||||
|
||||
const scrollState = StableEditorScrollState.capture(this._editor);
|
||||
|
||||
this._editor.changeDecorations(changeAccessor => {
|
||||
this._editor.changeViewZones(accessor => {
|
||||
|
||||
let codeInsetIndex = 0, groupsIndex = 0, helper = new CodeInsetHelper();
|
||||
|
||||
while (groupsIndex < groups.length && codeInsetIndex < this._insetWidgets.length) {
|
||||
|
||||
let symbolsLineNumber = groups[groupsIndex][0].symbol.range.startLineNumber;
|
||||
let codeInsetLineNumber = this._insetWidgets[codeInsetIndex].getLineNumber();
|
||||
|
||||
if (codeInsetLineNumber < symbolsLineNumber) {
|
||||
this._insetWidgets[codeInsetIndex].dispose(helper, accessor);
|
||||
this._insetWidgets.splice(codeInsetIndex, 1);
|
||||
} else if (codeInsetLineNumber === symbolsLineNumber) {
|
||||
this._insetWidgets[codeInsetIndex].updateCodeInsetSymbols(groups[groupsIndex], helper);
|
||||
groupsIndex++;
|
||||
codeInsetIndex++;
|
||||
} else {
|
||||
this._insetWidgets.splice(
|
||||
codeInsetIndex,
|
||||
0,
|
||||
new CodeInsetWidget(groups[groupsIndex], this._editor, helper)
|
||||
);
|
||||
codeInsetIndex++;
|
||||
groupsIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete extra code insets
|
||||
while (codeInsetIndex < this._insetWidgets.length) {
|
||||
this._insetWidgets[codeInsetIndex].dispose(helper, accessor);
|
||||
this._insetWidgets.splice(codeInsetIndex, 1);
|
||||
}
|
||||
|
||||
// Create extra symbols
|
||||
while (groupsIndex < groups.length) {
|
||||
this._insetWidgets.push(new CodeInsetWidget(
|
||||
groups[groupsIndex],
|
||||
this._editor, helper
|
||||
));
|
||||
groupsIndex++;
|
||||
}
|
||||
|
||||
helper.commit(changeAccessor);
|
||||
});
|
||||
});
|
||||
|
||||
scrollState.restore(this._editor);
|
||||
}
|
||||
|
||||
private _onViewportChanged(): void {
|
||||
if (this._currentResolveCodeInsetSymbolsPromise) {
|
||||
this._currentResolveCodeInsetSymbolsPromise.cancel();
|
||||
this._currentResolveCodeInsetSymbolsPromise = null;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allWidgetRequests: ICodeInsetData[][] = [];
|
||||
const insetWidgets: CodeInsetWidget[] = [];
|
||||
this._insetWidgets.forEach(inset => {
|
||||
const widgetRequests = inset.computeIfNecessary(model);
|
||||
if (widgetRequests) {
|
||||
allWidgetRequests.push(widgetRequests);
|
||||
insetWidgets.push(inset);
|
||||
}
|
||||
});
|
||||
|
||||
if (allWidgetRequests.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentResolveCodeInsetSymbolsPromise = createCancelablePromise(token => {
|
||||
|
||||
const allPromises = allWidgetRequests.map((widgetRequests, r) => {
|
||||
|
||||
const widgetPromises = widgetRequests.map(request => {
|
||||
if (request.resolved) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
let a = new Promise(resolve => {
|
||||
this._pendingWebviews.set(request.symbol.id, element => {
|
||||
request.resolved = true;
|
||||
insetWidgets[r].adoptWebview(element);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
let b = request.provider.resolveCodeInset(model, request.symbol, token);
|
||||
return Promise.all([a, b]);
|
||||
});
|
||||
|
||||
return Promise.all(widgetPromises);
|
||||
});
|
||||
|
||||
return Promise.all(allPromises);
|
||||
});
|
||||
|
||||
this._currentResolveCodeInsetSymbolsPromise.then(() => {
|
||||
this._currentResolveCodeInsetSymbolsPromise = null;
|
||||
}).catch(err => {
|
||||
this._currentResolveCodeInsetSymbolsPromise = null;
|
||||
onUnexpectedError(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(CodeInsetController);
|
||||
|
||||
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
|
||||
id: 'editor',
|
||||
properties: {
|
||||
// ['editor.codeInsets']: {
|
||||
// description: localize('editor.codeInsets', "Enable/disable editor code insets"),
|
||||
// type: 'boolean',
|
||||
// default: false
|
||||
// }
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .codelens-decoration {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > span,
|
||||
.monaco-editor .codelens-decoration > a {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > a:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration.invisible-cl {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
@-moz-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
@-o-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
@-webkit-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
|
||||
.monaco-editor .codelens-decoration.fadein {
|
||||
-webkit-animation: fadein 0.5s linear;
|
||||
-moz-animation: fadein 0.5s linear;
|
||||
-o-animation: fadein 0.5s linear;
|
||||
animation: fadein 0.5s linear;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./codeInsetWidget';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
|
||||
import { ICodeInsetData } from '../common/codeInset';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { IModelDeltaDecoration, IModelDecorationsChangeAccessor, ITextModel } from 'vs/editor/common/model';
|
||||
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
|
||||
|
||||
|
||||
export interface IDecorationIdCallback {
|
||||
(decorationId: string): void;
|
||||
}
|
||||
|
||||
export class CodeInsetHelper {
|
||||
|
||||
private _removeDecorations: string[];
|
||||
private _addDecorations: IModelDeltaDecoration[];
|
||||
private _addDecorationsCallbacks: IDecorationIdCallback[];
|
||||
|
||||
constructor() {
|
||||
this._removeDecorations = [];
|
||||
this._addDecorations = [];
|
||||
this._addDecorationsCallbacks = [];
|
||||
}
|
||||
|
||||
addDecoration(decoration: IModelDeltaDecoration, callback: IDecorationIdCallback): void {
|
||||
this._addDecorations.push(decoration);
|
||||
this._addDecorationsCallbacks.push(callback);
|
||||
}
|
||||
|
||||
removeDecoration(decorationId: string): void {
|
||||
this._removeDecorations.push(decorationId);
|
||||
}
|
||||
|
||||
commit(changeAccessor: IModelDecorationsChangeAccessor): void {
|
||||
let resultingDecorations = changeAccessor.deltaDecorations(this._removeDecorations, this._addDecorations);
|
||||
for (let i = 0, len = resultingDecorations.length; i < len; i++) {
|
||||
this._addDecorationsCallbacks[i](resultingDecorations[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeInsetWidget {
|
||||
|
||||
private readonly _editor: editorBrowser.ICodeEditor;
|
||||
private _webview: WebviewElement;
|
||||
private _viewZone: editorBrowser.IViewZone;
|
||||
private _viewZoneId?: number = undefined;
|
||||
private _decorationIds: string[];
|
||||
private _data: ICodeInsetData[];
|
||||
private _range: Range;
|
||||
|
||||
constructor(
|
||||
data: ICodeInsetData[], // all the insets on the same line (often just one)
|
||||
editor: editorBrowser.ICodeEditor,
|
||||
helper: CodeInsetHelper
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._data = data;
|
||||
this._decorationIds = new Array<string>(this._data.length);
|
||||
|
||||
this._data.forEach((codeInsetData, i) => {
|
||||
|
||||
helper.addDecoration({
|
||||
range: codeInsetData.symbol.range,
|
||||
options: ModelDecorationOptions.EMPTY
|
||||
}, id => this._decorationIds[i] = id);
|
||||
|
||||
// the range contains all insets on this line
|
||||
if (!this._range) {
|
||||
this._range = Range.lift(codeInsetData.symbol.range);
|
||||
} else {
|
||||
this._range = Range.plusRange(this._range, codeInsetData.symbol.range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(helper: CodeInsetHelper, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor | null): void {
|
||||
while (this._decorationIds.length) {
|
||||
const decoration = this._decorationIds.pop();
|
||||
if (decoration) {
|
||||
helper.removeDecoration(decoration);
|
||||
}
|
||||
}
|
||||
if (viewZoneChangeAccessor) {
|
||||
if (typeof this._viewZoneId !== 'undefined') {
|
||||
viewZoneChangeAccessor.removeZone(this._viewZoneId);
|
||||
}
|
||||
this._viewZone = undefined!;
|
||||
}
|
||||
if (this._webview) {
|
||||
this._webview.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this._editor.hasModel() && this._decorationIds.some((id, i) => {
|
||||
const range = this._editor.getModel()!.getDecorationRange(id);
|
||||
const symbol = this._data[i].symbol;
|
||||
return !!range && Range.isEmpty(symbol.range) === range.isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
public updateCodeInsetSymbols(data: ICodeInsetData[], helper: CodeInsetHelper): void {
|
||||
while (this._decorationIds.length) {
|
||||
const decoration = this._decorationIds.pop();
|
||||
if (decoration) {
|
||||
helper.removeDecoration(decoration);
|
||||
}
|
||||
}
|
||||
this._data = data;
|
||||
this._decorationIds = new Array<string>(this._data.length);
|
||||
this._data.forEach((codeInsetData, i) => {
|
||||
helper.addDecoration({
|
||||
range: codeInsetData.symbol.range,
|
||||
options: ModelDecorationOptions.EMPTY
|
||||
}, id => this._decorationIds[i] = id);
|
||||
});
|
||||
}
|
||||
|
||||
public computeIfNecessary(model: ITextModel): ICodeInsetData[] {
|
||||
// Read editor current state
|
||||
for (let i = 0; i < this._decorationIds.length; i++) {
|
||||
const range = model.getDecorationRange(this._decorationIds[i]);
|
||||
if (range) {
|
||||
this._data[i].symbol.range = range;
|
||||
}
|
||||
}
|
||||
return this._data;
|
||||
}
|
||||
|
||||
public getLineNumber(): number {
|
||||
if (this._editor.hasModel()) {
|
||||
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
|
||||
if (range) {
|
||||
return range.startLineNumber;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public adoptWebview(webview: WebviewElement): void {
|
||||
|
||||
const lineNumber = this._range.endLineNumber;
|
||||
this._editor.changeViewZones(accessor => {
|
||||
|
||||
if (this._viewZoneId) {
|
||||
accessor.removeZone(this._viewZoneId);
|
||||
this._webview.dispose();
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
webview.mountTo(div);
|
||||
webview.onMessage((e: { type: string, payload: any }) => {
|
||||
// The webview contents can use a "size-info" message to report its size.
|
||||
if (e && e.type === 'size-info') {
|
||||
const margin = e.payload.height > 0 ? 5 : 0;
|
||||
this._viewZone.heightInPx = e.payload.height + margin;
|
||||
this._editor.changeViewZones(accessor => {
|
||||
if (this._viewZoneId) {
|
||||
accessor.layoutZone(this._viewZoneId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this._viewZone = {
|
||||
afterLineNumber: lineNumber,
|
||||
heightInPx: 50,
|
||||
domNode: div
|
||||
};
|
||||
this._viewZoneId = accessor.addZone(this._viewZone);
|
||||
this._webview = webview;
|
||||
});
|
||||
}
|
||||
|
||||
public reposition(viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): void {
|
||||
if (this.isValid() && this._editor.hasModel()) {
|
||||
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
|
||||
if (range) {
|
||||
this._viewZone.afterLineNumber = range.endLineNumber;
|
||||
}
|
||||
if (this._viewZoneId) {
|
||||
viewZoneChangeAccessor.layoutZone(this._viewZoneId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/vs/workbench/contrib/comments/common/commentModel.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { Comment, CommentThread, CommentThreadChangedEvent } from 'vs/editor/common/modes';
|
||||
import { groupBy, firstIndex, flatten } from 'vs/base/common/arrays';
|
||||
import { localize } from 'vs/nls';
|
||||
import { values } from 'vs/base/common/map';
|
||||
|
||||
export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent {
|
||||
owner: string;
|
||||
}
|
||||
|
||||
export class CommentNode {
|
||||
threadId: string;
|
||||
range: IRange;
|
||||
comment: Comment;
|
||||
replies: CommentNode[] = [];
|
||||
resource: URI;
|
||||
|
||||
constructor(threadId: string, resource: URI, comment: Comment, range: IRange) {
|
||||
this.threadId = threadId;
|
||||
this.comment = comment;
|
||||
this.resource = resource;
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
hasReply(): boolean {
|
||||
return this.replies && this.replies.length !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceWithCommentThreads {
|
||||
id: string;
|
||||
commentThreads: CommentNode[]; // The top level comments on the file. Replys are nested under each node.
|
||||
resource: URI;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
if (commentNodes.length > 1) {
|
||||
commentNodes[0].replies = commentNodes.slice(1, commentNodes.length);
|
||||
}
|
||||
|
||||
return commentNodes[0];
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentsModel {
|
||||
resourceCommentThreads: ResourceWithCommentThreads[];
|
||||
commentThreadsMap: Map<string, ResourceWithCommentThreads[]>;
|
||||
|
||||
constructor() {
|
||||
this.resourceCommentThreads = [];
|
||||
this.commentThreadsMap = new Map<string, ResourceWithCommentThreads[]>();
|
||||
}
|
||||
|
||||
public setCommentThreads(owner: string, commentThreads: CommentThread[]): void {
|
||||
this.commentThreadsMap.set(owner, this.groupByResource(commentThreads));
|
||||
this.resourceCommentThreads = flatten(values(this.commentThreadsMap));
|
||||
}
|
||||
|
||||
public updateCommentThreads(event: ICommentThreadChangedEvent): boolean {
|
||||
const { owner, removed, changed, added } = event;
|
||||
|
||||
let threadsForOwner = this.commentThreadsMap.get(owner) || [];
|
||||
|
||||
removed.forEach(thread => {
|
||||
// Find resource that has the comment thread
|
||||
const matchingResourceIndex = firstIndex(threadsForOwner, (resourceData) => resourceData.id === thread.resource);
|
||||
const matchingResourceData = threadsForOwner[matchingResourceIndex];
|
||||
|
||||
// Find comment node on resource that is that thread and remove it
|
||||
const index = firstIndex(matchingResourceData.commentThreads, (commentThread) => commentThread.threadId === thread.threadId);
|
||||
matchingResourceData.commentThreads.splice(index, 1);
|
||||
|
||||
// If the comment thread was the last thread for a resource, remove that resource from the list
|
||||
if (matchingResourceData.commentThreads.length === 0) {
|
||||
threadsForOwner.splice(matchingResourceIndex, 1);
|
||||
}
|
||||
});
|
||||
|
||||
changed.forEach(thread => {
|
||||
// Find resource that has the comment thread
|
||||
const matchingResourceIndex = firstIndex(threadsForOwner, (resourceData) => resourceData.id === thread.resource);
|
||||
const matchingResourceData = threadsForOwner[matchingResourceIndex];
|
||||
|
||||
// Find comment node on resource that is that thread and replace it
|
||||
const index = firstIndex(matchingResourceData.commentThreads, (commentThread) => commentThread.threadId === thread.threadId);
|
||||
if (index >= 0) {
|
||||
matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(URI.parse(matchingResourceData.id), thread);
|
||||
} else {
|
||||
matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(URI.parse(matchingResourceData.id), thread));
|
||||
}
|
||||
});
|
||||
|
||||
added.forEach(thread => {
|
||||
const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource);
|
||||
if (existingResource.length) {
|
||||
const resource = existingResource[0];
|
||||
if (thread.comments.length) {
|
||||
resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(resource.resource, thread));
|
||||
}
|
||||
} else {
|
||||
threadsForOwner.push(new ResourceWithCommentThreads(URI.parse(thread.resource!), [thread]));
|
||||
}
|
||||
});
|
||||
|
||||
this.commentThreadsMap.set(owner, threadsForOwner);
|
||||
this.resourceCommentThreads = flatten(values(this.commentThreadsMap));
|
||||
|
||||
return removed.length > 0 || changed.length > 0 || added.length > 0;
|
||||
}
|
||||
|
||||
public hasCommentThreads(): boolean {
|
||||
return !!this.resourceCommentThreads.length;
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
if (!this.resourceCommentThreads.length) {
|
||||
return localize('noComments', "There are no comments on this review.");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private groupByResource(commentThreads: CommentThread[]): ResourceWithCommentThreads[] {
|
||||
const resourceCommentThreads: ResourceWithCommentThreads[] = [];
|
||||
const commentThreadsByResource = new Map<string, ResourceWithCommentThreads>();
|
||||
for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) {
|
||||
commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(URI.parse(group[0].resource!), group));
|
||||
}
|
||||
|
||||
commentThreadsByResource.forEach((v, i, m) => {
|
||||
resourceCommentThreads.push(v);
|
||||
});
|
||||
|
||||
return resourceCommentThreads;
|
||||
}
|
||||
|
||||
private static _compareURIs(a: CommentThread, b: CommentThread) {
|
||||
const resourceA = a.resource!.toString();
|
||||
const resourceB = b.resource!.toString();
|
||||
if (resourceA < resourceB) {
|
||||
return -1;
|
||||
} else if (resourceA > resourceB) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface ICommentThreadWidget {
|
||||
submitComment: () => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Color, RGBA } from 'vs/base/common/color';
|
||||
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
|
||||
import { IModelDecorationOptions, OverviewRulerLane } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
const overviewRulerDefault = new Color(new RGBA(197, 197, 197, 1));
|
||||
|
||||
export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: overviewRulerDefault, light: overviewRulerDefault, hc: overviewRulerDefault }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges.'));
|
||||
|
||||
export class CommentGlyphWidget {
|
||||
private _lineNumber: number;
|
||||
private _editor: ICodeEditor;
|
||||
private commentsDecorations: string[] = [];
|
||||
private _commentsOptions: ModelDecorationOptions;
|
||||
|
||||
constructor(editor: ICodeEditor, lineNumber: number) {
|
||||
this._commentsOptions = this.createDecorationOptions();
|
||||
this._editor = editor;
|
||||
this.setLineNumber(lineNumber);
|
||||
}
|
||||
|
||||
private createDecorationOptions(): ModelDecorationOptions {
|
||||
const decorationOptions: IModelDecorationOptions = {
|
||||
isWholeLine: true,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(overviewRulerCommentingRangeForeground),
|
||||
position: OverviewRulerLane.Center
|
||||
},
|
||||
linesDecorationsClassName: `comment-range-glyph comment-thread`
|
||||
};
|
||||
|
||||
return ModelDecorationOptions.createDynamic(decorationOptions);
|
||||
}
|
||||
|
||||
setLineNumber(lineNumber: number): void {
|
||||
this._lineNumber = lineNumber;
|
||||
let commentsDecorations = [{
|
||||
range: {
|
||||
startLineNumber: lineNumber, startColumn: 1,
|
||||
endLineNumber: lineNumber, endColumn: 1
|
||||
},
|
||||
options: this._commentsOptions
|
||||
}];
|
||||
|
||||
this.commentsDecorations = this._editor.deltaDecorations(this.commentsDecorations, commentsDecorations);
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition {
|
||||
const range = this._editor.hasModel() && this.commentsDecorations && this.commentsDecorations.length
|
||||
? this._editor.getModel().getDecorationRange(this.commentsDecorations[0])
|
||||
: null;
|
||||
|
||||
return {
|
||||
position: {
|
||||
lineNumber: range ? range.startLineNumber : this._lineNumber,
|
||||
column: 1
|
||||
},
|
||||
preference: [ContentWidgetPositionPreference.EXACT]
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.commentsDecorations) {
|
||||
this._editor.deltaDecorations(this.commentsDecorations, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { ActionsOrientation, ActionItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Button } from 'vs/base/browser/ui/button/button';
|
||||
import { Action, IActionRunner } from 'vs/base/common/actions';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ICommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService';
|
||||
import { SimpleCommentEditor } from 'vs/workbench/contrib/comments/electron-browser/simpleCommentEditor';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { ToggleReactionsAction, ReactionAction, ReactionActionItem } from './reactionsAction';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
|
||||
|
||||
const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment");
|
||||
const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment...");
|
||||
|
||||
export class CommentNode extends Disposable {
|
||||
private _domNode: HTMLElement;
|
||||
private _body: HTMLElement;
|
||||
private _md: HTMLElement;
|
||||
private _clearTimeout: any;
|
||||
|
||||
private _editAction: Action;
|
||||
private _commentEditContainer: HTMLElement;
|
||||
private _commentDetailsContainer: HTMLElement;
|
||||
private _actionsToolbarContainer: HTMLElement;
|
||||
private _reactionsActionBar?: ActionBar;
|
||||
private _reactionActionsContainer?: HTMLElement;
|
||||
private _commentEditor: SimpleCommentEditor | null;
|
||||
private _commentEditorDisposables: IDisposable[] = [];
|
||||
private _commentEditorModel: ITextModel;
|
||||
private _updateCommentButton: Button;
|
||||
private _errorEditingContainer: HTMLElement;
|
||||
private _isPendingLabel: HTMLElement;
|
||||
|
||||
private _deleteAction: Action;
|
||||
protected actionRunner?: IActionRunner;
|
||||
protected toolbar: ToolBar;
|
||||
|
||||
private _onDidDelete = new Emitter<CommentNode>();
|
||||
|
||||
public get domNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public isEditing: boolean;
|
||||
|
||||
constructor(
|
||||
private commentThread: modes.CommentThread | modes.CommentThread2,
|
||||
public comment: modes.Comment,
|
||||
private owner: string,
|
||||
private resource: URI,
|
||||
private parentEditor: ICodeEditor,
|
||||
private parentThread: ICommentThreadWidget,
|
||||
private markdownRenderer: MarkdownRenderer,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@ICommentService private commentService: ICommentService,
|
||||
@ICommandService private commandService: ICommandService,
|
||||
@IModelService private modelService: IModelService,
|
||||
@IModeService private modeService: IModeService,
|
||||
@IDialogService private dialogService: IDialogService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IContextMenuService private contextMenuService: IContextMenuService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._domNode = dom.$('div.review-comment');
|
||||
this._domNode.tabIndex = 0;
|
||||
const avatar = dom.append(this._domNode, dom.$('div.avatar-container'));
|
||||
if (comment.userIconPath) {
|
||||
const img = <HTMLImageElement>dom.append(avatar, dom.$('img.avatar'));
|
||||
img.src = comment.userIconPath.toString();
|
||||
img.onerror = _ => img.remove();
|
||||
}
|
||||
this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents'));
|
||||
|
||||
this.createHeader(this._commentDetailsContainer);
|
||||
|
||||
this._body = dom.append(this._commentDetailsContainer, dom.$('div.comment-body'));
|
||||
this._md = this.markdownRenderer.render(comment.body).element;
|
||||
this._body.appendChild(this._md);
|
||||
|
||||
if (this.comment.commentReactions && this.comment.commentReactions.length) {
|
||||
this.createReactionsContainer(this._commentDetailsContainer);
|
||||
}
|
||||
|
||||
this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`);
|
||||
this._domNode.setAttribute('role', 'treeitem');
|
||||
this._clearTimeout = null;
|
||||
}
|
||||
|
||||
public get onDidDelete(): Event<CommentNode> {
|
||||
return this._onDidDelete.event;
|
||||
}
|
||||
|
||||
private createHeader(commentDetailsContainer: HTMLElement): void {
|
||||
const header = dom.append(commentDetailsContainer, dom.$('div.comment-title'));
|
||||
const author = dom.append(header, dom.$('strong.author'));
|
||||
author.innerText = this.comment.userName;
|
||||
|
||||
this._isPendingLabel = dom.append(header, dom.$('span.isPending'));
|
||||
|
||||
if (this.comment.label) {
|
||||
this._isPendingLabel.innerText = this.comment.label;
|
||||
} else if (this.comment.isDraft) {
|
||||
this._isPendingLabel.innerText = 'Pending';
|
||||
} else {
|
||||
this._isPendingLabel.innerText = '';
|
||||
}
|
||||
|
||||
this._actionsToolbarContainer = dom.append(header, dom.$('.comment-actions.hidden'));
|
||||
this.createActionsToolbar();
|
||||
}
|
||||
|
||||
private createActionsToolbar() {
|
||||
const actions: Action[] = [];
|
||||
|
||||
let reactionGroup = this.commentService.getReactionGroup(this.owner);
|
||||
if (reactionGroup && reactionGroup.length) {
|
||||
let commentThread = this.commentThread as modes.CommentThread2;
|
||||
if (commentThread.commentThreadHandle) {
|
||||
let toggleReactionAction = this.createReactionPicker2();
|
||||
actions.push(toggleReactionAction);
|
||||
} else {
|
||||
let toggleReactionAction = this.createReactionPicker();
|
||||
actions.push(toggleReactionAction);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.comment.canEdit || this.comment.editCommand) {
|
||||
this._editAction = this.createEditAction(this._commentDetailsContainer);
|
||||
actions.push(this._editAction);
|
||||
}
|
||||
|
||||
if (this.comment.canDelete || this.comment.deleteCommand) {
|
||||
this._deleteAction = this.createDeleteAction();
|
||||
actions.push(this._deleteAction);
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
this.toolbar = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {
|
||||
actionItemProvider: action => {
|
||||
if (action.id === ToggleReactionsAction.ID) {
|
||||
return new DropdownMenuActionItem(
|
||||
action,
|
||||
(<ToggleReactionsAction>action).menuActions,
|
||||
this.contextMenuService,
|
||||
action => {
|
||||
return this.actionItemProvider(action as Action);
|
||||
},
|
||||
this.actionRunner!,
|
||||
undefined,
|
||||
'toolbar-toggle-pickReactions',
|
||||
() => { return AnchorAlignment.RIGHT; }
|
||||
);
|
||||
}
|
||||
return this.actionItemProvider(action as Action);
|
||||
},
|
||||
orientation: ActionsOrientation.HORIZONTAL
|
||||
});
|
||||
|
||||
this.registerActionBarListeners(this._actionsToolbarContainer);
|
||||
this.toolbar.setActions(actions, [])();
|
||||
this._toDispose.push(this.toolbar);
|
||||
}
|
||||
}
|
||||
|
||||
actionItemProvider(action: Action) {
|
||||
let options = {};
|
||||
if (action.id === 'comment.delete' || action.id === 'comment.edit' || action.id === ToggleReactionsAction.ID) {
|
||||
options = { label: false, icon: true };
|
||||
} else {
|
||||
options = { label: true, icon: true };
|
||||
}
|
||||
|
||||
if (action.id === ReactionAction.ID) {
|
||||
let item = new ReactionActionItem(action);
|
||||
return item;
|
||||
} else {
|
||||
let item = new ActionItem({}, action, options);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
private createReactionPicker2(): ToggleReactionsAction {
|
||||
let toggleReactionActionItem: DropdownMenuActionItem;
|
||||
let toggleReactionAction = this._register(new ToggleReactionsAction(() => {
|
||||
if (toggleReactionActionItem) {
|
||||
toggleReactionActionItem.show();
|
||||
}
|
||||
}, nls.localize('commentToggleReaction', "Toggle Reaction")));
|
||||
|
||||
let reactionMenuActions: Action[] = [];
|
||||
let reactionGroup = this.commentService.getReactionGroup(this.owner);
|
||||
if (reactionGroup && reactionGroup.length) {
|
||||
reactionMenuActions = reactionGroup.map((reaction) => {
|
||||
return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
|
||||
try {
|
||||
await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread as modes.CommentThread2, this.comment, reaction);
|
||||
} catch (e) {
|
||||
const error = e.message
|
||||
? nls.localize('commentToggleReactionError', "Toggling the comment reaction failed: {0}.", e.message)
|
||||
: nls.localize('commentToggleReactionDefaultError', "Toggling the comment reaction failed");
|
||||
this.notificationService.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleReactionAction.menuActions = reactionMenuActions;
|
||||
|
||||
toggleReactionActionItem = new DropdownMenuActionItem(
|
||||
toggleReactionAction,
|
||||
(<ToggleReactionsAction>toggleReactionAction).menuActions,
|
||||
this.contextMenuService,
|
||||
action => {
|
||||
if (action.id === ToggleReactionsAction.ID) {
|
||||
return toggleReactionActionItem;
|
||||
}
|
||||
return this.actionItemProvider(action as Action);
|
||||
},
|
||||
this.actionRunner,
|
||||
undefined,
|
||||
'toolbar-toggle-pickReactions',
|
||||
() => { return AnchorAlignment.RIGHT; }
|
||||
);
|
||||
|
||||
return toggleReactionAction;
|
||||
}
|
||||
|
||||
private createReactionPicker(): ToggleReactionsAction {
|
||||
let toggleReactionActionItem: DropdownMenuActionItem;
|
||||
let toggleReactionAction = this._register(new ToggleReactionsAction(() => {
|
||||
if (toggleReactionActionItem) {
|
||||
toggleReactionActionItem.show();
|
||||
}
|
||||
}, nls.localize('commentAddReaction', "Add Reaction")));
|
||||
|
||||
let reactionMenuActions: Action[] = [];
|
||||
let reactionGroup = this.commentService.getReactionGroup(this.owner);
|
||||
if (reactionGroup && reactionGroup.length) {
|
||||
reactionMenuActions = reactionGroup.map((reaction) => {
|
||||
return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
|
||||
try {
|
||||
await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction);
|
||||
} catch (e) {
|
||||
const error = e.message
|
||||
? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)
|
||||
: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");
|
||||
this.notificationService.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleReactionAction.menuActions = reactionMenuActions;
|
||||
|
||||
toggleReactionActionItem = new DropdownMenuActionItem(
|
||||
toggleReactionAction,
|
||||
(<ToggleReactionsAction>toggleReactionAction).menuActions,
|
||||
this.contextMenuService,
|
||||
action => {
|
||||
if (action.id === ToggleReactionsAction.ID) {
|
||||
return toggleReactionActionItem;
|
||||
}
|
||||
return this.actionItemProvider(action as Action);
|
||||
},
|
||||
this.actionRunner!,
|
||||
undefined,
|
||||
'toolbar-toggle-pickReactions',
|
||||
() => { return AnchorAlignment.RIGHT; }
|
||||
);
|
||||
|
||||
return toggleReactionAction;
|
||||
}
|
||||
|
||||
private createReactionsContainer(commentDetailsContainer: HTMLElement): void {
|
||||
this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));
|
||||
this._reactionsActionBar = new ActionBar(this._reactionActionsContainer, {
|
||||
actionItemProvider: action => {
|
||||
if (action.id === ToggleReactionsAction.ID) {
|
||||
return new DropdownMenuActionItem(
|
||||
action,
|
||||
(<ToggleReactionsAction>action).menuActions,
|
||||
this.contextMenuService,
|
||||
action => {
|
||||
return this.actionItemProvider(action as Action);
|
||||
},
|
||||
this.actionRunner!,
|
||||
undefined,
|
||||
'toolbar-toggle-pickReactions',
|
||||
() => { return AnchorAlignment.RIGHT; }
|
||||
);
|
||||
}
|
||||
return this.actionItemProvider(action as Action);
|
||||
}
|
||||
});
|
||||
this._toDispose.push(this._reactionsActionBar);
|
||||
|
||||
this.comment.commentReactions!.map(reaction => {
|
||||
let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => {
|
||||
try {
|
||||
let commentThread = this.commentThread as modes.CommentThread2;
|
||||
if (commentThread.commentThreadHandle) {
|
||||
await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread as modes.CommentThread2, this.comment, reaction);
|
||||
} else {
|
||||
if (reaction.hasReacted) {
|
||||
await this.commentService.deleteReaction(this.owner, this.resource, this.comment, reaction);
|
||||
} else {
|
||||
await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
let error: string;
|
||||
|
||||
if (reaction.hasReacted) {
|
||||
error = e.message
|
||||
? nls.localize('commentDeleteReactionError', "Deleting the comment reaction failed: {0}.", e.message)
|
||||
: nls.localize('commentDeleteReactionDefaultError', "Deleting the comment reaction failed");
|
||||
} else {
|
||||
error = e.message
|
||||
? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)
|
||||
: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");
|
||||
}
|
||||
this.notificationService.error(error);
|
||||
}
|
||||
}, reaction.iconPath, reaction.count);
|
||||
|
||||
if (this._reactionsActionBar) {
|
||||
this._reactionsActionBar.push(action, { label: true, icon: true });
|
||||
}
|
||||
});
|
||||
|
||||
let reactionGroup = this.commentService.getReactionGroup(this.owner);
|
||||
if (reactionGroup && reactionGroup.length) {
|
||||
let commentThread = this.commentThread as modes.CommentThread2;
|
||||
if (commentThread.commentThreadHandle) {
|
||||
let toggleReactionAction = this.createReactionPicker2();
|
||||
this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true });
|
||||
} else {
|
||||
let toggleReactionAction = this.createReactionPicker();
|
||||
this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createCommentEditor(): void {
|
||||
const container = dom.append(this._commentEditContainer, dom.$('.edit-textarea'));
|
||||
this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(), this.parentEditor, this.parentThread);
|
||||
const resource = URI.parse(`comment:commentinput-${this.comment.commentId}-${Date.now()}.md`);
|
||||
this._commentEditorModel = this.modelService.createModel('', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
|
||||
|
||||
this._commentEditor.setModel(this._commentEditorModel);
|
||||
this._commentEditor.setValue(this.comment.body.value);
|
||||
this._commentEditor.layout({ width: container.clientWidth - 14, height: 90 });
|
||||
this._commentEditor.focus();
|
||||
|
||||
const lastLine = this._commentEditorModel.getLineCount();
|
||||
const lastColumn = this._commentEditorModel.getLineContent(lastLine).length + 1;
|
||||
this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn));
|
||||
|
||||
let commentThread = this.commentThread as modes.CommentThread2;
|
||||
if (commentThread.commentThreadHandle) {
|
||||
commentThread.input = {
|
||||
uri: this._commentEditor.getModel()!.uri,
|
||||
value: this.comment.body.value
|
||||
};
|
||||
this.commentService.setActiveCommentThread(commentThread);
|
||||
|
||||
this._commentEditorDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => {
|
||||
commentThread.input = {
|
||||
uri: this._commentEditor!.getModel()!.uri,
|
||||
value: this.comment.body.value
|
||||
};
|
||||
this.commentService.setActiveCommentThread(commentThread);
|
||||
}));
|
||||
|
||||
this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => {
|
||||
if (commentThread.input && this._commentEditor && this._commentEditor.getModel()!.uri === commentThread.input.uri) {
|
||||
let newVal = this._commentEditor.getValue();
|
||||
if (newVal !== commentThread.input.value) {
|
||||
let input = commentThread.input;
|
||||
input.value = newVal;
|
||||
commentThread.input = input;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._toDispose.push(this._commentEditor);
|
||||
this._toDispose.push(this._commentEditorModel);
|
||||
}
|
||||
|
||||
private removeCommentEditor() {
|
||||
this.isEditing = false;
|
||||
this._editAction.enabled = true;
|
||||
this._body.classList.remove('hidden');
|
||||
|
||||
this._commentEditorModel.dispose();
|
||||
this._commentEditorDisposables.forEach(dispose => dispose.dispose());
|
||||
this._commentEditorDisposables = [];
|
||||
if (this._commentEditor) {
|
||||
this._commentEditor.dispose();
|
||||
this._commentEditor = null;
|
||||
}
|
||||
|
||||
this._commentEditContainer.remove();
|
||||
}
|
||||
|
||||
async editComment(): Promise<void> {
|
||||
if (!this._commentEditor) {
|
||||
throw new Error('No comment editor');
|
||||
}
|
||||
|
||||
this._updateCommentButton.enabled = false;
|
||||
this._updateCommentButton.label = UPDATE_IN_PROGRESS_LABEL;
|
||||
|
||||
try {
|
||||
const newBody = this._commentEditor.getValue();
|
||||
|
||||
if (this.comment.editCommand) {
|
||||
let commentThread = this.commentThread as modes.CommentThread2;
|
||||
commentThread.input = {
|
||||
uri: this._commentEditor.getModel()!.uri,
|
||||
value: newBody
|
||||
};
|
||||
this.commentService.setActiveCommentThread(commentThread);
|
||||
let commandId = this.comment.editCommand.id;
|
||||
let args = this.comment.editCommand.arguments || [];
|
||||
|
||||
await this.commandService.executeCommand(commandId, ...args);
|
||||
} else {
|
||||
await this.commentService.editComment(this.owner, this.resource, this.comment, newBody);
|
||||
}
|
||||
|
||||
this._updateCommentButton.enabled = true;
|
||||
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
|
||||
this._commentEditor.getDomNode()!.style.outline = '';
|
||||
this.removeCommentEditor();
|
||||
const editedComment = assign({}, this.comment, { body: new MarkdownString(newBody) });
|
||||
this.update(editedComment);
|
||||
} catch (e) {
|
||||
this._updateCommentButton.enabled = true;
|
||||
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
|
||||
|
||||
this._commentEditor.getDomNode()!.style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`;
|
||||
this._errorEditingContainer.textContent = e.message
|
||||
? nls.localize('commentEditError', "Updating the comment failed: {0}.", e.message)
|
||||
: nls.localize('commentEditDefaultError', "Updating the comment failed.");
|
||||
this._errorEditingContainer.classList.remove('hidden');
|
||||
this._commentEditor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private createDeleteAction(): Action {
|
||||
return new Action('comment.delete', nls.localize('label.delete', "Delete"), 'octicon octicon-x', true, () => {
|
||||
return this.dialogService.confirm({
|
||||
message: nls.localize('confirmDelete', "Delete comment?"),
|
||||
type: 'question',
|
||||
primaryButton: nls.localize('label.delete', "Delete")
|
||||
}).then(async result => {
|
||||
if (result.confirmed) {
|
||||
try {
|
||||
if (this.comment.deleteCommand) {
|
||||
this.commentService.setActiveCommentThread(this.commentThread as modes.CommentThread2);
|
||||
let commandId = this.comment.deleteCommand.id;
|
||||
let args = this.comment.deleteCommand.arguments || [];
|
||||
|
||||
await this.commandService.executeCommand(commandId, ...args);
|
||||
} else {
|
||||
const didDelete = await this.commentService.deleteComment(this.owner, this.resource, this.comment);
|
||||
if (didDelete) {
|
||||
this._onDidDelete.fire(this);
|
||||
} else {
|
||||
throw Error();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e.message
|
||||
? nls.localize('commentDeletionError', "Deleting the comment failed: {0}.", e.message)
|
||||
: nls.localize('commentDeletionDefaultError', "Deleting the comment failed");
|
||||
this.notificationService.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createEditAction(commentDetailsContainer: HTMLElement): Action {
|
||||
return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => {
|
||||
this.isEditing = true;
|
||||
this._body.classList.add('hidden');
|
||||
this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container'));
|
||||
this.createCommentEditor();
|
||||
|
||||
this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden'));
|
||||
const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions'));
|
||||
|
||||
const cancelEditButton = new Button(formActions);
|
||||
cancelEditButton.label = nls.localize('label.cancel', "Cancel");
|
||||
this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService));
|
||||
|
||||
this._toDispose.push(cancelEditButton.onDidClick(_ => {
|
||||
this.removeCommentEditor();
|
||||
}));
|
||||
|
||||
this._updateCommentButton = new Button(formActions);
|
||||
this._updateCommentButton.label = UPDATE_COMMENT_LABEL;
|
||||
this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService));
|
||||
|
||||
this._toDispose.push(this._updateCommentButton.onDidClick(_ => {
|
||||
this.editComment();
|
||||
}));
|
||||
|
||||
this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => {
|
||||
this._updateCommentButton.enabled = !!this._commentEditor!.getValue();
|
||||
}));
|
||||
|
||||
this._editAction.enabled = false;
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private registerActionBarListeners(actionsContainer: HTMLElement): void {
|
||||
this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseenter', () => {
|
||||
actionsContainer.classList.remove('hidden');
|
||||
}));
|
||||
|
||||
this._toDispose.push(dom.addDisposableListener(this._domNode, 'focus', () => {
|
||||
actionsContainer.classList.remove('hidden');
|
||||
}));
|
||||
|
||||
this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', () => {
|
||||
if (!this._domNode.contains(document.activeElement)) {
|
||||
actionsContainer.classList.add('hidden');
|
||||
}
|
||||
}));
|
||||
|
||||
this._toDispose.push(dom.addDisposableListener(this._domNode, 'focusout', (e: FocusEvent) => {
|
||||
if (!this._domNode.contains((<HTMLElement>e.relatedTarget))) {
|
||||
actionsContainer.classList.add('hidden');
|
||||
|
||||
if (this._commentEditor && this._commentEditor.getValue() === this.comment.body.value) {
|
||||
this.removeCommentEditor();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
update(newComment: modes.Comment) {
|
||||
|
||||
if (newComment.body !== this.comment.body) {
|
||||
this._body.removeChild(this._md);
|
||||
this._md = this.markdownRenderer.render(newComment.body).element;
|
||||
this._body.appendChild(this._md);
|
||||
}
|
||||
|
||||
const shouldUpdateActions = newComment.editCommand !== this.comment.editCommand || newComment.deleteCommand !== this.comment.deleteCommand;
|
||||
this.comment = newComment;
|
||||
|
||||
if (shouldUpdateActions) {
|
||||
dom.clearNode(this._actionsToolbarContainer);
|
||||
this.createActionsToolbar();
|
||||
}
|
||||
|
||||
|
||||
if (newComment.label) {
|
||||
this._isPendingLabel.innerText = newComment.label;
|
||||
} else if (newComment.isDraft) {
|
||||
this._isPendingLabel.innerText = 'Pending';
|
||||
} else {
|
||||
this._isPendingLabel.innerText = '';
|
||||
}
|
||||
|
||||
// update comment reactions
|
||||
if (this._reactionActionsContainer) {
|
||||
this._reactionActionsContainer.remove();
|
||||
}
|
||||
|
||||
if (this._reactionsActionBar) {
|
||||
this._reactionsActionBar.clear();
|
||||
}
|
||||
|
||||
if (this.comment.commentReactions && this.comment.commentReactions.length) {
|
||||
this.createReactionsContainer(this._commentDetailsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.domNode.focus();
|
||||
if (!this._clearTimeout) {
|
||||
dom.addClass(this.domNode, 'focus');
|
||||
this._clearTimeout = setTimeout(() => {
|
||||
dom.removeClass(this.domNode, 'focus');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._toDispose.forEach(disposeable => disposeable.dispose());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread2 } from 'vs/editor/common/modes';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { keys } from 'vs/base/common/map';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
|
||||
import { MainThreadCommentController } from 'vs/workbench/api/electron-browser/mainThreadComments';
|
||||
|
||||
export const ICommentService = createDecorator<ICommentService>('commentService');
|
||||
|
||||
export interface IResourceCommentThreadEvent {
|
||||
resource: URI;
|
||||
commentInfos: ICommentInfo[];
|
||||
}
|
||||
|
||||
export interface ICommentInfo extends CommentInfo {
|
||||
owner: string;
|
||||
}
|
||||
|
||||
export interface IWorkspaceCommentThreadsEvent {
|
||||
ownerId: string;
|
||||
commentThreads: CommentThread[];
|
||||
}
|
||||
|
||||
export interface ICommentService {
|
||||
_serviceBrand: any;
|
||||
readonly onDidSetResourceCommentInfos: Event<IResourceCommentThreadEvent>;
|
||||
readonly onDidSetAllCommentThreads: Event<IWorkspaceCommentThreadsEvent>;
|
||||
readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent>;
|
||||
readonly onDidChangeActiveCommentThread: Event<CommentThread | null>;
|
||||
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>;
|
||||
readonly onDidChangeInput: Event<string>;
|
||||
readonly onDidSetDataProvider: Event<void>;
|
||||
readonly onDidDeleteDataProvider: Event<string>;
|
||||
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void;
|
||||
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void;
|
||||
removeWorkspaceComments(owner: string): void;
|
||||
registerCommentController(owner: string, commentControl: MainThreadCommentController): void;
|
||||
unregisterCommentController(owner: string): void;
|
||||
registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void;
|
||||
unregisterDataProvider(owner: string): void;
|
||||
updateComments(ownerId: string, event: CommentThreadChangedEvent): void;
|
||||
createNewCommentThread(owner: string, resource: URI, range: Range, text: string): Promise<CommentThread | null>;
|
||||
replyToCommentThread(owner: string, resource: URI, range: Range, thread: CommentThread, text: string): Promise<CommentThread | null>;
|
||||
editComment(owner: string, resource: URI, comment: Comment, text: string): Promise<void>;
|
||||
deleteComment(owner: string, resource: URI, comment: Comment): Promise<boolean>;
|
||||
getComments(resource: URI): Promise<(ICommentInfo | null)[]>;
|
||||
getCommentingRanges(resource: URI): Promise<IRange[]>;
|
||||
startDraft(owner: string, resource: URI): void;
|
||||
deleteDraft(owner: string, resource: URI): void;
|
||||
finishDraft(owner: string, resource: URI): void;
|
||||
getStartDraftLabel(owner: string): string | undefined;
|
||||
getDeleteDraftLabel(owner: string): string | undefined;
|
||||
getFinishDraftLabel(owner: string): string | undefined;
|
||||
addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void>;
|
||||
deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void>;
|
||||
getReactionGroup(owner: string): CommentReaction[] | undefined;
|
||||
toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise<void>;
|
||||
setActiveCommentThread(commentThread: CommentThread | null);
|
||||
setInput(input: string);
|
||||
}
|
||||
|
||||
export class CommentService extends Disposable implements ICommentService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _onDidSetDataProvider: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidSetDataProvider: Event<void> = this._onDidSetDataProvider.event;
|
||||
|
||||
private readonly _onDidDeleteDataProvider: Emitter<string> = this._register(new Emitter<string>());
|
||||
readonly onDidDeleteDataProvider: Event<string> = this._onDidDeleteDataProvider.event;
|
||||
|
||||
private readonly _onDidSetResourceCommentInfos: Emitter<IResourceCommentThreadEvent> = this._register(new Emitter<IResourceCommentThreadEvent>());
|
||||
readonly onDidSetResourceCommentInfos: Event<IResourceCommentThreadEvent> = this._onDidSetResourceCommentInfos.event;
|
||||
|
||||
private readonly _onDidSetAllCommentThreads: Emitter<IWorkspaceCommentThreadsEvent> = this._register(new Emitter<IWorkspaceCommentThreadsEvent>());
|
||||
readonly onDidSetAllCommentThreads: Event<IWorkspaceCommentThreadsEvent> = this._onDidSetAllCommentThreads.event;
|
||||
|
||||
private readonly _onDidUpdateCommentThreads: Emitter<ICommentThreadChangedEvent> = this._register(new Emitter<ICommentThreadChangedEvent>());
|
||||
readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent> = this._onDidUpdateCommentThreads.event;
|
||||
|
||||
private readonly _onDidChangeActiveCommentThread = this._register(new Emitter<CommentThread | null>());
|
||||
readonly onDidChangeActiveCommentThread: Event<CommentThread | null> = this._onDidChangeActiveCommentThread.event;
|
||||
|
||||
private readonly _onDidChangeInput: Emitter<string> = this._register(new Emitter<string>());
|
||||
readonly onDidChangeInput: Event<string> = this._onDidChangeInput.event;
|
||||
private readonly _onDidChangeActiveCommentingRange: Emitter<{
|
||||
range: Range, commentingRangesInfo:
|
||||
CommentingRanges
|
||||
}> = this._register(new Emitter<{
|
||||
range: Range, commentingRangesInfo:
|
||||
CommentingRanges
|
||||
}>());
|
||||
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }> = this._onDidChangeActiveCommentingRange.event;
|
||||
|
||||
private _commentProviders = new Map<string, DocumentCommentProvider>();
|
||||
|
||||
private _commentControls = new Map<string, MainThreadCommentController>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
setActiveCommentThread(commentThread: CommentThread | null) {
|
||||
this._onDidChangeActiveCommentThread.fire(commentThread);
|
||||
}
|
||||
|
||||
setInput(input: string) {
|
||||
this._onDidChangeInput.fire(input);
|
||||
}
|
||||
|
||||
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void {
|
||||
this._onDidSetResourceCommentInfos.fire({ resource, commentInfos });
|
||||
}
|
||||
|
||||
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void {
|
||||
this._onDidSetAllCommentThreads.fire({ ownerId: owner, commentThreads: commentsByResource });
|
||||
}
|
||||
|
||||
removeWorkspaceComments(owner: string): void {
|
||||
this._onDidSetAllCommentThreads.fire({ ownerId: owner, commentThreads: [] });
|
||||
}
|
||||
|
||||
registerCommentController(owner: string, commentControl: MainThreadCommentController): void {
|
||||
this._commentControls.set(owner, commentControl);
|
||||
this._onDidSetDataProvider.fire();
|
||||
}
|
||||
|
||||
unregisterCommentController(owner: string): void {
|
||||
this._commentControls.delete(owner);
|
||||
this._onDidDeleteDataProvider.fire(owner);
|
||||
}
|
||||
|
||||
registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void {
|
||||
this._commentProviders.set(owner, commentProvider);
|
||||
this._onDidSetDataProvider.fire();
|
||||
}
|
||||
|
||||
unregisterDataProvider(owner: string): void {
|
||||
this._commentProviders.delete(owner);
|
||||
this._onDidDeleteDataProvider.fire(owner);
|
||||
}
|
||||
|
||||
updateComments(ownerId: string, event: CommentThreadChangedEvent): void {
|
||||
const evt: ICommentThreadChangedEvent = assign({}, event, { owner: ownerId });
|
||||
this._onDidUpdateCommentThreads.fire(evt);
|
||||
}
|
||||
|
||||
async createNewCommentThread(owner: string, resource: URI, range: Range, text: string): Promise<CommentThread | null> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return await commentProvider.createNewCommentThread(resource, range, text, CancellationToken.None);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async replyToCommentThread(owner: string, resource: URI, range: Range, thread: CommentThread, text: string): Promise<CommentThread | null> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return await commentProvider.replyToCommentThread(resource, range, thread, text, CancellationToken.None);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
editComment(owner: string, resource: URI, comment: Comment, text: string): Promise<void> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return commentProvider.editComment(resource, comment, text, CancellationToken.None);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
deleteComment(owner: string, resource: URI, comment: Comment): Promise<boolean> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return commentProvider.deleteComment(resource, comment, CancellationToken.None).then(() => true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
async startDraft(owner: string, resource: URI): Promise<void> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider && commentProvider.startDraft) {
|
||||
return commentProvider.startDraft(resource, CancellationToken.None);
|
||||
} else {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDraft(owner: string, resource: URI): Promise<void> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider && commentProvider.deleteDraft) {
|
||||
return commentProvider.deleteDraft(resource, CancellationToken.None);
|
||||
} else {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
}
|
||||
|
||||
async finishDraft(owner: string, resource: URI): Promise<void> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider && commentProvider.finishDraft) {
|
||||
return commentProvider.finishDraft(resource, CancellationToken.None);
|
||||
} else {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
}
|
||||
|
||||
async addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider && commentProvider.addReaction) {
|
||||
return commentProvider.addReaction(resource, comment, reaction, CancellationToken.None);
|
||||
} else {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void> {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider && commentProvider.deleteReaction) {
|
||||
return commentProvider.deleteReaction(resource, comment, reaction, CancellationToken.None);
|
||||
} else {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
}
|
||||
|
||||
async toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise<void> {
|
||||
const commentController = this._commentControls.get(owner);
|
||||
|
||||
if (commentController) {
|
||||
return commentController.toggleReaction(resource, thread, comment, reaction, CancellationToken.None);
|
||||
} else {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
}
|
||||
|
||||
getReactionGroup(owner: string): CommentReaction[] | undefined {
|
||||
const commentProvider = this._commentControls.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return commentProvider.getReactionGroup();
|
||||
}
|
||||
|
||||
const commentController = this._commentControls.get(owner);
|
||||
|
||||
if (commentController) {
|
||||
return commentController.getReactionGroup();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getStartDraftLabel(owner: string): string | undefined {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return commentProvider.startDraftLabel;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getDeleteDraftLabel(owner: string): string | undefined {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return commentProvider.deleteDraftLabel;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getFinishDraftLabel(owner: string): string | undefined {
|
||||
const commentProvider = this._commentProviders.get(owner);
|
||||
|
||||
if (commentProvider) {
|
||||
return commentProvider.finishDraftLabel;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getComments(resource: URI): Promise<(ICommentInfo | null)[]> {
|
||||
const result: Promise<ICommentInfo | null>[] = [];
|
||||
for (const owner of keys(this._commentProviders)) {
|
||||
const provider = this._commentProviders.get(owner);
|
||||
if (provider && provider.provideDocumentComments) {
|
||||
result.push(provider.provideDocumentComments(resource, CancellationToken.None).then(commentInfo => {
|
||||
if (commentInfo) {
|
||||
return <ICommentInfo>{
|
||||
owner: owner,
|
||||
threads: commentInfo.threads,
|
||||
commentingRanges: commentInfo.commentingRanges,
|
||||
reply: commentInfo.reply,
|
||||
draftMode: commentInfo.draftMode
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let commentControlResult: Promise<ICommentInfo>[] = [];
|
||||
|
||||
this._commentControls.forEach(control => {
|
||||
commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None));
|
||||
});
|
||||
|
||||
let ret = [...await Promise.all(result), ...await Promise.all(commentControlResult)];
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getCommentingRanges(resource: URI): Promise<IRange[]> {
|
||||
let commentControlResult: Promise<IRange[]>[] = [];
|
||||
|
||||
this._commentControls.forEach(control => {
|
||||
commentControlResult.push(control.getCommentingRanges(resource, CancellationToken.None));
|
||||
});
|
||||
|
||||
let ret = await Promise.all(commentControlResult);
|
||||
return ret.reduce((prev, curr) => { prev.push(...curr); return prev; }, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import 'vs/workbench/contrib/comments/electron-browser/commentsEditorContribution';
|
||||
import { ICommentService, CommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
|
||||
export interface ICommentsConfiguration {
|
||||
openPanel: 'neverOpen' | 'openOnSessionStart' | 'openOnSessionStartWithComments';
|
||||
}
|
||||
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
|
||||
id: 'comments',
|
||||
order: 20,
|
||||
title: nls.localize('commentsConfigurationTitle', "Comments"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'comments.openPanel': {
|
||||
enum: ['neverOpen', 'openOnSessionStart', 'openOnSessionStartWithComments'],
|
||||
default: 'openOnSessionStartWithComments',
|
||||
description: nls.localize('openComments', "Controls when the comments panel should open.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerSingleton(ICommentService, CommentService);
|
||||
@@ -0,0 +1,871 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/review';
|
||||
import * as nls from 'vs/nls';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { findFirstInSorted, coalesce } from 'vs/base/common/arrays';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor, IEditorMouseEvent, IViewZone, MouseTargetType, isDiffEditor, isCodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution, EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { IEditorContribution, IModelChangedEvent } from 'vs/editor/common/editorCommon';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { peekViewResultsBackground, peekViewResultsSelectionBackground, peekViewTitleBackground } from 'vs/editor/contrib/referenceSearch/referencesWidget';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { editorForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { CommentThreadCollapsibleState } from 'vs/workbench/api/node/extHostTypes';
|
||||
import { ReviewZoneWidget, COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/electron-browser/commentThreadWidget';
|
||||
import { ICommentService, ICommentInfo } from 'vs/workbench/contrib/comments/electron-browser/commentService';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { IModelDecorationOptions } from 'vs/editor/common/model';
|
||||
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async';
|
||||
import { overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/electron-browser/commentGlyphWidget';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/electron-browser/simpleCommentEditor';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
export const ctxCommentThreadVisible = new RawContextKey<boolean>('commentThreadVisible', false);
|
||||
|
||||
export const ID = 'editor.contrib.review';
|
||||
|
||||
export class ReviewViewZone implements IViewZone {
|
||||
public readonly afterLineNumber: number;
|
||||
public readonly domNode: HTMLElement;
|
||||
private callback: (top: number) => void;
|
||||
|
||||
constructor(afterLineNumber: number, onDomNodeTop: (top: number) => void) {
|
||||
this.afterLineNumber = afterLineNumber;
|
||||
this.callback = onDomNodeTop;
|
||||
|
||||
this.domNode = $('.review-viewzone');
|
||||
}
|
||||
|
||||
onDomNodeTop(top: number): void {
|
||||
this.callback(top);
|
||||
}
|
||||
}
|
||||
|
||||
class CommentingRangeDecoration {
|
||||
private _decorationId: string;
|
||||
|
||||
public get id(): string {
|
||||
return this._decorationId;
|
||||
}
|
||||
|
||||
constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private commentingRangesInfo?: modes.CommentingRanges) {
|
||||
const startLineNumber = _range.startLineNumber;
|
||||
const endLineNumber = _range.endLineNumber;
|
||||
let commentingRangeDecorations = [{
|
||||
range: {
|
||||
startLineNumber: startLineNumber, startColumn: 1,
|
||||
endLineNumber: endLineNumber, endColumn: 1
|
||||
},
|
||||
options: commentingOptions
|
||||
}];
|
||||
|
||||
let model = this._editor.getModel();
|
||||
if (model) {
|
||||
this._decorationId = model.deltaDecorations([this._decorationId], commentingRangeDecorations)[0];
|
||||
}
|
||||
}
|
||||
|
||||
public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined } {
|
||||
return {
|
||||
extensionId: this._extensionId,
|
||||
replyCommand: this._reply,
|
||||
ownerId: this._ownerId,
|
||||
commentingRangesInfo: this.commentingRangesInfo
|
||||
};
|
||||
}
|
||||
|
||||
public getOriginalRange() {
|
||||
return this._range;
|
||||
}
|
||||
|
||||
public getActiveRange() {
|
||||
return this._editor.getModel()!.getDecorationRange(this._decorationId);
|
||||
}
|
||||
}
|
||||
class CommentingRangeDecorator {
|
||||
|
||||
private decorationOptions: ModelDecorationOptions;
|
||||
private commentingRangeDecorations: CommentingRangeDecoration[] = [];
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor() {
|
||||
const decorationOptions: IModelDecorationOptions = {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: 'comment-range-glyph comment-diff-added'
|
||||
};
|
||||
|
||||
this.decorationOptions = ModelDecorationOptions.createDynamic(decorationOptions);
|
||||
}
|
||||
|
||||
public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) {
|
||||
let model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commentingRangeDecorations: CommentingRangeDecoration[] = [];
|
||||
for (const info of commentInfos) {
|
||||
if (Array.isArray(info.commentingRanges)) {
|
||||
info.commentingRanges.forEach(range => {
|
||||
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, range, info.reply, this.decorationOptions));
|
||||
});
|
||||
} else {
|
||||
(info.commentingRanges ? info.commentingRanges.ranges : []).forEach(range => {
|
||||
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, range, (info.commentingRanges as modes.CommentingRanges).newCommentThreadCommand, this.decorationOptions, info.commentingRanges as modes.CommentingRanges));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let oldDecorations = this.commentingRangeDecorations.map(decoration => decoration.id);
|
||||
editor.deltaDecorations(oldDecorations, []);
|
||||
|
||||
this.commentingRangeDecorations = commentingRangeDecorations;
|
||||
}
|
||||
|
||||
public getMatchedCommentAction(line: number) {
|
||||
for (const decoration of this.commentingRangeDecorations) {
|
||||
const range = decoration.getActiveRange();
|
||||
if (range && range.startLineNumber <= line && line <= range.endLineNumber) {
|
||||
return decoration.getCommentAction();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
this.commentingRangeDecorations = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class ReviewController implements IEditorContribution {
|
||||
private globalToDispose: IDisposable[];
|
||||
private localToDispose: IDisposable[];
|
||||
private editor: ICodeEditor;
|
||||
private _newCommentWidget?: ReviewZoneWidget;
|
||||
private _commentWidgets: ReviewZoneWidget[];
|
||||
private _commentThreadVisible: IContextKey<boolean>;
|
||||
private _commentInfos: ICommentInfo[];
|
||||
private _commentingRangeDecorator: CommentingRangeDecorator;
|
||||
private mouseDownInfo: { lineNumber: number } | null = null;
|
||||
private _commentingRangeSpaceReserved = false;
|
||||
private _computePromise: CancelablePromise<Array<ICommentInfo | null>> | null;
|
||||
private _computeCommentingRangePromise: CancelablePromise<ICommentInfo[]> | null;
|
||||
private _computeCommentingRangeScheduler: Delayer<Array<ICommentInfo | null>> | null;
|
||||
private _pendingCommentCache: { [key: number]: { [key: string]: string } };
|
||||
private _pendingNewCommentCache: { [key: string]: { lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, pendingComment: string, draftMode: modes.DraftMode | undefined } };
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextKeyService readonly contextKeyService: IContextKeyService,
|
||||
@ICommentService private readonly commentService: ICommentService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
|
||||
@IContextMenuService readonly contextMenuService: IContextMenuService,
|
||||
) {
|
||||
this.editor = editor;
|
||||
this.globalToDispose = [];
|
||||
this.localToDispose = [];
|
||||
this._commentInfos = [];
|
||||
this._commentWidgets = [];
|
||||
this._pendingCommentCache = {};
|
||||
this._pendingNewCommentCache = {};
|
||||
this._computePromise = null;
|
||||
|
||||
this._commentThreadVisible = ctxCommentThreadVisible.bindTo(contextKeyService);
|
||||
this._commentingRangeDecorator = new CommentingRangeDecorator();
|
||||
|
||||
this.globalToDispose.push(this.commentService.onDidDeleteDataProvider(ownerId => {
|
||||
// Remove new comment widget and glyph, refresh comments
|
||||
if (this._newCommentWidget && this._newCommentWidget.owner === ownerId) {
|
||||
this._newCommentWidget.dispose();
|
||||
this._newCommentWidget = undefined;
|
||||
}
|
||||
|
||||
delete this._pendingCommentCache[ownerId];
|
||||
this.beginCompute();
|
||||
}));
|
||||
this.globalToDispose.push(this.commentService.onDidSetDataProvider(_ => this.beginCompute()));
|
||||
|
||||
this.globalToDispose.push(this.commentService.onDidSetResourceCommentInfos(e => {
|
||||
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
|
||||
if (editorURI && editorURI.toString() === e.resource.toString()) {
|
||||
this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null));
|
||||
}
|
||||
}));
|
||||
|
||||
this.globalToDispose.push(this.editor.onDidChangeModel(e => this.onModelChanged(e)));
|
||||
this.codeEditorService.registerDecorationType(COMMENTEDITOR_DECORATION_KEY, {});
|
||||
this.beginCompute();
|
||||
}
|
||||
|
||||
private beginCompute(): Promise<void> {
|
||||
this._computePromise = createCancelablePromise(token => {
|
||||
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
|
||||
|
||||
if (editorURI) {
|
||||
return this.commentService.getComments(editorURI);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
return this._computePromise.then(commentInfos => {
|
||||
this.setComments(coalesce(commentInfos));
|
||||
this._computePromise = null;
|
||||
}, error => console.log(error));
|
||||
}
|
||||
|
||||
private beginComputeCommentingRanges() {
|
||||
if (this._computeCommentingRangeScheduler) {
|
||||
if (this._computeCommentingRangePromise) {
|
||||
this._computeCommentingRangePromise.cancel();
|
||||
this._computeCommentingRangePromise = null;
|
||||
}
|
||||
|
||||
this._computeCommentingRangeScheduler.trigger(() => {
|
||||
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
|
||||
|
||||
if (editorURI) {
|
||||
return this.commentService.getComments(editorURI);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}).then(commentInfos => {
|
||||
const meaningfulCommentInfos = coalesce(commentInfos);
|
||||
this._commentingRangeDecorator.update(this.editor, meaningfulCommentInfos);
|
||||
}, (err) => {
|
||||
onUnexpectedError(err);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static get(editor: ICodeEditor): ReviewController {
|
||||
return editor.getContribution<ReviewController>(ID);
|
||||
}
|
||||
|
||||
public revealCommentThread(threadId: string, commentId: string, fetchOnceIfNotExist: boolean): void {
|
||||
const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId);
|
||||
if (commentThreadWidget.length === 1) {
|
||||
commentThreadWidget[0].reveal(commentId);
|
||||
} else if (fetchOnceIfNotExist) {
|
||||
this.beginCompute().then(_ => {
|
||||
this.revealCommentThread(threadId, commentId, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public nextCommentThread(): void {
|
||||
if (!this._commentWidgets.length || !this.editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const after = this.editor.getSelection().getEndPosition();
|
||||
const sortedWidgets = this._commentWidgets.sort((a, b) => {
|
||||
if (a.commentThread.range.startLineNumber < b.commentThread.range.startLineNumber) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.commentThread.range.startLineNumber > b.commentThread.range.startLineNumber) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.commentThread.range.startColumn < b.commentThread.range.startColumn) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.commentThread.range.startColumn > b.commentThread.range.startColumn) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
let idx = findFirstInSorted(sortedWidgets, widget => {
|
||||
if (widget.commentThread.range.startLineNumber > after.lineNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (widget.commentThread.range.startLineNumber < after.lineNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (widget.commentThread.range.startColumn > after.column) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (idx === this._commentWidgets.length) {
|
||||
this._commentWidgets[0].reveal();
|
||||
this.editor.setSelection(this._commentWidgets[0].commentThread.range);
|
||||
} else {
|
||||
sortedWidgets[idx].reveal();
|
||||
this.editor.setSelection(sortedWidgets[idx].commentThread.range);
|
||||
}
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.globalToDispose = dispose(this.globalToDispose);
|
||||
this.localToDispose = dispose(this.localToDispose);
|
||||
|
||||
this._commentWidgets.forEach(widget => widget.dispose());
|
||||
|
||||
if (this._newCommentWidget) {
|
||||
this._newCommentWidget.dispose();
|
||||
this._newCommentWidget = undefined;
|
||||
}
|
||||
this.editor = null!; // Strict null override — nulling out in dispose
|
||||
}
|
||||
|
||||
public onModelChanged(e: IModelChangedEvent): void {
|
||||
this.localToDispose = dispose(this.localToDispose);
|
||||
if (this._newCommentWidget) {
|
||||
let pendingNewComment = this._newCommentWidget.getPendingComment();
|
||||
|
||||
if (e.oldModelUrl) {
|
||||
if (pendingNewComment) {
|
||||
// we can't fetch zone widget's position as the model is already gone
|
||||
const position = this._newCommentWidget.getPosition();
|
||||
if (position) {
|
||||
this._pendingNewCommentCache[e.oldModelUrl.toString()] = {
|
||||
lineNumber: position.lineNumber,
|
||||
ownerId: this._newCommentWidget.owner,
|
||||
extensionId: this._newCommentWidget.extensionId,
|
||||
replyCommand: this._newCommentWidget.commentThread.reply,
|
||||
pendingComment: pendingNewComment,
|
||||
draftMode: this._newCommentWidget.draftMode
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// clear cache if it is empty
|
||||
delete this._pendingNewCommentCache[e.oldModelUrl.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
this._newCommentWidget.dispose();
|
||||
this._newCommentWidget = undefined;
|
||||
}
|
||||
|
||||
this.removeCommentWidgetsAndStoreCache();
|
||||
|
||||
if (e.newModelUrl && this._pendingNewCommentCache[e.newModelUrl.toString()]) {
|
||||
let newCommentCache = this._pendingNewCommentCache[e.newModelUrl.toString()];
|
||||
this.addComment(newCommentCache.lineNumber, newCommentCache.replyCommand, newCommentCache.ownerId, newCommentCache.extensionId, newCommentCache.draftMode, newCommentCache.pendingComment);
|
||||
}
|
||||
|
||||
this.localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
|
||||
this.localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));
|
||||
|
||||
this._computeCommentingRangeScheduler = new Delayer<ICommentInfo[]>(200);
|
||||
this.localToDispose.push({
|
||||
dispose: () => {
|
||||
if (this._computeCommentingRangeScheduler) {
|
||||
this._computeCommentingRangeScheduler.cancel();
|
||||
}
|
||||
this._computeCommentingRangeScheduler = null;
|
||||
}
|
||||
});
|
||||
this.localToDispose.push(this.editor.onDidChangeModelContent(async () => {
|
||||
this.beginComputeCommentingRanges();
|
||||
}));
|
||||
this.localToDispose.push(this.commentService.onDidUpdateCommentThreads(e => {
|
||||
const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;
|
||||
if (!editorURI) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commentInfo = this._commentInfos.filter(info => info.owner === e.owner);
|
||||
if (!commentInfo || !commentInfo.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let added = e.added.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
|
||||
let removed = e.removed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
|
||||
let changed = e.changed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
|
||||
let draftMode = e.draftMode;
|
||||
|
||||
commentInfo.forEach(info => info.draftMode = draftMode);
|
||||
this._commentWidgets.filter(ZoneWidget => ZoneWidget.owner === e.owner).forEach(widget => widget.updateDraftMode(draftMode));
|
||||
if (this._newCommentWidget && this._newCommentWidget.owner === e.owner) {
|
||||
this._newCommentWidget.updateDraftMode(draftMode);
|
||||
}
|
||||
|
||||
removed.forEach(thread => {
|
||||
let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId);
|
||||
if (matchedZones.length) {
|
||||
let matchedZone = matchedZones[0];
|
||||
let index = this._commentWidgets.indexOf(matchedZone);
|
||||
this._commentWidgets.splice(index, 1);
|
||||
matchedZone.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
changed.forEach(thread => {
|
||||
let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId);
|
||||
if (matchedZones.length) {
|
||||
let matchedZone = matchedZones[0];
|
||||
matchedZone.update(thread);
|
||||
}
|
||||
});
|
||||
added.forEach(thread => {
|
||||
this.displayCommentThread(e.owner, thread, null, draftMode);
|
||||
this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread);
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
this.beginCompute();
|
||||
}
|
||||
|
||||
private displayCommentThread(owner: string, thread: modes.CommentThread | modes.CommentThread2, pendingComment: string | null, draftMode: modes.DraftMode | undefined): void {
|
||||
const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment, draftMode);
|
||||
zoneWidget.display(thread.range.startLineNumber);
|
||||
this._commentWidgets.push(zoneWidget);
|
||||
}
|
||||
|
||||
private addComment(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, draftMode: modes.DraftMode | undefined, pendingComment: string | null) {
|
||||
if (this._newCommentWidget) {
|
||||
this.notificationService.warn(`Please submit the comment at line ${this._newCommentWidget.position ? this._newCommentWidget.position.lineNumber : -1} before creating a new one.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// add new comment
|
||||
this._commentThreadVisible.set(true);
|
||||
this._newCommentWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, ownerId, {
|
||||
extensionId: extensionId,
|
||||
threadId: null,
|
||||
resource: null,
|
||||
comments: [],
|
||||
range: {
|
||||
startLineNumber: lineNumber,
|
||||
startColumn: 0,
|
||||
endLineNumber: lineNumber,
|
||||
endColumn: 0
|
||||
},
|
||||
reply: replyCommand,
|
||||
collapsibleState: CommentThreadCollapsibleState.Expanded,
|
||||
}, pendingComment, draftMode);
|
||||
|
||||
this.localToDispose.push(this._newCommentWidget!.onDidClose(e => {
|
||||
this.clearNewCommentWidget();
|
||||
}));
|
||||
|
||||
this.localToDispose.push(this._newCommentWidget!.onDidCreateThread(commentWidget => {
|
||||
const thread = commentWidget.commentThread;
|
||||
this._commentWidgets.push(commentWidget);
|
||||
this._commentInfos.filter(info => info.owner === commentWidget.owner)[0].threads.push(thread);
|
||||
this.clearNewCommentWidget();
|
||||
}));
|
||||
|
||||
this._newCommentWidget!.display(lineNumber);
|
||||
}
|
||||
|
||||
private clearNewCommentWidget() {
|
||||
this._newCommentWidget = undefined;
|
||||
|
||||
if (this.editor && this.editor.hasModel()) {
|
||||
delete this._pendingNewCommentCache[this.editor.getModel().uri.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorMouseDown(e: IEditorMouseEvent): void {
|
||||
this.mouseDownInfo = null;
|
||||
|
||||
const range = e.target.range;
|
||||
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.event.leftButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = e.target.detail as IMarginData;
|
||||
const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;
|
||||
|
||||
// don't collide with folding and git decorations
|
||||
if (gutterOffsetX > 14) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mouseDownInfo = { lineNumber: range.startLineNumber };
|
||||
}
|
||||
|
||||
private onEditorMouseUp(e: IEditorMouseEvent): void {
|
||||
if (!this.mouseDownInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lineNumber } = this.mouseDownInfo;
|
||||
this.mouseDownInfo = null;
|
||||
|
||||
const range = e.target.range;
|
||||
|
||||
if (!range || range.startLineNumber !== lineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.target.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.element.className.indexOf('comment-diff-added') >= 0) {
|
||||
const lineNumber = e.target.position!.lineNumber;
|
||||
this.addCommentAtLine(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public addOrToggleCommentAtLine(lineNumber: number): void {
|
||||
// The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead
|
||||
const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === lineNumber);
|
||||
if (existingCommentsAtLine.length) {
|
||||
existingCommentsAtLine.forEach(widget => widget.toggleExpand(lineNumber));
|
||||
return;
|
||||
} else {
|
||||
this.addCommentAtLine(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public addCommentAtLine(lineNumber: number): void {
|
||||
const newCommentInfo = this._commentingRangeDecorator.getMatchedCommentAction(lineNumber);
|
||||
if (!newCommentInfo || !this.editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { replyCommand, ownerId, extensionId, commentingRangesInfo } = newCommentInfo;
|
||||
|
||||
if (commentingRangesInfo) {
|
||||
let range = new Range(lineNumber, 1, lineNumber, 1);
|
||||
if (commentingRangesInfo.newCommentThreadCommand) {
|
||||
if (replyCommand) {
|
||||
const commandId = replyCommand.id;
|
||||
const args = replyCommand.arguments || [];
|
||||
|
||||
this._commandService.executeCommand(commandId, ...args);
|
||||
}
|
||||
} else if (commentingRangesInfo.newCommentThreadCallback) {
|
||||
commentingRangesInfo.newCommentThreadCallback(this.editor.getModel().uri, range);
|
||||
}
|
||||
} else {
|
||||
const commentInfo = this._commentInfos.filter(info => info.owner === ownerId);
|
||||
if (!commentInfo || !commentInfo.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draftMode = commentInfo[0].draftMode;
|
||||
this.addComment(lineNumber, replyCommand, ownerId, extensionId, draftMode, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private setComments(commentInfos: ICommentInfo[]): void {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commentInfos = commentInfos;
|
||||
let lineDecorationsWidth: number = this.editor.getConfiguration().layoutInfo.decorationsWidth;
|
||||
|
||||
if (this._commentInfos.some(info => Boolean(info.commentingRanges && (Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges.ranges).length))) {
|
||||
if (!this._commentingRangeSpaceReserved) {
|
||||
this._commentingRangeSpaceReserved = true;
|
||||
let extraEditorClassName: string[] = [];
|
||||
const configuredExtraClassName = this.editor.getRawConfiguration().extraEditorClassName;
|
||||
if (configuredExtraClassName) {
|
||||
extraEditorClassName = configuredExtraClassName.split(' ');
|
||||
}
|
||||
|
||||
if (this.editor.getConfiguration().contribInfo.folding) {
|
||||
lineDecorationsWidth -= 16;
|
||||
}
|
||||
lineDecorationsWidth += 9;
|
||||
extraEditorClassName.push('inline-comment');
|
||||
this.editor.updateOptions({
|
||||
extraEditorClassName: extraEditorClassName.join(' '),
|
||||
lineDecorationsWidth: lineDecorationsWidth
|
||||
});
|
||||
|
||||
// we only update the lineDecorationsWidth property but keep the width of the whole editor.
|
||||
const originalLayoutInfo = this.editor.getLayoutInfo();
|
||||
|
||||
this.editor.layout({
|
||||
width: originalLayoutInfo.width,
|
||||
height: originalLayoutInfo.height
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// create viewzones
|
||||
this.removeCommentWidgetsAndStoreCache();
|
||||
|
||||
this._commentInfos.forEach(info => {
|
||||
let providerCacheStore = this._pendingCommentCache[info.owner];
|
||||
info.threads.forEach(thread => {
|
||||
let pendingComment: string | null = null;
|
||||
if (providerCacheStore) {
|
||||
pendingComment = providerCacheStore[thread.threadId];
|
||||
}
|
||||
|
||||
if (pendingComment) {
|
||||
thread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded;
|
||||
}
|
||||
|
||||
this.displayCommentThread(info.owner, thread, pendingComment, info.draftMode);
|
||||
});
|
||||
});
|
||||
|
||||
const commentingRanges: IRange[] = [];
|
||||
this._commentInfos.forEach(info => {
|
||||
commentingRanges.push(...(Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges ? info.commentingRanges.ranges : []));
|
||||
});
|
||||
this._commentingRangeDecorator.update(this.editor, this._commentInfos);
|
||||
}
|
||||
|
||||
public closeWidget(): void {
|
||||
this._commentThreadVisible.reset();
|
||||
|
||||
if (this._newCommentWidget) {
|
||||
this._newCommentWidget.dispose();
|
||||
this._newCommentWidget = undefined;
|
||||
}
|
||||
|
||||
if (this._commentWidgets) {
|
||||
this._commentWidgets.forEach(widget => widget.hide());
|
||||
}
|
||||
|
||||
this.editor.focus();
|
||||
this.editor.revealRangeInCenter(this.editor.getSelection()!);
|
||||
}
|
||||
|
||||
private removeCommentWidgetsAndStoreCache() {
|
||||
if (this._commentWidgets) {
|
||||
this._commentWidgets.forEach(zone => {
|
||||
let pendingComment = zone.getPendingComment();
|
||||
let providerCacheStore = this._pendingCommentCache[zone.owner];
|
||||
|
||||
if (pendingComment) {
|
||||
if (!providerCacheStore) {
|
||||
this._pendingCommentCache[zone.owner] = {};
|
||||
}
|
||||
|
||||
this._pendingCommentCache[zone.owner][zone.commentThread.threadId] = pendingComment;
|
||||
} else {
|
||||
if (providerCacheStore) {
|
||||
delete providerCacheStore[zone.commentThread.threadId];
|
||||
}
|
||||
}
|
||||
|
||||
zone.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
this._commentWidgets = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class NextCommentThreadAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.nextCommentThreadAction',
|
||||
label: nls.localize('nextCommentThreadAction', "Go to Next Comment Thread"),
|
||||
alias: 'Go to Next Comment Thread',
|
||||
precondition: null,
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
let controller = ReviewController.get(editor);
|
||||
if (controller) {
|
||||
controller.nextCommentThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerEditorContribution(ReviewController);
|
||||
registerEditorAction(NextCommentThreadAction);
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: 'workbench.action.addComment',
|
||||
handler: (accessor) => {
|
||||
const activeEditor = getActiveEditor(accessor);
|
||||
if (!activeEditor) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const controller = ReviewController.get(activeEditor);
|
||||
if (!controller) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const position = activeEditor.getPosition();
|
||||
controller.addOrToggleCommentAtLine(position.lineNumber);
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'workbench.action.submitComment',
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Enter,
|
||||
when: ctxCommentEditorFocused,
|
||||
handler: (accessor, args) => {
|
||||
const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
|
||||
if (activeCodeEditor instanceof SimpleCommentEditor) {
|
||||
activeCodeEditor.getParentThread().submitComment();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'closeReviewPanel',
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape],
|
||||
when: ctxCommentThreadVisible,
|
||||
handler: closeReviewPanel
|
||||
});
|
||||
|
||||
export function getActiveEditor(accessor: ServicesAccessor): IActiveCodeEditor | null {
|
||||
let activeTextEditorWidget = accessor.get(IEditorService).activeTextEditorWidget;
|
||||
|
||||
if (isDiffEditor(activeTextEditorWidget)) {
|
||||
if (activeTextEditorWidget.getOriginalEditor().hasTextFocus()) {
|
||||
activeTextEditorWidget = activeTextEditorWidget.getOriginalEditor();
|
||||
} else {
|
||||
activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCodeEditor(activeTextEditorWidget) || !activeTextEditorWidget.hasModel()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return activeTextEditorWidget;
|
||||
}
|
||||
|
||||
function closeReviewPanel(accessor: ServicesAccessor, args: any) {
|
||||
const outerEditor = getActiveEditor(accessor);
|
||||
if (!outerEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = ReviewController.get(outerEditor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
controller.closeWidget();
|
||||
}
|
||||
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const peekViewBackground = theme.getColor(peekViewResultsBackground);
|
||||
if (peekViewBackground) {
|
||||
collector.addRule(
|
||||
`.monaco-editor .review-widget,` +
|
||||
`.monaco-editor .review-widget {` +
|
||||
` background-color: ${peekViewBackground};` +
|
||||
`}`);
|
||||
}
|
||||
|
||||
const monacoEditorBackground = theme.getColor(peekViewTitleBackground);
|
||||
if (monacoEditorBackground) {
|
||||
collector.addRule(
|
||||
`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` +
|
||||
` background-color: ${monacoEditorBackground}` +
|
||||
`}`
|
||||
);
|
||||
}
|
||||
|
||||
const monacoEditorForeground = theme.getColor(editorForeground);
|
||||
if (monacoEditorForeground) {
|
||||
collector.addRule(
|
||||
`.monaco-editor .review-widget .body .monaco-editor {` +
|
||||
` color: ${monacoEditorForeground}` +
|
||||
`}` +
|
||||
`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` +
|
||||
` color: ${monacoEditorForeground};` +
|
||||
` font-size: inherit` +
|
||||
`}`
|
||||
);
|
||||
}
|
||||
|
||||
const selectionBackground = theme.getColor(peekViewResultsSelectionBackground);
|
||||
|
||||
if (selectionBackground) {
|
||||
collector.addRule(
|
||||
`@keyframes monaco-review-widget-focus {` +
|
||||
` 0% { background: ${selectionBackground}; }` +
|
||||
` 100% { background: transparent; }` +
|
||||
`}` +
|
||||
`.monaco-editor .review-widget .body .review-comment.focus {` +
|
||||
` animation: monaco-review-widget-focus 3s ease 0s;` +
|
||||
`}`
|
||||
);
|
||||
}
|
||||
|
||||
const commentingRangeForeground = theme.getColor(overviewRulerCommentingRangeForeground);
|
||||
if (commentingRangeForeground) {
|
||||
collector.addRule(`
|
||||
.monaco-editor .comment-diff-added {
|
||||
border-left: 3px solid ${commentingRangeForeground};
|
||||
}
|
||||
.monaco-editor .comment-diff-added:before {
|
||||
background: ${commentingRangeForeground};
|
||||
}
|
||||
.monaco-editor .comment-thread {
|
||||
border-left: 3px solid ${commentingRangeForeground};
|
||||
}
|
||||
.monaco-editor .comment-thread:before {
|
||||
background: ${commentingRangeForeground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);
|
||||
if (statusBarItemHoverBackground) {
|
||||
collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active:hover { background-color: ${statusBarItemHoverBackground};}`);
|
||||
}
|
||||
|
||||
const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND);
|
||||
if (statusBarItemActiveBackground) {
|
||||
collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid transparent;}`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/panel';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { CollapseAllAction, DefaultAccessibilityProvider, DefaultController, DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { TreeResourceNavigator, WorkbenchTree } from 'vs/platform/list/browser/listService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { Panel } from 'vs/workbench/browser/panel';
|
||||
import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
|
||||
import { ReviewController } from 'vs/workbench/contrib/comments/electron-browser/commentsEditorContribution';
|
||||
import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'vs/workbench/contrib/comments/electron-browser/commentsTreeViewer';
|
||||
import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/electron-browser/commentService';
|
||||
import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
|
||||
export const COMMENTS_PANEL_ID = 'workbench.panel.comments';
|
||||
export const COMMENTS_PANEL_TITLE = 'Comments';
|
||||
|
||||
export class CommentsPanel extends Panel {
|
||||
private treeLabels: ResourceLabels;
|
||||
private tree: WorkbenchTree;
|
||||
private treeContainer: HTMLElement;
|
||||
private messageBoxContainer: HTMLElement;
|
||||
private messageBox: HTMLElement;
|
||||
private commentsModel: CommentsModel;
|
||||
private collapseAllAction: IAction;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ICommentService private readonly commentService: ICommentService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(COMMENTS_PANEL_ID, telemetryService, themeService, storageService);
|
||||
}
|
||||
|
||||
public create(parent: HTMLElement): void {
|
||||
super.create(parent);
|
||||
|
||||
dom.addClass(parent, 'comments-panel');
|
||||
|
||||
let container = dom.append(parent, dom.$('.comments-panel-container'));
|
||||
this.treeContainer = dom.append(container, dom.$('.tree-container'));
|
||||
this.commentsModel = new CommentsModel();
|
||||
|
||||
this.createTree();
|
||||
this.createMessageBox(container);
|
||||
|
||||
this._register(this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this));
|
||||
this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this));
|
||||
|
||||
const styleElement = dom.createStyleSheet(parent);
|
||||
this.applyStyles(styleElement);
|
||||
this._register(this.themeService.onThemeChange(_ => this.applyStyles(styleElement)));
|
||||
|
||||
this._register(this.onDidChangeVisibility(visible => {
|
||||
if (visible) {
|
||||
this.refresh();
|
||||
}
|
||||
}));
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
private applyStyles(styleElement: HTMLStyleElement) {
|
||||
const content: string[] = [];
|
||||
|
||||
const theme = this.themeService.getTheme();
|
||||
const linkColor = theme.getColor(textLinkForeground);
|
||||
if (linkColor) {
|
||||
content.push(`.comments-panel .comments-panel-container a { color: ${linkColor}; }`);
|
||||
}
|
||||
|
||||
const linkActiveColor = theme.getColor(textLinkActiveForeground);
|
||||
if (linkActiveColor) {
|
||||
content.push(`.comments-panel .comments-panel-container a:hover, a:active { color: ${linkActiveColor}; }`);
|
||||
}
|
||||
|
||||
const focusColor = theme.getColor(focusBorder);
|
||||
if (focusColor) {
|
||||
content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`);
|
||||
}
|
||||
|
||||
const codeTextForegroundColor = theme.getColor(textPreformatForeground);
|
||||
if (codeTextForegroundColor) {
|
||||
content.push(`.comments-panel .comments-panel-container .text code { color: ${codeTextForegroundColor}; }`);
|
||||
}
|
||||
|
||||
styleElement.innerHTML = content.join('\n');
|
||||
}
|
||||
|
||||
private async render(): Promise<void> {
|
||||
dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads());
|
||||
await this.tree.setInput(this.commentsModel);
|
||||
this.renderMessage();
|
||||
}
|
||||
|
||||
public getActions(): IAction[] {
|
||||
if (!this.collapseAllAction) {
|
||||
this.collapseAllAction = this.instantiationService.createInstance(CollapseAllAction, this.tree, this.commentsModel.hasCommentThreads());
|
||||
this._register(this.collapseAllAction);
|
||||
}
|
||||
|
||||
return [this.collapseAllAction];
|
||||
}
|
||||
|
||||
public layout(dimensions: dom.Dimension): void {
|
||||
this.tree.layout(dimensions.height, dimensions.width);
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return COMMENTS_PANEL_TITLE;
|
||||
}
|
||||
|
||||
private createMessageBox(parent: HTMLElement): void {
|
||||
this.messageBoxContainer = dom.append(parent, dom.$('.message-box-container'));
|
||||
this.messageBox = dom.append(this.messageBoxContainer, dom.$('span'));
|
||||
this.messageBox.setAttribute('tabindex', '0');
|
||||
}
|
||||
|
||||
private renderMessage(): void {
|
||||
this.messageBox.textContent = this.commentsModel.getMessage();
|
||||
dom.toggleClass(this.messageBoxContainer, 'hidden', this.commentsModel.hasCommentThreads());
|
||||
}
|
||||
|
||||
private createTree(): void {
|
||||
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
|
||||
|
||||
this.tree = this._register(this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, {
|
||||
dataSource: new CommentsDataSource(),
|
||||
renderer: new CommentsModelRenderer(this.treeLabels, this.openerService),
|
||||
accessibilityProvider: new DefaultAccessibilityProvider,
|
||||
controller: new DefaultController(),
|
||||
dnd: new DefaultDragAndDrop(),
|
||||
filter: new CommentsDataFilter()
|
||||
}, {
|
||||
twistiePixels: 20,
|
||||
ariaLabel: COMMENTS_PANEL_TITLE
|
||||
}));
|
||||
|
||||
const commentsNavigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: true }));
|
||||
this._register(Event.debounce(commentsNavigator.openResource, (last, event) => event, 100, true)(options => {
|
||||
this.openFile(options.element, options.editorOptions.pinned, options.editorOptions.preserveFocus, options.sideBySide);
|
||||
}));
|
||||
}
|
||||
|
||||
private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): boolean {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(element instanceof ResourceWithCommentThreads || element instanceof CommentNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const range = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].range : element.range;
|
||||
|
||||
const activeEditor = this.editorService.activeEditor;
|
||||
let currentActiveResource = activeEditor ? activeEditor.getResource() : undefined;
|
||||
if (currentActiveResource && currentActiveResource.toString() === element.resource.toString()) {
|
||||
const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId;
|
||||
const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.commentId : element.comment.commentId;
|
||||
const control = this.editorService.activeTextEditorWidget;
|
||||
if (threadToReveal && isCodeEditor(control)) {
|
||||
const controller = ReviewController.get(control);
|
||||
controller.revealCommentThread(threadToReveal, commentToReveal, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId;
|
||||
const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment : element.comment;
|
||||
|
||||
if (commentToReveal.selectCommand) {
|
||||
this.commandService.executeCommand(commentToReveal.selectCommand.id, ...(commentToReveal.selectCommand.arguments || [])).then(_ => {
|
||||
let activeWidget = this.editorService.activeTextEditorWidget;
|
||||
if (isDiffEditor(activeWidget)) {
|
||||
const originalEditorWidget = activeWidget.getOriginalEditor();
|
||||
const modifiedEditorWidget = activeWidget.getModifiedEditor();
|
||||
|
||||
let controller;
|
||||
if (originalEditorWidget.getModel()!.uri.toString() === element.resource.toString()) {
|
||||
controller = ReviewController.get(originalEditorWidget);
|
||||
} else if (modifiedEditorWidget.getModel()!.uri.toString() === element.resource.toString()) {
|
||||
controller = ReviewController.get(modifiedEditorWidget);
|
||||
}
|
||||
|
||||
if (controller) {
|
||||
controller.revealCommentThread(threadToReveal, commentToReveal.commentId, true);
|
||||
}
|
||||
} else {
|
||||
let activeEditor = this.editorService.activeEditor;
|
||||
let currentActiveResource = activeEditor ? activeEditor.getResource() : undefined;
|
||||
if (currentActiveResource && currentActiveResource.toString() === element.resource.toString()) {
|
||||
const control = this.editorService.activeTextEditorWidget;
|
||||
if (threadToReveal && isCodeEditor(control)) {
|
||||
const controller = ReviewController.get(control);
|
||||
controller.revealCommentThread(threadToReveal, commentToReveal.commentId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
this.editorService.openEditor({
|
||||
resource: element.resource,
|
||||
options: {
|
||||
pinned: pinned,
|
||||
preserveFocus: preserveFocus,
|
||||
selection: range
|
||||
}
|
||||
}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {
|
||||
if (editor) {
|
||||
const control = editor.getControl();
|
||||
if (threadToReveal && isCodeEditor(control)) {
|
||||
const controller = ReviewController.get(control);
|
||||
controller.revealCommentThread(threadToReveal, commentToReveal.commentId, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
if (this.isVisible()) {
|
||||
this.collapseAllAction.enabled = this.commentsModel.hasCommentThreads();
|
||||
|
||||
dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads());
|
||||
this.tree.refresh().then(() => {
|
||||
this.renderMessage();
|
||||
}, (e) => {
|
||||
console.log(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void {
|
||||
this.commentsModel.setCommentThreads(e.ownerId, e.commentThreads);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private onCommentsUpdated(e: ICommentThreadChangedEvent): void {
|
||||
const didUpdate = this.commentsModel.updateCommentThreads(e);
|
||||
if (didUpdate) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import * as nls from 'vs/nls';
|
||||
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDataSource, IFilter, IRenderer as ITreeRenderer, ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel';
|
||||
|
||||
export class CommentsDataSource implements IDataSource {
|
||||
public getId(tree: ITree, element: any): string {
|
||||
if (element instanceof CommentsModel) {
|
||||
return 'root';
|
||||
}
|
||||
if (element instanceof ResourceWithCommentThreads) {
|
||||
return element.id;
|
||||
}
|
||||
if (element instanceof CommentNode) {
|
||||
return `${element.resource.toString()}-${element.comment.commentId}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public hasChildren(tree: ITree, element: any): boolean {
|
||||
return element instanceof CommentsModel || element instanceof ResourceWithCommentThreads || (element instanceof CommentNode && !!element.replies.length);
|
||||
}
|
||||
|
||||
public getChildren(tree: ITree, element: any): Promise<ResourceWithCommentThreads[] | CommentNode[]> {
|
||||
if (element instanceof CommentsModel) {
|
||||
return Promise.resolve(element.resourceCommentThreads);
|
||||
}
|
||||
if (element instanceof ResourceWithCommentThreads) {
|
||||
return Promise.resolve(element.commentThreads);
|
||||
}
|
||||
if (element instanceof CommentNode) {
|
||||
return Promise.resolve(element.replies);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
public getParent(tree: ITree, element: any): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public shouldAutoexpand(tree: ITree, element: any): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
interface IResourceTemplateData {
|
||||
resourceLabel: IResourceLabel;
|
||||
}
|
||||
|
||||
interface ICommentThreadTemplateData {
|
||||
icon: HTMLImageElement;
|
||||
userName: HTMLSpanElement;
|
||||
commentText: HTMLElement;
|
||||
disposables: Disposable[];
|
||||
}
|
||||
|
||||
export class CommentsModelRenderer implements ITreeRenderer {
|
||||
private static RESOURCE_ID = 'resource-with-comments';
|
||||
private static COMMENT_ID = 'comment-node';
|
||||
|
||||
constructor(
|
||||
private labels: ResourceLabels,
|
||||
@IOpenerService private readonly openerService: IOpenerService
|
||||
) {
|
||||
}
|
||||
|
||||
public getHeight(tree: ITree, element: any): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
public getTemplateId(tree: ITree, element: any): string {
|
||||
if (element instanceof ResourceWithCommentThreads) {
|
||||
return CommentsModelRenderer.RESOURCE_ID;
|
||||
}
|
||||
if (element instanceof CommentNode) {
|
||||
return CommentsModelRenderer.COMMENT_ID;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public renderTemplate(ITree: ITree, templateId: string, container: HTMLElement): any {
|
||||
switch (templateId) {
|
||||
case CommentsModelRenderer.RESOURCE_ID:
|
||||
return this.renderResourceTemplate(container);
|
||||
case CommentsModelRenderer.COMMENT_ID:
|
||||
return this.renderCommentTemplate(container);
|
||||
}
|
||||
}
|
||||
|
||||
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
|
||||
switch (templateId) {
|
||||
case CommentsModelRenderer.RESOURCE_ID:
|
||||
(<IResourceTemplateData>templateData).resourceLabel.dispose();
|
||||
break;
|
||||
case CommentsModelRenderer.COMMENT_ID:
|
||||
(<ICommentThreadTemplateData>templateData).disposables.forEach(disposeable => disposeable.dispose());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
|
||||
switch (templateId) {
|
||||
case CommentsModelRenderer.RESOURCE_ID:
|
||||
return this.renderResourceElement(tree, element, templateData);
|
||||
case CommentsModelRenderer.COMMENT_ID:
|
||||
return this.renderCommentElement(tree, element, templateData);
|
||||
}
|
||||
}
|
||||
|
||||
private renderResourceTemplate(container: HTMLElement): IResourceTemplateData {
|
||||
const data = <IResourceTemplateData>Object.create(null);
|
||||
const labelContainer = dom.append(container, dom.$('.resource-container'));
|
||||
data.resourceLabel = this.labels.create(labelContainer);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private renderCommentTemplate(container: HTMLElement): ICommentThreadTemplateData {
|
||||
const data = <ICommentThreadTemplateData>Object.create(null);
|
||||
const labelContainer = dom.append(container, dom.$('.comment-container'));
|
||||
data.userName = dom.append(labelContainer, dom.$('.user'));
|
||||
data.commentText = dom.append(labelContainer, dom.$('.text'));
|
||||
data.disposables = [];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private renderResourceElement(tree: ITree, element: ResourceWithCommentThreads, templateData: IResourceTemplateData) {
|
||||
templateData.resourceLabel.setFile(element.resource);
|
||||
}
|
||||
|
||||
private renderCommentElement(tree: ITree, element: CommentNode, templateData: ICommentThreadTemplateData) {
|
||||
templateData.userName.textContent = element.comment.userName;
|
||||
templateData.commentText.innerHTML = '';
|
||||
const renderedComment = renderMarkdown(element.comment.body, {
|
||||
inline: true,
|
||||
actionHandler: {
|
||||
callback: (content) => {
|
||||
try {
|
||||
const uri = URI.parse(content);
|
||||
this.openerService.open(uri).catch(onUnexpectedError);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
disposeables: templateData.disposables
|
||||
}
|
||||
});
|
||||
|
||||
const images = renderedComment.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i];
|
||||
const textDescription = dom.$('');
|
||||
textDescription.textContent = image.alt ? nls.localize('imageWithLabel', "Image: {0}", image.alt) : nls.localize('image', "Image");
|
||||
image.parentNode!.replaceChild(textDescription, image);
|
||||
}
|
||||
|
||||
templateData.commentText.appendChild(renderedComment);
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentsDataFilter implements IFilter {
|
||||
public isVisible(tree: ITree, element: any): boolean {
|
||||
if (element instanceof CommentsModel) {
|
||||
return element.resourceCommentThreads.length > 0;
|
||||
}
|
||||
if (element instanceof ResourceWithCommentThreads) {
|
||||
return element.commentThreads.length > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" aria-hidden="true" height="16" width="16"><path fill="#C5C5C5" fill-rule="evenodd" d="M14 1H2c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1h2v3.5L7.5 11H14c.55 0 1-.45 1-1V2c0-.55-.45-1-1-1zm0 9H7l-2 2v-2H2V2h12v8z"></path></svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.comments-panel .comments-panel-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container.hidden {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container .resource-container,
|
||||
.comments-panel .comments-panel-container .tree-container .comment-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.comments-panel .user {
|
||||
padding-right: 5px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container .comment-container .text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container .comment-container .text * {
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container .comment-container .text code {
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .message-box-container {
|
||||
line-height: 22px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .message-box-container span:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container .count-badge-wrapper {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.comments-panel .comments-panel-container .tree-container .comment-container {
|
||||
line-height: 22px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8.25008H4.08333V11.1667H2.91667V8.25008H0V7.08341H2.91667V4.16675H4.08333V7.08341H7V8.25008V8.25008Z" fill="#C8C8C8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 0C13.58 0 10 3.58 10 8C10 12.42 13.58 16 18 16C22.42 16 26 12.42 26 8C26 3.58 22.42 0 18 0V0ZM22.81 12.81C22.18 13.44 21.45 13.92 20.64 14.26C19.81 14.62 18.92 14.79 18 14.79C17.08 14.79 16.19 14.62 15.36 14.26C14.55 13.92 13.81 13.43 13.19 12.81C12.57 12.19 12.08 11.45 11.74 10.64C11.38 9.81 11.21 8.92 11.21 8C11.21 7.08 11.38 6.19 11.74 5.36C12.08 4.55 12.57 3.81 13.19 3.19C13.81 2.57 14.55 2.08 15.36 1.74C16.19 1.38 17.08 1.21 18 1.21C18.92 1.21 19.81 1.38 20.64 1.74C21.45 2.08 22.19 2.57 22.81 3.19C23.43 3.81 23.92 4.55 24.26 5.36C24.62 6.19 24.79 7.08 24.79 8C24.79 8.92 24.62 9.81 24.26 10.64C23.92 11.45 23.43 12.19 22.81 12.81V12.81ZM14 6.8V6.21C14 5.55 14.53 5.02 15.2 5.02H15.79C16.45 5.02 16.98 5.55 16.98 6.21V6.8C16.98 7.47 16.45 8 15.79 8H15.2C14.53 8 14 7.47 14 6.8V6.8ZM19 6.8V6.21C19 5.55 19.53 5.02 20.2 5.02H20.79C21.45 5.02 21.98 5.55 21.98 6.21V6.8C21.98 7.47 21.45 8 20.79 8H20.2C19.53 8 19 7.47 19 6.8V6.8ZM23 10C22.28 11.88 20.09 13 18 13C15.91 13 13.72 11.87 13 10C12.86 9.61 13.23 9 13.66 9H22.25C22.66 9 23.14 9.61 23 10V10Z" fill="#C8C8C8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8.25008H4.08333V11.1667H2.91667V8.25008H0V7.08341H2.91667V4.16675H4.08333V7.08341H7V8.25008V8.25008Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 0C13.58 0 10 3.58 10 8C10 12.42 13.58 16 18 16C22.42 16 26 12.42 26 8C26 3.58 22.42 0 18 0V0ZM22.81 12.81C22.18 13.44 21.45 13.92 20.64 14.26C19.81 14.62 18.92 14.79 18 14.79C17.08 14.79 16.19 14.62 15.36 14.26C14.55 13.92 13.81 13.43 13.19 12.81C12.57 12.19 12.08 11.45 11.74 10.64C11.38 9.81 11.21 8.92 11.21 8C11.21 7.08 11.38 6.19 11.74 5.36C12.08 4.55 12.57 3.81 13.19 3.19C13.81 2.57 14.55 2.08 15.36 1.74C16.19 1.38 17.08 1.21 18 1.21C18.92 1.21 19.81 1.38 20.64 1.74C21.45 2.08 22.19 2.57 22.81 3.19C23.43 3.81 23.92 4.55 24.26 5.36C24.62 6.19 24.79 7.08 24.79 8C24.79 8.92 24.62 9.81 24.26 10.64C23.92 11.45 23.43 12.19 22.81 12.81V12.81ZM14 6.8V6.21C14 5.55 14.53 5.02 15.2 5.02H15.79C16.45 5.02 16.98 5.55 16.98 6.21V6.8C16.98 7.47 16.45 8 15.79 8H15.2C14.53 8 14 7.47 14 6.8V6.8ZM19 6.8V6.21C19 5.55 19.53 5.02 20.2 5.02H20.79C21.45 5.02 21.98 5.55 21.98 6.21V6.8C21.98 7.47 21.45 8 20.79 8H20.2C19.53 8 19 7.47 19 6.8V6.8ZM23 10C22.28 11.88 20.09 13 18 13C15.91 13 13.72 11.87 13 10C12.86 9.61 13.23 9 13.66 9H22.25C22.66 9 23.14 9.61 23 10V10Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8.25008H4.08333V11.1667H2.91667V8.25008H0V7.08341H2.91667V4.16675H4.08333V7.08341H7V8.25008V8.25008Z" fill="#4B4B4B"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 0C13.58 0 10 3.58 10 8C10 12.42 13.58 16 18 16C22.42 16 26 12.42 26 8C26 3.58 22.42 0 18 0V0ZM22.81 12.81C22.18 13.44 21.45 13.92 20.64 14.26C19.81 14.62 18.92 14.79 18 14.79C17.08 14.79 16.19 14.62 15.36 14.26C14.55 13.92 13.81 13.43 13.19 12.81C12.57 12.19 12.08 11.45 11.74 10.64C11.38 9.81 11.21 8.92 11.21 8C11.21 7.08 11.38 6.19 11.74 5.36C12.08 4.55 12.57 3.81 13.19 3.19C13.81 2.57 14.55 2.08 15.36 1.74C16.19 1.38 17.08 1.21 18 1.21C18.92 1.21 19.81 1.38 20.64 1.74C21.45 2.08 22.19 2.57 22.81 3.19C23.43 3.81 23.92 4.55 24.26 5.36C24.62 6.19 24.79 7.08 24.79 8C24.79 8.92 24.62 9.81 24.26 10.64C23.92 11.45 23.43 12.19 22.81 12.81V12.81ZM14 6.8V6.21C14 5.55 14.53 5.02 15.2 5.02H15.79C16.45 5.02 16.98 5.55 16.98 6.21V6.8C16.98 7.47 16.45 8 15.79 8H15.2C14.53 8 14 7.47 14 6.8V6.8ZM19 6.8V6.21C19 5.55 19.53 5.02 20.2 5.02H20.79C21.45 5.02 21.98 5.55 21.98 6.21V6.8C21.98 7.47 21.45 8 20.79 8H20.2C19.53 8 19 7.47 19 6.8V6.8ZM23 10C22.28 11.88 20.09 13 18 13C15.91 13 13.72 11.87 13 10C12.86 9.61 13.23 9 13.66 9H22.25C22.66 9 23.14 9.61 23 10V10Z" fill="#4B4B4B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,486 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .margin-view-overlays .review {
|
||||
background-image: url('comment.svg');
|
||||
cursor: pointer;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.monaco-editor .comment-hint {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
padding-left: 2px;
|
||||
background: url('comment.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor .comment-hint.commenting-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.monaco-editor .comment-hint:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment {
|
||||
padding: 8px 16px 8px 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .comment-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .comment-actions .monaco-toolbar {
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .comment-actions .action-item {
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .comment-title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .comment-title .action-label.octicon {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .comment-title .monaco-dropdown .toolbar-toggle-more {
|
||||
width: 16px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body blockquote {
|
||||
margin: 0 7px 0 5px;
|
||||
padding: 0 16px 0 10px;
|
||||
border-left-width: 5px;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .avatar-container {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .avatar-container img.avatar {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
|
||||
.monaco-editor .review-widget .body .comment-reactions .monaco-text-button {
|
||||
margin: 0 7px 0 0;
|
||||
width: 30px;
|
||||
background-color: transparent;
|
||||
border: 1px solid grey;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents {
|
||||
padding-left: 20px;
|
||||
user-select: text;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body pre {
|
||||
overflow: auto;
|
||||
word-wrap: normal;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .review-widget .body .comment-body h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .author {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .author {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .isPending {
|
||||
margin: 0 5px 0 5px;
|
||||
padding: 0 2px 0 2px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-body {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions {
|
||||
margin-top: 8px;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .monaco-action-bar .actions-container {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label {
|
||||
padding: 1px 4px;
|
||||
white-space: pre;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-icon {
|
||||
background-size: 12px;
|
||||
background-position: left center;
|
||||
background-repeat: no-repeat;
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-label {
|
||||
line-height: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions {
|
||||
display: none;
|
||||
background-image: url(./reaction.svg);
|
||||
width: 26px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
margin-top: 3px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions:hover .action-item a.action-label.toolbar-toggle-pickReactions {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions {
|
||||
background-image: url(./reaction-dark.svg);
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions {
|
||||
background-image: url(./reaction-hc.svg);
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions {
|
||||
background-image: url(./reaction.svg);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: 100% auto;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions {
|
||||
background-image: url(./reaction-dark.svg);
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions {
|
||||
background-image: url(./reaction-hc.svg);
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label{
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active {
|
||||
border: 1px solid grey;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .review-widget .body span.created_at {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body p,
|
||||
.monaco-editor .review-widget .body .comment-body ul {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body p:first-child,
|
||||
.monaco-editor .review-widget .body .comment-body ul:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body p:last-child,
|
||||
.monaco-editor .review-widget .body.comment-body ul:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body li>p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body li>ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body code {
|
||||
border-radius: 3px;
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body span {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-body img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form {
|
||||
margin: 8px 20px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .validation-error {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
padding: 0.4em;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
min-height: 34px;
|
||||
margin-top: -1px;
|
||||
margin-left: -1px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form.expand .monaco-editor,
|
||||
.monaco-editor .review-widget .body .comment-form.expand .form-actions {
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {
|
||||
text-align: left;
|
||||
display: block;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
border: 0px;
|
||||
cursor: text;
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form .review-thread-reply-button:focus {
|
||||
outline-style: solid;
|
||||
outline-width: 1px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form .monaco-editor,
|
||||
.monaco-editor .review-widget .body .edit-container .monaco-editor {
|
||||
width: 100%;
|
||||
min-height: 90px;
|
||||
max-height: 500px;
|
||||
border-radius: 3px;
|
||||
border: 0px;
|
||||
box-sizing: content-box;
|
||||
padding: 6px 0 6px 12px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form .monaco-editor,
|
||||
.monaco-editor .review-widget .body .comment-form .form-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form .form-actions,
|
||||
.monaco-editor .review-widget .body .edit-container .form-actions {
|
||||
overflow: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .edit-container .form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .edit-textarea {
|
||||
height: 90px;
|
||||
margin: 5px 0 10px 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form .monaco-text-button,
|
||||
.monaco-editor .review-widget .body .edit-container .monaco-text-button {
|
||||
width: auto;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .body .comment-form .monaco-text-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head {
|
||||
-webkit-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-title {
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
margin-left: 20px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-title .dirname:not(:empty) {
|
||||
font-size: 0.9em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-actions {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar,
|
||||
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar>.actions-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar .action-item {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar .action-label {
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
line-height: inherit;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-actions>.monaco-action-bar .action-label.octicon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget .head .review-actions .action-label.icon.close-review-action {
|
||||
background: url('close.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor .review-widget>.body {
|
||||
border-top: 1px solid;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-editor .comment-range-glyph {
|
||||
margin-left: 5px;
|
||||
width: 4px !important;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.monaco-editor .comment-range-glyph:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 100%;
|
||||
width: 0;
|
||||
left: -2px;
|
||||
transition: width 80ms linear, left 80ms linear;
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays>div:hover>.comment-range-glyph.comment-diff-added:before,
|
||||
.monaco-editor .comment-range-glyph.comment-thread:before {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 9px;
|
||||
left: -6px;
|
||||
z-index: 10;
|
||||
color: black;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays>div:hover>.comment-range-glyph.comment-diff-added:before {
|
||||
content: '+';
|
||||
}
|
||||
|
||||
.monaco-editor .comment-range-glyph.comment-thread {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.monaco-editor .comment-range-glyph.comment-thread:before {
|
||||
content: '◆';
|
||||
font-size: 10px;
|
||||
line-height: 100%;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.monaco-editor.inline-comment .margin-view-overlays .folding {
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.monaco-editor.inline-comment .margin-view-overlays .dirty-diff-glyph {
|
||||
margin-left: 14px;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { ActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
|
||||
export class ToggleReactionsAction extends Action {
|
||||
static readonly ID = 'toolbar.toggle.pickReactions';
|
||||
private _menuActions: IAction[];
|
||||
private toggleDropdownMenu: () => void;
|
||||
constructor(toggleDropdownMenu: () => void, title?: string) {
|
||||
title = title || nls.localize('pickReactions', "Pick Reactions...");
|
||||
super(ToggleReactionsAction.ID, title, 'toggle-reactions', true);
|
||||
this.toggleDropdownMenu = toggleDropdownMenu;
|
||||
}
|
||||
run(): Promise<any> {
|
||||
this.toggleDropdownMenu();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
get menuActions() {
|
||||
return this._menuActions;
|
||||
}
|
||||
set menuActions(actions: IAction[]) {
|
||||
this._menuActions = actions;
|
||||
}
|
||||
}
|
||||
export class ReactionActionItem extends ActionItem {
|
||||
constructor(action: ReactionAction) {
|
||||
super(null, action, {});
|
||||
}
|
||||
updateLabel(): void {
|
||||
let action = this.getAction() as ReactionAction;
|
||||
if (action.class) {
|
||||
this.label.classList.add(action.class);
|
||||
}
|
||||
|
||||
if (!action.icon) {
|
||||
let reactionLabel = dom.append(this.label, dom.$('span.reaction-label'));
|
||||
reactionLabel.innerText = action.label;
|
||||
} else {
|
||||
let reactionIcon = dom.append(this.label, dom.$('.reaction-icon'));
|
||||
reactionIcon.style.display = '';
|
||||
let uri = URI.revive(action.icon);
|
||||
reactionIcon.style.backgroundImage = `url('${uri}')`;
|
||||
reactionIcon.title = action.label;
|
||||
}
|
||||
if (action.count) {
|
||||
let reactionCount = dom.append(this.label, dom.$('span.reaction-count'));
|
||||
reactionCount.innerText = `${action.count}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
export class ReactionAction extends Action {
|
||||
static readonly ID = 'toolbar.toggle.reaction';
|
||||
constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: any) => Promise<any>, public icon?: UriComponents, public count?: number) {
|
||||
super(ReactionAction.ID, label, cssClass, enabled, actionCallback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { EditorAction, EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
|
||||
// Allowed Editor Contributions:
|
||||
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
|
||||
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
|
||||
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
|
||||
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
|
||||
import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
|
||||
|
||||
export const ctxCommentEditorFocused = new RawContextKey<boolean>('commentEditorFocused', false);
|
||||
|
||||
|
||||
export class SimpleCommentEditor extends CodeEditorWidget {
|
||||
private _parentEditor: ICodeEditor;
|
||||
private _parentThread: ICommentThreadWidget;
|
||||
private _commentEditorFocused: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
domElement: HTMLElement,
|
||||
options: IEditorOptions,
|
||||
parentEditor: ICodeEditor,
|
||||
parentThread: ICommentThreadWidget,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IAccessibilityService accessibilityService: IAccessibilityService
|
||||
) {
|
||||
const codeEditorWidgetOptions = {
|
||||
contributions: [
|
||||
MenuPreventer,
|
||||
ContextMenuController,
|
||||
SuggestController,
|
||||
SnippetController2,
|
||||
TabCompletionController,
|
||||
]
|
||||
};
|
||||
|
||||
super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService);
|
||||
|
||||
this._commentEditorFocused = ctxCommentEditorFocused.bindTo(this._contextKeyService);
|
||||
this._parentEditor = parentEditor;
|
||||
this._parentThread = parentThread;
|
||||
|
||||
this._register(this.onDidFocusEditorWidget(_ => this._commentEditorFocused.set(true)));
|
||||
this._register(this.onDidBlurEditorWidget(_ => this._commentEditorFocused.reset()));
|
||||
}
|
||||
|
||||
getParentEditor(): ICodeEditor {
|
||||
return this._parentEditor;
|
||||
}
|
||||
|
||||
getParentThread(): ICommentThreadWidget {
|
||||
return this._parentThread;
|
||||
}
|
||||
|
||||
protected _getActions(): EditorAction[] {
|
||||
return EditorExtensionsRegistry.getEditorActions();
|
||||
}
|
||||
|
||||
public static getEditorOptions(): IEditorOptions {
|
||||
return {
|
||||
wordWrap: 'on',
|
||||
glyphMargin: false,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
selectOnLineNumbers: false,
|
||||
scrollbar: {
|
||||
vertical: 'visible',
|
||||
verticalScrollbarSize: 14,
|
||||
horizontal: 'auto',
|
||||
useShadows: true,
|
||||
verticalHasArrows: false,
|
||||
horizontalHasArrows: false
|
||||
},
|
||||
overviewRulerLanes: 2,
|
||||
lineDecorationsWidth: 0,
|
||||
scrollBeyondLastLine: false,
|
||||
renderLineHighlight: 'none',
|
||||
fixedOverflowWidgets: true,
|
||||
acceptSuggestionOnEnter: 'smart',
|
||||
minimap: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
224
src/vs/workbench/contrib/debug/browser/baseDebugView.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IExpression, IDebugService } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Expression, Variable, ExpressionContainer } from 'vs/workbench/contrib/debug/common/debugModel';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
|
||||
|
||||
export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;
|
||||
export const twistiePixels = 20;
|
||||
const booleanRegex = /^true|false$/i;
|
||||
const stringRegex = /^(['"]).*\1$/;
|
||||
const $ = dom.$;
|
||||
|
||||
export interface IRenderValueOptions {
|
||||
preserveWhitespace?: boolean;
|
||||
showChanged?: boolean;
|
||||
maxValueLength?: number;
|
||||
showHover?: boolean;
|
||||
colorize?: boolean;
|
||||
}
|
||||
|
||||
export interface IVariableTemplateData {
|
||||
expression: HTMLElement;
|
||||
name: HTMLElement;
|
||||
value: HTMLElement;
|
||||
label: HighlightedLabel;
|
||||
}
|
||||
|
||||
export function renderViewTree(container: HTMLElement): HTMLElement {
|
||||
const treeContainer = document.createElement('div');
|
||||
dom.addClass(treeContainer, 'debug-view-content');
|
||||
container.appendChild(treeContainer);
|
||||
return treeContainer;
|
||||
}
|
||||
|
||||
export function replaceWhitespace(value: string): string {
|
||||
const map: { [x: string]: string } = { '\n': '\\n', '\r': '\\r', '\t': '\\t' };
|
||||
return value.replace(/[\n\r\t]/g, char => map[char]);
|
||||
}
|
||||
|
||||
export function renderExpressionValue(expressionOrValue: IExpression | string, container: HTMLElement, options: IRenderValueOptions): void {
|
||||
let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value;
|
||||
|
||||
// remove stale classes
|
||||
container.className = 'value';
|
||||
// when resolving expressions we represent errors from the server as a variable with name === null.
|
||||
if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable) && !expressionOrValue.available)) {
|
||||
dom.addClass(container, 'unavailable');
|
||||
if (value !== Expression.DEFAULT_VALUE) {
|
||||
dom.addClass(container, 'error');
|
||||
}
|
||||
} else if ((expressionOrValue instanceof ExpressionContainer) && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) {
|
||||
// value changed color has priority over other colors.
|
||||
container.className = 'value changed';
|
||||
expressionOrValue.valueChanged = false;
|
||||
}
|
||||
|
||||
if (options.colorize && typeof expressionOrValue !== 'string') {
|
||||
if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') {
|
||||
dom.addClass(container, expressionOrValue.type);
|
||||
} else if (!isNaN(+value)) {
|
||||
dom.addClass(container, 'number');
|
||||
} else if (booleanRegex.test(value)) {
|
||||
dom.addClass(container, 'boolean');
|
||||
} else if (stringRegex.test(value)) {
|
||||
dom.addClass(container, 'string');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.maxValueLength && value && value.length > options.maxValueLength) {
|
||||
value = value.substr(0, options.maxValueLength) + '...';
|
||||
}
|
||||
if (value && !options.preserveWhitespace) {
|
||||
container.textContent = replaceWhitespace(value);
|
||||
} else {
|
||||
container.textContent = value || '';
|
||||
}
|
||||
if (options.showHover) {
|
||||
container.title = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
export function renderVariable(variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[]): void {
|
||||
if (variable.available) {
|
||||
let text = replaceWhitespace(variable.name);
|
||||
if (variable.value && typeof variable.name === 'string') {
|
||||
text += ':';
|
||||
}
|
||||
data.label.set(text, highlights, variable.type ? variable.type : variable.name);
|
||||
dom.toggleClass(data.name, 'virtual', !!variable.presentationHint && variable.presentationHint.kind === 'virtual');
|
||||
} else if (variable.value && typeof variable.name === 'string') {
|
||||
data.label.set(':');
|
||||
}
|
||||
|
||||
renderExpressionValue(variable, data.value, {
|
||||
showChanged,
|
||||
maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
|
||||
preserveWhitespace: false,
|
||||
showHover: true,
|
||||
colorize: true
|
||||
});
|
||||
}
|
||||
|
||||
export interface IInputBoxOptions {
|
||||
initialValue: string;
|
||||
ariaLabel: string;
|
||||
placeholder?: string;
|
||||
validationOptions?: IInputValidationOptions;
|
||||
onFinish: (value: string, success: boolean) => void;
|
||||
}
|
||||
|
||||
export interface IExpressionTemplateData {
|
||||
expression: HTMLElement;
|
||||
name: HTMLSpanElement;
|
||||
value: HTMLSpanElement;
|
||||
inputBoxContainer: HTMLElement;
|
||||
enableInputBox(expression: IExpression, options: IInputBoxOptions);
|
||||
toDispose: IDisposable[];
|
||||
label: HighlightedLabel;
|
||||
}
|
||||
|
||||
export abstract class AbstractExpressionsRenderer implements ITreeRenderer<IExpression, FuzzyScore, IExpressionTemplateData> {
|
||||
|
||||
constructor(
|
||||
@IDebugService protected debugService: IDebugService,
|
||||
@IContextViewService private readonly contextViewService: IContextViewService,
|
||||
@IThemeService private readonly themeService: IThemeService
|
||||
) { }
|
||||
|
||||
abstract get templateId(): string;
|
||||
|
||||
renderTemplate(container: HTMLElement): IExpressionTemplateData {
|
||||
const expression = dom.append(container, $('.expression'));
|
||||
const name = dom.append(expression, $('span.name'));
|
||||
const value = dom.append(expression, $('span.value'));
|
||||
const label = new HighlightedLabel(name, false);
|
||||
|
||||
const inputBoxContainer = dom.append(expression, $('.inputBoxContainer'));
|
||||
const toDispose: IDisposable[] = [];
|
||||
|
||||
const enableInputBox = (expression: IExpression, options: IInputBoxOptions) => {
|
||||
name.style.display = 'none';
|
||||
value.style.display = 'none';
|
||||
inputBoxContainer.style.display = 'initial';
|
||||
|
||||
const inputBox = new InputBox(inputBoxContainer, this.contextViewService, {
|
||||
placeholder: options.placeholder,
|
||||
ariaLabel: options.ariaLabel
|
||||
});
|
||||
const styler = attachInputBoxStyler(inputBox, this.themeService);
|
||||
|
||||
inputBox.value = options.initialValue;
|
||||
inputBox.focus();
|
||||
inputBox.select();
|
||||
|
||||
let disposed = false;
|
||||
toDispose.push(inputBox);
|
||||
toDispose.push(styler);
|
||||
|
||||
const wrapUp = (renamed: boolean) => {
|
||||
if (!disposed) {
|
||||
disposed = true;
|
||||
this.debugService.getViewModel().setSelectedExpression(undefined);
|
||||
options.onFinish(inputBox.value, renamed);
|
||||
|
||||
// need to remove the input box since this template will be reused.
|
||||
inputBoxContainer.removeChild(inputBox.element);
|
||||
name.style.display = 'initial';
|
||||
value.style.display = 'initial';
|
||||
inputBoxContainer.style.display = 'none';
|
||||
dispose(toDispose);
|
||||
}
|
||||
};
|
||||
|
||||
toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => {
|
||||
const isEscape = e.equals(KeyCode.Escape);
|
||||
const isEnter = e.equals(KeyCode.Enter);
|
||||
if (isEscape || isEnter) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
wrapUp(isEnter);
|
||||
}
|
||||
}));
|
||||
toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => {
|
||||
wrapUp(true);
|
||||
}));
|
||||
toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'click', e => {
|
||||
// Do not expand / collapse selected elements
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
};
|
||||
|
||||
return { expression, name, value, label, enableInputBox, inputBoxContainer, toDispose };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
|
||||
const { element } = node;
|
||||
if (element === this.debugService.getViewModel().getSelectedExpression()) {
|
||||
data.enableInputBox(element, this.getInputBoxOptions(element));
|
||||
} else {
|
||||
this.renderExpression(element, data, createMatches(node.filterData));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void;
|
||||
protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions;
|
||||
|
||||
disposeTemplate(templateData: IExpressionTemplateData): void {
|
||||
dispose(templateData.toDispose);
|
||||
}
|
||||
}
|
||||
361
src/vs/workbench/contrib/debug/browser/breakpointWidget.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/breakpointWidget';
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox';
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { Position, IPosition } from 'vs/editor/common/core/position';
|
||||
import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IDebugService, IBreakpoint, BreakpointWidgetContext as Context, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DEBUG_SCHEME, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, CONTEXT_IN_BREAKPOINT_WIDGET } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ServicesAccessor, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { URI as uri } from 'vs/base/common/uri';
|
||||
import { CompletionProviderRegistry, CompletionList, CompletionContext, CompletionItemKind } from 'vs/editor/common/modes';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { provideSuggestionItems, CompletionOptions } from 'vs/editor/contrib/suggest/suggest';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { IDecorationOptions } from 'vs/editor/common/editorCommon';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { getSimpleEditorOptions, getSimpleCodeEditorWidgetOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
|
||||
const $ = dom.$;
|
||||
const IPrivateBreakpointWidgetService = createDecorator<IPrivateBreakpointWidgetService>('privateBreakopintWidgetService');
|
||||
export interface IPrivateBreakpointWidgetService {
|
||||
_serviceBrand: any;
|
||||
close(success: boolean): void;
|
||||
}
|
||||
const DECORATION_KEY = 'breakpointwidgetdecoration';
|
||||
|
||||
export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWidgetService {
|
||||
public _serviceBrand: any;
|
||||
|
||||
private selectContainer: HTMLElement;
|
||||
private input: IActiveCodeEditor;
|
||||
private toDispose: lifecycle.IDisposable[];
|
||||
private conditionInput = '';
|
||||
private hitCountInput = '';
|
||||
private logMessageInput = '';
|
||||
private breakpoint: IBreakpoint | undefined;
|
||||
|
||||
constructor(editor: ICodeEditor, private lineNumber: number, private context: Context,
|
||||
@IContextViewService private readonly contextViewService: IContextViewService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
|
||||
) {
|
||||
super(editor, { showFrame: true, showArrow: false, frameWidth: 1 });
|
||||
|
||||
this.toDispose = [];
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
const uri = model.uri;
|
||||
const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber: this.lineNumber, uri });
|
||||
this.breakpoint = breakpoints.length ? breakpoints[0] : undefined;
|
||||
}
|
||||
|
||||
if (this.context === undefined) {
|
||||
if (this.breakpoint && !this.breakpoint.condition && !this.breakpoint.hitCondition && this.breakpoint.logMessage) {
|
||||
this.context = Context.LOG_MESSAGE;
|
||||
} else if (this.breakpoint && !this.breakpoint.condition && this.breakpoint.hitCondition) {
|
||||
this.context = Context.HIT_COUNT;
|
||||
} else {
|
||||
this.context = Context.CONDITION;
|
||||
}
|
||||
}
|
||||
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(e => {
|
||||
if (this.breakpoint && e && e.removed && e.removed.indexOf(this.breakpoint) >= 0) {
|
||||
this.dispose();
|
||||
}
|
||||
}));
|
||||
this.codeEditorService.registerDecorationType(DECORATION_KEY, {});
|
||||
|
||||
this.create();
|
||||
}
|
||||
|
||||
private get placeholder(): string {
|
||||
switch (this.context) {
|
||||
case Context.LOG_MESSAGE:
|
||||
return nls.localize('breakpointWidgetLogMessagePlaceholder', "Message to log when breakpoint is hit. Expressions within {} are interpolated. 'Enter' to accept, 'esc' to cancel.");
|
||||
case Context.HIT_COUNT:
|
||||
return nls.localize('breakpointWidgetHitCountPlaceholder', "Break when hit count condition is met. 'Enter' to accept, 'esc' to cancel.");
|
||||
default:
|
||||
return nls.localize('breakpointWidgetExpressionPlaceholder', "Break when expression evaluates to true. 'Enter' to accept, 'esc' to cancel.");
|
||||
}
|
||||
}
|
||||
|
||||
private getInputValue(breakpoint: IBreakpoint | undefined): string {
|
||||
switch (this.context) {
|
||||
case Context.LOG_MESSAGE:
|
||||
return breakpoint && breakpoint.logMessage ? breakpoint.logMessage : this.logMessageInput;
|
||||
case Context.HIT_COUNT:
|
||||
return breakpoint && breakpoint.hitCondition ? breakpoint.hitCondition : this.hitCountInput;
|
||||
default:
|
||||
return breakpoint && breakpoint.condition ? breakpoint.condition : this.conditionInput;
|
||||
}
|
||||
}
|
||||
|
||||
private rememberInput(): void {
|
||||
const value = this.input.getModel().getValue();
|
||||
switch (this.context) {
|
||||
case Context.LOG_MESSAGE:
|
||||
this.logMessageInput = value;
|
||||
break;
|
||||
case Context.HIT_COUNT:
|
||||
this.hitCountInput = value;
|
||||
break;
|
||||
default:
|
||||
this.conditionInput = value;
|
||||
}
|
||||
}
|
||||
|
||||
public show(rangeOrPos: IRange | IPosition, heightInLines: number) {
|
||||
const lineNum = this.input.getModel().getLineCount();
|
||||
super.show(rangeOrPos, lineNum + 1);
|
||||
}
|
||||
|
||||
public fitHeightToContent() {
|
||||
const lineNum = this.input.getModel().getLineCount();
|
||||
this._relayout(lineNum + 1);
|
||||
}
|
||||
|
||||
protected _fillContainer(container: HTMLElement): void {
|
||||
this.setCssClass('breakpoint-widget');
|
||||
const selectBox = new SelectBox(<ISelectOptionItem[]>[{ text: nls.localize('expression', "Expression") }, { text: nls.localize('hitCount', "Hit Count") }, { text: nls.localize('logMessage', "Log Message") }], this.context, this.contextViewService, undefined, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') });
|
||||
this.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService));
|
||||
this.selectContainer = $('.breakpoint-select-container');
|
||||
selectBox.render(dom.append(container, this.selectContainer));
|
||||
selectBox.onDidSelect(e => {
|
||||
this.rememberInput();
|
||||
this.context = e.index;
|
||||
|
||||
const value = this.getInputValue(this.breakpoint);
|
||||
this.input.getModel().setValue(value);
|
||||
});
|
||||
|
||||
this.createBreakpointInput(dom.append(container, $('.inputContainer')));
|
||||
|
||||
this.input.getModel().setValue(this.getInputValue(this.breakpoint));
|
||||
this.toDispose.push(this.input.getModel().onDidChangeContent(() => {
|
||||
this.fitHeightToContent();
|
||||
}));
|
||||
this.input.setPosition({ lineNumber: 1, column: this.input.getModel().getLineMaxColumn(1) });
|
||||
// Due to an electron bug we have to do the timeout, otherwise we do not get focus
|
||||
setTimeout(() => this.input.focus(), 150);
|
||||
}
|
||||
|
||||
public close(success: boolean): void {
|
||||
if (success) {
|
||||
// if there is already a breakpoint on this location - remove it.
|
||||
|
||||
let condition = this.breakpoint && this.breakpoint.condition;
|
||||
let hitCondition = this.breakpoint && this.breakpoint.hitCondition;
|
||||
let logMessage = this.breakpoint && this.breakpoint.logMessage;
|
||||
this.rememberInput();
|
||||
|
||||
if (this.conditionInput || this.context === Context.CONDITION) {
|
||||
condition = this.conditionInput;
|
||||
}
|
||||
if (this.hitCountInput || this.context === Context.HIT_COUNT) {
|
||||
hitCondition = this.hitCountInput;
|
||||
}
|
||||
if (this.logMessageInput || this.context === Context.LOG_MESSAGE) {
|
||||
logMessage = this.logMessageInput;
|
||||
}
|
||||
|
||||
if (this.breakpoint) {
|
||||
this.debugService.updateBreakpoints(this.breakpoint.uri, {
|
||||
[this.breakpoint.getId()]: {
|
||||
condition,
|
||||
hitCondition,
|
||||
logMessage
|
||||
}
|
||||
}, false);
|
||||
} else {
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
this.debugService.addBreakpoints(model.uri, [{
|
||||
lineNumber: this.lineNumber,
|
||||
enabled: true,
|
||||
condition,
|
||||
hitCondition,
|
||||
logMessage
|
||||
}], `breakpointWidget`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
protected _doLayout(heightInPixel: number, widthInPixel: number): void {
|
||||
this.input.layout({ height: heightInPixel, width: widthInPixel - 113 });
|
||||
}
|
||||
|
||||
private createBreakpointInput(container: HTMLElement): void {
|
||||
const scopedContextKeyService = this.contextKeyService.createScoped(container);
|
||||
this.toDispose.push(scopedContextKeyService);
|
||||
|
||||
const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection(
|
||||
[IContextKeyService, scopedContextKeyService], [IPrivateBreakpointWidgetService, this]));
|
||||
|
||||
const options = getSimpleEditorOptions();
|
||||
const codeEditorWidgetOptions = getSimpleCodeEditorWidgetOptions();
|
||||
this.input = <IActiveCodeEditor>scopedInstatiationService.createInstance(CodeEditorWidget, container, options, codeEditorWidgetOptions);
|
||||
CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true);
|
||||
const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true);
|
||||
this.input.setModel(model);
|
||||
this.toDispose.push(model);
|
||||
const setDecorations = () => {
|
||||
const value = this.input.getModel().getValue();
|
||||
const decorations = !!value ? [] : this.createDecorations();
|
||||
this.input.setDecorations(DECORATION_KEY, decorations);
|
||||
};
|
||||
this.input.getModel().onDidChangeContent(() => setDecorations());
|
||||
this.themeService.onThemeChange(() => setDecorations());
|
||||
|
||||
this.toDispose.push(CompletionProviderRegistry.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, {
|
||||
provideCompletionItems: (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise<CompletionList> => {
|
||||
let suggestionsPromise: Promise<CompletionList>;
|
||||
const underlyingModel = this.editor.getModel();
|
||||
if (underlyingModel && (this.context === Context.CONDITION || this.context === Context.LOG_MESSAGE && this.isCurlyBracketOpen())) {
|
||||
suggestionsPromise = provideSuggestionItems(underlyingModel, new Position(this.lineNumber, 1), new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)), _context, token).then(suggestions => {
|
||||
|
||||
let overwriteBefore = 0;
|
||||
if (this.context === Context.CONDITION) {
|
||||
overwriteBefore = position.column - 1;
|
||||
} else {
|
||||
// Inside the currly brackets, need to count how many useful characters are behind the position so they would all be taken into account
|
||||
const value = this.input.getModel().getValue();
|
||||
while ((position.column - 2 - overwriteBefore >= 0) && value[position.column - 2 - overwriteBefore] !== '{' && value[position.column - 2 - overwriteBefore] !== ' ') {
|
||||
overwriteBefore++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: suggestions.map(s => {
|
||||
s.completion.range = Range.fromPositions(position.delta(0, -overwriteBefore), position);
|
||||
return s.completion;
|
||||
})
|
||||
};
|
||||
});
|
||||
} else {
|
||||
suggestionsPromise = Promise.resolve({ suggestions: [] });
|
||||
}
|
||||
|
||||
return suggestionsPromise;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private createDecorations(): IDecorationOptions[] {
|
||||
const transparentForeground = transparent(editorForeground, 0.4)(this.themeService.getTheme());
|
||||
return [{
|
||||
range: {
|
||||
startLineNumber: 0,
|
||||
endLineNumber: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 1
|
||||
},
|
||||
renderOptions: {
|
||||
after: {
|
||||
contentText: this.placeholder,
|
||||
color: transparentForeground ? transparentForeground.toString() : undefined
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
private isCurlyBracketOpen(): boolean {
|
||||
const value = this.input.getModel().getValue();
|
||||
const position = this.input.getPosition();
|
||||
if (position) {
|
||||
for (let i = position.column - 2; i >= 0; i--) {
|
||||
if (value[i] === '{') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value[i] === '}') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this.input.dispose();
|
||||
lifecycle.dispose(this.toDispose);
|
||||
setTimeout(() => this.editor.focus(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
class AcceptBreakpointWidgetInputAction extends EditorCommand {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'breakpointWidget.action.acceptInput',
|
||||
precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE,
|
||||
kbOpts: {
|
||||
kbExpr: CONTEXT_IN_BREAKPOINT_WIDGET,
|
||||
primary: KeyCode.Enter,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
accessor.get(IPrivateBreakpointWidgetService).close(true);
|
||||
}
|
||||
}
|
||||
|
||||
class CloseBreakpointWidgetCommand extends EditorCommand {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'closeBreakpointWidget',
|
||||
precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape],
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
|
||||
const debugContribution = editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID);
|
||||
if (debugContribution) {
|
||||
// if focus is in outer editor we need to use the debug contribution to close
|
||||
return debugContribution.closeBreakpointWidget();
|
||||
}
|
||||
|
||||
accessor.get(IPrivateBreakpointWidgetService).close(false);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorCommand(new AcceptBreakpointWidgetInputAction());
|
||||
registerEditorCommand(new CloseBreakpointWidgetCommand());
|
||||
645
src/vs/workbench/contrib/debug/browser/breakpointsView.ts
Normal file
@@ -0,0 +1,645 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IAction, Action } from 'vs/base/common/actions';
|
||||
import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, EDITOR_CONTRIBUTION_ID, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, IDebugEditorContribution } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel';
|
||||
import { AddFunctionBreakpointAction, ToggleBreakpointsActivatedAction, RemoveAllBreakpointsAction, RemoveBreakpointAction, EnableAllBreakpointsAction, DisableAllBreakpointsAction, ReapplyBreakpointsAction } from 'vs/workbench/contrib/debug/browser/debugActions';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { Constants } from 'vs/editor/common/core/uint';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IListVirtualDelegate, IListContextMenuEvent, IListRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { IEditor } from 'vs/workbench/common/editor';
|
||||
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { WorkbenchList } from 'vs/platform/list/browser/listService';
|
||||
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
function createCheckbox(): HTMLInputElement {
|
||||
const checkbox = <HTMLInputElement>$('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.tabIndex = -1;
|
||||
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
export class BreakpointsView extends ViewletPanel {
|
||||
|
||||
private static readonly MAX_VISIBLE_FILES = 9;
|
||||
private list: WorkbenchList<IEnablement>;
|
||||
private needsRefresh: boolean;
|
||||
|
||||
constructor(
|
||||
options: IViewletViewOptions,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IContextViewService private readonly contextViewService: IContextViewService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService);
|
||||
|
||||
this.minimumBodySize = this.maximumBodySize = this.getExpandedBodySize();
|
||||
this.disposables.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange()));
|
||||
}
|
||||
|
||||
public renderBody(container: HTMLElement): void {
|
||||
dom.addClass(container, 'debug-breakpoints');
|
||||
const delegate = new BreakpointsDelegate(this.debugService);
|
||||
|
||||
this.list = this.instantiationService.createInstance(WorkbenchList, container, delegate, [
|
||||
this.instantiationService.createInstance(BreakpointsRenderer),
|
||||
new ExceptionBreakpointsRenderer(this.debugService),
|
||||
this.instantiationService.createInstance(FunctionBreakpointsRenderer),
|
||||
new FunctionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService)
|
||||
], {
|
||||
identityProvider: { getId: (element: IEnablement) => element.getId() },
|
||||
multipleSelectionSupport: false,
|
||||
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: e => e }
|
||||
}) as WorkbenchList<IEnablement>;
|
||||
|
||||
CONTEXT_BREAKPOINTS_FOCUSED.bindTo(this.list.contextKeyService);
|
||||
|
||||
this.list.onContextMenu(this.onListContextMenu, this, this.disposables);
|
||||
|
||||
this.disposables.push(this.list.onDidOpen(e => {
|
||||
let isSingleClick = false;
|
||||
let isDoubleClick = false;
|
||||
let isMiddleClick = false;
|
||||
let openToSide = false;
|
||||
|
||||
const browserEvent = e.browserEvent;
|
||||
if (browserEvent instanceof MouseEvent) {
|
||||
isSingleClick = browserEvent.detail === 1;
|
||||
isDoubleClick = browserEvent.detail === 2;
|
||||
isMiddleClick = browserEvent.button === 1;
|
||||
openToSide = (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey);
|
||||
}
|
||||
|
||||
const focused = this.list.getFocusedElements();
|
||||
const element = focused.length ? focused[0] : undefined;
|
||||
|
||||
if (isMiddleClick) {
|
||||
if (element instanceof Breakpoint) {
|
||||
this.debugService.removeBreakpoints(element.getId());
|
||||
} else if (element instanceof FunctionBreakpoint) {
|
||||
this.debugService.removeFunctionBreakpoints(element.getId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (element instanceof Breakpoint) {
|
||||
openBreakpointSource(element, openToSide, isSingleClick, this.debugService, this.editorService);
|
||||
}
|
||||
if (isDoubleClick && element instanceof FunctionBreakpoint && element !== this.debugService.getViewModel().getSelectedFunctionBreakpoint()) {
|
||||
this.debugService.getViewModel().setSelectedFunctionBreakpoint(element);
|
||||
this.onBreakpointsChange();
|
||||
}
|
||||
}));
|
||||
|
||||
this.list.splice(0, this.list.length, this.elements);
|
||||
|
||||
this.disposables.push(this.onDidChangeBodyVisibility(visible => {
|
||||
if (visible && this.needsRefresh) {
|
||||
this.onBreakpointsChange();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
super.focus();
|
||||
if (this.list) {
|
||||
this.list.domFocus();
|
||||
}
|
||||
}
|
||||
|
||||
protected layoutBody(height: number, width: number): void {
|
||||
if (this.list) {
|
||||
this.list.layout(height, width);
|
||||
}
|
||||
}
|
||||
|
||||
private onListContextMenu(e: IListContextMenuEvent<IEnablement>): void {
|
||||
if (!e.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions: IAction[] = [];
|
||||
const element = e.element;
|
||||
|
||||
const breakpointType = element instanceof Breakpoint && element.logMessage ? nls.localize('Logpoint', "Logpoint") : nls.localize('Breakpoint', "Breakpoint");
|
||||
if (element instanceof Breakpoint || element instanceof FunctionBreakpoint) {
|
||||
actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), '', true, () => {
|
||||
if (element instanceof Breakpoint) {
|
||||
return openBreakpointSource(element, false, false, this.debugService, this.editorService).then(editor => {
|
||||
if (editor) {
|
||||
const codeEditor = editor.getControl();
|
||||
if (isCodeEditor(codeEditor)) {
|
||||
codeEditor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(element.lineNumber, element.column);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.debugService.getViewModel().setSelectedFunctionBreakpoint(element);
|
||||
this.onBreakpointsChange();
|
||||
return Promise.resolve(undefined);
|
||||
}));
|
||||
actions.push(new Separator());
|
||||
}
|
||||
|
||||
actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService, this.keybindingService));
|
||||
|
||||
if (this.debugService.getModel().getBreakpoints().length + this.debugService.getModel().getFunctionBreakpoints().length > 1) {
|
||||
actions.push(new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService));
|
||||
actions.push(new Separator());
|
||||
|
||||
actions.push(new EnableAllBreakpointsAction(EnableAllBreakpointsAction.ID, EnableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService));
|
||||
actions.push(new DisableAllBreakpointsAction(DisableAllBreakpointsAction.ID, DisableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService));
|
||||
}
|
||||
|
||||
actions.push(new Separator());
|
||||
actions.push(new ReapplyBreakpointsAction(ReapplyBreakpointsAction.ID, ReapplyBreakpointsAction.LABEL, this.debugService, this.keybindingService));
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => actions,
|
||||
getActionsContext: () => element
|
||||
});
|
||||
}
|
||||
|
||||
public getActions(): IAction[] {
|
||||
return [
|
||||
new AddFunctionBreakpointAction(AddFunctionBreakpointAction.ID, AddFunctionBreakpointAction.LABEL, this.debugService, this.keybindingService),
|
||||
new ToggleBreakpointsActivatedAction(ToggleBreakpointsActivatedAction.ID, ToggleBreakpointsActivatedAction.ACTIVATE_LABEL, this.debugService, this.keybindingService),
|
||||
new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)
|
||||
];
|
||||
}
|
||||
|
||||
private onBreakpointsChange(): void {
|
||||
if (this.isBodyVisible()) {
|
||||
this.minimumBodySize = this.getExpandedBodySize();
|
||||
if (this.maximumBodySize < Number.POSITIVE_INFINITY) {
|
||||
this.maximumBodySize = this.minimumBodySize;
|
||||
}
|
||||
if (this.list) {
|
||||
this.list.splice(0, this.list.length, this.elements);
|
||||
this.needsRefresh = false;
|
||||
}
|
||||
} else {
|
||||
this.needsRefresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
private get elements(): IEnablement[] {
|
||||
const model = this.debugService.getModel();
|
||||
const elements = (<ReadonlyArray<IEnablement>>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getBreakpoints());
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
private getExpandedBodySize(): number {
|
||||
const model = this.debugService.getModel();
|
||||
const length = model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length;
|
||||
return Math.min(BreakpointsView.MAX_VISIBLE_FILES, length) * 22;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakpointsDelegate implements IListVirtualDelegate<IEnablement> {
|
||||
|
||||
constructor(private debugService: IDebugService) {
|
||||
// noop
|
||||
}
|
||||
|
||||
getHeight(element: IEnablement): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: IEnablement): string {
|
||||
if (element instanceof Breakpoint) {
|
||||
return BreakpointsRenderer.ID;
|
||||
}
|
||||
if (element instanceof FunctionBreakpoint) {
|
||||
const selected = this.debugService.getViewModel().getSelectedFunctionBreakpoint();
|
||||
if (!element.name || (selected && selected.getId() === element.getId())) {
|
||||
return FunctionBreakpointInputRenderer.ID;
|
||||
}
|
||||
|
||||
return FunctionBreakpointsRenderer.ID;
|
||||
}
|
||||
if (element instanceof ExceptionBreakpoint) {
|
||||
return ExceptionBreakpointsRenderer.ID;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
interface IBaseBreakpointTemplateData {
|
||||
breakpoint: HTMLElement;
|
||||
name: HTMLElement;
|
||||
checkbox: HTMLInputElement;
|
||||
context: IEnablement;
|
||||
toDispose: IDisposable[];
|
||||
}
|
||||
|
||||
interface IBaseBreakpointWithIconTemplateData extends IBaseBreakpointTemplateData {
|
||||
icon: HTMLElement;
|
||||
}
|
||||
|
||||
interface IBreakpointTemplateData extends IBaseBreakpointWithIconTemplateData {
|
||||
lineNumber: HTMLElement;
|
||||
filePath: HTMLElement;
|
||||
}
|
||||
|
||||
interface IInputTemplateData {
|
||||
inputBox: InputBox;
|
||||
checkbox: HTMLInputElement;
|
||||
icon: HTMLElement;
|
||||
breakpoint: IFunctionBreakpoint;
|
||||
reactedOnEvent: boolean;
|
||||
toDispose: IDisposable[];
|
||||
}
|
||||
|
||||
class BreakpointsRenderer implements IListRenderer<IBreakpoint, IBreakpointTemplateData> {
|
||||
|
||||
constructor(
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@ILabelService private readonly labelService: ILabelService
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
static readonly ID = 'breakpoints';
|
||||
|
||||
get templateId() {
|
||||
return BreakpointsRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IBreakpointTemplateData {
|
||||
const data: IBreakpointTemplateData = Object.create(null);
|
||||
data.breakpoint = dom.append(container, $('.breakpoint'));
|
||||
|
||||
data.icon = $('.icon');
|
||||
data.checkbox = createCheckbox();
|
||||
data.toDispose = [];
|
||||
data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => {
|
||||
this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context);
|
||||
}));
|
||||
|
||||
dom.append(data.breakpoint, data.icon);
|
||||
dom.append(data.breakpoint, data.checkbox);
|
||||
|
||||
data.name = dom.append(data.breakpoint, $('span.name'));
|
||||
|
||||
data.filePath = dom.append(data.breakpoint, $('span.file-path'));
|
||||
const lineNumberContainer = dom.append(data.breakpoint, $('.line-number-container'));
|
||||
data.lineNumber = dom.append(lineNumberContainer, $('span.line-number'));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
renderElement(breakpoint: IBreakpoint, index: number, data: IBreakpointTemplateData): void {
|
||||
data.context = breakpoint;
|
||||
dom.toggleClass(data.breakpoint, 'disabled', !this.debugService.getModel().areBreakpointsActivated());
|
||||
|
||||
data.name.textContent = resources.basenameOrAuthority(breakpoint.uri);
|
||||
data.lineNumber.textContent = breakpoint.lineNumber.toString();
|
||||
if (breakpoint.column) {
|
||||
data.lineNumber.textContent += `:${breakpoint.column}`;
|
||||
}
|
||||
data.filePath.textContent = this.labelService.getUriLabel(resources.dirname(breakpoint.uri), { relative: true });
|
||||
data.checkbox.checked = breakpoint.enabled;
|
||||
|
||||
const { message, className } = getBreakpointMessageAndClassName(this.debugService, breakpoint);
|
||||
data.icon.className = className + ' icon';
|
||||
data.breakpoint.title = breakpoint.message || message || '';
|
||||
|
||||
const debugActive = this.debugService.state === State.Running || this.debugService.state === State.Stopped;
|
||||
if (debugActive && !breakpoint.verified) {
|
||||
dom.addClass(data.breakpoint, 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IBreakpointTemplateData): void {
|
||||
dispose(templateData.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
class ExceptionBreakpointsRenderer implements IListRenderer<IExceptionBreakpoint, IBaseBreakpointTemplateData> {
|
||||
|
||||
constructor(
|
||||
private debugService: IDebugService
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
static readonly ID = 'exceptionbreakpoints';
|
||||
|
||||
get templateId() {
|
||||
return ExceptionBreakpointsRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IBaseBreakpointTemplateData {
|
||||
const data: IBreakpointTemplateData = Object.create(null);
|
||||
data.breakpoint = dom.append(container, $('.breakpoint'));
|
||||
|
||||
data.checkbox = createCheckbox();
|
||||
data.toDispose = [];
|
||||
data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => {
|
||||
this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context);
|
||||
}));
|
||||
|
||||
dom.append(data.breakpoint, data.checkbox);
|
||||
|
||||
data.name = dom.append(data.breakpoint, $('span.name'));
|
||||
dom.addClass(data.breakpoint, 'exception');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
renderElement(exceptionBreakpoint: IExceptionBreakpoint, index: number, data: IBaseBreakpointTemplateData): void {
|
||||
data.context = exceptionBreakpoint;
|
||||
data.name.textContent = exceptionBreakpoint.label || `${exceptionBreakpoint.filter} exceptions`;
|
||||
data.breakpoint.title = data.name.textContent;
|
||||
data.checkbox.checked = exceptionBreakpoint.enabled;
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IBaseBreakpointTemplateData): void {
|
||||
dispose(templateData.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionBreakpointsRenderer implements IListRenderer<FunctionBreakpoint, IBaseBreakpointWithIconTemplateData> {
|
||||
|
||||
constructor(
|
||||
@IDebugService private readonly debugService: IDebugService
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
static readonly ID = 'functionbreakpoints';
|
||||
|
||||
get templateId() {
|
||||
return FunctionBreakpointsRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IBaseBreakpointWithIconTemplateData {
|
||||
const data: IBreakpointTemplateData = Object.create(null);
|
||||
data.breakpoint = dom.append(container, $('.breakpoint'));
|
||||
|
||||
data.icon = $('.icon');
|
||||
data.checkbox = createCheckbox();
|
||||
data.toDispose = [];
|
||||
data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => {
|
||||
this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context);
|
||||
}));
|
||||
|
||||
dom.append(data.breakpoint, data.icon);
|
||||
dom.append(data.breakpoint, data.checkbox);
|
||||
|
||||
data.name = dom.append(data.breakpoint, $('span.name'));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
renderElement(functionBreakpoint: FunctionBreakpoint, index: number, data: IBaseBreakpointWithIconTemplateData): void {
|
||||
data.context = functionBreakpoint;
|
||||
data.name.textContent = functionBreakpoint.name;
|
||||
const { className, message } = getBreakpointMessageAndClassName(this.debugService, functionBreakpoint);
|
||||
data.icon.className = className + ' icon';
|
||||
data.icon.title = message ? message : '';
|
||||
data.checkbox.checked = functionBreakpoint.enabled;
|
||||
data.breakpoint.title = functionBreakpoint.name;
|
||||
|
||||
// Mark function breakpoints as disabled if deactivated or if debug type does not support them #9099
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
dom.toggleClass(data.breakpoint, 'disabled', (session && !session.capabilities.supportsFunctionBreakpoints) || !this.debugService.getModel().areBreakpointsActivated());
|
||||
if (session && !session.capabilities.supportsFunctionBreakpoints) {
|
||||
data.breakpoint.title = nls.localize('functionBreakpointsNotSupported', "Function breakpoints are not supported by this debug type");
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void {
|
||||
dispose(templateData.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoint, IInputTemplateData> {
|
||||
|
||||
constructor(
|
||||
private debugService: IDebugService,
|
||||
private contextViewService: IContextViewService,
|
||||
private themeService: IThemeService
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
static readonly ID = 'functionbreakpointinput';
|
||||
|
||||
get templateId() {
|
||||
return FunctionBreakpointInputRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IInputTemplateData {
|
||||
const template: IInputTemplateData = Object.create(null);
|
||||
|
||||
const breakpoint = dom.append(container, $('.breakpoint'));
|
||||
template.icon = $('.icon');
|
||||
template.checkbox = createCheckbox();
|
||||
|
||||
dom.append(breakpoint, template.icon);
|
||||
dom.append(breakpoint, template.checkbox);
|
||||
const inputBoxContainer = dom.append(breakpoint, $('.inputBoxContainer'));
|
||||
const inputBox = new InputBox(inputBoxContainer, this.contextViewService, {
|
||||
placeholder: nls.localize('functionBreakpointPlaceholder', "Function to break on"),
|
||||
ariaLabel: nls.localize('functionBreakPointInputAriaLabel', "Type function breakpoint")
|
||||
});
|
||||
const styler = attachInputBoxStyler(inputBox, this.themeService);
|
||||
const toDispose: IDisposable[] = [inputBox, styler];
|
||||
|
||||
const wrapUp = (renamed: boolean) => {
|
||||
if (!template.reactedOnEvent) {
|
||||
template.reactedOnEvent = true;
|
||||
this.debugService.getViewModel().setSelectedFunctionBreakpoint(undefined);
|
||||
if (inputBox.value && (renamed || template.breakpoint.name)) {
|
||||
this.debugService.renameFunctionBreakpoint(template.breakpoint.getId(), renamed ? inputBox.value : template.breakpoint.name);
|
||||
} else {
|
||||
this.debugService.removeFunctionBreakpoints(template.breakpoint.getId());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => {
|
||||
const isEscape = e.equals(KeyCode.Escape);
|
||||
const isEnter = e.equals(KeyCode.Enter);
|
||||
if (isEscape || isEnter) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
wrapUp(isEnter);
|
||||
}
|
||||
}));
|
||||
toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => {
|
||||
// Need to react with a timeout on the blur event due to possible concurent splices #56443
|
||||
setTimeout(() => {
|
||||
if (!template.breakpoint.name) {
|
||||
wrapUp(true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
template.inputBox = inputBox;
|
||||
template.toDispose = toDispose;
|
||||
return template;
|
||||
}
|
||||
|
||||
renderElement(functionBreakpoint: FunctionBreakpoint, index: number, data: IInputTemplateData): void {
|
||||
data.breakpoint = functionBreakpoint;
|
||||
data.reactedOnEvent = false;
|
||||
const { className, message } = getBreakpointMessageAndClassName(this.debugService, functionBreakpoint);
|
||||
|
||||
data.icon.className = className + ' icon';
|
||||
data.icon.title = message ? message : '';
|
||||
data.checkbox.checked = functionBreakpoint.enabled;
|
||||
data.checkbox.disabled = true;
|
||||
data.inputBox.value = functionBreakpoint.name || '';
|
||||
setTimeout(() => {
|
||||
data.inputBox.focus();
|
||||
data.inputBox.select();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IInputTemplateData): void {
|
||||
dispose(templateData.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolean, preserveFocus: boolean, debugService: IDebugService, editorService: IEditorService): Promise<IEditor | null> {
|
||||
if (breakpoint.uri.scheme === DEBUG_SCHEME && debugService.state === State.Inactive) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const selection = breakpoint.endLineNumber ? {
|
||||
startLineNumber: breakpoint.lineNumber,
|
||||
endLineNumber: breakpoint.endLineNumber,
|
||||
startColumn: breakpoint.column || 1,
|
||||
endColumn: breakpoint.endColumn || Constants.MAX_SAFE_SMALL_INTEGER
|
||||
} : {
|
||||
startLineNumber: breakpoint.lineNumber,
|
||||
startColumn: breakpoint.column || 1,
|
||||
endLineNumber: breakpoint.lineNumber,
|
||||
endColumn: breakpoint.column || Constants.MAX_SAFE_SMALL_INTEGER
|
||||
};
|
||||
|
||||
return editorService.openEditor({
|
||||
resource: breakpoint.uri,
|
||||
options: {
|
||||
preserveFocus,
|
||||
selection,
|
||||
revealIfVisible: true,
|
||||
revealInCenterIfOutsideViewport: true,
|
||||
pinned: !preserveFocus
|
||||
}
|
||||
}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
|
||||
}
|
||||
|
||||
export function getBreakpointMessageAndClassName(debugService: IDebugService, breakpoint: IBreakpoint | FunctionBreakpoint): { message?: string, className: string } {
|
||||
const state = debugService.state;
|
||||
const debugActive = state === State.Running || state === State.Stopped;
|
||||
|
||||
if (!breakpoint.enabled || !debugService.getModel().areBreakpointsActivated()) {
|
||||
return {
|
||||
className: breakpoint instanceof FunctionBreakpoint ? 'debug-function-breakpoint-disabled' : breakpoint.logMessage ? 'debug-breakpoint-log-disabled' : 'debug-breakpoint-disabled',
|
||||
message: breakpoint.logMessage ? nls.localize('disabledLogpoint', "Disabled logpoint") : nls.localize('disabledBreakpoint', "Disabled breakpoint"),
|
||||
};
|
||||
}
|
||||
|
||||
const appendMessage = (text: string): string => {
|
||||
return !(breakpoint instanceof FunctionBreakpoint) && breakpoint.message ? text.concat(', ' + breakpoint.message) : text;
|
||||
};
|
||||
if (debugActive && !breakpoint.verified) {
|
||||
return {
|
||||
className: breakpoint instanceof FunctionBreakpoint ? 'debug-function-breakpoint-unverified' : breakpoint.logMessage ? 'debug-breakpoint-log-unverified' : 'debug-breakpoint-unverified',
|
||||
message: breakpoint.logMessage ? nls.localize('unverifiedLogpoint', "Unverified logpoint") : nls.localize('unverifiedBreakopint', "Unverified breakpoint"),
|
||||
};
|
||||
}
|
||||
|
||||
const session = debugService.getViewModel().focusedSession;
|
||||
if (breakpoint instanceof FunctionBreakpoint) {
|
||||
if (session && !session.capabilities.supportsFunctionBreakpoints) {
|
||||
return {
|
||||
className: 'debug-function-breakpoint-unverified',
|
||||
message: nls.localize('functionBreakpointUnsupported', "Function breakpoints not supported by this debug type"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
className: 'debug-function-breakpoint',
|
||||
};
|
||||
}
|
||||
|
||||
if (breakpoint.logMessage || breakpoint.condition || breakpoint.hitCondition) {
|
||||
const messages: string[] = [];
|
||||
if (breakpoint.logMessage) {
|
||||
if (session && !session.capabilities.supportsLogPoints) {
|
||||
return {
|
||||
className: 'debug-breakpoint-unsupported',
|
||||
message: nls.localize('logBreakpointUnsupported', "Logpoints not supported by this debug type"),
|
||||
};
|
||||
}
|
||||
|
||||
messages.push(nls.localize('logMessage', "Log Message: {0}", breakpoint.logMessage));
|
||||
}
|
||||
|
||||
if (session && breakpoint.condition && !session.capabilities.supportsConditionalBreakpoints) {
|
||||
return {
|
||||
className: 'debug-breakpoint-unsupported',
|
||||
message: nls.localize('conditionalBreakpointUnsupported', "Conditional breakpoints not supported by this debug type"),
|
||||
};
|
||||
}
|
||||
if (session && breakpoint.hitCondition && !session.capabilities.supportsHitConditionalBreakpoints) {
|
||||
return {
|
||||
className: 'debug-breakpoint-unsupported',
|
||||
message: nls.localize('hitBreakpointUnsupported', "Hit conditional breakpoints not supported by this debug type"),
|
||||
};
|
||||
}
|
||||
|
||||
if (breakpoint.condition) {
|
||||
messages.push(nls.localize('expression', "Expression: {0}", breakpoint.condition));
|
||||
}
|
||||
if (breakpoint.hitCondition) {
|
||||
messages.push(nls.localize('hitCount', "Hit Count: {0}", breakpoint.hitCondition));
|
||||
}
|
||||
|
||||
return {
|
||||
className: breakpoint.logMessage ? 'debug-breakpoint-log' : 'debug-breakpoint-conditional',
|
||||
message: appendMessage(messages.join('\n'))
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
className: 'debug-breakpoint',
|
||||
message: breakpoint.message
|
||||
};
|
||||
}
|
||||
686
src/vs/workbench/contrib/debug/browser/callStackView.ts
Normal file
@@ -0,0 +1,686 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { RunOnceScheduler, ignoreErrors } from 'vs/base/common/async';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { MenuId, IMenu, IMenuService } from 'vs/platform/actions/common/actions';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { RestartAction, StopAction, ContinueAction, StepOverAction, StepIntoAction, StepOutAction, PauseAction, RestartFrameAction, TerminateThreadAction, CopyStackTraceAction } from 'vs/workbench/contrib/debug/browser/debugActions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { ITreeRenderer, ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
|
||||
import { TreeResourceNavigator2, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import { createMatches, FuzzyScore } from 'vs/base/common/filters';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
type CallStackItem = IStackFrame | IThread | IDebugSession | string | ThreadAndSessionIds | IStackFrame[];
|
||||
|
||||
export class CallStackView extends ViewletPanel {
|
||||
|
||||
private pauseMessage: HTMLSpanElement;
|
||||
private pauseMessageLabel: HTMLSpanElement;
|
||||
private onCallStackChangeScheduler: RunOnceScheduler;
|
||||
private needsRefresh: boolean;
|
||||
private ignoreSelectionChangedEvent: boolean;
|
||||
private ignoreFocusStackFrameEvent: boolean;
|
||||
private callStackItemType: IContextKey<string>;
|
||||
private dataSource: CallStackDataSource;
|
||||
private tree: WorkbenchAsyncDataTree<CallStackItem | IDebugModel, CallStackItem, FuzzyScore>;
|
||||
private contributedContextMenu: IMenu;
|
||||
|
||||
constructor(
|
||||
private options: IViewletViewOptions,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IContextKeyService readonly contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService);
|
||||
this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService);
|
||||
|
||||
this.contributedContextMenu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService);
|
||||
this.disposables.push(this.contributedContextMenu);
|
||||
|
||||
// Create scheduler to prevent unnecessary flashing of tree when reacting to changes
|
||||
this.onCallStackChangeScheduler = new RunOnceScheduler(() => {
|
||||
// Only show the global pause message if we do not display threads.
|
||||
// Otherwise there will be a pause message per thread and there is no need for a global one.
|
||||
const sessions = this.debugService.getModel().getSessions();
|
||||
const thread = sessions.length === 1 && sessions[0].getAllThreads().length === 1 ? sessions[0].getAllThreads()[0] : undefined;
|
||||
if (thread && thread.stoppedDetails) {
|
||||
this.pauseMessageLabel.textContent = thread.stoppedDetails.description || nls.localize('debugStopped', "Paused on {0}", thread.stoppedDetails.reason || '');
|
||||
this.pauseMessageLabel.title = thread.stoppedDetails.text || '';
|
||||
dom.toggleClass(this.pauseMessageLabel, 'exception', thread.stoppedDetails.reason === 'exception');
|
||||
this.pauseMessage.hidden = false;
|
||||
} else {
|
||||
this.pauseMessage.hidden = true;
|
||||
}
|
||||
|
||||
this.needsRefresh = false;
|
||||
this.dataSource.deemphasizedStackFramesToShow = [];
|
||||
this.tree.updateChildren().then(() => this.updateTreeSelection());
|
||||
}, 50);
|
||||
}
|
||||
|
||||
protected renderHeaderTitle(container: HTMLElement): void {
|
||||
const titleContainer = dom.append(container, $('.debug-call-stack-title'));
|
||||
super.renderHeaderTitle(titleContainer, this.options.title);
|
||||
|
||||
this.pauseMessage = dom.append(titleContainer, $('span.pause-message'));
|
||||
this.pauseMessage.hidden = true;
|
||||
this.pauseMessageLabel = dom.append(this.pauseMessage, $('span.label'));
|
||||
}
|
||||
|
||||
renderBody(container: HTMLElement): void {
|
||||
dom.addClass(container, 'debug-call-stack');
|
||||
const treeContainer = renderViewTree(container);
|
||||
|
||||
this.dataSource = new CallStackDataSource();
|
||||
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, treeContainer, new CallStackDelegate(), [
|
||||
new SessionsRenderer(),
|
||||
new ThreadsRenderer(),
|
||||
this.instantiationService.createInstance(StackFramesRenderer),
|
||||
new ErrorsRenderer(),
|
||||
new LoadMoreRenderer(),
|
||||
new ShowMoreRenderer()
|
||||
], this.dataSource, {
|
||||
accessibilityProvider: new CallStackAccessibilityProvider(),
|
||||
ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"),
|
||||
identityProvider: {
|
||||
getId: element => {
|
||||
if (typeof element === 'string') {
|
||||
return element;
|
||||
}
|
||||
if (element instanceof Array) {
|
||||
return `showMore ${element[0].getId()}`;
|
||||
}
|
||||
|
||||
return (<IStackFrame | IThread | IDebugSession | ThreadAndSessionIds>element).getId();
|
||||
}
|
||||
},
|
||||
keyboardNavigationLabelProvider: {
|
||||
getKeyboardNavigationLabel: e => {
|
||||
if (isDebugSession(e)) {
|
||||
return e.getLabel();
|
||||
}
|
||||
if (e instanceof Thread) {
|
||||
return `${e.name} ${e.stateLabel}`;
|
||||
}
|
||||
if (e instanceof StackFrame || typeof e === 'string') {
|
||||
return e;
|
||||
}
|
||||
if (e instanceof ThreadAndSessionIds) {
|
||||
return LoadMoreRenderer.LABEL;
|
||||
}
|
||||
|
||||
return nls.localize('showMoreStackFrames2', "Show More Stack Frames");
|
||||
}
|
||||
}
|
||||
}) as WorkbenchAsyncDataTree<CallStackItem | IDebugModel, CallStackItem, FuzzyScore>;
|
||||
|
||||
this.tree.setInput(this.debugService.getModel()).then(undefined, onUnexpectedError);
|
||||
|
||||
const callstackNavigator = new TreeResourceNavigator2(this.tree);
|
||||
this.disposables.push(callstackNavigator);
|
||||
this.disposables.push(callstackNavigator.onDidOpenResource(e => {
|
||||
if (this.ignoreSelectionChangedEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession) => {
|
||||
this.ignoreFocusStackFrameEvent = true;
|
||||
try {
|
||||
this.debugService.focusStackFrame(stackFrame, thread, session, true);
|
||||
} finally {
|
||||
this.ignoreFocusStackFrameEvent = false;
|
||||
}
|
||||
};
|
||||
|
||||
const element = e.element;
|
||||
if (element instanceof StackFrame) {
|
||||
focusStackFrame(element, element.thread, element.thread.session);
|
||||
element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned);
|
||||
}
|
||||
if (element instanceof Thread) {
|
||||
focusStackFrame(undefined, element, element.session);
|
||||
}
|
||||
if (isDebugSession(element)) {
|
||||
focusStackFrame(undefined, undefined, element);
|
||||
}
|
||||
if (element instanceof ThreadAndSessionIds) {
|
||||
const session = this.debugService.getModel().getSessions().filter(p => p.getId() === element.sessionId).pop();
|
||||
const thread = session && session.getThread(element.threadId);
|
||||
if (thread) {
|
||||
(<Thread>thread).fetchCallStack()
|
||||
.then(() => this.tree.updateChildren());
|
||||
}
|
||||
}
|
||||
if (element instanceof Array) {
|
||||
this.dataSource.deemphasizedStackFramesToShow.push(...element);
|
||||
this.tree.updateChildren();
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(this.debugService.getModel().onDidChangeCallStack(() => {
|
||||
if (!this.isBodyVisible()) {
|
||||
this.needsRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.onCallStackChangeScheduler.isScheduled()) {
|
||||
this.onCallStackChangeScheduler.schedule();
|
||||
}
|
||||
}));
|
||||
this.disposables.push(this.debugService.getViewModel().onDidFocusStackFrame(() => {
|
||||
if (this.ignoreFocusStackFrameEvent) {
|
||||
return;
|
||||
}
|
||||
if (!this.isBodyVisible()) {
|
||||
this.needsRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateTreeSelection();
|
||||
}));
|
||||
this.disposables.push(this.tree.onContextMenu(e => this.onContextMenu(e)));
|
||||
|
||||
// Schedule the update of the call stack tree if the viewlet is opened after a session started #14684
|
||||
if (this.debugService.state === State.Stopped) {
|
||||
this.onCallStackChangeScheduler.schedule(0);
|
||||
}
|
||||
|
||||
this.disposables.push(this.onDidChangeBodyVisibility(visible => {
|
||||
if (visible && this.needsRefresh) {
|
||||
this.onCallStackChangeScheduler.schedule();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
layoutBody(height: number, width: number): void {
|
||||
this.tree.layout(height, width);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.tree.domFocus();
|
||||
}
|
||||
|
||||
private updateTreeSelection(): void {
|
||||
if (!this.tree || !this.tree.getInput()) {
|
||||
// Tree not initialized yet
|
||||
return;
|
||||
}
|
||||
|
||||
const updateSelectionAndReveal = (element: IStackFrame | IDebugSession) => {
|
||||
this.ignoreSelectionChangedEvent = true;
|
||||
try {
|
||||
this.tree.setSelection([element]);
|
||||
this.tree.reveal(element);
|
||||
} catch (e) { }
|
||||
finally {
|
||||
this.ignoreSelectionChangedEvent = false;
|
||||
}
|
||||
};
|
||||
|
||||
const thread = this.debugService.getViewModel().focusedThread;
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
|
||||
if (!thread) {
|
||||
if (!session) {
|
||||
this.tree.setSelection([]);
|
||||
} else {
|
||||
updateSelectionAndReveal(session);
|
||||
}
|
||||
} else {
|
||||
const expansionsPromise = ignoreErrors(this.tree.expand(thread.session))
|
||||
.then(() => ignoreErrors(this.tree.expand(thread)));
|
||||
if (stackFrame) {
|
||||
expansionsPromise.then(() => updateSelectionAndReveal(stackFrame));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onContextMenu(e: ITreeContextMenuEvent<CallStackItem>): void {
|
||||
const actions: IAction[] = [];
|
||||
const element = e.element;
|
||||
if (isDebugSession(element)) {
|
||||
this.callStackItemType.set('session');
|
||||
actions.push(this.instantiationService.createInstance(RestartAction, RestartAction.ID, RestartAction.LABEL));
|
||||
actions.push(new StopAction(StopAction.ID, StopAction.LABEL, this.debugService, this.keybindingService));
|
||||
} else if (element instanceof Thread) {
|
||||
this.callStackItemType.set('thread');
|
||||
const thread = <Thread>element;
|
||||
if (thread.stopped) {
|
||||
actions.push(new ContinueAction(ContinueAction.ID, ContinueAction.LABEL, this.debugService, this.keybindingService));
|
||||
actions.push(new StepOverAction(StepOverAction.ID, StepOverAction.LABEL, this.debugService, this.keybindingService));
|
||||
actions.push(new StepIntoAction(StepIntoAction.ID, StepIntoAction.LABEL, this.debugService, this.keybindingService));
|
||||
actions.push(new StepOutAction(StepOutAction.ID, StepOutAction.LABEL, this.debugService, this.keybindingService));
|
||||
} else {
|
||||
actions.push(new PauseAction(PauseAction.ID, PauseAction.LABEL, this.debugService, this.keybindingService));
|
||||
}
|
||||
|
||||
actions.push(new Separator());
|
||||
actions.push(new TerminateThreadAction(TerminateThreadAction.ID, TerminateThreadAction.LABEL, this.debugService, this.keybindingService));
|
||||
} else if (element instanceof StackFrame) {
|
||||
this.callStackItemType.set('stackFrame');
|
||||
if (element.thread.session.capabilities.supportsRestartFrame) {
|
||||
actions.push(new RestartFrameAction(RestartFrameAction.ID, RestartFrameAction.LABEL, this.debugService, this.keybindingService));
|
||||
}
|
||||
actions.push(this.instantiationService.createInstance(CopyStackTraceAction, CopyStackTraceAction.ID, CopyStackTraceAction.LABEL));
|
||||
} else {
|
||||
this.callStackItemType.reset();
|
||||
}
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => {
|
||||
fillInContextMenuActions(this.contributedContextMenu, { arg: this.getContextForContributedActions(element) }, actions, this.contextMenuService);
|
||||
return actions;
|
||||
},
|
||||
getActionsContext: () => element
|
||||
});
|
||||
}
|
||||
|
||||
private getContextForContributedActions(element: CallStackItem | null): string | number | undefined {
|
||||
if (element instanceof StackFrame) {
|
||||
if (element.source.inMemory) {
|
||||
return element.source.raw.path || element.source.reference;
|
||||
}
|
||||
|
||||
return element.source.uri.toString();
|
||||
}
|
||||
if (element instanceof Thread) {
|
||||
return element.threadId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface IThreadTemplateData {
|
||||
thread: HTMLElement;
|
||||
name: HTMLElement;
|
||||
state: HTMLElement;
|
||||
stateLabel: HTMLSpanElement;
|
||||
label: HighlightedLabel;
|
||||
}
|
||||
|
||||
interface ISessionTemplateData {
|
||||
session: HTMLElement;
|
||||
name: HTMLElement;
|
||||
state: HTMLElement;
|
||||
stateLabel: HTMLSpanElement;
|
||||
label: HighlightedLabel;
|
||||
}
|
||||
|
||||
interface IErrorTemplateData {
|
||||
label: HTMLElement;
|
||||
}
|
||||
|
||||
interface ILabelTemplateData {
|
||||
label: HTMLElement;
|
||||
}
|
||||
|
||||
interface IStackFrameTemplateData {
|
||||
stackFrame: HTMLElement;
|
||||
file: HTMLElement;
|
||||
fileName: HTMLElement;
|
||||
lineNumber: HTMLElement;
|
||||
label: HighlightedLabel;
|
||||
}
|
||||
|
||||
class SessionsRenderer implements ITreeRenderer<IDebugSession, FuzzyScore, ISessionTemplateData> {
|
||||
static readonly ID = 'session';
|
||||
|
||||
get templateId(): string {
|
||||
return SessionsRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): ISessionTemplateData {
|
||||
const session = dom.append(container, $('.session'));
|
||||
const name = dom.append(session, $('.name'));
|
||||
const state = dom.append(session, $('.state'));
|
||||
const stateLabel = dom.append(state, $('span.label'));
|
||||
const label = new HighlightedLabel(name, false);
|
||||
|
||||
return { session, name, state, stateLabel, label };
|
||||
}
|
||||
|
||||
renderElement(element: ITreeNode<IDebugSession, FuzzyScore>, index: number, data: ISessionTemplateData): void {
|
||||
const session = element.element;
|
||||
data.session.title = nls.localize({ key: 'session', comment: ['Session is a noun'] }, "Session");
|
||||
data.label.set(session.getLabel(), createMatches(element.filterData));
|
||||
const stoppedThread = session.getAllThreads().filter(t => t.stopped).pop();
|
||||
|
||||
data.stateLabel.textContent = stoppedThread ? nls.localize('paused', "Paused")
|
||||
: nls.localize({ key: 'running', comment: ['indicates state'] }, "Running");
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ISessionTemplateData): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
class ThreadsRenderer implements ITreeRenderer<IThread, FuzzyScore, IThreadTemplateData> {
|
||||
static readonly ID = 'thread';
|
||||
|
||||
get templateId(): string {
|
||||
return ThreadsRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IThreadTemplateData {
|
||||
const thread = dom.append(container, $('.thread'));
|
||||
const name = dom.append(thread, $('.name'));
|
||||
const state = dom.append(thread, $('.state'));
|
||||
const stateLabel = dom.append(state, $('span.label'));
|
||||
const label = new HighlightedLabel(name, false);
|
||||
|
||||
return { thread, name, state, stateLabel, label };
|
||||
}
|
||||
|
||||
renderElement(element: ITreeNode<IThread, FuzzyScore>, index: number, data: IThreadTemplateData): void {
|
||||
const thread = element.element;
|
||||
data.thread.title = nls.localize('thread', "Thread");
|
||||
data.label.set(thread.name, createMatches(element.filterData));
|
||||
data.stateLabel.textContent = thread.stateLabel;
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IThreadTemplateData): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
class StackFramesRenderer implements ITreeRenderer<IStackFrame, FuzzyScore, IStackFrameTemplateData> {
|
||||
static readonly ID = 'stackFrame';
|
||||
|
||||
constructor(@ILabelService private readonly labelService: ILabelService) { }
|
||||
|
||||
get templateId(): string {
|
||||
return StackFramesRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IStackFrameTemplateData {
|
||||
const stackFrame = dom.append(container, $('.stack-frame'));
|
||||
const labelDiv = dom.append(stackFrame, $('span.label.expression'));
|
||||
const file = dom.append(stackFrame, $('.file'));
|
||||
const fileName = dom.append(file, $('span.file-name'));
|
||||
const wrapper = dom.append(file, $('span.line-number-wrapper'));
|
||||
const lineNumber = dom.append(wrapper, $('span.line-number'));
|
||||
const label = new HighlightedLabel(labelDiv, false);
|
||||
|
||||
return { file, fileName, label, lineNumber, stackFrame };
|
||||
}
|
||||
|
||||
renderElement(element: ITreeNode<IStackFrame, FuzzyScore>, index: number, data: IStackFrameTemplateData): void {
|
||||
const stackFrame = element.element;
|
||||
dom.toggleClass(data.stackFrame, 'disabled', !stackFrame.source || !stackFrame.source.available || isDeemphasized(stackFrame));
|
||||
dom.toggleClass(data.stackFrame, 'label', stackFrame.presentationHint === 'label');
|
||||
dom.toggleClass(data.stackFrame, 'subtle', stackFrame.presentationHint === 'subtle');
|
||||
|
||||
data.file.title = stackFrame.source.inMemory ? stackFrame.source.uri.path : this.labelService.getUriLabel(stackFrame.source.uri);
|
||||
if (stackFrame.source.raw.origin) {
|
||||
data.file.title += `\n${stackFrame.source.raw.origin}`;
|
||||
}
|
||||
data.label.set(stackFrame.name, createMatches(element.filterData), stackFrame.name);
|
||||
data.fileName.textContent = stackFrame.getSpecificSourceName();
|
||||
if (stackFrame.range.startLineNumber !== undefined) {
|
||||
data.lineNumber.textContent = `${stackFrame.range.startLineNumber}`;
|
||||
if (stackFrame.range.startColumn) {
|
||||
data.lineNumber.textContent += `:${stackFrame.range.startColumn}`;
|
||||
}
|
||||
dom.removeClass(data.lineNumber, 'unavailable');
|
||||
} else {
|
||||
dom.addClass(data.lineNumber, 'unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IStackFrameTemplateData): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorsRenderer implements ITreeRenderer<string, FuzzyScore, IErrorTemplateData> {
|
||||
static readonly ID = 'error';
|
||||
|
||||
get templateId(): string {
|
||||
return ErrorsRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IErrorTemplateData {
|
||||
const label = dom.append(container, $('.error'));
|
||||
|
||||
return { label };
|
||||
}
|
||||
|
||||
renderElement(element: ITreeNode<string, FuzzyScore>, index: number, data: IErrorTemplateData): void {
|
||||
const error = element.element;
|
||||
data.label.textContent = error;
|
||||
data.label.title = error;
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IErrorTemplateData): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
class LoadMoreRenderer implements ITreeRenderer<ThreadAndSessionIds, FuzzyScore, ILabelTemplateData> {
|
||||
static readonly ID = 'loadMore';
|
||||
static readonly LABEL = nls.localize('loadMoreStackFrames', "Load More Stack Frames");
|
||||
|
||||
get templateId(): string {
|
||||
return LoadMoreRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IErrorTemplateData {
|
||||
const label = dom.append(container, $('.load-more'));
|
||||
|
||||
return { label };
|
||||
}
|
||||
|
||||
renderElement(element: ITreeNode<ThreadAndSessionIds, FuzzyScore>, index: number, data: ILabelTemplateData): void {
|
||||
data.label.textContent = LoadMoreRenderer.LABEL;
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ILabelTemplateData): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
class ShowMoreRenderer implements ITreeRenderer<IStackFrame[], FuzzyScore, ILabelTemplateData> {
|
||||
static readonly ID = 'showMore';
|
||||
|
||||
get templateId(): string {
|
||||
return ShowMoreRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IErrorTemplateData {
|
||||
const label = dom.append(container, $('.show-more'));
|
||||
|
||||
return { label };
|
||||
}
|
||||
|
||||
renderElement(element: ITreeNode<IStackFrame[], FuzzyScore>, index: number, data: ILabelTemplateData): void {
|
||||
const stackFrames = element.element;
|
||||
if (stackFrames.every(sf => !!(sf.source && sf.source.origin && sf.source.origin === stackFrames[0].source.origin))) {
|
||||
data.label.textContent = nls.localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin);
|
||||
} else {
|
||||
data.label.textContent = nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length);
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ILabelTemplateData): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
class CallStackDelegate implements IListVirtualDelegate<CallStackItem> {
|
||||
|
||||
getHeight(element: CallStackItem): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: CallStackItem): string {
|
||||
if (isDebugSession(element)) {
|
||||
return SessionsRenderer.ID;
|
||||
}
|
||||
if (element instanceof Thread) {
|
||||
return ThreadsRenderer.ID;
|
||||
}
|
||||
if (element instanceof StackFrame) {
|
||||
return StackFramesRenderer.ID;
|
||||
}
|
||||
if (typeof element === 'string') {
|
||||
return ErrorsRenderer.ID;
|
||||
}
|
||||
if (element instanceof ThreadAndSessionIds) {
|
||||
return LoadMoreRenderer.ID;
|
||||
}
|
||||
|
||||
// element instanceof Array
|
||||
return ShowMoreRenderer.ID;
|
||||
}
|
||||
}
|
||||
|
||||
function isDebugModel(obj: any): obj is IDebugModel {
|
||||
return typeof obj.getSessions === 'function';
|
||||
}
|
||||
|
||||
function isDebugSession(obj: any): obj is IDebugSession {
|
||||
return typeof obj.getAllThreads === 'function';
|
||||
}
|
||||
|
||||
function isDeemphasized(frame: IStackFrame): boolean {
|
||||
return frame.source.presentationHint === 'deemphasize' || frame.presentationHint === 'deemphasize';
|
||||
}
|
||||
|
||||
class CallStackDataSource implements IAsyncDataSource<IDebugModel, CallStackItem> {
|
||||
deemphasizedStackFramesToShow: IStackFrame[];
|
||||
|
||||
hasChildren(element: IDebugModel | CallStackItem): boolean {
|
||||
return isDebugModel(element) || isDebugSession(element) || (element instanceof Thread && element.stopped);
|
||||
}
|
||||
|
||||
getChildren(element: IDebugModel | CallStackItem): Promise<CallStackItem[]> {
|
||||
if (isDebugModel(element)) {
|
||||
const sessions = element.getSessions();
|
||||
if (sessions.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
if (sessions.length > 1) {
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
const threads = sessions[0].getAllThreads();
|
||||
// Only show the threads in the call stack if there is more than 1 thread.
|
||||
return threads.length === 1 ? this.getThreadChildren(<Thread>threads[0]) : Promise.resolve(threads);
|
||||
} else if (isDebugSession(element)) {
|
||||
return Promise.resolve(element.getAllThreads());
|
||||
} else {
|
||||
return this.getThreadChildren(<Thread>element);
|
||||
}
|
||||
}
|
||||
|
||||
private getThreadChildren(thread: Thread): Promise<CallStackItem[]> {
|
||||
return this.getThreadCallstack(thread).then(children => {
|
||||
// Check if some stack frames should be hidden under a parent element since they are deemphasized
|
||||
const result: CallStackItem[] = [];
|
||||
children.forEach((child, index) => {
|
||||
if (child instanceof StackFrame && child.source && isDeemphasized(child)) {
|
||||
// Check if the user clicked to show the deemphasized source
|
||||
if (this.deemphasizedStackFramesToShow.indexOf(child) === -1) {
|
||||
if (result.length) {
|
||||
const last = result[result.length - 1];
|
||||
if (last instanceof Array) {
|
||||
// Collect all the stackframes that will be "collapsed"
|
||||
last.push(child);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nextChild = index < children.length - 1 ? children[index + 1] : undefined;
|
||||
if (nextChild instanceof StackFrame && nextChild.source && isDeemphasized(nextChild)) {
|
||||
// Start collecting stackframes that will be "collapsed"
|
||||
result.push([child]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(child);
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private getThreadCallstack(thread: Thread): Promise<Array<IStackFrame | string | ThreadAndSessionIds>> {
|
||||
let callStack: any[] = thread.getCallStack();
|
||||
let callStackPromise: Promise<any> = Promise.resolve(null);
|
||||
if (!callStack || !callStack.length) {
|
||||
callStackPromise = thread.fetchCallStack().then(() => callStack = thread.getCallStack());
|
||||
}
|
||||
|
||||
return callStackPromise.then(() => {
|
||||
if (callStack.length === 1 && thread.session.capabilities.supportsDelayedStackTraceLoading && thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > 1) {
|
||||
// To reduce flashing of the call stack view simply append the stale call stack
|
||||
// once we have the correct data the tree will refresh and we will no longer display it.
|
||||
callStack = callStack.concat(thread.getStaleCallStack().slice(1));
|
||||
}
|
||||
|
||||
if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) {
|
||||
callStack = callStack.concat([thread.stoppedDetails.framesErrorMessage]);
|
||||
}
|
||||
if (thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > callStack.length && callStack.length > 1) {
|
||||
callStack = callStack.concat([new ThreadAndSessionIds(thread.session.getId(), thread.threadId)]);
|
||||
}
|
||||
|
||||
return callStack;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CallStackAccessibilityProvider implements IAccessibilityProvider<CallStackItem> {
|
||||
getAriaLabel(element: CallStackItem): string {
|
||||
if (element instanceof Thread) {
|
||||
return nls.localize('threadAriaLabel', "Thread {0}, callstack, debug", (<Thread>element).name);
|
||||
}
|
||||
if (element instanceof StackFrame) {
|
||||
return nls.localize('stackFrameAriaLabel', "Stack Frame {0} line {1} {2}, callstack, debug", element.name, element.range.startLineNumber, element.getSpecificSourceName());
|
||||
}
|
||||
if (isDebugSession(element)) {
|
||||
return nls.localize('sessionLabel', "Debug Session {0}", element.getLabel());
|
||||
}
|
||||
if (typeof element === 'string') {
|
||||
return element;
|
||||
}
|
||||
if (element instanceof Array) {
|
||||
return nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length);
|
||||
}
|
||||
|
||||
// element instanceof ThreadAndSessionIds
|
||||
return nls.localize('loadMoreStackFrames', "Load More Stack Frames");
|
||||
}
|
||||
}
|
||||
133
src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
|
||||
|
||||
/**
|
||||
* @param text The content to stylize.
|
||||
* @returns An {@link HTMLSpanElement} that contains the potentially stylized text.
|
||||
*/
|
||||
export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTMLSpanElement {
|
||||
|
||||
const root: HTMLSpanElement = document.createElement('span');
|
||||
const textLength: number = text.length;
|
||||
|
||||
let styleNames: string[] = [];
|
||||
let currentPos: number = 0;
|
||||
let buffer: string = '';
|
||||
|
||||
while (currentPos < textLength) {
|
||||
|
||||
let sequenceFound: boolean = false;
|
||||
|
||||
// Potentially an ANSI escape sequence.
|
||||
// See http://ascii-table.com/ansi-escape-sequences.php & https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
if (text.charCodeAt(currentPos) === 27 && text.charAt(currentPos + 1) === '[') {
|
||||
|
||||
const startPos: number = currentPos;
|
||||
currentPos += 2; // Ignore 'Esc[' as it's in every sequence.
|
||||
|
||||
let ansiSequence: string = '';
|
||||
|
||||
while (currentPos < textLength) {
|
||||
const char: string = text.charAt(currentPos);
|
||||
ansiSequence += char;
|
||||
|
||||
currentPos++;
|
||||
|
||||
// Look for a known sequence terminating character.
|
||||
if (char.match(/^[ABCDHIJKfhmpsu]$/)) {
|
||||
sequenceFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (sequenceFound) {
|
||||
|
||||
// Flush buffer with previous styles.
|
||||
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector);
|
||||
|
||||
buffer = '';
|
||||
|
||||
/*
|
||||
* Certain ranges that are matched here do not contain real graphics rendition sequences. For
|
||||
* the sake of having a simpler expression, they have been included anyway.
|
||||
*/
|
||||
if (ansiSequence.match(/^(?:[349][0-7]|10[0-7]|[013]|4|[34]9)(?:;(?:[349][0-7]|10[0-7]|[013]|4|[34]9))*;?m$/)) {
|
||||
|
||||
const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.
|
||||
.split(';') // Separate style codes.
|
||||
.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].
|
||||
.map(elem => parseInt(elem, 10)); // Convert to numbers.
|
||||
|
||||
for (let code of styleCodes) {
|
||||
if (code === 0) {
|
||||
styleNames = [];
|
||||
} else if (code === 1) {
|
||||
styleNames.push('code-bold');
|
||||
} else if (code === 3) {
|
||||
styleNames.push('code-italic');
|
||||
} else if (code === 4) {
|
||||
styleNames.push('code-underline');
|
||||
} else if (code === 39 || (code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
|
||||
// Remove all previous foreground colour codes
|
||||
styleNames = styleNames.filter(style => !style.match(/^code-foreground-\d+$/));
|
||||
|
||||
if (code !== 39) {
|
||||
styleNames.push('code-foreground-' + code);
|
||||
}
|
||||
} else if (code === 49 || (code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
|
||||
// Remove all previous background colour codes
|
||||
styleNames = styleNames.filter(style => !style.match(/^code-background-\d+$/));
|
||||
|
||||
if (code !== 49) {
|
||||
styleNames.push('code-background-' + code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Unsupported sequence so simply hide it.
|
||||
}
|
||||
|
||||
} else {
|
||||
currentPos = startPos;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (sequenceFound === false) {
|
||||
buffer += text.charAt(currentPos);
|
||||
currentPos++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Flush remaining text buffer if not empty.
|
||||
if (buffer) {
|
||||
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector);
|
||||
}
|
||||
|
||||
return root;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param root The {@link HTMLElement} to append the content to.
|
||||
* @param stringContent The text content to be appended.
|
||||
* @param cssClasses The list of CSS styles to apply to the text content.
|
||||
* @param linkDetector The {@link LinkDetector} responsible for generating links from {@param stringContent}.
|
||||
*/
|
||||
export function appendStylizedStringToContainer(root: HTMLElement, stringContent: string, cssClasses: string[], linkDetector: LinkDetector): void {
|
||||
if (!root || !stringContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = linkDetector.handleLinks(stringContent);
|
||||
|
||||
container.className = cssClasses.join(' ');
|
||||
root.appendChild(container);
|
||||
}
|
||||
227
src/vs/workbench/contrib/debug/browser/debugActionItems.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox';
|
||||
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 { 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';
|
||||
import { selectBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class StartDebugActionItem implements IActionItem {
|
||||
|
||||
private static readonly SEPARATOR = '─────────';
|
||||
|
||||
public actionRunner: IActionRunner;
|
||||
private container: HTMLElement;
|
||||
private start: HTMLElement;
|
||||
private selectBox: SelectBox;
|
||||
private options: { label: string, handler?: (() => boolean) }[];
|
||||
private toDispose: IDisposable[];
|
||||
private selected: number;
|
||||
|
||||
constructor(
|
||||
private context: any,
|
||||
private action: IAction,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
) {
|
||||
this.toDispose = [];
|
||||
this.selectBox = new SelectBox([], -1, contextViewService, undefined, { ariaLabel: nls.localize('debugLaunchConfigurations', 'Debug Launch Configurations') });
|
||||
this.toDispose.push(this.selectBox);
|
||||
this.toDispose.push(attachSelectBoxStyler(this.selectBox, themeService, {
|
||||
selectBackground: SIDE_BAR_BACKGROUND
|
||||
}));
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('launch')) {
|
||||
this.updateOptions();
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.debugService.getConfigurationManager().onDidSelectConfiguration(() => {
|
||||
this.updateOptions();
|
||||
}));
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
dom.addClass(container, 'start-debug-action-item');
|
||||
this.start = dom.append(container, $('.icon'));
|
||||
this.start.title = this.action.label;
|
||||
this.start.setAttribute('role', 'button');
|
||||
this.start.tabIndex = 0;
|
||||
|
||||
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.CLICK, () => {
|
||||
this.start.blur();
|
||||
this.actionRunner.run(this.action, this.context);
|
||||
}));
|
||||
|
||||
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
|
||||
if (this.action.enabled && e.button === 0) {
|
||||
dom.addClass(this.start, 'active');
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_UP, () => {
|
||||
dom.removeClass(this.start, 'active');
|
||||
}));
|
||||
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_OUT, () => {
|
||||
dom.removeClass(this.start, 'active');
|
||||
}));
|
||||
|
||||
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.Enter)) {
|
||||
this.actionRunner.run(this.action, this.context);
|
||||
}
|
||||
if (event.equals(KeyCode.RightArrow)) {
|
||||
this.selectBox.focus();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.selectBox.onDidSelect(e => {
|
||||
const target = this.options[e.index];
|
||||
const shouldBeSelected = target.handler ? target.handler() : false;
|
||||
if (shouldBeSelected) {
|
||||
this.selected = e.index;
|
||||
} else {
|
||||
// Some select options should not remain selected https://github.com/Microsoft/vscode/issues/31526
|
||||
this.selectBox.select(this.selected);
|
||||
}
|
||||
}));
|
||||
|
||||
const selectBoxContainer = $('.configuration');
|
||||
this.selectBox.render(dom.append(container, selectBoxContainer));
|
||||
this.toDispose.push(dom.addDisposableListener(selectBoxContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
this.start.focus();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(attachStylerCallback(this.themeService, { selectBorder }, colors => {
|
||||
this.container.style.border = colors.selectBorder ? `1px solid ${colors.selectBorder}` : null;
|
||||
selectBoxContainer.style.borderLeft = colors.selectBorder ? `1px solid ${colors.selectBorder}` : null;
|
||||
}));
|
||||
|
||||
this.updateOptions();
|
||||
}
|
||||
|
||||
public setActionContext(context: any): void {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public focus(fromRight?: boolean): void {
|
||||
if (fromRight) {
|
||||
this.selectBox.focus();
|
||||
} else {
|
||||
this.start.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
this.container.blur();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.toDispose = dispose(this.toDispose);
|
||||
}
|
||||
|
||||
private updateOptions(): void {
|
||||
this.selected = 0;
|
||||
this.options = [];
|
||||
const manager = this.debugService.getConfigurationManager();
|
||||
const launches = manager.getLaunches();
|
||||
const inWorkspace = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
|
||||
launches.forEach(launch =>
|
||||
launch.getConfigurationNames().forEach(name => {
|
||||
if (name === manager.selectedConfiguration.name && launch === manager.selectedConfiguration.launch) {
|
||||
this.selected = this.options.length;
|
||||
}
|
||||
const label = inWorkspace ? `${name} (${launch.name})` : name;
|
||||
this.options.push({ label, handler: () => { manager.selectConfiguration(launch, name); return true; } });
|
||||
}));
|
||||
|
||||
if (this.options.length === 0) {
|
||||
this.options.push({ label: nls.localize('noConfigurations', "No Configurations"), handler: () => false });
|
||||
} else {
|
||||
this.options.push({ label: StartDebugActionItem.SEPARATOR, handler: undefined });
|
||||
}
|
||||
|
||||
const disabledIdx = this.options.length - 1;
|
||||
launches.filter(l => !l.hidden).forEach(l => {
|
||||
const label = inWorkspace ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration...");
|
||||
this.options.push({
|
||||
label, handler: () => {
|
||||
this.commandService.executeCommand('debug.addConfiguration', l.uri.toString());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.selectBox.setOptions(this.options.map((data, index) => <ISelectOptionItem>{ text: data.label, isDisabled: (index === disabledIdx ? true : undefined) }), this.selected);
|
||||
}
|
||||
}
|
||||
|
||||
export class FocusSessionActionItem extends SelectActionItem {
|
||||
constructor(
|
||||
action: IAction,
|
||||
@IDebugService protected debugService: IDebugService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
) {
|
||||
super(null, action, [], -1, contextViewService, { ariaLabel: nls.localize('debugSession', 'Debug Session') });
|
||||
|
||||
this.toDispose.push(attachSelectBoxStyler(this.selectBox, themeService));
|
||||
|
||||
this.toDispose.push(this.debugService.getViewModel().onDidFocusSession(() => {
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
if (session) {
|
||||
const index = this.getSessions().indexOf(session);
|
||||
this.select(index);
|
||||
}
|
||||
}));
|
||||
|
||||
this.toDispose.push(this.debugService.onDidNewSession(() => this.update()));
|
||||
this.toDispose.push(this.debugService.onDidEndSession(() => this.update()));
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
const sessions = this.getSessions();
|
||||
const names = sessions.map(s => s.getLabel());
|
||||
this.setOptions(names.map(data => <ISelectOptionItem>{ text: data }), session ? sessions.indexOf(session) : undefined);
|
||||
}
|
||||
|
||||
protected getSessions(): ReadonlyArray<IDebugSession> {
|
||||
return this.debugService.getModel().getSessions();
|
||||
}
|
||||
}
|
||||
896
src/vs/workbench/contrib/debug/browser/debugActions.ts
Normal file
@@ -0,0 +1,896 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IDebugService, State, IDebugSession, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID }
|
||||
from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Variable, Expression, Thread, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { TogglePanelAction } from 'vs/workbench/browser/panel';
|
||||
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { CollapseAction } from 'vs/workbench/browser/viewlet';
|
||||
import { first } from 'vs/base/common/arrays';
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
|
||||
export abstract class AbstractDebugAction extends Action {
|
||||
|
||||
protected toDispose: lifecycle.IDisposable[];
|
||||
|
||||
constructor(
|
||||
id: string, label: string, cssClass: string,
|
||||
@IDebugService protected debugService: IDebugService,
|
||||
@IKeybindingService protected keybindingService: IKeybindingService,
|
||||
public weight?: number
|
||||
) {
|
||||
super(id, label, cssClass, false);
|
||||
this.toDispose = [];
|
||||
this.toDispose.push(this.debugService.onDidChangeState(state => this.updateEnablement(state)));
|
||||
|
||||
this.updateLabel(label);
|
||||
this.updateEnablement();
|
||||
}
|
||||
|
||||
public run(e?: any): Promise<any> {
|
||||
throw new Error('implement me');
|
||||
}
|
||||
|
||||
public get tooltip(): string {
|
||||
const keybinding = this.keybindingService.lookupKeybinding(this.id);
|
||||
const keybindingLabel = keybinding && keybinding.getLabel();
|
||||
|
||||
return keybindingLabel ? `${this.label} (${keybindingLabel})` : this.label;
|
||||
}
|
||||
|
||||
protected updateLabel(newLabel: string): void {
|
||||
this.label = newLabel;
|
||||
}
|
||||
|
||||
protected updateEnablement(state = this.debugService.state): void {
|
||||
this.enabled = this.isEnabled(state);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this.toDispose = lifecycle.dispose(this.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigureAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.configure';
|
||||
static LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json');
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IDebugService debugService: IDebugService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
|
||||
) {
|
||||
super(id, label, 'debug-action configure', debugService, keybindingService);
|
||||
this.toDispose.push(debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateClass()));
|
||||
this.updateClass();
|
||||
}
|
||||
|
||||
public get tooltip(): string {
|
||||
if (this.debugService.getConfigurationManager().selectedConfiguration.name) {
|
||||
return ConfigureAction.LABEL;
|
||||
}
|
||||
|
||||
return nls.localize('launchJsonNeedsConfigurtion', "Configure or Fix 'launch.json'");
|
||||
}
|
||||
|
||||
private updateClass(): void {
|
||||
const configurationManager = this.debugService.getConfigurationManager();
|
||||
const configurationCount = configurationManager.getLaunches().map(l => l.getConfigurationNames().length).reduce((sum, current) => sum + current);
|
||||
this.class = configurationCount > 0 ? 'debug-action configure' : 'debug-action configure notification';
|
||||
}
|
||||
|
||||
public run(event?: any): Promise<any> {
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
this.notificationService.info(nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration."));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const sideBySide = !!(event && (event.ctrlKey || event.metaKey));
|
||||
const configurationManager = this.debugService.getConfigurationManager();
|
||||
if (!configurationManager.selectedConfiguration.launch) {
|
||||
configurationManager.selectConfiguration(configurationManager.getLaunches()[0]);
|
||||
}
|
||||
|
||||
return configurationManager.selectedConfiguration.launch!.openConfigFile(sideBySide, false);
|
||||
}
|
||||
}
|
||||
|
||||
export class StartAction extends AbstractDebugAction {
|
||||
static ID = 'workbench.action.debug.start';
|
||||
static LABEL = nls.localize('startDebug', "Start Debugging");
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IDebugService debugService: IDebugService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IHistoryService private readonly historyService: IHistoryService
|
||||
) {
|
||||
super(id, label, 'debug-action start', debugService, keybindingService);
|
||||
|
||||
this.toDispose.push(this.debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateEnablement()));
|
||||
this.toDispose.push(this.debugService.onDidNewSession(() => this.updateEnablement()));
|
||||
this.toDispose.push(this.debugService.onDidEndSession(() => this.updateEnablement()));
|
||||
this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
// Note: When this action is executed from the process explorer, a config is passed. For all
|
||||
// other cases it is run with no arguments.
|
||||
public run(): Promise<any> {
|
||||
const configurationManager = this.debugService.getConfigurationManager();
|
||||
let launch = configurationManager.selectedConfiguration.launch;
|
||||
if (!launch || launch.getConfigurationNames().length === 0) {
|
||||
const rootUri = this.historyService.getLastActiveWorkspaceRoot();
|
||||
launch = configurationManager.getLaunch(rootUri);
|
||||
if (!launch || launch.getConfigurationNames().length === 0) {
|
||||
const launches = configurationManager.getLaunches();
|
||||
launch = first(launches, l => !!(l && l.getConfigurationNames().length), launch);
|
||||
}
|
||||
|
||||
configurationManager.selectConfiguration(launch);
|
||||
}
|
||||
|
||||
return this.debugService.startDebugging(launch, undefined, this.isNoDebug());
|
||||
}
|
||||
|
||||
protected isNoDebug(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static isEnabled(debugService: IDebugService) {
|
||||
const sessions = debugService.getModel().getSessions();
|
||||
|
||||
if (debugService.state === State.Initializing) {
|
||||
return false;
|
||||
}
|
||||
if ((sessions.length > 0) && debugService.getConfigurationManager().getLaunches().every(l => l.getConfigurationNames().length === 0)) {
|
||||
// There is already a debug session running and we do not have any launch configuration selected
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Disabled if the launch drop down shows the launch config that is already running.
|
||||
protected isEnabled(): boolean {
|
||||
return StartAction.isEnabled(this.debugService);
|
||||
}
|
||||
}
|
||||
|
||||
export class RunAction extends StartAction {
|
||||
static readonly ID = 'workbench.action.debug.run';
|
||||
static LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging");
|
||||
|
||||
protected isNoDebug(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectAndStartAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.selectandstart';
|
||||
static LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging");
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IDebugService debugService: IDebugService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IQuickOpenService private readonly quickOpenService: IQuickOpenService
|
||||
) {
|
||||
super(id, label, '', debugService, keybindingService);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
return this.quickOpenService.show('debug ');
|
||||
}
|
||||
}
|
||||
|
||||
export class RestartAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.restart';
|
||||
static LABEL = nls.localize('restartDebug', "Restart");
|
||||
static RECONNECT_LABEL = nls.localize('reconnectDebug', "Reconnect");
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IDebugService debugService: IDebugService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IHistoryService private readonly historyService: IHistoryService
|
||||
) {
|
||||
super(id, label, 'debug-action restart', debugService, keybindingService, 70);
|
||||
this.setLabel(this.debugService.getViewModel().focusedSession);
|
||||
this.toDispose.push(this.debugService.getViewModel().onDidFocusSession(() => this.setLabel(this.debugService.getViewModel().focusedSession)));
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get startAction(): StartAction {
|
||||
return new StartAction(StartAction.ID, StartAction.LABEL, this.debugService, this.keybindingService, this.contextService, this.historyService);
|
||||
}
|
||||
|
||||
private setLabel(session: IDebugSession | undefined): void {
|
||||
if (session) {
|
||||
this.updateLabel(session && session.configuration.request === 'attach' ? RestartAction.RECONNECT_LABEL : RestartAction.LABEL);
|
||||
}
|
||||
}
|
||||
|
||||
public run(session: IDebugSession | undefined): Promise<any> {
|
||||
if (!session || !session.getId) {
|
||||
session = this.debugService.getViewModel().focusedSession;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return this.startAction.run();
|
||||
}
|
||||
|
||||
session.removeReplExpressions();
|
||||
return this.debugService.restartSession(session);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && (
|
||||
state === State.Running ||
|
||||
state === State.Stopped ||
|
||||
StartAction.isEnabled(this.debugService)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class StepOverAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.stepOver';
|
||||
static LABEL = nls.localize('stepOverDebug', "Step Over");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action step-over', debugService, keybindingService, 20);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
}
|
||||
|
||||
return thread ? thread.next() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && state === State.Stopped;
|
||||
}
|
||||
}
|
||||
|
||||
export class StepIntoAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.stepInto';
|
||||
static LABEL = nls.localize('stepIntoDebug', "Step Into");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action step-into', debugService, keybindingService, 30);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
}
|
||||
|
||||
return thread ? thread.stepIn() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && state === State.Stopped;
|
||||
}
|
||||
}
|
||||
|
||||
export class StepOutAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.stepOut';
|
||||
static LABEL = nls.localize('stepOutDebug', "Step Out");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action step-out', debugService, keybindingService, 40);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
}
|
||||
|
||||
return thread ? thread.stepOut() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && state === State.Stopped;
|
||||
}
|
||||
}
|
||||
|
||||
export class StopAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.stop';
|
||||
static LABEL = nls.localize('stopDebug', "Stop");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action stop', debugService, keybindingService, 80);
|
||||
}
|
||||
|
||||
public run(session: IDebugSession | undefined): Promise<any> {
|
||||
if (!session || !session.getId) {
|
||||
session = this.debugService.getViewModel().focusedSession;
|
||||
}
|
||||
|
||||
return this.debugService.stopSession(session);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && (state !== State.Inactive);
|
||||
}
|
||||
}
|
||||
|
||||
export class DisconnectAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.disconnect';
|
||||
static LABEL = nls.localize('disconnectDebug', "Disconnect");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action disconnect', debugService, keybindingService, 80);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
return this.debugService.stopSession(session);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && (state === State.Running || state === State.Stopped);
|
||||
}
|
||||
}
|
||||
|
||||
export class ContinueAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.continue';
|
||||
static LABEL = nls.localize('continueDebug', "Continue");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action continue', debugService, keybindingService, 10);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
}
|
||||
|
||||
return thread ? thread.continue() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && state === State.Stopped;
|
||||
}
|
||||
}
|
||||
|
||||
export class PauseAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.pause';
|
||||
static LABEL = nls.localize('pauseDebug', "Pause");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action pause', debugService, keybindingService, 10);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
if (!thread) {
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
const threads = session && session.getAllThreads();
|
||||
thread = threads && threads.length ? threads[0] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return thread ? thread.pause() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && state === State.Running;
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminateThreadAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.terminateThread';
|
||||
static LABEL = nls.localize('terminateThread', "Terminate Thread");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, '', debugService, keybindingService);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
}
|
||||
|
||||
return thread ? thread.terminate() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && (state === State.Running || state === State.Stopped);
|
||||
}
|
||||
}
|
||||
|
||||
export class RestartFrameAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.restartFrame';
|
||||
static LABEL = nls.localize('restartFrame', "Restart Frame");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, '', debugService, keybindingService);
|
||||
}
|
||||
|
||||
public run(frame: IStackFrame | undefined): Promise<any> {
|
||||
if (!frame) {
|
||||
frame = this.debugService.getViewModel().focusedStackFrame;
|
||||
}
|
||||
|
||||
return frame!.restart();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveBreakpointAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.removeBreakpoint';
|
||||
static LABEL = nls.localize('removeBreakpoint', "Remove Breakpoint");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action remove', debugService, keybindingService);
|
||||
}
|
||||
|
||||
public run(breakpoint: IBreakpoint): Promise<any> {
|
||||
return breakpoint instanceof Breakpoint ? this.debugService.removeBreakpoints(breakpoint.getId())
|
||||
: this.debugService.removeFunctionBreakpoints(breakpoint.getId());
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveAllBreakpointsAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.removeAllBreakpoints';
|
||||
static LABEL = nls.localize('removeAllBreakpoints', "Remove All Breakpoints");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action remove-all', debugService, keybindingService);
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
return Promise.all([this.debugService.removeBreakpoints(), this.debugService.removeFunctionBreakpoints()]);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
const model = this.debugService.getModel();
|
||||
return super.isEnabled(state) && (model.getBreakpoints().length > 0 || model.getFunctionBreakpoints().length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class EnableAllBreakpointsAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.enableAllBreakpoints';
|
||||
static LABEL = nls.localize('enableAllBreakpoints', "Enable All Breakpoints");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action enable-all-breakpoints', debugService, keybindingService);
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
return this.debugService.enableOrDisableBreakpoints(true);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
const model = this.debugService.getModel();
|
||||
return super.isEnabled(state) && (<ReadonlyArray<IEnablement>>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => !bp.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
export class DisableAllBreakpointsAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.disableAllBreakpoints';
|
||||
static LABEL = nls.localize('disableAllBreakpoints', "Disable All Breakpoints");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action disable-all-breakpoints', debugService, keybindingService);
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
return this.debugService.enableOrDisableBreakpoints(false);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
const model = this.debugService.getModel();
|
||||
return super.isEnabled(state) && (<ReadonlyArray<IEnablement>>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => bp.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
export class ToggleBreakpointsActivatedAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.toggleBreakpointsActivatedAction';
|
||||
static ACTIVATE_LABEL = nls.localize('activateBreakpoints', "Activate Breakpoints");
|
||||
static DEACTIVATE_LABEL = nls.localize('deactivateBreakpoints', "Deactivate Breakpoints");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action breakpoints-activate', debugService, keybindingService);
|
||||
this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL);
|
||||
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => {
|
||||
this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL);
|
||||
this.updateEnablement();
|
||||
}));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
return this.debugService.setBreakpointsActivated(!this.debugService.getModel().areBreakpointsActivated());
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return (this.debugService.getModel().getFunctionBreakpoints().length + this.debugService.getModel().getBreakpoints().length) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class ReapplyBreakpointsAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.reapplyBreakpointsAction';
|
||||
static LABEL = nls.localize('reapplyAllBreakpoints', "Reapply All Breakpoints");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, '', debugService, keybindingService);
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
return this.debugService.setBreakpointsActivated(true);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
const model = this.debugService.getModel();
|
||||
return super.isEnabled(state) && (state === State.Running || state === State.Stopped) &&
|
||||
(model.getFunctionBreakpoints().length + model.getBreakpoints().length + model.getExceptionBreakpoints().length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddFunctionBreakpointAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.addFunctionBreakpointAction';
|
||||
static LABEL = nls.localize('addFunctionBreakpoint', "Add Function Breakpoint");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action add-function-breakpoint', debugService, keybindingService);
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
this.debugService.addFunctionBreakpoint();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return !this.debugService.getViewModel().getSelectedFunctionBreakpoint()
|
||||
&& this.debugService.getModel().getFunctionBreakpoints().every(fbp => !!fbp.name);
|
||||
}
|
||||
}
|
||||
|
||||
export class SetValueAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.setValue';
|
||||
static LABEL = nls.localize('setValue', "Set Value");
|
||||
|
||||
constructor(id: string, label: string, private variable: Variable, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, '', debugService, keybindingService);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
if (this.variable instanceof Variable) {
|
||||
this.debugService.getViewModel().setSelectedExpression(this.variable);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
return !!(super.isEnabled(state) && state === State.Stopped && session && session.capabilities.supportsSetVariable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class AddWatchExpressionAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.addWatchExpression';
|
||||
static LABEL = nls.localize('addWatchExpression', "Add Expression");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action add-watch-expression', debugService, keybindingService);
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
this.debugService.addWatchExpression();
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && this.debugService.getModel().getWatchExpressions().every(we => !!we.name);
|
||||
}
|
||||
}
|
||||
|
||||
export class EditWatchExpressionAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.editWatchExpression';
|
||||
static LABEL = nls.localize('editWatchExpression', "Edit Expression");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, '', debugService, keybindingService);
|
||||
}
|
||||
|
||||
public run(expression: Expression): Promise<any> {
|
||||
this.debugService.getViewModel().setSelectedExpression(expression);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class AddToWatchExpressionsAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.addToWatchExpressions';
|
||||
static LABEL = nls.localize('addToWatchExpressions', "Add to Watch");
|
||||
|
||||
constructor(id: string, label: string, private variable: Variable, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action add-to-watch', debugService, keybindingService);
|
||||
this.updateEnablement();
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
this.debugService.addWatchExpression(this.variable.evaluateName);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && this.variable && !!this.variable.evaluateName;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveWatchExpressionAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.removeWatchExpression';
|
||||
static LABEL = nls.localize('removeWatchExpression', "Remove Expression");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, '', debugService, keybindingService);
|
||||
}
|
||||
|
||||
public run(expression: Expression): Promise<any> {
|
||||
this.debugService.removeWatchExpressions(expression.getId());
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveAllWatchExpressionsAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.removeAllWatchExpressions';
|
||||
static LABEL = nls.localize('removeAllWatchExpressions', "Remove All Expressions");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action remove-all', debugService, keybindingService);
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement()));
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
this.debugService.removeWatchExpressions();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
return super.isEnabled(state) && this.debugService.getModel().getWatchExpressions().length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class ToggleReplAction extends TogglePanelAction {
|
||||
static readonly ID = 'workbench.debug.action.toggleRepl';
|
||||
static LABEL = nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugConsoleAction' }, 'Debug Console');
|
||||
private toDispose: lifecycle.IDisposable[];
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@IPanelService panelService: IPanelService
|
||||
) {
|
||||
super(id, label, REPL_ID, panelService, layoutService, 'debug-action toggle-repl');
|
||||
this.toDispose = [];
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.toDispose.push(this.panelService.onDidPanelOpen(({ panel }) => {
|
||||
if (panel.getId() === REPL_ID) {
|
||||
this.class = 'debug-action toggle-repl';
|
||||
this.tooltip = ToggleReplAction.LABEL;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this.toDispose = lifecycle.dispose(this.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
export class FocusReplAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.debug.action.focusRepl';
|
||||
static LABEL = nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, 'Focus on Debug Console View');
|
||||
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IPanelService private readonly panelService: IPanelService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
this.panelService.openPanel(REPL_ID, true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class FocusSessionAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.focusProcess';
|
||||
static LABEL = nls.localize('focusSession', "Focus Session");
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IDebugService debugService: IDebugService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IEditorService private readonly editorService: IEditorService
|
||||
) {
|
||||
super(id, label, '', debugService, keybindingService, 100);
|
||||
}
|
||||
|
||||
public run(sessionName: string): Promise<any> {
|
||||
const session = this.debugService.getModel().getSessions().filter(p => p.getLabel() === sessionName).pop();
|
||||
this.debugService.focusStackFrame(undefined, undefined, session, true);
|
||||
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
|
||||
if (stackFrame) {
|
||||
return stackFrame.openInEditor(this.editorService, true);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// Actions used by the chakra debugger
|
||||
export class StepBackAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.stepBack';
|
||||
static LABEL = nls.localize('stepBackDebug', "Step Back");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action step-back', debugService, keybindingService, 50);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
}
|
||||
|
||||
return thread ? thread.stepBack() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
return !!(super.isEnabled(state) && state === State.Stopped &&
|
||||
session && session.capabilities.supportsStepBack);
|
||||
}
|
||||
}
|
||||
|
||||
export class ReverseContinueAction extends AbstractDebugAction {
|
||||
static readonly ID = 'workbench.action.debug.reverseContinue';
|
||||
static LABEL = nls.localize('reverseContinue', "Reverse");
|
||||
|
||||
constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) {
|
||||
super(id, label, 'debug-action reverse-continue', debugService, keybindingService, 60);
|
||||
}
|
||||
|
||||
public run(thread: IThread | undefined): Promise<any> {
|
||||
if (!(thread instanceof Thread)) {
|
||||
thread = this.debugService.getViewModel().focusedThread;
|
||||
}
|
||||
|
||||
return thread ? thread.reverseContinue() : Promise.resolve();
|
||||
}
|
||||
|
||||
protected isEnabled(state: State): boolean {
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
return !!(super.isEnabled(state) && state === State.Stopped &&
|
||||
session && session.capabilities.supportsStepBack);
|
||||
}
|
||||
}
|
||||
|
||||
export class ReplCollapseAllAction extends CollapseAction {
|
||||
constructor(tree: AsyncDataTree<any, any, any>, private toFocus: { focus(): void; }) {
|
||||
super(tree, true, undefined);
|
||||
}
|
||||
|
||||
public run(event?: any): Promise<any> {
|
||||
return super.run(event).then(() => {
|
||||
this.toFocus.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyValueAction extends Action {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.copyValue';
|
||||
static LABEL = nls.localize('copyValue', "Copy Value");
|
||||
|
||||
constructor(
|
||||
id: string, label: string, private value: any, private context: string,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IClipboardService private readonly clipboardService: IClipboardService
|
||||
) {
|
||||
super(id, label, 'debug-action copy-value');
|
||||
this._enabled = typeof this.value === 'string' || (this.value instanceof Variable && !!this.value.evaluateName);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
|
||||
if (this.value instanceof Variable && stackFrame && session && this.value.evaluateName) {
|
||||
return session.evaluate(this.value.evaluateName, stackFrame.frameId, this.context).then(result => {
|
||||
this.clipboardService.writeText(result.body.result);
|
||||
}, err => this.clipboardService.writeText(this.value.value));
|
||||
}
|
||||
|
||||
this.clipboardService.writeText(this.value);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyEvaluatePathAction extends Action {
|
||||
static readonly ID = 'workbench.debug.viewlet.action.copyEvaluatePath';
|
||||
static LABEL = nls.localize('copyAsExpression', "Copy as Expression");
|
||||
|
||||
constructor(
|
||||
id: string, label: string, private value: Variable,
|
||||
@IClipboardService private readonly clipboardService: IClipboardService
|
||||
) {
|
||||
super(id, label);
|
||||
this._enabled = this.value && !!this.value.evaluateName;
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
this.clipboardService.writeText(this.value.evaluateName!);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyAction extends Action {
|
||||
static readonly ID = 'workbench.debug.action.copy';
|
||||
static LABEL = nls.localize('copy', "Copy");
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IClipboardService private readonly clipboardService: IClipboardService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
this.clipboardService.writeText(window.getSelection().toString());
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyStackTraceAction extends Action {
|
||||
static readonly ID = 'workbench.action.debug.copyStackTrace';
|
||||
static LABEL = nls.localize('copyStackTrace', "Copy Call Stack");
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IClipboardService private readonly clipboardService: IClipboardService,
|
||||
@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(frame: IStackFrame): Promise<any> {
|
||||
const eol = this.textResourcePropertiesService.getEOL(frame.source.uri);
|
||||
this.clipboardService.writeText(frame.thread.getCallStack().map(sf => sf.toString()).join(eol));
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
277
src/vs/workbench/contrib/debug/browser/debugCommands.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
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 } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Expression, Variable, Breakpoint, FunctionBreakpoint } 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';
|
||||
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys';
|
||||
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { PanelFocusContext } from 'vs/workbench/common/panel';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
export const ADD_CONFIGURATION_ID = 'debug.addConfiguration';
|
||||
export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint';
|
||||
|
||||
export function registerCommands(): void {
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: 'debug.startFromConfig',
|
||||
handler: (accessor, config: IConfig) => {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
debugService.startDebugging(undefined, config).then(undefined, onUnexpectedError);
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.toggleBreakpoint',
|
||||
weight: KeybindingWeight.WorkbenchContrib + 5,
|
||||
when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, InputFocusedContext.toNegated()),
|
||||
primary: KeyCode.Space,
|
||||
handler: (accessor) => {
|
||||
const listService = accessor.get(IListService);
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const list = listService.lastFocusedList;
|
||||
if (list instanceof List) {
|
||||
const focused = <IEnablement[]>list.getFocusedElements();
|
||||
if (focused && focused.length) {
|
||||
debugService.enableOrDisableBreakpoints(!focused[0].enabled, focused[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.enableOrDisableBreakpoint',
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: undefined,
|
||||
when: EditorContextKeys.editorTextFocus,
|
||||
handler: (accessor) => {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const widget = editorService.activeTextEditorWidget;
|
||||
if (isCodeEditor(widget)) {
|
||||
const model = widget.getModel();
|
||||
if (model) {
|
||||
const position = widget.getPosition();
|
||||
if (position) {
|
||||
const bps = debugService.getModel().getBreakpoints({ uri: model.uri, lineNumber: position.lineNumber });
|
||||
if (bps.length) {
|
||||
debugService.enableOrDisableBreakpoints(!bps[0].enabled, bps[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.renameWatchExpression',
|
||||
weight: KeybindingWeight.WorkbenchContrib + 5,
|
||||
when: CONTEXT_WATCH_EXPRESSIONS_FOCUSED,
|
||||
primary: KeyCode.F2,
|
||||
mac: { primary: KeyCode.Enter },
|
||||
handler: (accessor) => {
|
||||
const listService = accessor.get(IListService);
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const focused = listService.lastFocusedList;
|
||||
|
||||
if (focused) {
|
||||
const elements = focused.getFocus();
|
||||
if (Array.isArray(elements) && elements[0] instanceof Expression) {
|
||||
debugService.getViewModel().setSelectedExpression(elements[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.setVariable',
|
||||
weight: KeybindingWeight.WorkbenchContrib + 5,
|
||||
when: CONTEXT_VARIABLES_FOCUSED,
|
||||
primary: KeyCode.F2,
|
||||
mac: { primary: KeyCode.Enter },
|
||||
handler: (accessor) => {
|
||||
const listService = accessor.get(IListService);
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const focused = listService.lastFocusedList;
|
||||
|
||||
if (focused) {
|
||||
const elements = focused.getFocus();
|
||||
if (Array.isArray(elements) && elements[0] instanceof Variable) {
|
||||
debugService.getViewModel().setSelectedExpression(elements[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.removeWatchExpression',
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: ContextKeyExpr.and(CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_EXPRESSION_SELECTED.toNegated()),
|
||||
primary: KeyCode.Delete,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace },
|
||||
handler: (accessor) => {
|
||||
const listService = accessor.get(IListService);
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const focused = listService.lastFocusedList;
|
||||
|
||||
if (focused) {
|
||||
const elements = focused.getFocus();
|
||||
if (Array.isArray(elements) && elements[0] instanceof Expression) {
|
||||
debugService.removeWatchExpressions(elements[0].getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.removeBreakpoint',
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_SELECTED.toNegated()),
|
||||
primary: KeyCode.Delete,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace },
|
||||
handler: (accessor) => {
|
||||
const listService = accessor.get(IListService);
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const list = listService.lastFocusedList;
|
||||
|
||||
if (list instanceof List) {
|
||||
const focused = list.getFocusedElements();
|
||||
const element = focused.length ? focused[0] : undefined;
|
||||
if (element instanceof Breakpoint) {
|
||||
debugService.removeBreakpoints(element.getId());
|
||||
} else if (element instanceof FunctionBreakpoint) {
|
||||
debugService.removeFunctionBreakpoints(element.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.installAdditionalDebuggers',
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: undefined,
|
||||
primary: undefined,
|
||||
handler: (accessor) => {
|
||||
const viewletService = accessor.get(IViewletService);
|
||||
return viewletService.openViewlet(EXTENSIONS_VIEWLET_ID, true)
|
||||
.then(viewlet => viewlet as IExtensionsViewlet)
|
||||
.then(viewlet => {
|
||||
viewlet.search('tag:debuggers @sort:installs');
|
||||
viewlet.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: ADD_CONFIGURATION_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: undefined,
|
||||
primary: undefined,
|
||||
handler: (accessor, launchUri: string) => {
|
||||
const manager = accessor.get(IDebugService).getConfigurationManager();
|
||||
if (accessor.get(IWorkspaceContextService).getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
accessor.get(INotificationService).info(nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration."));
|
||||
return undefined;
|
||||
}
|
||||
const launch = manager.getLaunches().filter(l => l.uri.toString() === launchUri).pop() || manager.selectedConfiguration.launch;
|
||||
|
||||
return launch!.openConfigFile(false, false).then(({ editor, created }) => {
|
||||
if (editor && !created) {
|
||||
const codeEditor = <ICodeEditor>editor.getControl();
|
||||
if (codeEditor) {
|
||||
return codeEditor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).addLaunchConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const inlineBreakpointHandler = (accessor: ServicesAccessor) => {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const widget = editorService.activeTextEditorWidget;
|
||||
if (isCodeEditor(widget)) {
|
||||
const position = widget.getPosition();
|
||||
if (!position || !widget.hasModel()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modelUri = widget.getModel().uri;
|
||||
const bp = debugService.getModel().getBreakpoints({ lineNumber: position.lineNumber, uri: modelUri })
|
||||
.filter(bp => (bp.column === position.column || !bp.column && position.column <= 1)).pop();
|
||||
|
||||
if (bp) {
|
||||
return undefined;
|
||||
}
|
||||
if (debugService.getConfigurationManager().canSetBreakpointsIn(widget.getModel())) {
|
||||
return debugService.addBreakpoints(modelUri, [{ lineNumber: position.lineNumber, column: position.column > 1 ? position.column : undefined }], 'debugCommands.inlineBreakpointCommand');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyMod.Shift | KeyCode.F9,
|
||||
when: EditorContextKeys.editorTextFocus,
|
||||
id: TOGGLE_INLINE_BREAKPOINT_ID,
|
||||
handler: inlineBreakpointHandler
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
|
||||
command: {
|
||||
id: TOGGLE_INLINE_BREAKPOINT_ID,
|
||||
title: { value: nls.localize('inlineBreakpoint', "Inline Breakpoint"), original: 'Debug: Inline Breakpoint' },
|
||||
category: nls.localize('debug', "Debug")
|
||||
}
|
||||
});
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
|
||||
command: {
|
||||
id: TOGGLE_INLINE_BREAKPOINT_ID,
|
||||
title: nls.localize('addInlineBreakpoint', "Add Inline Breakpoint")
|
||||
},
|
||||
when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, PanelFocusContext.toNegated(), EditorContextKeys.editorTextFocus),
|
||||
group: 'debug',
|
||||
order: 1
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'debug.openBreakpointToSide',
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: CONTEXT_BREAKPOINTS_FOCUSED,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Enter,
|
||||
secondary: [KeyMod.Alt | KeyCode.Enter],
|
||||
handler: (accessor) => {
|
||||
const listService = accessor.get(IListService);
|
||||
const list = listService.lastFocusedList;
|
||||
if (list instanceof List) {
|
||||
const focus = list.getFocusedElements();
|
||||
if (focus.length && focus[0] instanceof Breakpoint) {
|
||||
return openBreakpointSource(focus[0], true, false, accessor.get(IDebugService), accessor.get(IEditorService));
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
324
src/vs/workbench/contrib/debug/browser/debugEditorActions.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { PanelFocusContext } from 'vs/workbench/common/panel';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
|
||||
export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint';
|
||||
class ToggleBreakpointAction extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: TOGGLE_BREAKPOINT_ID,
|
||||
label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"),
|
||||
alias: 'Debug: Toggle Breakpoint',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyCode.F9,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<any> {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
|
||||
const position = editor.getPosition();
|
||||
if (editor.hasModel() && position) {
|
||||
const modelUri = editor.getModel().uri;
|
||||
const bps = debugService.getModel().getBreakpoints({ lineNumber: position.lineNumber, uri: modelUri });
|
||||
|
||||
if (bps.length) {
|
||||
return Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId())));
|
||||
}
|
||||
if (debugService.getConfigurationManager().canSetBreakpointsIn(editor.getModel())) {
|
||||
return debugService.addBreakpoints(modelUri, [{ lineNumber: position.lineNumber }], 'debugEditorActions.toggleBreakpointAction');
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_CONDITIONAL_BREAKPOINT_ID = 'editor.debug.action.conditionalBreakpoint';
|
||||
class ConditionalBreakpointAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: TOGGLE_CONDITIONAL_BREAKPOINT_ID,
|
||||
label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."),
|
||||
alias: 'Debug: Add Conditional Breakpoint...',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
|
||||
const position = editor.getPosition();
|
||||
if (position && editor.hasModel() && debugService.getConfigurationManager().canSetBreakpointsIn(editor.getModel())) {
|
||||
editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(position.lineNumber, position.column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_LOG_POINT_ID = 'editor.debug.action.toggleLogPoint';
|
||||
class LogPointAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: TOGGLE_LOG_POINT_ID,
|
||||
label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."),
|
||||
alias: 'Debug: Add Logpoint...',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
|
||||
const position = editor.getPosition();
|
||||
if (position && editor.hasModel() && debugService.getConfigurationManager().canSetBreakpointsIn(editor.getModel())) {
|
||||
editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(position.lineNumber, position.column, BreakpointWidgetContext.LOG_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RunToCursorAction extends EditorAction {
|
||||
|
||||
public static ID = 'editor.debug.action.runToCursor';
|
||||
public static LABEL = nls.localize('runToCursor', "Run to Cursor");
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: RunToCursorAction.ID,
|
||||
label: RunToCursorAction.LABEL,
|
||||
alias: 'Debug: Run to Cursor',
|
||||
precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, PanelFocusContext.toNegated(), CONTEXT_DEBUG_STATE.isEqualTo('stopped'), EditorContextKeys.editorTextFocus),
|
||||
menuOpts: {
|
||||
group: 'debug',
|
||||
order: 2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const focusedSession = debugService.getViewModel().focusedSession;
|
||||
if (debugService.state !== State.Stopped || !focusedSession) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
let breakpointToRemove: IBreakpoint;
|
||||
const oneTimeListener = focusedSession.onDidChangeState(() => {
|
||||
const state = focusedSession.state;
|
||||
if (state === State.Stopped || state === State.Inactive) {
|
||||
if (breakpointToRemove) {
|
||||
debugService.removeBreakpoints(breakpointToRemove.getId());
|
||||
}
|
||||
oneTimeListener.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
const position = editor.getPosition();
|
||||
if (!editor.hasModel() || !position) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const uri = editor.getModel().uri;
|
||||
const bpExists = !!(debugService.getModel().getBreakpoints({ column: position.column, lineNumber: position.lineNumber, uri }).length);
|
||||
return (bpExists ? Promise.resolve(null) : <Promise<any>>debugService.addBreakpoints(uri, [{ lineNumber: position.lineNumber, column: position.column }], 'debugEditorActions.runToCursorAction')).then((breakpoints) => {
|
||||
if (breakpoints && breakpoints.length) {
|
||||
breakpointToRemove = breakpoints[0];
|
||||
}
|
||||
debugService.getViewModel().focusedThread!.continue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SelectionToReplAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.debug.action.selectionToRepl',
|
||||
label: nls.localize('debugEvaluate', "Debug: Evaluate"),
|
||||
alias: 'Debug: Evaluate',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus),
|
||||
menuOpts: {
|
||||
group: 'debug',
|
||||
order: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const panelService = accessor.get(IPanelService);
|
||||
const viewModel = debugService.getViewModel();
|
||||
const session = viewModel.focusedSession;
|
||||
if (!editor.hasModel() || !session) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const text = editor.getModel().getValueInRange(editor.getSelection());
|
||||
return session.addReplExpression(viewModel.focusedStackFrame!, text)
|
||||
.then(() => panelService.openPanel(REPL_ID, true))
|
||||
.then(_ => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectionToWatchExpressionsAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.debug.action.selectionToWatch',
|
||||
label: nls.localize('debugAddToWatch', "Debug: Add to Watch"),
|
||||
alias: 'Debug: Add to Watch',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus),
|
||||
menuOpts: {
|
||||
group: 'debug',
|
||||
order: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const viewletService = accessor.get(IViewletService);
|
||||
if (!editor.hasModel()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const text = editor.getModel().getValueInRange(editor.getSelection());
|
||||
return viewletService.openViewlet(VIEWLET_ID).then(() => debugService.addWatchExpression(text));
|
||||
}
|
||||
}
|
||||
|
||||
class ShowDebugHoverAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.debug.action.showDebugHover',
|
||||
label: nls.localize('showDebugHover', "Debug: Show Hover"),
|
||||
alias: 'Debug: Show Hover',
|
||||
precondition: CONTEXT_IN_DEBUG_MODE,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_I),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const position = editor.getPosition();
|
||||
if (!position || !editor.hasModel()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const word = editor.getModel().getWordAtPosition(position);
|
||||
if (!word) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const range = new Range(position.lineNumber, position.column, position.lineNumber, word.endColumn);
|
||||
return editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showHover(range, true);
|
||||
}
|
||||
}
|
||||
|
||||
class GoToBreakpointAction extends EditorAction {
|
||||
constructor(private isNext: boolean, opts: IActionOptions) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise<any> {
|
||||
const debugService = accessor.get(IDebugService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
if (editor.hasModel()) {
|
||||
const currentUri = editor.getModel().uri;
|
||||
const currentLine = editor.getPosition().lineNumber;
|
||||
//Breakpoints returned from `getBreakpoints` are already sorted.
|
||||
const allEnabledBreakpoints = debugService.getModel().getBreakpoints({ enabledOnly: true });
|
||||
|
||||
//Try to find breakpoint in current file
|
||||
let moveBreakpoint =
|
||||
this.isNext
|
||||
? allEnabledBreakpoints.filter(bp => bp.uri.toString() === currentUri.toString() && bp.lineNumber > currentLine).shift()
|
||||
: allEnabledBreakpoints.filter(bp => bp.uri.toString() === currentUri.toString() && bp.lineNumber < currentLine).pop();
|
||||
|
||||
//Try to find breakpoints in following files
|
||||
if (!moveBreakpoint) {
|
||||
moveBreakpoint =
|
||||
this.isNext
|
||||
? allEnabledBreakpoints.filter(bp => bp.uri.toString() > currentUri.toString()).shift()
|
||||
: allEnabledBreakpoints.filter(bp => bp.uri.toString() < currentUri.toString()).pop();
|
||||
}
|
||||
|
||||
//Move to first or last possible breakpoint
|
||||
if (!moveBreakpoint && allEnabledBreakpoints.length) {
|
||||
moveBreakpoint = this.isNext ? allEnabledBreakpoints[0] : allEnabledBreakpoints[allEnabledBreakpoints.length - 1];
|
||||
}
|
||||
|
||||
if (moveBreakpoint) {
|
||||
return openBreakpointSource(moveBreakpoint, false, true, debugService, editorService);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
class GoToNextBreakpointAction extends GoToBreakpointAction {
|
||||
constructor() {
|
||||
super(true, {
|
||||
id: 'editor.debug.action.goToNextBreakpoint',
|
||||
label: nls.localize('goToNextBreakpoint', "Debug: Go To Next Breakpoint"),
|
||||
alias: 'Debug: Go To Next Breakpoint',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class GoToPreviousBreakpointAction extends GoToBreakpointAction {
|
||||
constructor() {
|
||||
super(false, {
|
||||
id: 'editor.debug.action.goToPreviousBreakpoint',
|
||||
label: nls.localize('goToPreviousBreakpoint', "Debug: Go To Previous Breakpoint"),
|
||||
alias: 'Debug: Go To Previous Breakpoint',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(ToggleBreakpointAction);
|
||||
registerEditorAction(ConditionalBreakpointAction);
|
||||
registerEditorAction(LogPointAction);
|
||||
registerEditorAction(RunToCursorAction);
|
||||
registerEditorAction(SelectionToReplAction);
|
||||
registerEditorAction(SelectionToWatchExpressionsAction);
|
||||
registerEditorAction(ShowDebugHoverAction);
|
||||
registerEditorAction(GoToNextBreakpointAction);
|
||||
registerEditorAction(GoToPreviousBreakpointAction);
|
||||
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
|
||||
command: {
|
||||
id: RunToCursorAction.ID,
|
||||
title: { value: RunToCursorAction.LABEL, original: 'Debug: Run to Cursor' },
|
||||
category: nls.localize('debug', "Debug")
|
||||
},
|
||||
group: 'debug',
|
||||
when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')),
|
||||
});
|
||||
@@ -0,0 +1,776 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import * as env from 'vs/base/common/platform';
|
||||
import { URI as uri } from 'vs/base/common/uri';
|
||||
import { visit } from 'vs/base/common/json';
|
||||
import severity from 'vs/base/common/severity';
|
||||
import { Constants } from 'vs/editor/common/core/uint';
|
||||
import { IAction, Action } from 'vs/base/common/actions';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { StandardTokenType } from 'vs/editor/common/modes';
|
||||
import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/model/wordHelper';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { IDecorationOptions } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { RemoveBreakpointAction } from 'vs/workbench/contrib/debug/browser/debugActions';
|
||||
import { IDebugEditorContribution, IDebugService, State, IBreakpoint, EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, IStackFrame, IDebugConfiguration, IExpression, IExceptionInfo, BreakpointWidgetContext } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWidget';
|
||||
import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
|
||||
import { first } from 'vs/base/common/arrays';
|
||||
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ContextSubMenu } from 'vs/base/browser/contextmenu';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { getHover } from 'vs/editor/contrib/hover/getHover';
|
||||
import { IEditorHoverOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { BreakpointWidget } from 'vs/workbench/contrib/debug/browser/breakpointWidget';
|
||||
import { DebugHoverWidget } from 'vs/workbench/contrib/debug/browser/debugHover';
|
||||
|
||||
const HOVER_DELAY = 300;
|
||||
const LAUNCH_JSON_REGEX = /launch\.json$/;
|
||||
const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration';
|
||||
const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons
|
||||
const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added
|
||||
const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped
|
||||
|
||||
export class DebugEditorContribution implements IDebugEditorContribution {
|
||||
|
||||
private toDispose: lifecycle.IDisposable[];
|
||||
private hoverWidget: DebugHoverWidget;
|
||||
private nonDebugHoverPosition: Position;
|
||||
private hoverRange: Range;
|
||||
private mouseDown = false;
|
||||
|
||||
private breakpointHintDecoration: string[];
|
||||
private breakpointWidget: BreakpointWidget | undefined;
|
||||
private breakpointWidgetVisible: IContextKey<boolean>;
|
||||
private wordToLineNumbersMap: Map<string, Position[]> | undefined;
|
||||
|
||||
private exceptionWidget: ExceptionWidget | undefined;
|
||||
|
||||
private configurationWidget: FloatingClickWidget;
|
||||
|
||||
constructor(
|
||||
private editor: ICodeEditor,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
) {
|
||||
this.breakpointHintDecoration = [];
|
||||
this.hoverWidget = this.instantiationService.createInstance(DebugHoverWidget, this.editor);
|
||||
this.toDispose = [];
|
||||
this.registerListeners();
|
||||
this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService);
|
||||
this.updateConfigurationWidgetVisibility();
|
||||
this.codeEditorService.registerDecorationType(INLINE_VALUE_DECORATION_KEY, {});
|
||||
this.toggleExceptionWidget();
|
||||
}
|
||||
|
||||
private getContextMenuActions(breakpoints: ReadonlyArray<IBreakpoint>, uri: uri, lineNumber: number): Array<IAction | ContextSubMenu> {
|
||||
const actions: Array<IAction | ContextSubMenu> = [];
|
||||
if (breakpoints.length === 1) {
|
||||
const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint");
|
||||
actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService, this.keybindingService));
|
||||
actions.push(new Action(
|
||||
'workbench.debug.action.editBreakpointAction',
|
||||
nls.localize('editBreakpoint', "Edit {0}...", breakpointType),
|
||||
undefined,
|
||||
true,
|
||||
() => Promise.resolve(this.editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(breakpoints[0].lineNumber, breakpoints[0].column))
|
||||
));
|
||||
|
||||
actions.push(new Action(
|
||||
`workbench.debug.viewlet.action.toggleBreakpoint`,
|
||||
breakpoints[0].enabled ? nls.localize('disableBreakpoint', "Disable {0}", breakpointType) : nls.localize('enableBreakpoint', "Enable {0}", breakpointType),
|
||||
undefined,
|
||||
true,
|
||||
() => this.debugService.enableOrDisableBreakpoints(!breakpoints[0].enabled, breakpoints[0])
|
||||
));
|
||||
} else if (breakpoints.length > 1) {
|
||||
const sorted = breakpoints.slice().sort((first, second) => (first.column && second.column) ? first.column - second.column : 1);
|
||||
actions.push(new ContextSubMenu(nls.localize('removeBreakpoints', "Remove Breakpoints"), sorted.map(bp => new Action(
|
||||
'removeInlineBreakpoint',
|
||||
bp.column ? nls.localize('removeInlineBreakpointOnColumn', "Remove Inline Breakpoint on Column {0}", bp.column) : nls.localize('removeLineBreakpoint', "Remove Line Breakpoint"),
|
||||
undefined,
|
||||
true,
|
||||
() => this.debugService.removeBreakpoints(bp.getId())
|
||||
))));
|
||||
|
||||
actions.push(new ContextSubMenu(nls.localize('editBreakpoints', "Edit Breakpoints"), sorted.map(bp =>
|
||||
new Action('editBreakpoint',
|
||||
bp.column ? nls.localize('editInlineBreakpointOnColumn', "Edit Inline Breakpoint on Column {0}", bp.column) : nls.localize('editLineBrekapoint', "Edit Line Breakpoint"),
|
||||
undefined,
|
||||
true,
|
||||
() => Promise.resolve(this.editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(bp.lineNumber, bp.column))
|
||||
)
|
||||
)));
|
||||
|
||||
actions.push(new ContextSubMenu(nls.localize('enableDisableBreakpoints', "Enable/Disable Breakpoints"), sorted.map(bp => new Action(
|
||||
bp.enabled ? 'disableColumnBreakpoint' : 'enableColumnBreakpoint',
|
||||
bp.enabled ? (bp.column ? nls.localize('disableInlineColumnBreakpoint', "Disable Inline Breakpoint on Column {0}", bp.column) : nls.localize('disableBreakpointOnLine', "Disable Line Breakpoint"))
|
||||
: (bp.column ? nls.localize('enableBreakpoints', "Enable Inline Breakpoint on Column {0}", bp.column) : nls.localize('enableBreakpointOnLine', "Enable Line Breakpoint")),
|
||||
undefined,
|
||||
true,
|
||||
() => this.debugService.enableOrDisableBreakpoints(!bp.enabled, bp)
|
||||
))));
|
||||
} else {
|
||||
actions.push(new Action(
|
||||
'addBreakpoint',
|
||||
nls.localize('addBreakpoint', "Add Breakpoint"),
|
||||
undefined,
|
||||
true,
|
||||
() => this.debugService.addBreakpoints(uri, [{ lineNumber }], `debugEditorContextMenu`)
|
||||
));
|
||||
actions.push(new Action(
|
||||
'addConditionalBreakpoint',
|
||||
nls.localize('addConditionalBreakpoint', "Add Conditional Breakpoint..."),
|
||||
undefined,
|
||||
true,
|
||||
() => Promise.resolve(this.editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(lineNumber, undefined))
|
||||
));
|
||||
actions.push(new Action(
|
||||
'addLogPoint',
|
||||
nls.localize('addLogPoint', "Add Logpoint..."),
|
||||
undefined,
|
||||
true,
|
||||
() => Promise.resolve(this.editor.getContribution<IDebugEditorContribution>(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(lineNumber, undefined, BreakpointWidgetContext.LOG_MESSAGE))
|
||||
));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.toDispose.push(this.editor.onMouseDown((e: IEditorMouseEvent) => {
|
||||
const data = e.target.detail as IMarginData;
|
||||
const model = this.editor.getModel();
|
||||
if (!e.target.position || !model || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || data.isAfterLines || !this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) {
|
||||
return;
|
||||
}
|
||||
const canSetBreakpoints = this.debugService.getConfigurationManager().canSetBreakpointsIn(model);
|
||||
const lineNumber = e.target.position.lineNumber;
|
||||
const uri = model.uri;
|
||||
|
||||
if (e.event.rightButton || (env.isMacintosh && e.event.leftButton && e.event.ctrlKey)) {
|
||||
if (!canSetBreakpoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = { x: e.event.posx, y: e.event.posy };
|
||||
const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri });
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => this.getContextMenuActions(breakpoints, uri, lineNumber),
|
||||
getActionsContext: () => breakpoints.length ? breakpoints[0] : undefined
|
||||
});
|
||||
} else {
|
||||
const breakpoints = this.debugService.getModel().getBreakpoints({ uri, lineNumber });
|
||||
|
||||
if (breakpoints.length) {
|
||||
// Show the dialog if there is a potential condition to be accidently lost.
|
||||
// Do not show dialog on linux due to electron issue freezing the mouse #50026
|
||||
if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) {
|
||||
const logPoint = breakpoints.every(bp => !!bp.logMessage);
|
||||
const breakpointType = logPoint ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint");
|
||||
const disable = breakpoints.some(bp => bp.enabled);
|
||||
|
||||
const enabling = nls.localize('breakpointHasConditionDisabled',
|
||||
"This {0} has a {1} that will get lost on remove. Consider enabling the {0} instead.",
|
||||
breakpointType.toLowerCase(),
|
||||
logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition")
|
||||
);
|
||||
const disabling = nls.localize('breakpointHasConditionEnabled',
|
||||
"This {0} has a {1} that will get lost on remove. Consider disabling the {0} instead.",
|
||||
breakpointType.toLowerCase(),
|
||||
logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition")
|
||||
);
|
||||
|
||||
this.dialogService.show(severity.Info, disable ? disabling : enabling, [
|
||||
nls.localize('removeLogPoint', "Remove {0}", breakpointType),
|
||||
nls.localize('disableLogPoint', "{0} {1}", disable ? nls.localize('disable', "Disable") : nls.localize('enable', "Enable"), breakpointType),
|
||||
nls.localize('cancel', "Cancel")
|
||||
], { cancelId: 2 }).then(choice => {
|
||||
if (choice === 0) {
|
||||
breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId()));
|
||||
}
|
||||
if (choice === 1) {
|
||||
breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!disable, bp));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId()));
|
||||
}
|
||||
} else if (canSetBreakpoints) {
|
||||
this.debugService.addBreakpoints(uri, [{ lineNumber }], `debugEditorGutter`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => {
|
||||
let showBreakpointHintAtLineNumber = -1;
|
||||
const model = this.editor.getModel();
|
||||
if (model && e.target.position && e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN && this.debugService.getConfigurationManager().canSetBreakpointsIn(model) &&
|
||||
this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) {
|
||||
const data = e.target.detail as IMarginData;
|
||||
if (!data.isAfterLines) {
|
||||
showBreakpointHintAtLineNumber = e.target.position.lineNumber;
|
||||
}
|
||||
}
|
||||
this.ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber);
|
||||
}));
|
||||
this.toDispose.push(this.editor.onMouseLeave((e: IEditorMouseEvent) => {
|
||||
this.ensureBreakpointHintDecoration(-1);
|
||||
}));
|
||||
this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(e => this.onFocusStackFrame(e.stackFrame)));
|
||||
|
||||
// hover listeners & hover widget
|
||||
this.toDispose.push(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e)));
|
||||
this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false));
|
||||
this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e)));
|
||||
this.toDispose.push(this.editor.onMouseLeave((e: IEditorMouseEvent) => {
|
||||
this.provideNonDebugHoverScheduler.cancel();
|
||||
const hoverDomNode = this.hoverWidget.getDomNode();
|
||||
if (!hoverDomNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = hoverDomNode.getBoundingClientRect();
|
||||
// Only hide the hover widget if the editor mouse leave event is outside the hover widget #3528
|
||||
if (e.event.posx < rect.left || e.event.posx > rect.right || e.event.posy < rect.top || e.event.posy > rect.bottom) {
|
||||
this.hideHoverWidget();
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e)));
|
||||
this.toDispose.push(this.editor.onDidChangeModelContent(() => {
|
||||
this.wordToLineNumbersMap = undefined;
|
||||
this.updateInlineValuesScheduler.schedule();
|
||||
}));
|
||||
this.toDispose.push(this.editor.onDidChangeModel(() => {
|
||||
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
this._applyHoverConfiguration(model, stackFrame);
|
||||
}
|
||||
this.closeBreakpointWidget();
|
||||
this.toggleExceptionWidget();
|
||||
this.hideHoverWidget();
|
||||
this.updateConfigurationWidgetVisibility();
|
||||
this.wordToLineNumbersMap = undefined;
|
||||
this.updateInlineValueDecorations(stackFrame);
|
||||
}));
|
||||
this.toDispose.push(this.editor.onDidScrollChange(() => this.hideHoverWidget));
|
||||
this.toDispose.push(this.debugService.onDidChangeState((state: State) => {
|
||||
if (state !== State.Stopped) {
|
||||
this.toggleExceptionWidget();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _applyHoverConfiguration(model: ITextModel, stackFrame: IStackFrame | undefined): void {
|
||||
if (stackFrame && model.uri.toString() === stackFrame.source.uri.toString()) {
|
||||
this.editor.updateOptions({
|
||||
hover: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let overrides = {
|
||||
resource: model.uri,
|
||||
overrideIdentifier: model.getLanguageIdentifier().language
|
||||
};
|
||||
const defaultConfiguration = this.configurationService.getValue<IEditorHoverOptions>('editor.hover', overrides);
|
||||
this.editor.updateOptions({
|
||||
hover: {
|
||||
enabled: defaultConfiguration.enabled,
|
||||
delay: defaultConfiguration.delay,
|
||||
sticky: defaultConfiguration.sticky
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return EDITOR_CONTRIBUTION_ID;
|
||||
}
|
||||
|
||||
public showHover(range: Range, focus: boolean): Promise<void> {
|
||||
const sf = this.debugService.getViewModel().focusedStackFrame;
|
||||
const model = this.editor.getModel();
|
||||
if (sf && model && sf.source.uri.toString() === model.uri.toString()) {
|
||||
return this.hoverWidget.showAt(range, focus);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private marginFreeFromNonDebugDecorations(line: number): boolean {
|
||||
const decorations = this.editor.getLineDecorations(line);
|
||||
if (decorations) {
|
||||
for (const { options } of decorations) {
|
||||
if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('debug') === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber: number): void {
|
||||
const newDecoration: IModelDeltaDecoration[] = [];
|
||||
if (showBreakpointHintAtLineNumber !== -1) {
|
||||
newDecoration.push({
|
||||
options: DebugEditorContribution.BREAKPOINT_HELPER_DECORATION,
|
||||
range: {
|
||||
startLineNumber: showBreakpointHintAtLineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: showBreakpointHintAtLineNumber,
|
||||
endColumn: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.breakpointHintDecoration = this.editor.deltaDecorations(this.breakpointHintDecoration, newDecoration);
|
||||
}
|
||||
|
||||
private onFocusStackFrame(sf: IStackFrame | undefined): void {
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
this._applyHoverConfiguration(model, sf);
|
||||
if (sf && sf.source.uri.toString() === model.uri.toString()) {
|
||||
this.toggleExceptionWidget();
|
||||
} else {
|
||||
this.hideHoverWidget();
|
||||
}
|
||||
}
|
||||
|
||||
this.updateInlineValueDecorations(sf);
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get showHoverScheduler(): RunOnceScheduler {
|
||||
const scheduler = new RunOnceScheduler(() => this.showHover(this.hoverRange, false), HOVER_DELAY);
|
||||
this.toDispose.push(scheduler);
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get hideHoverScheduler(): RunOnceScheduler {
|
||||
const scheduler = new RunOnceScheduler(() => this.hoverWidget.hide(), 2 * HOVER_DELAY);
|
||||
this.toDispose.push(scheduler);
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get provideNonDebugHoverScheduler(): RunOnceScheduler {
|
||||
const scheduler = new RunOnceScheduler(() => {
|
||||
if (this.editor.hasModel()) {
|
||||
getHover(this.editor.getModel(), this.nonDebugHoverPosition, CancellationToken.None);
|
||||
}
|
||||
}, HOVER_DELAY);
|
||||
this.toDispose.push(scheduler);
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
private hideHoverWidget(): void {
|
||||
if (!this.hideHoverScheduler.isScheduled() && this.hoverWidget.isVisible()) {
|
||||
this.hideHoverScheduler.schedule();
|
||||
}
|
||||
this.showHoverScheduler.cancel();
|
||||
this.provideNonDebugHoverScheduler.cancel();
|
||||
}
|
||||
|
||||
// hover business
|
||||
|
||||
private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {
|
||||
this.mouseDown = true;
|
||||
if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === DebugHoverWidget.ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideHoverWidget();
|
||||
}
|
||||
|
||||
private onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {
|
||||
if (this.debugService.state !== State.Stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.configurationService.getValue<IDebugConfiguration>('debug').enableAllHovers && mouseEvent.target.position) {
|
||||
this.nonDebugHoverPosition = mouseEvent.target.position;
|
||||
this.provideNonDebugHoverScheduler.schedule();
|
||||
}
|
||||
const targetType = mouseEvent.target.type;
|
||||
const stopKey = env.isMacintosh ? 'metaKey' : 'ctrlKey';
|
||||
|
||||
if (targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === DebugHoverWidget.ID && !(<any>mouseEvent.event)[stopKey]) {
|
||||
// mouse moved on top of debug hover widget
|
||||
return;
|
||||
}
|
||||
if (targetType === MouseTargetType.CONTENT_TEXT) {
|
||||
if (mouseEvent.target.range && !mouseEvent.target.range.equalsRange(this.hoverRange)) {
|
||||
this.hoverRange = mouseEvent.target.range;
|
||||
this.showHoverScheduler.schedule();
|
||||
}
|
||||
} else if (!this.mouseDown) {
|
||||
// Do not hide debug hover when the mouse is pressed because it usually leads to accidental closing #64620
|
||||
this.hideHoverWidget();
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyDown(e: IKeyboardEvent): void {
|
||||
const stopKey = env.isMacintosh ? KeyCode.Meta : KeyCode.Ctrl;
|
||||
if (e.keyCode !== stopKey) {
|
||||
// do not hide hover when Ctrl/Meta is pressed
|
||||
this.hideHoverWidget();
|
||||
}
|
||||
}
|
||||
|
||||
// end hover business
|
||||
|
||||
// breakpoint widget
|
||||
public showBreakpointWidget(lineNumber: number, column: number, context?: BreakpointWidgetContext): void {
|
||||
if (this.breakpointWidget) {
|
||||
this.breakpointWidget.dispose();
|
||||
}
|
||||
|
||||
this.breakpointWidget = this.instantiationService.createInstance(BreakpointWidget, this.editor, lineNumber, context);
|
||||
this.breakpointWidget.show({ lineNumber, column: 1 }, 2);
|
||||
this.breakpointWidgetVisible.set(true);
|
||||
}
|
||||
|
||||
public closeBreakpointWidget(): void {
|
||||
if (this.breakpointWidget) {
|
||||
this.breakpointWidget.dispose();
|
||||
this.breakpointWidget = undefined;
|
||||
this.breakpointWidgetVisible.reset();
|
||||
this.editor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// exception widget
|
||||
private toggleExceptionWidget(): void {
|
||||
// Toggles exception widget based on the state of the current editor model and debug stack frame
|
||||
const model = this.editor.getModel();
|
||||
const focusedSf = this.debugService.getViewModel().focusedStackFrame;
|
||||
const callStack = focusedSf ? focusedSf.thread.getCallStack() : null;
|
||||
if (!model || !focusedSf || !callStack || callStack.length === 0) {
|
||||
this.closeExceptionWidget();
|
||||
return;
|
||||
}
|
||||
|
||||
// First call stack frame that is available is the frame where exception has been thrown
|
||||
const exceptionSf = first(callStack, sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'), undefined);
|
||||
if (!exceptionSf || exceptionSf !== focusedSf) {
|
||||
this.closeExceptionWidget();
|
||||
return;
|
||||
}
|
||||
|
||||
const sameUri = exceptionSf.source.uri.toString() === model.uri.toString();
|
||||
if (this.exceptionWidget && !sameUri) {
|
||||
this.closeExceptionWidget();
|
||||
} else if (sameUri) {
|
||||
focusedSf.thread.exceptionInfo.then(exceptionInfo => {
|
||||
if (exceptionInfo && exceptionSf.range.startLineNumber && exceptionSf.range.startColumn) {
|
||||
this.showExceptionWidget(exceptionInfo, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private showExceptionWidget(exceptionInfo: IExceptionInfo, lineNumber: number, column: number): void {
|
||||
if (this.exceptionWidget) {
|
||||
this.exceptionWidget.dispose();
|
||||
}
|
||||
|
||||
this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo);
|
||||
this.exceptionWidget.show({ lineNumber, column }, 0);
|
||||
}
|
||||
|
||||
private closeExceptionWidget(): void {
|
||||
if (this.exceptionWidget) {
|
||||
this.exceptionWidget.dispose();
|
||||
this.exceptionWidget = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// configuration widget
|
||||
private updateConfigurationWidgetVisibility(): void {
|
||||
const model = this.editor.getModel();
|
||||
if (this.configurationWidget) {
|
||||
this.configurationWidget.dispose();
|
||||
}
|
||||
if (model && LAUNCH_JSON_REGEX.test(model.uri.toString()) && !this.editor.getConfiguration().readOnly) {
|
||||
this.configurationWidget = this.instantiationService.createInstance(FloatingClickWidget, this.editor, nls.localize('addConfiguration', "Add Configuration..."), null);
|
||||
this.configurationWidget.render();
|
||||
this.toDispose.push(this.configurationWidget.onClick(() => this.addLaunchConfiguration()));
|
||||
}
|
||||
}
|
||||
|
||||
public addLaunchConfiguration(): Promise<any> {
|
||||
/* __GDPR__
|
||||
"debug/addLaunchConfiguration" : {}
|
||||
*/
|
||||
this.telemetryService.publicLog('debug/addLaunchConfiguration');
|
||||
let configurationsArrayPosition: Position | undefined;
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let depthInArray = 0;
|
||||
let lastProperty: string;
|
||||
|
||||
visit(model.getValue(), {
|
||||
onObjectProperty: (property, offset, length) => {
|
||||
lastProperty = property;
|
||||
},
|
||||
onArrayBegin: (offset: number, length: number) => {
|
||||
if (lastProperty === 'configurations' && depthInArray === 0) {
|
||||
configurationsArrayPosition = model.getPositionAt(offset + 1);
|
||||
}
|
||||
depthInArray++;
|
||||
},
|
||||
onArrayEnd: () => {
|
||||
depthInArray--;
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.focus();
|
||||
if (!configurationsArrayPosition) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const insertLine = (position: Position): Promise<any> => {
|
||||
// Check if there are more characters on a line after a "configurations": [, if yes enter a newline
|
||||
if (model.getLineLastNonWhitespaceColumn(position.lineNumber) > position.column) {
|
||||
this.editor.setPosition(position);
|
||||
CoreEditingCommands.LineBreakInsert.runEditorCommand(null, this.editor, null);
|
||||
}
|
||||
this.editor.setPosition(position);
|
||||
return this.commandService.executeCommand('editor.action.insertLineAfter');
|
||||
};
|
||||
|
||||
return insertLine(configurationsArrayPosition).then(() => this.commandService.executeCommand('editor.action.triggerSuggest'));
|
||||
}
|
||||
|
||||
private static BREAKPOINT_HELPER_DECORATION: IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'debug-breakpoint-hint',
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
|
||||
};
|
||||
|
||||
// Inline Decorations
|
||||
|
||||
@memoize
|
||||
private get removeInlineValuesScheduler(): RunOnceScheduler {
|
||||
return new RunOnceScheduler(
|
||||
() => this.editor.removeDecorations(INLINE_VALUE_DECORATION_KEY),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get updateInlineValuesScheduler(): RunOnceScheduler {
|
||||
return new RunOnceScheduler(
|
||||
() => this.updateInlineValueDecorations(this.debugService.getViewModel().focusedStackFrame),
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
private updateInlineValueDecorations(stackFrame: IStackFrame | undefined): void {
|
||||
const model = this.editor.getModel();
|
||||
if (!this.configurationService.getValue<IDebugConfiguration>('debug').inlineValues ||
|
||||
!model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) {
|
||||
if (!this.removeInlineValuesScheduler.isScheduled()) {
|
||||
this.removeInlineValuesScheduler.schedule();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeInlineValuesScheduler.cancel();
|
||||
|
||||
stackFrame.getMostSpecificScopes(stackFrame.range)
|
||||
// Get all top level children in the scope chain
|
||||
.then(scopes => Promise.all(scopes.map(scope => scope.getChildren()
|
||||
.then(children => {
|
||||
let range = new Range(0, 0, stackFrame.range.startLineNumber, stackFrame.range.startColumn);
|
||||
if (scope.range) {
|
||||
range = range.setStartPosition(scope.range.startLineNumber, scope.range.startColumn);
|
||||
}
|
||||
|
||||
return this.createInlineValueDecorationsInsideRange(children, range, model);
|
||||
}))).then(decorationsPerScope => {
|
||||
const allDecorations = decorationsPerScope.reduce((previous, current) => previous.concat(current), []);
|
||||
this.editor.setDecorations(INLINE_VALUE_DECORATION_KEY, allDecorations);
|
||||
}));
|
||||
}
|
||||
|
||||
private createInlineValueDecorationsInsideRange(expressions: ReadonlyArray<IExpression>, range: Range, model: ITextModel): IDecorationOptions[] {
|
||||
const nameValueMap = new Map<string, string>();
|
||||
for (let expr of expressions) {
|
||||
nameValueMap.set(expr.name, expr.value);
|
||||
// Limit the size of map. Too large can have a perf impact
|
||||
if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();
|
||||
const wordToPositionsMap = this.getWordToPositionsMap();
|
||||
|
||||
// Compute unique set of names on each line
|
||||
nameValueMap.forEach((value, name) => {
|
||||
const positions = wordToPositionsMap.get(name);
|
||||
if (positions) {
|
||||
for (let position of positions) {
|
||||
if (range.containsPosition(position)) {
|
||||
if (!lineToNamesMap.has(position.lineNumber)) {
|
||||
lineToNamesMap.set(position.lineNumber, []);
|
||||
}
|
||||
|
||||
if (lineToNamesMap.get(position.lineNumber)!.indexOf(name) === -1) {
|
||||
lineToNamesMap.get(position.lineNumber)!.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const decorations: IDecorationOptions[] = [];
|
||||
// Compute decorators for each line
|
||||
lineToNamesMap.forEach((names, line) => {
|
||||
const contentText = names.sort((first, second) => {
|
||||
const content = model.getLineContent(line);
|
||||
return content.indexOf(first) - content.indexOf(second);
|
||||
}).map(name => `${name} = ${nameValueMap.get(name)}`).join(', ');
|
||||
decorations.push(this.createInlineValueDecoration(line, contentText));
|
||||
});
|
||||
|
||||
return decorations;
|
||||
}
|
||||
|
||||
private createInlineValueDecoration(lineNumber: number, contentText: string): IDecorationOptions {
|
||||
// If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line
|
||||
if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) {
|
||||
contentText = contentText.substr(0, MAX_INLINE_DECORATOR_LENGTH) + '...';
|
||||
}
|
||||
|
||||
return {
|
||||
range: {
|
||||
startLineNumber: lineNumber,
|
||||
endLineNumber: lineNumber,
|
||||
startColumn: Constants.MAX_SAFE_SMALL_INTEGER,
|
||||
endColumn: Constants.MAX_SAFE_SMALL_INTEGER
|
||||
},
|
||||
renderOptions: {
|
||||
after: {
|
||||
contentText,
|
||||
backgroundColor: 'rgba(255, 200, 0, 0.2)',
|
||||
margin: '10px'
|
||||
},
|
||||
dark: {
|
||||
after: {
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
}
|
||||
},
|
||||
light: {
|
||||
after: {
|
||||
color: 'rgba(0, 0, 0, 0.5)',
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getWordToPositionsMap(): Map<string, Position[]> {
|
||||
if (!this.wordToLineNumbersMap) {
|
||||
this.wordToLineNumbersMap = new Map<string, Position[]>();
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return this.wordToLineNumbersMap;
|
||||
}
|
||||
|
||||
// For every word in every line, map its ranges for fast lookup
|
||||
for (let lineNumber = 1, len = model.getLineCount(); lineNumber <= len; ++lineNumber) {
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
|
||||
// If line is too long then skip the line
|
||||
if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
model.forceTokenization(lineNumber);
|
||||
const lineTokens = model.getLineTokens(lineNumber);
|
||||
for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) {
|
||||
const tokenStartOffset = lineTokens.getStartOffset(tokenIndex);
|
||||
const tokenEndOffset = lineTokens.getEndOffset(tokenIndex);
|
||||
const tokenType = lineTokens.getStandardTokenType(tokenIndex);
|
||||
const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset);
|
||||
|
||||
// Token is a word and not a comment
|
||||
if (tokenType === StandardTokenType.Other) {
|
||||
DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match
|
||||
const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr);
|
||||
|
||||
if (wordMatch) {
|
||||
const word = wordMatch[0];
|
||||
if (!this.wordToLineNumbersMap.has(word)) {
|
||||
this.wordToLineNumbersMap.set(word, []);
|
||||
}
|
||||
|
||||
this.wordToLineNumbersMap.get(word)!.push(new Position(lineNumber, tokenStartOffset));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.wordToLineNumbersMap;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.breakpointWidget) {
|
||||
this.breakpointWidget.dispose();
|
||||
}
|
||||
if (this.hoverWidget) {
|
||||
this.hoverWidget.dispose();
|
||||
}
|
||||
if (this.configurationWidget) {
|
||||
this.configurationWidget.dispose();
|
||||
}
|
||||
this.toDispose = lifecycle.dispose(this.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(DebugEditorContribution);
|
||||
@@ -0,0 +1,333 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import { Constants } from 'vs/editor/common/core/uint';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationOptions } from 'vs/editor/common/model';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IDebugService, IBreakpoint, State, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { getBreakpointMessageAndClassName } from 'vs/workbench/contrib/debug/browser/breakpointsView';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
interface IBreakpointDecoration {
|
||||
decorationId: string;
|
||||
modelId: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
interface IDebugEditorModelData {
|
||||
model: ITextModel;
|
||||
toDispose: lifecycle.IDisposable[];
|
||||
breakpointDecorations: IBreakpointDecoration[];
|
||||
currentStackDecorations: string[];
|
||||
topStackFrameRange: Range | undefined;
|
||||
}
|
||||
|
||||
export class DebugEditorModelManager implements IWorkbenchContribution {
|
||||
static readonly ID = 'breakpointManager';
|
||||
static readonly STICKINESS = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges;
|
||||
private modelDataMap: Map<string, IDebugEditorModelData>;
|
||||
private toDispose: lifecycle.IDisposable[];
|
||||
private ignoreDecorationsChangedEvent: boolean;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
) {
|
||||
this.modelDataMap = new Map<string, IDebugEditorModelData>();
|
||||
this.toDispose = [];
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.modelDataMap.forEach(modelData => {
|
||||
lifecycle.dispose(modelData.toDispose);
|
||||
modelData.model.deltaDecorations(modelData.breakpointDecorations.map(bpd => bpd.decorationId), []);
|
||||
modelData.model.deltaDecorations(modelData.currentStackDecorations, []);
|
||||
});
|
||||
this.toDispose = lifecycle.dispose(this.toDispose);
|
||||
|
||||
this.modelDataMap.clear();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.toDispose.push(this.modelService.onModelAdded(this.onModelAdded, this));
|
||||
this.modelService.getModels().forEach(model => this.onModelAdded(model));
|
||||
this.toDispose.push(this.modelService.onModelRemoved(this.onModelRemoved, this));
|
||||
|
||||
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange()));
|
||||
this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(() => this.onFocusStackFrame()));
|
||||
this.toDispose.push(this.debugService.onDidChangeState(state => {
|
||||
if (state === State.Inactive) {
|
||||
this.modelDataMap.forEach(modelData => {
|
||||
modelData.topStackFrameRange = undefined;
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private onModelAdded(model: ITextModel): void {
|
||||
const modelUriStr = model.uri.toString();
|
||||
const breakpoints = this.debugService.getModel().getBreakpoints({ uri: model.uri });
|
||||
|
||||
const currentStackDecorations = model.deltaDecorations([], this.createCallStackDecorations(modelUriStr));
|
||||
const desiredDecorations = this.createBreakpointDecorations(model, breakpoints);
|
||||
const breakpointDecorationIds = model.deltaDecorations([], desiredDecorations);
|
||||
const toDispose: lifecycle.IDisposable[] = [model.onDidChangeDecorations((e) => this.onModelDecorationsChanged(modelUriStr))];
|
||||
|
||||
this.modelDataMap.set(modelUriStr, {
|
||||
model: model,
|
||||
toDispose: toDispose,
|
||||
breakpointDecorations: breakpointDecorationIds.map((decorationId, index) => ({ decorationId, modelId: breakpoints[index].getId(), range: desiredDecorations[index].range })),
|
||||
currentStackDecorations: currentStackDecorations,
|
||||
topStackFrameRange: undefined
|
||||
});
|
||||
}
|
||||
|
||||
private onModelRemoved(model: ITextModel): void {
|
||||
const modelUriStr = model.uri.toString();
|
||||
const data = this.modelDataMap.get(modelUriStr);
|
||||
if (data) {
|
||||
lifecycle.dispose(data.toDispose);
|
||||
this.modelDataMap.delete(modelUriStr);
|
||||
}
|
||||
}
|
||||
|
||||
// call stack management. Represent data coming from the debug service.
|
||||
|
||||
private onFocusStackFrame(): void {
|
||||
this.modelDataMap.forEach((modelData, uri) => {
|
||||
modelData.currentStackDecorations = modelData.model.deltaDecorations(modelData.currentStackDecorations, this.createCallStackDecorations(uri));
|
||||
});
|
||||
}
|
||||
|
||||
private createCallStackDecorations(modelUriStr: string): IModelDeltaDecoration[] {
|
||||
const result: IModelDeltaDecoration[] = [];
|
||||
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
|
||||
if (!stackFrame || stackFrame.source.uri.toString() !== modelUriStr) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// only show decorations for the currently focused thread.
|
||||
const columnUntilEOLRange = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER);
|
||||
const range = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1);
|
||||
|
||||
// compute how to decorate the editor. Different decorations are used if this is a top stack frame, focused stack frame,
|
||||
// an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line).
|
||||
const callStack = stackFrame.thread.getCallStack();
|
||||
if (callStack && callStack.length && stackFrame === callStack[0]) {
|
||||
result.push({
|
||||
options: DebugEditorModelManager.TOP_STACK_FRAME_MARGIN,
|
||||
range
|
||||
});
|
||||
|
||||
result.push({
|
||||
options: DebugEditorModelManager.TOP_STACK_FRAME_DECORATION,
|
||||
range: columnUntilEOLRange
|
||||
});
|
||||
|
||||
const modelData = this.modelDataMap.get(modelUriStr);
|
||||
if (modelData) {
|
||||
if (modelData.topStackFrameRange && modelData.topStackFrameRange.startLineNumber === stackFrame.range.startLineNumber && modelData.topStackFrameRange.startColumn !== stackFrame.range.startColumn) {
|
||||
result.push({
|
||||
options: DebugEditorModelManager.TOP_STACK_FRAME_INLINE_DECORATION,
|
||||
range: columnUntilEOLRange
|
||||
});
|
||||
}
|
||||
modelData.topStackFrameRange = columnUntilEOLRange;
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
options: DebugEditorModelManager.FOCUSED_STACK_FRAME_MARGIN,
|
||||
range
|
||||
});
|
||||
|
||||
result.push({
|
||||
options: DebugEditorModelManager.FOCUSED_STACK_FRAME_DECORATION,
|
||||
range: columnUntilEOLRange
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// breakpoints management. Represent data coming from the debug service and also send data back.
|
||||
private onModelDecorationsChanged(modelUrlStr: string): void {
|
||||
const modelData = this.modelDataMap.get(modelUrlStr);
|
||||
if (!modelData || modelData.breakpointDecorations.length === 0 || this.ignoreDecorationsChangedEvent) {
|
||||
// I have no decorations
|
||||
return;
|
||||
}
|
||||
let somethingChanged = false;
|
||||
modelData.breakpointDecorations.forEach(breakpointDecoration => {
|
||||
if (somethingChanged) {
|
||||
return;
|
||||
}
|
||||
const newBreakpointRange = modelData.model.getDecorationRange(breakpointDecoration.decorationId);
|
||||
if (newBreakpointRange && (!breakpointDecoration.range.equalsRange(newBreakpointRange))) {
|
||||
somethingChanged = true;
|
||||
}
|
||||
});
|
||||
if (!somethingChanged) {
|
||||
// nothing to do, my decorations did not change.
|
||||
return;
|
||||
}
|
||||
|
||||
const data: { [id: string]: IBreakpointUpdateData } = Object.create(null);
|
||||
const breakpoints = this.debugService.getModel().getBreakpoints();
|
||||
const modelUri = modelData.model.uri;
|
||||
for (let i = 0, len = modelData.breakpointDecorations.length; i < len; i++) {
|
||||
const breakpointDecoration = modelData.breakpointDecorations[i];
|
||||
const decorationRange = modelData.model.getDecorationRange(breakpointDecoration.decorationId);
|
||||
// check if the line got deleted.
|
||||
if (decorationRange) {
|
||||
const breakpoint = breakpoints.filter(bp => bp.getId() === breakpointDecoration.modelId).pop();
|
||||
// since we know it is collapsed, it cannot grow to multiple lines
|
||||
if (breakpoint) {
|
||||
data[breakpoint.getId()] = {
|
||||
lineNumber: decorationRange.startLineNumber,
|
||||
column: breakpoint.column ? decorationRange.startColumn : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.debugService.updateBreakpoints(modelUri, data, true);
|
||||
}
|
||||
|
||||
private onBreakpointsChange(): void {
|
||||
const breakpointsMap = new Map<string, IBreakpoint[]>();
|
||||
this.debugService.getModel().getBreakpoints().forEach(bp => {
|
||||
const uriStr = bp.uri.toString();
|
||||
const breakpoints = breakpointsMap.get(uriStr);
|
||||
if (breakpoints) {
|
||||
breakpoints.push(bp);
|
||||
} else {
|
||||
breakpointsMap.set(uriStr, [bp]);
|
||||
}
|
||||
});
|
||||
|
||||
breakpointsMap.forEach((bps, uri) => {
|
||||
const data = this.modelDataMap.get(uri);
|
||||
if (data) {
|
||||
this.updateBreakpoints(data, breakpointsMap.get(uri)!);
|
||||
}
|
||||
});
|
||||
this.modelDataMap.forEach((modelData, uri) => {
|
||||
if (!breakpointsMap.has(uri)) {
|
||||
this.updateBreakpoints(modelData, []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateBreakpoints(modelData: IDebugEditorModelData, newBreakpoints: IBreakpoint[]): void {
|
||||
const desiredDecorations = this.createBreakpointDecorations(modelData.model, newBreakpoints);
|
||||
try {
|
||||
this.ignoreDecorationsChangedEvent = true;
|
||||
const breakpointDecorationIds = modelData.model.deltaDecorations(modelData.breakpointDecorations.map(bpd => bpd.decorationId), desiredDecorations);
|
||||
modelData.breakpointDecorations = breakpointDecorationIds.map((decorationId, index) => ({
|
||||
decorationId,
|
||||
modelId: newBreakpoints[index].getId(),
|
||||
range: desiredDecorations[index].range
|
||||
}));
|
||||
} finally {
|
||||
this.ignoreDecorationsChangedEvent = false;
|
||||
}
|
||||
}
|
||||
|
||||
private createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArray<IBreakpoint>): { range: Range; options: IModelDecorationOptions; }[] {
|
||||
const result: { range: Range; options: IModelDecorationOptions; }[] = [];
|
||||
breakpoints.forEach((breakpoint) => {
|
||||
if (breakpoint.lineNumber <= model.getLineCount()) {
|
||||
const column = model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber);
|
||||
const range = model.validateRange(
|
||||
breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1)
|
||||
: new Range(breakpoint.lineNumber, column, breakpoint.lineNumber, column + 1) // Decoration has to have a width #20688
|
||||
);
|
||||
|
||||
result.push({
|
||||
options: this.getBreakpointDecorationOptions(breakpoint),
|
||||
range
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getBreakpointDecorationOptions(breakpoint: IBreakpoint): IModelDecorationOptions {
|
||||
const { className, message } = getBreakpointMessageAndClassName(this.debugService, breakpoint);
|
||||
let glyphMarginHoverMessage: MarkdownString | undefined;
|
||||
|
||||
if (message) {
|
||||
if (breakpoint.condition || breakpoint.hitCondition) {
|
||||
const modelData = this.modelDataMap.get(breakpoint.uri.toString());
|
||||
const modeId = modelData ? modelData.model.getLanguageIdentifier().language : '';
|
||||
glyphMarginHoverMessage = new MarkdownString().appendCodeblock(modeId, message);
|
||||
} else {
|
||||
glyphMarginHoverMessage = new MarkdownString().appendText(message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
glyphMarginClassName: className,
|
||||
glyphMarginHoverMessage,
|
||||
stickiness: DebugEditorModelManager.STICKINESS,
|
||||
beforeContentClassName: breakpoint.column ? `debug-breakpoint-column ${className}-column` : undefined
|
||||
};
|
||||
}
|
||||
|
||||
// editor decorations
|
||||
|
||||
// we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement.
|
||||
private static TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'debug-top-stack-frame',
|
||||
stickiness: DebugEditorModelManager.STICKINESS
|
||||
};
|
||||
|
||||
private static FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'debug-focused-stack-frame',
|
||||
stickiness: DebugEditorModelManager.STICKINESS
|
||||
};
|
||||
|
||||
private static TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = {
|
||||
isWholeLine: true,
|
||||
inlineClassName: 'debug-remove-token-colors',
|
||||
className: 'debug-top-stack-frame-line',
|
||||
stickiness: DebugEditorModelManager.STICKINESS
|
||||
};
|
||||
|
||||
private static TOP_STACK_FRAME_INLINE_DECORATION: IModelDecorationOptions = {
|
||||
beforeContentClassName: 'debug-top-stack-frame-column'
|
||||
};
|
||||
|
||||
private static FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = {
|
||||
isWholeLine: true,
|
||||
inlineClassName: 'debug-remove-token-colors',
|
||||
className: 'debug-focused-stack-frame-line',
|
||||
stickiness: DebugEditorModelManager.STICKINESS
|
||||
};
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const topStackFrame = theme.getColor(topStackFrameColor);
|
||||
if (topStackFrame) {
|
||||
collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`);
|
||||
collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`);
|
||||
}
|
||||
|
||||
const focusedStackFrame = theme.getColor(focusedStackFrameColor);
|
||||
if (focusedStackFrame) {
|
||||
collector.addRule(`.monaco-editor .view-overlays .debug-focused-stack-frame-line { background: ${focusedStackFrame}; }`);
|
||||
}
|
||||
});
|
||||
|
||||
const topStackFrameColor = registerColor('editor.stackFrameHighlightBackground', { dark: '#ffff0033', light: '#ffff6673', hc: '#fff600' }, localize('topStackFrameLineHighlight', 'Background color for the highlight of line at the top stack frame position.'));
|
||||
const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightBackground', { dark: '#7abd7a4d', light: '#cee7ce73', hc: '#cee7ce' }, localize('focusedStackFrameLineHighlight', 'Background color for the highlight of line at focused stack frame position.'));
|
||||
310
src/vs/workbench/contrib/debug/browser/debugHover.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IContentWidget, ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDebugService, IExpression, IExpressionContainer } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Expression } from 'vs/workbench/contrib/debug/common/debugModel';
|
||||
import { renderExpressionValue } from 'vs/workbench/contrib/debug/browser/baseDebugView';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { getExactExpressionStartAndEnd } from 'vs/workbench/contrib/debug/common/debugUtils';
|
||||
import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree';
|
||||
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
|
||||
import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView';
|
||||
|
||||
const $ = dom.$;
|
||||
const MAX_TREE_HEIGHT = 324;
|
||||
|
||||
export class DebugHoverWidget implements IContentWidget {
|
||||
|
||||
static readonly ID = 'debug.hoverWidget';
|
||||
// editor.IContentWidget.allowEditorOverflow
|
||||
allowEditorOverflow = true;
|
||||
|
||||
private _isVisible: boolean;
|
||||
private domNode: HTMLElement;
|
||||
private tree: AsyncDataTree<IExpression, IExpression, any>;
|
||||
private showAtPosition: Position | null;
|
||||
private highlightDecorations: string[];
|
||||
private complexValueContainer: HTMLElement;
|
||||
private complexValueTitle: HTMLElement;
|
||||
private valueContainer: HTMLElement;
|
||||
private treeContainer: HTMLElement;
|
||||
private toDispose: lifecycle.IDisposable[];
|
||||
private scrollbar: DomScrollableElement;
|
||||
private dataSource: DebugHoverDataSource;
|
||||
|
||||
constructor(
|
||||
private editor: ICodeEditor,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
) {
|
||||
this.toDispose = [];
|
||||
|
||||
this._isVisible = false;
|
||||
this.showAtPosition = null;
|
||||
this.highlightDecorations = [];
|
||||
}
|
||||
|
||||
private create(): void {
|
||||
this.domNode = $('.debug-hover-widget');
|
||||
this.complexValueContainer = dom.append(this.domNode, $('.complex-value'));
|
||||
this.complexValueTitle = dom.append(this.complexValueContainer, $('.title'));
|
||||
this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree'));
|
||||
this.treeContainer.setAttribute('role', 'tree');
|
||||
this.dataSource = new DebugHoverDataSource();
|
||||
|
||||
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new DebugHoverDelegate(), [this.instantiationService.createInstance(VariablesRenderer)],
|
||||
this.dataSource, {
|
||||
ariaLabel: nls.localize('treeAriaLabel', "Debug Hover"),
|
||||
accessibilityProvider: new DebugHoverAccessibilityProvider(),
|
||||
mouseSupport: false,
|
||||
horizontalScrolling: true
|
||||
}) as any as AsyncDataTree<IExpression, IExpression, any>;
|
||||
|
||||
this.valueContainer = $('.value');
|
||||
this.valueContainer.tabIndex = 0;
|
||||
this.valueContainer.setAttribute('role', 'tooltip');
|
||||
this.scrollbar = new DomScrollableElement(this.valueContainer, { horizontal: ScrollbarVisibility.Hidden });
|
||||
this.domNode.appendChild(this.scrollbar.getDomNode());
|
||||
this.toDispose.push(this.scrollbar);
|
||||
|
||||
this.editor.applyFontInfo(this.domNode);
|
||||
|
||||
this.toDispose.push(attachStylerCallback(this.themeService, { editorHoverBackground, editorHoverBorder }, colors => {
|
||||
if (colors.editorHoverBackground) {
|
||||
this.domNode.style.backgroundColor = colors.editorHoverBackground.toString();
|
||||
} else {
|
||||
this.domNode.style.backgroundColor = null;
|
||||
}
|
||||
if (colors.editorHoverBorder) {
|
||||
this.domNode.style.border = `1px solid ${colors.editorHoverBorder}`;
|
||||
} else {
|
||||
this.domNode.style.border = null;
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.tree.onDidChangeContentHeight(() => this.layoutTreeAndContainer()));
|
||||
|
||||
this.registerListeners();
|
||||
this.editor.addContentWidget(this);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.toDispose.push(dom.addStandardDisposableListener(this.domNode, 'keydown', (e: IKeyboardEvent) => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.hide();
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
|
||||
if (e.fontInfo) {
|
||||
this.editor.applyFontInfo(this.domNode);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return this._isVisible;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return DebugHoverWidget.ID;
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
showAt(range: Range, focus: boolean): Promise<void> {
|
||||
const pos = range.getStartPosition();
|
||||
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
if (!this.editor.hasModel()) {
|
||||
return Promise.resolve(this.hide());
|
||||
}
|
||||
|
||||
const lineContent = this.editor.getModel().getLineContent(pos.lineNumber);
|
||||
const { start, end } = getExactExpressionStartAndEnd(lineContent, range.startColumn, range.endColumn);
|
||||
// use regex to extract the sub-expression #9821
|
||||
const matchingExpression = lineContent.substring(start - 1, end);
|
||||
if (!matchingExpression || !session) {
|
||||
return Promise.resolve(this.hide());
|
||||
}
|
||||
|
||||
let promise: Promise<IExpression | undefined>;
|
||||
if (session.capabilities.supportsEvaluateForHovers) {
|
||||
const result = new Expression(matchingExpression);
|
||||
promise = result.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover').then(() => result);
|
||||
} else {
|
||||
promise = this.findExpressionInStackFrame(coalesce(matchingExpression.split('.').map(word => word.trim())));
|
||||
}
|
||||
|
||||
return promise.then(expression => {
|
||||
if (!expression || (expression instanceof Expression && !expression.available)) {
|
||||
this.hide();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{
|
||||
range: new Range(pos.lineNumber, start, pos.lineNumber, start + matchingExpression.length),
|
||||
options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS
|
||||
}]);
|
||||
|
||||
return this.doShow(pos, expression, focus);
|
||||
});
|
||||
}
|
||||
|
||||
private static _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({
|
||||
className: 'hoverHighlight'
|
||||
});
|
||||
|
||||
private doFindExpression(container: IExpressionContainer, namesToFind: string[]): Promise<IExpression | null> {
|
||||
if (!container) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return container.getChildren().then(children => {
|
||||
// look for our variable in the list. First find the parents of the hovered variable if there are any.
|
||||
const filtered = children.filter(v => namesToFind[0] === v.name);
|
||||
if (filtered.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (namesToFind.length === 1) {
|
||||
return filtered[0];
|
||||
} else {
|
||||
return this.doFindExpression(filtered[0], namesToFind.slice(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private findExpressionInStackFrame(namesToFind: string[]): Promise<IExpression | undefined> {
|
||||
return this.debugService.getViewModel().focusedStackFrame!.getScopes()
|
||||
.then(scopes => scopes.filter(s => !s.expensive))
|
||||
.then(scopes => Promise.all(scopes.map(scope => this.doFindExpression(scope, namesToFind))))
|
||||
.then(coalesce)
|
||||
// only show if all expressions found have the same value
|
||||
.then(expressions => (expressions.length > 0 && expressions.every(e => e.value === expressions[0].value)) ? expressions[0] : undefined);
|
||||
}
|
||||
|
||||
private doShow(position: Position, expression: IExpression, focus: boolean, forceValueHover = false): Promise<void> {
|
||||
if (!this.domNode) {
|
||||
this.create();
|
||||
}
|
||||
|
||||
this.showAtPosition = position;
|
||||
this._isVisible = true;
|
||||
|
||||
if (!expression.hasChildren || forceValueHover) {
|
||||
this.complexValueContainer.hidden = true;
|
||||
this.valueContainer.hidden = false;
|
||||
renderExpressionValue(expression, this.valueContainer, {
|
||||
showChanged: false,
|
||||
preserveWhitespace: true,
|
||||
colorize: true
|
||||
});
|
||||
this.valueContainer.title = '';
|
||||
this.editor.layoutContentWidget(this);
|
||||
this.scrollbar.scanDomNode();
|
||||
if (focus) {
|
||||
this.editor.render();
|
||||
this.valueContainer.focus();
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
this.valueContainer.hidden = true;
|
||||
this.complexValueContainer.hidden = false;
|
||||
|
||||
return this.tree.setInput(expression).then(() => {
|
||||
this.complexValueTitle.textContent = expression.value;
|
||||
this.complexValueTitle.title = expression.value;
|
||||
this.layoutTreeAndContainer();
|
||||
this.editor.layoutContentWidget(this);
|
||||
this.scrollbar.scanDomNode();
|
||||
if (focus) {
|
||||
this.editor.render();
|
||||
this.tree.domFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private layoutTreeAndContainer(): void {
|
||||
const treeHeight = Math.min(MAX_TREE_HEIGHT, this.tree.contentHeight);
|
||||
this.treeContainer.style.height = `${treeHeight}px`;
|
||||
this.tree.layout(treeHeight, 324);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isVisible = false;
|
||||
this.editor.deltaDecorations(this.highlightDecorations, []);
|
||||
this.highlightDecorations = [];
|
||||
this.editor.layoutContentWidget(this);
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition | null {
|
||||
return this._isVisible ? {
|
||||
position: this.showAtPosition,
|
||||
preference: [
|
||||
ContentWidgetPositionPreference.ABOVE,
|
||||
ContentWidgetPositionPreference.BELOW
|
||||
]
|
||||
} : null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose = lifecycle.dispose(this.toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
class DebugHoverAccessibilityProvider implements IAccessibilityProvider<IExpression> {
|
||||
getAriaLabel(element: IExpression): string {
|
||||
return nls.localize('variableAriaLabel', "{0} value {1}, variables, debug", element.name, element.value);
|
||||
}
|
||||
}
|
||||
|
||||
class DebugHoverDataSource implements IAsyncDataSource<IExpression, IExpression> {
|
||||
|
||||
hasChildren(element: IExpression): boolean {
|
||||
return element.hasChildren;
|
||||
}
|
||||
|
||||
getChildren(element: IExpression): Promise<IExpression[]> {
|
||||
return element.getChildren();
|
||||
}
|
||||
}
|
||||
|
||||
class DebugHoverDelegate implements IListVirtualDelegate<IExpression> {
|
||||
getHeight(element: IExpression): number {
|
||||
return 18;
|
||||
}
|
||||
|
||||
getTemplateId(element: IExpression): string {
|
||||
return VariablesRenderer.ID;
|
||||
}
|
||||
}
|
||||
139
src/vs/workbench/contrib/debug/browser/debugQuickOpen.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IDebugService, ILaunch } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { StartAction } from 'vs/workbench/contrib/debug/browser/debugActions';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup, IHighlight } from 'vs/base/parts/quickopen/browser/quickOpenModel';
|
||||
import { Mode, IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen';
|
||||
import { QuickOpenHandler } from 'vs/workbench/browser/quickopen';
|
||||
import { matchesFuzzy } from 'vs/base/common/filters';
|
||||
|
||||
class AddConfigEntry extends QuickOpenEntry {
|
||||
|
||||
constructor(private label: string, private launch: ILaunch, private commandService: ICommandService, private contextService: IWorkspaceContextService, highlights: IHighlight[] = []) {
|
||||
super(highlights);
|
||||
}
|
||||
|
||||
public getLabel(): string {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
public getDescription(): string {
|
||||
return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.launch.name : '';
|
||||
}
|
||||
|
||||
public getAriaLabel(): string {
|
||||
return nls.localize('entryAriaLabel', "{0}, debug", this.getLabel());
|
||||
}
|
||||
|
||||
public run(mode: Mode): boolean {
|
||||
if (mode === Mode.PREVIEW) {
|
||||
return false;
|
||||
}
|
||||
this.commandService.executeCommand('debug.addConfiguration', this.launch.uri.toString());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class StartDebugEntry extends QuickOpenEntry {
|
||||
|
||||
constructor(private debugService: IDebugService, private contextService: IWorkspaceContextService, private notificationService: INotificationService, private launch: ILaunch, private configurationName: string, highlights: IHighlight[] = []) {
|
||||
super(highlights);
|
||||
}
|
||||
|
||||
public getLabel(): string {
|
||||
return this.configurationName;
|
||||
}
|
||||
|
||||
public getDescription(): string {
|
||||
return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.launch.name : '';
|
||||
}
|
||||
|
||||
public getAriaLabel(): string {
|
||||
return nls.localize('entryAriaLabel', "{0}, debug", this.getLabel());
|
||||
}
|
||||
|
||||
public run(mode: Mode): boolean {
|
||||
if (mode === Mode.PREVIEW || !StartAction.isEnabled(this.debugService)) {
|
||||
return false;
|
||||
}
|
||||
// Run selected debug configuration
|
||||
this.debugService.getConfigurationManager().selectConfiguration(this.launch, this.configurationName);
|
||||
this.debugService.startDebugging(this.launch).then(undefined, e => this.notificationService.error(e));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugQuickOpenHandler extends QuickOpenHandler {
|
||||
|
||||
public static readonly ID = 'workbench.picker.launch';
|
||||
|
||||
private autoFocusIndex: number;
|
||||
|
||||
constructor(
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@INotificationService private readonly notificationService: INotificationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getAriaLabel(): string {
|
||||
return nls.localize('debugAriaLabel', "Type a name of a launch configuration to run.");
|
||||
}
|
||||
|
||||
public getResults(input: string, token: CancellationToken): Promise<QuickOpenModel> {
|
||||
const configurations: QuickOpenEntry[] = [];
|
||||
|
||||
const configManager = this.debugService.getConfigurationManager();
|
||||
const launches = configManager.getLaunches();
|
||||
for (let launch of launches) {
|
||||
launch.getConfigurationNames().map(config => ({ config: config, highlights: matchesFuzzy(input, config, true) || undefined }))
|
||||
.filter(({ highlights }) => !!highlights)
|
||||
.forEach(({ config, highlights }) => {
|
||||
if (launch === configManager.selectedConfiguration.launch && config === configManager.selectedConfiguration.name) {
|
||||
this.autoFocusIndex = configurations.length;
|
||||
}
|
||||
configurations.push(new StartDebugEntry(this.debugService, this.contextService, this.notificationService, launch, config, highlights));
|
||||
});
|
||||
}
|
||||
launches.filter(l => !l.hidden).forEach((l, index) => {
|
||||
|
||||
const label = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration...");
|
||||
const entry = new AddConfigEntry(label, l, this.commandService, this.contextService, matchesFuzzy(input, label, true) || undefined);
|
||||
if (index === 0) {
|
||||
configurations.push(new QuickOpenEntryGroup(entry, undefined, true));
|
||||
} else {
|
||||
configurations.push(entry);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return Promise.resolve(new QuickOpenModel(configurations));
|
||||
}
|
||||
|
||||
public getAutoFocus(input: string): IAutoFocus {
|
||||
return {
|
||||
autoFocusFirstEntry: !!input,
|
||||
autoFocusIndex: this.autoFocusIndex
|
||||
};
|
||||
}
|
||||
|
||||
public getEmptyLabel(searchString: string): string {
|
||||
if (searchString.length > 0) {
|
||||
return nls.localize('noConfigurationsMatching', "No debug configurations matching");
|
||||
}
|
||||
|
||||
return nls.localize('noConfigurationsFound', "No debug configurations found. Please create a 'launch.json' file.");
|
||||
}
|
||||
}
|
||||
100
src/vs/workbench/contrib/debug/browser/debugStatus.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
import { IDebugService, State, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Themable, STATUS_BAR_FOREGROUND } from 'vs/workbench/common/theme';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { STATUS_BAR_DEBUGGING_FOREGROUND, isStatusbarInDebugMode } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class DebugStatus extends Themable implements IStatusbarItem {
|
||||
private container: HTMLElement;
|
||||
private statusBarItem: HTMLElement;
|
||||
private label: HTMLElement;
|
||||
private icon: HTMLElement;
|
||||
private showInStatusBar: string;
|
||||
|
||||
constructor(
|
||||
@IQuickOpenService private readonly quickOpenService: IQuickOpenService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(themeService);
|
||||
this._register(this.debugService.getConfigurationManager().onDidSelectConfiguration(e => {
|
||||
this.setLabel();
|
||||
}));
|
||||
this._register(this.debugService.onDidChangeState(state => {
|
||||
if (state !== State.Inactive && this.showInStatusBar === 'onFirstSessionStart') {
|
||||
this.doRender();
|
||||
}
|
||||
}));
|
||||
this.showInStatusBar = configurationService.getValue<IDebugConfiguration>('debug').showInStatusBar;
|
||||
this._register(configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('debug.showInStatusBar')) {
|
||||
this.showInStatusBar = configurationService.getValue<IDebugConfiguration>('debug').showInStatusBar;
|
||||
if (this.showInStatusBar === 'always') {
|
||||
this.doRender();
|
||||
}
|
||||
if (this.statusBarItem) {
|
||||
dom.toggleClass(this.statusBarItem, 'hidden', this.showInStatusBar === 'never');
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected updateStyles(): void {
|
||||
super.updateStyles();
|
||||
if (this.icon) {
|
||||
if (isStatusbarInDebugMode(this.debugService)) {
|
||||
this.icon.style.backgroundColor = this.getColor(STATUS_BAR_DEBUGGING_FOREGROUND);
|
||||
} else {
|
||||
this.icon.style.backgroundColor = this.getColor(STATUS_BAR_FOREGROUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): IDisposable {
|
||||
this.container = container;
|
||||
if (this.showInStatusBar === 'always') {
|
||||
this.doRender();
|
||||
}
|
||||
// noop, we render when we decide is best
|
||||
return this;
|
||||
}
|
||||
|
||||
private doRender(): void {
|
||||
if (!this.statusBarItem && this.container) {
|
||||
this.statusBarItem = dom.append(this.container, $('.debug-statusbar-item'));
|
||||
this._register(dom.addDisposableListener(this.statusBarItem, 'click', () => this.quickOpenService.show('debug ')));
|
||||
this.statusBarItem.title = nls.localize('selectAndStartDebug', "Select and start debug configuration");
|
||||
const a = dom.append(this.statusBarItem, $('a'));
|
||||
this.icon = dom.append(a, $('.icon'));
|
||||
this.label = dom.append(a, $('span.label'));
|
||||
this.setLabel();
|
||||
}
|
||||
|
||||
this.updateStyles();
|
||||
}
|
||||
|
||||
private setLabel(): void {
|
||||
if (this.label && this.statusBarItem) {
|
||||
const manager = this.debugService.getConfigurationManager();
|
||||
const name = manager.selectedConfiguration.name || '';
|
||||
const nameAndLaunchPresent = name && manager.selectedConfiguration.launch;
|
||||
dom.toggleClass(this.statusBarItem, 'hidden', this.showInStatusBar === 'never' || !nameAndLaunchPresent);
|
||||
if (nameAndLaunchPresent) {
|
||||
this.label.textContent = manager.getLaunches().length > 1 ? `${name} (${manager.selectedConfiguration.launch!.name})` : name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
285
src/vs/workbench/contrib/debug/browser/debugToolbar.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/debugToolbar';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IAction, IRunEvent } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IDebugConfiguration, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { FocusSessionActionItem } from 'vs/workbench/contrib/debug/browser/debugActionItems';
|
||||
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { Themable } from 'vs/workbench/common/theme';
|
||||
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 { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { fillInActionBarActions, MenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions';
|
||||
|
||||
const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition';
|
||||
const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety';
|
||||
|
||||
export const debugToolBarBackground = registerColor('debugToolBar.background', {
|
||||
dark: '#333333',
|
||||
light: '#F3F3F3',
|
||||
hc: '#000000'
|
||||
}, localize('debugToolBarBackground', "Debug toolbar background color."));
|
||||
export const debugToolBarBorder = registerColor('debugToolBar.border', {
|
||||
dark: null,
|
||||
light: null,
|
||||
hc: null
|
||||
}, localize('debugToolBarBorder', "Debug toolbar border color."));
|
||||
|
||||
export class DebugToolbar extends Themable implements IWorkbenchContribution {
|
||||
|
||||
private $el: HTMLElement;
|
||||
private dragArea: HTMLElement;
|
||||
private actionBar: ActionBar;
|
||||
private activeActions: IAction[];
|
||||
private updateScheduler: RunOnceScheduler;
|
||||
private debugToolbarMenu: IMenu;
|
||||
|
||||
private isVisible: boolean;
|
||||
private isBuilt: boolean;
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@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,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(themeService);
|
||||
|
||||
this.$el = dom.$('div.debug-toolbar');
|
||||
this.$el.style.top = `${layoutService.getTitleBarOffset()}px`;
|
||||
|
||||
this.dragArea = dom.append(this.$el, dom.$('div.drag-area'));
|
||||
|
||||
const actionBarContainer = dom.append(this.$el, dom.$('div.action-bar-container'));
|
||||
this.debugToolbarMenu = menuService.createMenu(MenuId.DebugToolbar, contextKeyService);
|
||||
this.toDispose.push(this.debugToolbarMenu);
|
||||
|
||||
this.activeActions = [];
|
||||
this.actionBar = this._register(new ActionBar(actionBarContainer, {
|
||||
orientation: ActionsOrientation.HORIZONTAL,
|
||||
actionItemProvider: (action: IAction) => {
|
||||
if (action.id === FocusSessionAction.ID) {
|
||||
return new FocusSessionActionItem(action, this.debugService, this.themeService, contextViewService);
|
||||
}
|
||||
if (action instanceof MenuItemAction) {
|
||||
return new MenuItemActionItem(action, this.keybindingService, this.notificationService, contextMenuService);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
this.updateScheduler = this._register(new RunOnceScheduler(() => {
|
||||
const state = this.debugService.state;
|
||||
const toolBarLocation = this.configurationService.getValue<IDebugConfiguration>('debug').toolBarLocation;
|
||||
if (state === State.Inactive || toolBarLocation === 'docked' || toolBarLocation === 'hidden') {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
const actions = DebugToolbar.getActions(this.debugToolbarMenu, this.debugService, this.instantiationService);
|
||||
if (!arrays.equals(actions, this.activeActions, (first, second) => first.id === second.id)) {
|
||||
this.actionBar.clear();
|
||||
this.actionBar.push(actions, { icon: true, label: false });
|
||||
this.activeActions = actions;
|
||||
}
|
||||
this.show();
|
||||
}, 20));
|
||||
|
||||
this.updateStyles();
|
||||
|
||||
this.registerListeners();
|
||||
|
||||
this.hide();
|
||||
this.isBuilt = false;
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule()));
|
||||
this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule()));
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e)));
|
||||
this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => {
|
||||
// check for error
|
||||
if (e.error && !errors.isPromiseCanceledError(e.error)) {
|
||||
this.notificationService.error(e.error);
|
||||
}
|
||||
|
||||
// log in telemetry
|
||||
if (this.telemetryService) {
|
||||
/* __GDPR__
|
||||
"workbenchActionExecuted" : {
|
||||
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'debugActionsWidget' });
|
||||
}
|
||||
}));
|
||||
this._register(dom.addDisposableListener(window, dom.EventType.RESIZE, () => this.setCoordinates()));
|
||||
|
||||
this._register(dom.addDisposableListener(this.dragArea, dom.EventType.MOUSE_UP, (event: MouseEvent) => {
|
||||
const mouseClickEvent = new StandardMouseEvent(event);
|
||||
if (mouseClickEvent.detail === 2) {
|
||||
// double click on debug bar centers it again #8250
|
||||
const widgetWidth = this.$el.clientWidth;
|
||||
this.setCoordinates(0.5 * window.innerWidth - 0.5 * widgetWidth, 0);
|
||||
this.storePosition();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(dom.addDisposableListener(this.dragArea, dom.EventType.MOUSE_DOWN, (event: MouseEvent) => {
|
||||
dom.addClass(this.dragArea, 'dragged');
|
||||
|
||||
const mouseMoveListener = dom.addDisposableListener(window, 'mousemove', (e: MouseEvent) => {
|
||||
const mouseMoveEvent = new StandardMouseEvent(e);
|
||||
// Prevent default to stop editor selecting text #8524
|
||||
mouseMoveEvent.preventDefault();
|
||||
// Reduce x by width of drag handle to reduce jarring #16604
|
||||
this.setCoordinates(mouseMoveEvent.posx - 14, mouseMoveEvent.posy - this.layoutService.getTitleBarOffset());
|
||||
});
|
||||
|
||||
const mouseUpListener = dom.addDisposableListener(window, 'mouseup', (e: MouseEvent) => {
|
||||
this.storePosition();
|
||||
dom.removeClass(this.dragArea, 'dragged');
|
||||
|
||||
mouseMoveListener.dispose();
|
||||
mouseUpListener.dispose();
|
||||
});
|
||||
}));
|
||||
|
||||
this._register(this.layoutService.onTitleBarVisibilityChange(() => this.setYCoordinate()));
|
||||
this._register(browser.onDidChangeZoomLevel(() => this.setYCoordinate()));
|
||||
}
|
||||
|
||||
private storePosition(): void {
|
||||
const left = dom.getComputedStyle(this.$el).left;
|
||||
if (left) {
|
||||
const position = parseFloat(left) / window.innerWidth;
|
||||
this.storageService.store(DEBUG_TOOLBAR_POSITION_KEY, position, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
protected updateStyles(): void {
|
||||
super.updateStyles();
|
||||
|
||||
if (this.$el) {
|
||||
this.$el.style.backgroundColor = this.getColor(debugToolBarBackground);
|
||||
|
||||
const widgetShadowColor = this.getColor(widgetShadow);
|
||||
this.$el.style.boxShadow = widgetShadowColor ? `0 5px 8px ${widgetShadowColor}` : null;
|
||||
|
||||
const contrastBorderColor = this.getColor(contrastBorder);
|
||||
const borderColor = this.getColor(debugToolBarBorder);
|
||||
|
||||
if (contrastBorderColor) {
|
||||
this.$el.style.border = `1px solid ${contrastBorderColor}`;
|
||||
} else {
|
||||
this.$el.style.border = borderColor ? `solid ${borderColor}` : 'none';
|
||||
this.$el.style.border = '1px 0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setYCoordinate(y = 0): void {
|
||||
const titlebarOffset = this.layoutService.getTitleBarOffset();
|
||||
this.$el.style.top = `${titlebarOffset + y}px`;
|
||||
}
|
||||
|
||||
private setCoordinates(x?: number, y?: number): void {
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
const widgetWidth = this.$el.clientWidth;
|
||||
if (x === undefined) {
|
||||
const positionPercentage = this.storageService.get(DEBUG_TOOLBAR_POSITION_KEY, StorageScope.GLOBAL);
|
||||
x = positionPercentage !== undefined ? parseFloat(positionPercentage) * window.innerWidth : (0.5 * window.innerWidth - 0.5 * widgetWidth);
|
||||
}
|
||||
|
||||
x = Math.max(0, Math.min(x, window.innerWidth - widgetWidth)); // do not allow the widget to overflow on the right
|
||||
this.$el.style.left = `${x}px`;
|
||||
|
||||
if (y === undefined) {
|
||||
y = this.storageService.getNumber(DEBUG_TOOLBAR_Y_KEY, StorageScope.GLOBAL, 0);
|
||||
}
|
||||
const titleAreaHeight = 35;
|
||||
if ((y < titleAreaHeight / 2) || (y > titleAreaHeight + titleAreaHeight / 2)) {
|
||||
const moveToTop = y < titleAreaHeight;
|
||||
this.setYCoordinate(moveToTop ? 0 : titleAreaHeight);
|
||||
this.storageService.store(DEBUG_TOOLBAR_Y_KEY, moveToTop ? 0 : 2 * titleAreaHeight, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidConfigurationChange(event: IConfigurationChangeEvent): void {
|
||||
if (event.affectsConfiguration('debug.hideActionBar') || event.affectsConfiguration('debug.toolBarLocation')) {
|
||||
this.updateScheduler.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private show(): void {
|
||||
if (this.isVisible) {
|
||||
this.setCoordinates();
|
||||
return;
|
||||
}
|
||||
if (!this.isBuilt) {
|
||||
this.isBuilt = true;
|
||||
this.layoutService.getWorkbenchElement().appendChild(this.$el);
|
||||
}
|
||||
|
||||
this.isVisible = true;
|
||||
dom.show(this.$el);
|
||||
this.setCoordinates();
|
||||
}
|
||||
|
||||
private hide(): void {
|
||||
this.isVisible = false;
|
||||
dom.hide(this.$el);
|
||||
}
|
||||
|
||||
public static getActions(menu: IMenu, debugService: IDebugService, instantiationService: IInstantiationService): IAction[] {
|
||||
const actions: IAction[] = [];
|
||||
fillInActionBarActions(menu, undefined, actions, () => false);
|
||||
if (debugService.getViewModel().isMultiSessionView()) {
|
||||
actions.push(instantiationService.createInstance(FocusSessionAction, FocusSessionAction.ID, FocusSessionAction.LABEL));
|
||||
}
|
||||
|
||||
return actions.filter(a => !(a instanceof Separator)); // do not render separators for now
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
if (this.$el) {
|
||||
this.$el.remove();
|
||||
delete this.$el;
|
||||
}
|
||||
}
|
||||
}
|
||||
195
src/vs/workbench/contrib/debug/browser/debugViewlet.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/debugViewlet';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { StartAction, ToggleReplAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions';
|
||||
import { StartDebugActionItem, FocusSessionActionItem } from 'vs/workbench/contrib/debug/browser/debugActionItems';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/progress';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { DebugToolbar } from 'vs/workbench/contrib/debug/browser/debugToolbar';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IMenu, MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { MenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export class DebugViewlet extends ViewContainerViewlet {
|
||||
|
||||
private startDebugActionItem: StartDebugActionItem;
|
||||
private progressRunner: IProgressRunner;
|
||||
private breakpointView: ViewletPanel;
|
||||
private panelListeners = new Map<string, IDisposable>();
|
||||
private debugToolbarMenu: IMenu;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IProgressService private readonly progressService: IProgressService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IWorkspaceContextService contextService: IWorkspaceContextService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IContextViewService private readonly contextViewService: IContextViewService,
|
||||
@IMenuService private readonly menuService: IMenuService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@INotificationService private readonly notificationService: INotificationService
|
||||
) {
|
||||
super(VIEWLET_ID, `${VIEWLET_ID}.state`, false, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
|
||||
|
||||
this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state)));
|
||||
this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateTitleArea()));
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('debug.toolBarLocation')) {
|
||||
this.updateTitleArea();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
create(parent: HTMLElement): void {
|
||||
super.create(parent);
|
||||
DOM.addClass(parent, 'debug-viewlet');
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
|
||||
if (this.startDebugActionItem) {
|
||||
this.startDebugActionItem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get startAction(): StartAction {
|
||||
return this._register(this.instantiationService.createInstance(StartAction, StartAction.ID, StartAction.LABEL));
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get configureAction(): ConfigureAction {
|
||||
return this._register(this.instantiationService.createInstance(ConfigureAction, ConfigureAction.ID, ConfigureAction.LABEL));
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get toggleReplAction(): ToggleReplAction {
|
||||
return this._register(this.instantiationService.createInstance(ToggleReplAction, ToggleReplAction.ID, ToggleReplAction.LABEL));
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get selectAndStartAction(): SelectAndStartAction {
|
||||
return this._register(this.instantiationService.createInstance(SelectAndStartAction, SelectAndStartAction.ID, nls.localize('startAdditionalSession', "Start Additional Session")));
|
||||
}
|
||||
|
||||
getActions(): IAction[] {
|
||||
if (this.showInitialDebugActions) {
|
||||
return [this.startAction, this.configureAction, this.toggleReplAction];
|
||||
}
|
||||
|
||||
if (!this.debugToolbarMenu) {
|
||||
this.debugToolbarMenu = this.menuService.createMenu(MenuId.DebugToolbar, this.contextKeyService);
|
||||
this.toDispose.push(this.debugToolbarMenu);
|
||||
}
|
||||
return DebugToolbar.getActions(this.debugToolbarMenu, this.debugService, this.instantiationService);
|
||||
}
|
||||
|
||||
get showInitialDebugActions(): boolean {
|
||||
const state = this.debugService.state;
|
||||
return state === State.Inactive || this.configurationService.getValue<IDebugConfiguration>('debug').toolBarLocation !== 'docked';
|
||||
}
|
||||
|
||||
getSecondaryActions(): IAction[] {
|
||||
if (this.showInitialDebugActions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [this.selectAndStartAction, this.configureAction, this.toggleReplAction];
|
||||
}
|
||||
|
||||
getActionItem(action: IAction): IActionItem | null {
|
||||
if (action.id === StartAction.ID) {
|
||||
this.startDebugActionItem = this.instantiationService.createInstance(StartDebugActionItem, null, action);
|
||||
return this.startDebugActionItem;
|
||||
}
|
||||
if (action.id === FocusSessionAction.ID) {
|
||||
return new FocusSessionActionItem(action, this.debugService, this.themeService, this.contextViewService);
|
||||
}
|
||||
if (action instanceof MenuItemAction) {
|
||||
return new MenuItemActionItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
focusView(id: string): void {
|
||||
const view = this.getView(id);
|
||||
if (view) {
|
||||
view.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private onDebugServiceStateChange(state: State): void {
|
||||
if (this.progressRunner) {
|
||||
this.progressRunner.done();
|
||||
}
|
||||
|
||||
if (state === State.Initializing) {
|
||||
this.progressRunner = this.progressService.show(true);
|
||||
}
|
||||
|
||||
if (this.configurationService.getValue<IDebugConfiguration>('debug').toolBarLocation === 'docked') {
|
||||
this.updateTitleArea();
|
||||
}
|
||||
}
|
||||
|
||||
addPanels(panels: { panel: ViewletPanel, size: number, index?: number }[]): void {
|
||||
super.addPanels(panels);
|
||||
|
||||
for (const { panel } of panels) {
|
||||
// attach event listener to
|
||||
if (panel.id === BREAKPOINTS_VIEW_ID) {
|
||||
this.breakpointView = panel;
|
||||
this.updateBreakpointsMaxSize();
|
||||
} else {
|
||||
this.panelListeners.set(panel.id, panel.onDidChange(() => this.updateBreakpointsMaxSize()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removePanels(panels: ViewletPanel[]): void {
|
||||
super.removePanels(panels);
|
||||
for (const panel of panels) {
|
||||
dispose(this.panelListeners.get(panel.id));
|
||||
this.panelListeners.delete(panel.id);
|
||||
}
|
||||
}
|
||||
|
||||
private updateBreakpointsMaxSize(): void {
|
||||
if (this.breakpointView) {
|
||||
// We need to update the breakpoints view since all other views are collapsed #25384
|
||||
const allOtherCollapsed = this.panels.every(view => !view.isExpanded() || view === this.breakpointView);
|
||||
this.breakpointView.maximumBodySize = allOtherCollapsed ? Number.POSITIVE_INFINITY : this.breakpointView.minimumBodySize;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/vs/workbench/contrib/debug/browser/exceptionWidget.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/exceptionWidget';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IExceptionInfo } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
|
||||
const $ = dom.$;
|
||||
|
||||
// theming
|
||||
|
||||
export const debugExceptionWidgetBorder = registerColor('debugExceptionWidget.border', { dark: '#a31515', light: '#a31515', hc: '#a31515' }, nls.localize('debugExceptionWidgetBorder', 'Exception widget border color.'));
|
||||
export const debugExceptionWidgetBackground = registerColor('debugExceptionWidget.background', { dark: '#420b0d', light: '#f1dfde', hc: '#420b0d' }, nls.localize('debugExceptionWidgetBackground', 'Exception widget background color.'));
|
||||
|
||||
export class ExceptionWidget extends ZoneWidget {
|
||||
|
||||
private _backgroundColor?: Color;
|
||||
|
||||
constructor(editor: ICodeEditor, private exceptionInfo: IExceptionInfo,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
super(editor, { showFrame: true, showArrow: true, frameWidth: 1, className: 'exception-widget-container' });
|
||||
|
||||
this._backgroundColor = Color.white;
|
||||
|
||||
this._applyTheme(themeService.getTheme());
|
||||
this._disposables.push(themeService.onThemeChange(this._applyTheme.bind(this)));
|
||||
|
||||
|
||||
this.create();
|
||||
const onDidLayoutChangeScheduler = new RunOnceScheduler(() => this._doLayout(undefined, undefined), 50);
|
||||
this._disposables.push(this.editor.onDidLayoutChange(() => onDidLayoutChangeScheduler.schedule()));
|
||||
this._disposables.push(onDidLayoutChangeScheduler);
|
||||
}
|
||||
|
||||
private _applyTheme(theme: ITheme): void {
|
||||
this._backgroundColor = theme.getColor(debugExceptionWidgetBackground);
|
||||
const frameColor = theme.getColor(debugExceptionWidgetBorder);
|
||||
this.style({
|
||||
arrowColor: frameColor,
|
||||
frameColor: frameColor
|
||||
}); // style() will trigger _applyStyles
|
||||
}
|
||||
|
||||
protected _applyStyles(): void {
|
||||
if (this.container) {
|
||||
this.container.style.backgroundColor = this._backgroundColor ? this._backgroundColor.toString() : '';
|
||||
}
|
||||
super._applyStyles();
|
||||
}
|
||||
|
||||
protected _fillContainer(container: HTMLElement): void {
|
||||
this.setCssClass('exception-widget');
|
||||
// Set the font size and line height to the one from the editor configuration.
|
||||
const fontInfo = this.editor.getConfiguration().fontInfo;
|
||||
this.container.style.fontSize = `${fontInfo.fontSize}px`;
|
||||
this.container.style.lineHeight = `${fontInfo.lineHeight}px`;
|
||||
|
||||
let title = $('.title');
|
||||
title.textContent = this.exceptionInfo.id ? nls.localize('exceptionThrownWithId', 'Exception has occurred: {0}', this.exceptionInfo.id) : nls.localize('exceptionThrown', 'Exception has occurred.');
|
||||
dom.append(container, title);
|
||||
|
||||
if (this.exceptionInfo.description) {
|
||||
let description = $('.description');
|
||||
description.textContent = this.exceptionInfo.description;
|
||||
dom.append(container, description);
|
||||
}
|
||||
|
||||
if (this.exceptionInfo.details && this.exceptionInfo.details.stackTrace) {
|
||||
let stackTrace = $('.stack-trace');
|
||||
const linkDetector = this.instantiationService.createInstance(LinkDetector);
|
||||
const linkedStackTrace = linkDetector.handleLinks(this.exceptionInfo.details.stackTrace);
|
||||
stackTrace.appendChild(linkedStackTrace);
|
||||
dom.append(container, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
protected _doLayout(_heightInPixel: number | undefined, _widthInPixel: number | undefined): void {
|
||||
// Reload the height with respect to the exception text content and relayout it to match the line count.
|
||||
this.container.style.height = 'initial';
|
||||
|
||||
const lineHeight = this.editor.getConfiguration().lineHeight;
|
||||
const arrowHeight = Math.round(lineHeight / 3);
|
||||
const computedLinesNumber = Math.ceil((this.container.offsetHeight + arrowHeight) / lineHeight);
|
||||
|
||||
this._relayout(computedLinesNumber);
|
||||
}
|
||||
}
|
||||
158
src/vs/workbench/contrib/debug/browser/linkDetector.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import strings = require('vs/base/common/strings');
|
||||
import { isAbsolute } from 'vs/base/common/path';
|
||||
import { URI as uri } from 'vs/base/common/uri';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
export class LinkDetector {
|
||||
private static readonly MAX_LENGTH = 500;
|
||||
private static FILE_LOCATION_PATTERNS: RegExp[] = [
|
||||
// group 0: full path with line and column
|
||||
// group 1: full path without line and column, matched by `*.*` in the end to work only on paths with extensions in the end (s.t. node:10352 would not match)
|
||||
// group 2: drive letter on windows with trailing backslash or leading slash on mac/linux
|
||||
// group 3: line number, matched by (:(\d+))
|
||||
// group 4: column number, matched by ((?::(\d+))?)
|
||||
// eg: at Context.<anonymous> (c:\Users\someone\Desktop\mocha-runner\test\test.js:26:11)
|
||||
/(?![\(])(?:file:\/\/)?((?:([a-zA-Z]+:)|[^\(\)<>\'\"\[\]:\s]+)(?:[\\/][^\(\)<>\'\"\[\]:]*)?\.[a-zA-Z]+[0-9]*):(\d+)(?::(\d+))?/g
|
||||
];
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly editorService: IEditorService
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches and handles absolute file links in the string provided.
|
||||
* Returns <span/> element that wraps the processed string, where matched links are replaced by <a/> and unmatched parts are surrounded by <span/> elements.
|
||||
* 'onclick' event is attached to all anchored links that opens them in the editor.
|
||||
* Each line of the text, even if it contains no links, is wrapped in a <span> and added as a child of the returned <span>.
|
||||
*/
|
||||
handleLinks(text: string): HTMLElement {
|
||||
const container = document.createElement('span');
|
||||
|
||||
// Handle the text one line at a time
|
||||
const lines = text.split('\n');
|
||||
|
||||
if (strings.endsWith(text, '\n')) {
|
||||
// Remove the last element ('') that split added
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
|
||||
// Re-introduce the newline for every line except the last (unless the last line originally ended with a newline)
|
||||
if (i < lines.length - 1 || strings.endsWith(text, '\n')) {
|
||||
line += '\n';
|
||||
}
|
||||
|
||||
// Don't handle links for lines that are too long
|
||||
if (line.length > LinkDetector.MAX_LENGTH) {
|
||||
let span = document.createElement('span');
|
||||
span.textContent = line;
|
||||
container.appendChild(span);
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineContainer = document.createElement('span');
|
||||
|
||||
for (let pattern of LinkDetector.FILE_LOCATION_PATTERNS) {
|
||||
// Reset the state of the pattern
|
||||
pattern = new RegExp(pattern);
|
||||
let lastMatchIndex = 0;
|
||||
|
||||
let match = pattern.exec(line);
|
||||
|
||||
while (match !== null) {
|
||||
let resource: uri | null = isAbsolute(match[1]) ? uri.file(match[1]) : null;
|
||||
|
||||
if (!resource) {
|
||||
match = pattern.exec(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const textBeforeLink = line.substring(lastMatchIndex, match.index);
|
||||
if (textBeforeLink) {
|
||||
// textBeforeLink may have matches for other patterns, so we run handleLinks on it before adding it.
|
||||
lineContainer.appendChild(this.handleLinks(textBeforeLink));
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.textContent = line.substr(match.index, match[0].length);
|
||||
link.title = isMacintosh ? nls.localize('fileLinkMac', "Click to follow (Cmd + click opens to the side)") : nls.localize('fileLink', "Click to follow (Ctrl + click opens to the side)");
|
||||
lineContainer.appendChild(link);
|
||||
const lineNumber = Number(match[3]);
|
||||
const columnNumber = match[4] ? Number(match[4]) : undefined;
|
||||
link.onclick = (e) => this.onLinkClick(new StandardMouseEvent(e), resource!, lineNumber, columnNumber);
|
||||
|
||||
lastMatchIndex = pattern.lastIndex;
|
||||
const currentMatch = match;
|
||||
match = pattern.exec(line);
|
||||
|
||||
// Append last string part if no more link matches
|
||||
if (!match) {
|
||||
const textAfterLink = line.substr(currentMatch.index + currentMatch[0].length);
|
||||
if (textAfterLink) {
|
||||
// textAfterLink may have matches for other patterns, so we run handleLinks on it before adding it.
|
||||
lineContainer.appendChild(this.handleLinks(textAfterLink));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found any matches for this pattern, don't check any more patterns. Other parts of the line will be checked for the other patterns due to the recursion.
|
||||
if (lineContainer.hasChildNodes()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length === 1) {
|
||||
if (lineContainer.hasChildNodes()) {
|
||||
// Adding lineContainer to container would introduce an unnecessary surrounding span since there is only one line, so instead we just return lineContainer
|
||||
return lineContainer;
|
||||
} else {
|
||||
container.textContent = line;
|
||||
}
|
||||
} else {
|
||||
if (lineContainer.hasChildNodes()) {
|
||||
// Add this line to the container
|
||||
container.appendChild(lineContainer);
|
||||
} else {
|
||||
// No links were added, but we still need to surround the unmodified line with a span before adding it
|
||||
let span = document.createElement('span');
|
||||
span.textContent = line;
|
||||
container.appendChild(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private onLinkClick(event: IMouseEvent, resource: uri, line: number, column: number = 0): void {
|
||||
const selection = window.getSelection();
|
||||
if (selection.type === 'Range') {
|
||||
return; // do not navigate when user is selecting
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const group = event.ctrlKey || event.metaKey ? SIDE_GROUP : ACTIVE_GROUP;
|
||||
|
||||
this.editorService.openEditor({
|
||||
resource,
|
||||
options: {
|
||||
selection: {
|
||||
startLineNumber: line,
|
||||
startColumn: column
|
||||
}
|
||||
}
|
||||
}, group);
|
||||
}
|
||||
}
|
||||
648
src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts
Normal file
@@ -0,0 +1,648 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { normalize, isAbsolute, posix } from 'vs/base/common/path';
|
||||
import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView';
|
||||
import { IDebugSession, IDebugService, IDebugModel, CONTEXT_LOADED_SCRIPTS_ITEM_TYPE } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Source } from 'vs/workbench/contrib/debug/common/debugSource';
|
||||
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { tildify } from 'vs/base/common/labels';
|
||||
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 { 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';
|
||||
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService';
|
||||
import { dispose } from 'vs/base/common/lifecycle';
|
||||
import { createMatches, FuzzyScore } from 'vs/base/common/filters';
|
||||
import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider';
|
||||
|
||||
const SMART = true;
|
||||
|
||||
type LoadedScriptsItem = BaseTreeItem;
|
||||
|
||||
class BaseTreeItem {
|
||||
|
||||
private _showedMoreThanOne: boolean;
|
||||
private _children: { [key: string]: BaseTreeItem; };
|
||||
private _source: Source;
|
||||
|
||||
constructor(private _parent: BaseTreeItem | undefined, private _label: string) {
|
||||
this._children = {};
|
||||
this._showedMoreThanOne = false;
|
||||
}
|
||||
|
||||
isLeaf(): boolean {
|
||||
return Object.keys(this._children).length === 0;
|
||||
}
|
||||
|
||||
getSession(): IDebugSession | undefined {
|
||||
if (this._parent) {
|
||||
return this._parent.getSession();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setSource(session: IDebugSession, source: Source): void {
|
||||
this._source = source;
|
||||
this._children = {};
|
||||
if (source.raw && source.raw.sources) {
|
||||
for (const src of source.raw.sources) {
|
||||
if (src.name && src.path) {
|
||||
const s = new BaseTreeItem(this, src.name);
|
||||
this._children[src.path] = s;
|
||||
const ss = session.getSource(src);
|
||||
s.setSource(session, ss);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createIfNeeded<T extends BaseTreeItem>(key: string, factory: (parent: BaseTreeItem, label: string) => T): T {
|
||||
let child = <T>this._children[key];
|
||||
if (!child) {
|
||||
child = factory(this, key);
|
||||
this._children[key] = child;
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
getChild(key: string): BaseTreeItem {
|
||||
return this._children[key];
|
||||
}
|
||||
|
||||
remove(key: string): void {
|
||||
delete this._children[key];
|
||||
}
|
||||
|
||||
removeFromParent(): void {
|
||||
if (this._parent) {
|
||||
this._parent.remove(this._label);
|
||||
if (Object.keys(this._parent._children).length === 0) {
|
||||
this._parent.removeFromParent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTemplateId(): string {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
// a dynamic ID based on the parent chain; required for reparenting (see #55448)
|
||||
getId(): string {
|
||||
const parent = this.getParent();
|
||||
return parent ? `${parent.getId()}/${this._label}` : this._label;
|
||||
}
|
||||
|
||||
// skips intermediate single-child nodes
|
||||
getParent(): BaseTreeItem | undefined {
|
||||
if (this._parent) {
|
||||
if (this._parent.isSkipped()) {
|
||||
return this._parent.getParent();
|
||||
}
|
||||
return this._parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isSkipped(): boolean {
|
||||
if (this._parent) {
|
||||
if (this._parent.oneChild()) {
|
||||
return true; // skipped if I'm the only child of my parents
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true; // roots are never skipped
|
||||
}
|
||||
|
||||
// skips intermediate single-child nodes
|
||||
hasChildren(): boolean {
|
||||
const child = this.oneChild();
|
||||
if (child) {
|
||||
return child.hasChildren();
|
||||
}
|
||||
return Object.keys(this._children).length > 0;
|
||||
}
|
||||
|
||||
// skips intermediate single-child nodes
|
||||
getChildren(): Promise<BaseTreeItem[]> {
|
||||
const child = this.oneChild();
|
||||
if (child) {
|
||||
return child.getChildren();
|
||||
}
|
||||
const array = Object.keys(this._children).map(key => this._children[key]);
|
||||
return Promise.resolve(array.sort((a, b) => this.compare(a, b)));
|
||||
}
|
||||
|
||||
// skips intermediate single-child nodes
|
||||
getLabel(separateRootFolder = true): string {
|
||||
const child = this.oneChild();
|
||||
if (child) {
|
||||
const sep = (this instanceof RootFolderTreeItem && separateRootFolder) ? ' • ' : posix.sep;
|
||||
return `${this._label}${sep}${child.getLabel()}`;
|
||||
}
|
||||
return this._label;
|
||||
}
|
||||
|
||||
// skips intermediate single-child nodes
|
||||
getHoverLabel(): string | undefined {
|
||||
if (this._source && this._parent && this._parent._source) {
|
||||
return this._source.raw.path || this._source.raw.name;
|
||||
}
|
||||
let label = this.getLabel(false);
|
||||
const parent = this.getParent();
|
||||
if (parent) {
|
||||
const hover = parent.getHoverLabel();
|
||||
if (hover) {
|
||||
return `${hover}/${label}`;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
// skips intermediate single-child nodes
|
||||
getSource(): Source {
|
||||
const child = this.oneChild();
|
||||
if (child) {
|
||||
return child.getSource();
|
||||
}
|
||||
return this._source;
|
||||
}
|
||||
|
||||
protected compare(a: BaseTreeItem, b: BaseTreeItem): number {
|
||||
if (a._label && b._label) {
|
||||
return a._label.localeCompare(b._label);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private oneChild(): BaseTreeItem | undefined {
|
||||
if (SMART && !this._source && !this._showedMoreThanOne && !(this instanceof RootFolderTreeItem) && !(this instanceof SessionTreeItem)) {
|
||||
const keys = Object.keys(this._children);
|
||||
if (keys.length === 1) {
|
||||
return this._children[keys[0]];
|
||||
}
|
||||
// if a node had more than one child once, it will never be skipped again
|
||||
if (keys.length > 1) {
|
||||
this._showedMoreThanOne = true;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class RootFolderTreeItem extends BaseTreeItem {
|
||||
|
||||
constructor(parent: BaseTreeItem, public folder: IWorkspaceFolder) {
|
||||
super(parent, folder.name);
|
||||
}
|
||||
}
|
||||
|
||||
class RootTreeItem extends BaseTreeItem {
|
||||
|
||||
constructor(private _debugModel: IDebugModel, private _environmentService: IEnvironmentService, private _contextService: IWorkspaceContextService) {
|
||||
super(undefined, 'Root');
|
||||
this._debugModel.getSessions().forEach(session => {
|
||||
this.add(session);
|
||||
});
|
||||
}
|
||||
|
||||
add(session: IDebugSession): SessionTreeItem {
|
||||
return this.createIfNeeded(session.getId(), () => new SessionTreeItem(this, session, this._environmentService, this._contextService));
|
||||
}
|
||||
|
||||
find(session: IDebugSession): SessionTreeItem {
|
||||
return <SessionTreeItem>this.getChild(session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
class SessionTreeItem extends BaseTreeItem {
|
||||
|
||||
private static URL_REGEXP = /^(https?:\/\/[^/]+)(\/.*)$/;
|
||||
|
||||
private _session: IDebugSession;
|
||||
private _initialized: boolean;
|
||||
private _map: Map<string, BaseTreeItem>;
|
||||
|
||||
constructor(parent: BaseTreeItem, session: IDebugSession, private _environmentService: IEnvironmentService, private rootProvider: IWorkspaceContextService) {
|
||||
super(parent, session.getLabel());
|
||||
this._initialized = false;
|
||||
this._session = session;
|
||||
this._map = new Map();
|
||||
}
|
||||
|
||||
getSession(): IDebugSession {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
getHoverLabel(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
hasChildren(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getChildren(): Promise<BaseTreeItem[]> {
|
||||
|
||||
if (!this._initialized) {
|
||||
this._initialized = true;
|
||||
return this._session.getLoadedSources().then(paths => {
|
||||
paths.forEach(path => this.addPath(path));
|
||||
return super.getChildren();
|
||||
});
|
||||
}
|
||||
|
||||
return super.getChildren();
|
||||
}
|
||||
|
||||
protected compare(a: BaseTreeItem, b: BaseTreeItem): number {
|
||||
const acat = this.category(a);
|
||||
const bcat = this.category(b);
|
||||
if (acat !== bcat) {
|
||||
return acat - bcat;
|
||||
}
|
||||
return super.compare(a, b);
|
||||
}
|
||||
|
||||
private category(item: BaseTreeItem): number {
|
||||
|
||||
// workspace scripts come at the beginning in "folder" order
|
||||
if (item instanceof RootFolderTreeItem) {
|
||||
return item.folder.index;
|
||||
}
|
||||
|
||||
// <...> come at the very end
|
||||
const l = item.getLabel();
|
||||
if (l && /^<.+>$/.test(l)) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
// everything else in between
|
||||
return 999;
|
||||
}
|
||||
|
||||
addPath(source: Source): void {
|
||||
|
||||
let folder: IWorkspaceFolder | null;
|
||||
let url: string;
|
||||
|
||||
let path = source.raw.path;
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = SessionTreeItem.URL_REGEXP.exec(path);
|
||||
if (match && match.length === 3) {
|
||||
url = match[1];
|
||||
path = decodeURI(match[2]);
|
||||
} else {
|
||||
if (isAbsolute(path)) {
|
||||
const resource = URI.file(path);
|
||||
|
||||
// return early if we can resolve a relative path label from the root folder
|
||||
folder = this.rootProvider ? this.rootProvider.getWorkspaceFolder(resource) : null;
|
||||
if (folder) {
|
||||
// strip off the root folder path
|
||||
path = normalize(ltrim(resource.path.substr(folder.uri.path.length), posix.sep));
|
||||
const hasMultipleRoots = this.rootProvider.getWorkspace().folders.length > 1;
|
||||
if (hasMultipleRoots) {
|
||||
path = posix.sep + path;
|
||||
} else {
|
||||
// don't show root folder
|
||||
folder = null;
|
||||
}
|
||||
} else {
|
||||
// on unix try to tildify absolute paths
|
||||
path = normalize(path);
|
||||
if (!isWindows) {
|
||||
path = tildify(path, this._environmentService.userHome);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let leaf: BaseTreeItem = this;
|
||||
path.split(/[\/\\]/).forEach((segment, i) => {
|
||||
if (i === 0 && folder) {
|
||||
const f = folder;
|
||||
leaf = leaf.createIfNeeded(folder.name, parent => new RootFolderTreeItem(parent, f));
|
||||
} else if (i === 0 && url) {
|
||||
leaf = leaf.createIfNeeded(url, parent => new BaseTreeItem(parent, url));
|
||||
} else {
|
||||
leaf = leaf.createIfNeeded(segment, parent => new BaseTreeItem(parent, segment));
|
||||
}
|
||||
});
|
||||
|
||||
leaf.setSource(this._session, source);
|
||||
if (source.raw.path) {
|
||||
this._map.set(source.raw.path, leaf);
|
||||
}
|
||||
}
|
||||
|
||||
removePath(source: Source): boolean {
|
||||
if (source.raw.path) {
|
||||
const leaf = this._map.get(source.raw.path);
|
||||
if (leaf) {
|
||||
leaf.removeFromParent();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadedScriptsView extends ViewletPanel {
|
||||
|
||||
private treeContainer: HTMLElement;
|
||||
private loadedScriptsItemType: IContextKey<string>;
|
||||
private tree: WorkbenchAsyncDataTree<LoadedScriptsItem, LoadedScriptsItem, FuzzyScore>;
|
||||
private treeLabels: ResourceLabels;
|
||||
private changeScheduler: RunOnceScheduler;
|
||||
private treeNeedsRefreshOnVisible: boolean;
|
||||
private filter: LoadedScriptsFilter;
|
||||
|
||||
constructor(
|
||||
options: IViewletViewOptions,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IContextKeyService readonly contextKeyService: IContextKeyService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
) {
|
||||
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService);
|
||||
this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
renderBody(container: HTMLElement): void {
|
||||
dom.addClass(container, 'debug-loaded-scripts');
|
||||
dom.addClass(container, 'show-file-icons');
|
||||
|
||||
this.treeContainer = renderViewTree(container);
|
||||
|
||||
this.filter = new LoadedScriptsFilter();
|
||||
|
||||
const root = new RootTreeItem(this.debugService.getModel(), this.environmentService, this.contextService);
|
||||
|
||||
this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
|
||||
this.disposables.push(this.treeLabels);
|
||||
|
||||
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new LoadedScriptsDelegate(),
|
||||
[new LoadedScriptsRenderer(this.treeLabels)],
|
||||
new LoadedScriptsDataSource(),
|
||||
{
|
||||
identityProvider: {
|
||||
getId: element => (<LoadedScriptsItem>element).getId()
|
||||
},
|
||||
keyboardNavigationLabelProvider: {
|
||||
getKeyboardNavigationLabel: element => (<LoadedScriptsItem>element).getLabel()
|
||||
},
|
||||
filter: this.filter,
|
||||
accessibilityProvider: new LoadedSciptsAccessibilityProvider(),
|
||||
ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts"),
|
||||
}
|
||||
) as WorkbenchAsyncDataTree<LoadedScriptsItem, LoadedScriptsItem, FuzzyScore>;
|
||||
|
||||
this.tree.setInput(root);
|
||||
|
||||
this.changeScheduler = new RunOnceScheduler(() => {
|
||||
this.treeNeedsRefreshOnVisible = false;
|
||||
if (this.tree) {
|
||||
this.tree.updateChildren();
|
||||
}
|
||||
}, 300);
|
||||
this.disposables.push(this.changeScheduler);
|
||||
|
||||
const loadedScriptsNavigator = new TreeResourceNavigator2(this.tree);
|
||||
this.disposables.push(loadedScriptsNavigator);
|
||||
this.disposables.push(loadedScriptsNavigator.onDidOpenResource(e => {
|
||||
if (e.element instanceof BaseTreeItem) {
|
||||
const source = e.element.getSource();
|
||||
if (source && source.available) {
|
||||
const nullRange = { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 };
|
||||
source.openInEditor(this.editorService, nullRange, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(this.tree.onDidChangeFocus(() => {
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus instanceof SessionTreeItem) {
|
||||
this.loadedScriptsItemType.set('session');
|
||||
} else {
|
||||
this.loadedScriptsItemType.reset();
|
||||
}
|
||||
}));
|
||||
|
||||
const registerLoadedSourceListener = (session: IDebugSession) => {
|
||||
this.disposables.push(session.onDidLoadedSource(event => {
|
||||
let sessionRoot: SessionTreeItem;
|
||||
switch (event.reason) {
|
||||
case 'new':
|
||||
case 'changed':
|
||||
sessionRoot = root.add(session);
|
||||
sessionRoot.addPath(event.source);
|
||||
if (this.isBodyVisible()) {
|
||||
this.changeScheduler.schedule();
|
||||
} else {
|
||||
this.treeNeedsRefreshOnVisible = true;
|
||||
}
|
||||
if (event.reason === 'changed') {
|
||||
DebugContentProvider.refreshDebugContent(event.source.uri);
|
||||
}
|
||||
break;
|
||||
case 'removed':
|
||||
sessionRoot = root.find(session);
|
||||
if (sessionRoot && sessionRoot.removePath(event.source)) {
|
||||
if (this.isBodyVisible()) {
|
||||
this.changeScheduler.schedule();
|
||||
} else {
|
||||
this.treeNeedsRefreshOnVisible = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.filter.setFilter(event.source.name);
|
||||
this.tree.refilter();
|
||||
break;
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
this.disposables.push(this.debugService.onDidNewSession(registerLoadedSourceListener));
|
||||
this.debugService.getModel().getSessions().forEach(registerLoadedSourceListener);
|
||||
|
||||
this.disposables.push(this.debugService.onDidEndSession(session => {
|
||||
root.remove(session.getId());
|
||||
this.changeScheduler.schedule();
|
||||
}));
|
||||
|
||||
this.changeScheduler.schedule(0);
|
||||
|
||||
this.disposables.push(this.onDidChangeBodyVisibility(visible => {
|
||||
if (visible && this.treeNeedsRefreshOnVisible) {
|
||||
this.changeScheduler.schedule();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
layoutBody(height: number, width: number): void {
|
||||
this.tree.layout(height, width);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.tree = dispose(this.tree);
|
||||
this.treeLabels = dispose(this.treeLabels);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class LoadedScriptsDelegate implements IListVirtualDelegate<LoadedScriptsItem> {
|
||||
|
||||
getHeight(element: LoadedScriptsItem): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: LoadedScriptsItem): string {
|
||||
return LoadedScriptsRenderer.ID;
|
||||
}
|
||||
}
|
||||
|
||||
class LoadedScriptsDataSource implements IAsyncDataSource<LoadedScriptsItem, LoadedScriptsItem> {
|
||||
|
||||
hasChildren(element: LoadedScriptsItem): boolean {
|
||||
return element.hasChildren();
|
||||
}
|
||||
|
||||
getChildren(element: LoadedScriptsItem): Promise<LoadedScriptsItem[]> {
|
||||
return element.getChildren();
|
||||
}
|
||||
}
|
||||
|
||||
interface ILoadedScriptsItemTemplateData {
|
||||
label: IResourceLabel;
|
||||
}
|
||||
|
||||
class LoadedScriptsRenderer implements ITreeRenderer<BaseTreeItem, FuzzyScore, ILoadedScriptsItemTemplateData> {
|
||||
|
||||
static readonly ID = 'lsrenderer';
|
||||
|
||||
constructor(
|
||||
private labels: ResourceLabels
|
||||
) {
|
||||
}
|
||||
|
||||
get templateId(): string {
|
||||
return LoadedScriptsRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): ILoadedScriptsItemTemplateData {
|
||||
const label = this.labels.create(container, { supportHighlights: true });
|
||||
return { label };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<BaseTreeItem, FuzzyScore>, index: number, data: ILoadedScriptsItemTemplateData): void {
|
||||
|
||||
const element = node.element;
|
||||
|
||||
const label: IResourceLabelProps = {
|
||||
name: element.getLabel()
|
||||
};
|
||||
const options: IResourceLabelOptions = {
|
||||
title: element.getHoverLabel()
|
||||
};
|
||||
|
||||
if (element instanceof RootFolderTreeItem) {
|
||||
|
||||
options.fileKind = FileKind.ROOT_FOLDER;
|
||||
|
||||
} else if (element instanceof SessionTreeItem) {
|
||||
|
||||
options.title = nls.localize('loadedScriptsSession', "Debug Session");
|
||||
options.hideIcon = true;
|
||||
|
||||
} else if (element instanceof BaseTreeItem) {
|
||||
|
||||
const src = element.getSource();
|
||||
if (src && src.uri) {
|
||||
label.resource = src.uri;
|
||||
options.fileKind = FileKind.FILE;
|
||||
} else {
|
||||
options.fileKind = FileKind.FOLDER;
|
||||
}
|
||||
}
|
||||
options.matches = createMatches(node.filterData);
|
||||
|
||||
data.label.setResource(label, options);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ILoadedScriptsItemTemplateData): void {
|
||||
templateData.label.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class LoadedSciptsAccessibilityProvider implements IAccessibilityProvider<LoadedScriptsItem> {
|
||||
|
||||
getAriaLabel(element: LoadedScriptsItem): string {
|
||||
|
||||
if (element instanceof RootFolderTreeItem) {
|
||||
return nls.localize('loadedScriptsRootFolderAriaLabel', "Workspace folder {0}, loaded script, debug", element.getLabel());
|
||||
}
|
||||
|
||||
if (element instanceof SessionTreeItem) {
|
||||
return nls.localize('loadedScriptsSessionAriaLabel', "Session {0}, loaded script, debug", element.getLabel());
|
||||
}
|
||||
|
||||
if (element.hasChildren()) {
|
||||
return nls.localize('loadedScriptsFolderAriaLabel', "Folder {0}, loaded script, debug", element.getLabel());
|
||||
} else {
|
||||
return nls.localize('loadedScriptsSourceAriaLabel', "{0}, loaded script, debug", element.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LoadedScriptsFilter implements ITreeFilter<BaseTreeItem, FuzzyScore> {
|
||||
|
||||
private filterText: string;
|
||||
|
||||
setFilter(filterText: string) {
|
||||
this.filterText = filterText;
|
||||
}
|
||||
|
||||
filter(element: BaseTreeItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
|
||||
|
||||
if (!this.filterText) {
|
||||
return TreeVisibility.Visible;
|
||||
}
|
||||
|
||||
if (element.isLeaf()) {
|
||||
const name = element.getLabel();
|
||||
if (name.indexOf(this.filterText) >= 0) {
|
||||
return TreeVisibility.Visible;
|
||||
}
|
||||
return TreeVisibility.Hidden;
|
||||
}
|
||||
return TreeVisibility.Recurse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0;}.icon-white{fill:#fff;}</style></defs><title>add-focus</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="iconBg"><path class="icon-white" d="M13,7V9H9v4H7V9H3V7H7V3H9V7Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>add</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M14,6v4H10v4H6V10H2V6H6V2h4V6Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,7V9H9v4H7V9H3V7H7V3H9V7Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 486 B |
1
src/vs/workbench/contrib/debug/browser/media/add.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>add</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M14,6v4H10v4H6V10H2V6H6V2h4V6Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,7V9H9v4H7V9H3V7H7V3H9V7Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 486 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.icon-vs-out{fill:#f6f6f6;}.cls-1{fill-opacity:0;}.icon-disabled-grey{fill:#848484;}.icon-white{fill:#fff;}</style></defs><title>breakpoint-conditional-disabled</title><g id="canvas"><path class="cls-1" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.8,8A4.8,4.8,0,1,1,8,3.2,4.806,4.806,0,0,1,12.8,8Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M11.8,8A3.8,3.8,0,1,1,8,4.2,3.8,3.8,0,0,1,11.8,8Z"/><path class="icon-white" d="M10.1,6.7v.8H5.9V6.7ZM5.9,9.2h4.2V8.4H5.9Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 620 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-disabled-grey{fill:#848484;}</style></defs><title>breakpoint-conditional-unverified</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.632,8A4.632,4.632,0,1,1,8,3.368,4.638,4.638,0,0,1,12.632,8Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M6.526,8.421H9.474v.842H6.526ZM11.789,8A3.789,3.789,0,1,1,8,4.211,3.788,3.788,0,0,1,11.789,8Zm-1,0A2.789,2.789,0,1,0,8,10.79,2.793,2.793,0,0,0,10.789,8ZM6.526,7.579H9.474V6.737H6.526Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 719 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.icon-vs-out{fill:#f6f6f6;}.cls-1{fill-opacity:0;}.icon-vs-red{fill:#e51400;}.icon-white{fill:#fff;}</style></defs><title>breakpoint-conditional</title><g id="canvas"><path class="cls-1" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.8,8A4.8,4.8,0,1,1,8,3.2,4.806,4.806,0,0,1,12.8,8Z"/></g><g id="iconBg"><path class="icon-vs-red" d="M11.8,8A3.8,3.8,0,1,1,8,4.2,3.8,3.8,0,0,1,11.8,8Z"/><path class="icon-white" d="M10.1,6.7v.8H5.9V6.7ZM5.9,9.2h4.2V8.4H5.9Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 597 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-disabled-grey{fill:#848484;}</style></defs><title>breakpoint-disabled</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.632,8A4.632,4.632,0,1,1,8,3.368,4.638,4.638,0,0,1,12.632,8Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M11.789,8A3.789,3.789,0,1,1,8,4.211,3.788,3.788,0,0,1,11.789,8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 585 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-disabled-grey{fill:#848484;}</style></defs><title>breakpoint-function-disabled</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.7,13H2.3L8,2.741Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M12,12H4L8,4.8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 504 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-disabled-grey{fill:#848484;}</style></defs><title>breakpoint-function-unverified</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.95,13H2.05L8,2.291Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M8,4.35,3.75,12h8.5ZM8,6.924l2.125,3.826H5.875Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 540 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-red{fill:#e51400;}</style></defs><title>breakpoint-function</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.7,13H2.3L8,2.741Z"/></g><g id="iconBg"><path class="icon-vs-red" d="M12,12H4L8,4.8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.cls-1{fill:#e51400;opacity:0.4;}</style></defs><title>breakpoint-hint</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.632,8A4.632,4.632,0,1,1,8,3.368,4.638,4.638,0,0,1,12.632,8Z"/></g><g id="iconBg"><path class="cls-1" d="M11.789,8A3.789,3.789,0,1,1,8,4.211,3.788,3.788,0,0,1,11.789,8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 567 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-disabled-grey{fill:#848484;}</style></defs><title>breakpoint-log-disabled</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.414,8,8,13.414,2.586,8,8,2.586Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M12,8,8,12,4,8,8,4Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-disabled-grey{fill:#848484;}</style></defs><title>breakpoint-log-unverified</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.664,8,8,13.664,2.336,8,8,2.336Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M8,3.75,3.75,8,8,12.25,12.25,8ZM5.518,8,8,5.518,10.482,8,8,10.482Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 566 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-red{fill:#e51400;}</style></defs><title>breakpoint-log</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.414,8,8,13.414,2.586,8,8,2.586Z"/></g><g id="iconBg"><path class="icon-vs-red" d="M12,8,8,12,4,8,8,4Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 494 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-red{fill:#e51400;}.icon-white{fill:#fff;}</style></defs><title>breakpoint-unsupported</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.632,8A4.632,4.632,0,1,1,8,3.368,4.638,4.638,0,0,1,12.632,8Z"/></g><g id="iconBg"><path class="icon-vs-red" d="M11.789,8A3.789,3.789,0,1,1,8,4.211,3.788,3.788,0,0,1,11.789,8Z"/><path class="icon-white" d="M7.5,9.5h1v1h-1Zm0-4v3h1v-3Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 656 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-disabled-grey{fill:#848484;}</style></defs><title>breakpoint-unverified</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.632,8A4.632,4.632,0,1,1,8,3.368,4.638,4.638,0,0,1,12.632,8Z"/></g><g id="iconBg"><path class="icon-disabled-grey" d="M8,4.211A3.789,3.789,0,1,0,11.79,8,3.788,3.788,0,0,0,8,4.211ZM8,10.29A2.29,2.29,0,1,1,10.29,8,2.292,2.292,0,0,1,8,10.29Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 644 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-red{fill:#e51400;}</style></defs><title>breakpoint</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M12.632,8A4.632,4.632,0,1,1,8,3.368,4.638,4.638,0,0,1,12.632,8Z"/></g><g id="iconBg"><path class="icon-vs-red" d="M11.789,8A3.789,3.789,0,1,1,8,4.211,3.788,3.788,0,0,1,11.789,8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 562 B |
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget {
|
||||
display: flex;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
|
||||
.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .breakpoint-select-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .inputContainer {
|
||||
flex: 1;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>breakpoints-activate</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M16,5.5A5.536,5.536,0,0,1,11,11a5.536,5.536,0,0,1-5.5,5A5.549,5.549,0,0,1,0,10.5,5.465,5.465,0,0,1,5,5a5.512,5.512,0,0,1,11,.5Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M15,5.5A4.395,4.395,0,0,1,11,10a2.957,2.957,0,0,0-.2-1A3.565,3.565,0,0,0,14,5.5a3.507,3.507,0,0,0-7-.3A3.552,3.552,0,0,0,6,5a4.622,4.622,0,0,1,4.5-4A4.481,4.481,0,0,1,15,5.5ZM5.5,6A4.5,4.5,0,1,0,10,10.5,4.5,4.5,0,0,0,5.5,6Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 795 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>breakpoints-activate</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M16,5.5A5.536,5.536,0,0,1,11,11a5.536,5.536,0,0,1-5.5,5A5.549,5.549,0,0,1,0,10.5,5.465,5.465,0,0,1,5,5a5.512,5.512,0,0,1,11,.5Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M15,5.5A4.395,4.395,0,0,1,11,10a2.957,2.957,0,0,0-.2-1A3.565,3.565,0,0,0,14,5.5a3.507,3.507,0,0,0-7-.3A3.552,3.552,0,0,0,6,5a4.622,4.622,0,0,1,4.5-4A4.481,4.481,0,0,1,15,5.5ZM5.5,6A4.5,4.5,0,1,0,10,10.5,4.5,4.5,0,0,0,5.5,6Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 794 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.065 13H15v2H2.056v-2h5.009zm3.661-12H7.385L8.44 2.061 7.505 3H15V1h-4.274zM3.237 9H2.056v2H15V9H3.237zm4.208-4l.995 1-.995 1H15V5H7.445z" fill="#C5C5C5"/><path d="M5.072 4.03L7.032 6 5.978 7.061l-1.96-1.97-1.961 1.97L1 6l1.96-1.97L1 2.061 2.056 1l1.96 1.97L5.977 1l1.057 1.061L5.072 4.03z" fill="#F48771"/></svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.065 13H15v2H2.056v-2h5.009zm3.661-12H7.385L8.44 2.061 7.505 3H15V1h-4.274zM3.237 9H2.056v2H15V9H3.237zm4.208-4l.995 1-.995 1H15V5H7.445z" fill="#424242"/><path d="M5.072 4.03L7.032 6 5.978 7.061l-1.96-1.97-1.961 1.97L1 6l1.96-1.97L1 2.061 2.056 1l1.96 1.97L5.977 1l1.057 1.061L5.072 4.03z" fill="#A1260D"/></svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>configure</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M16,10.015l-2.238.372,1.318,1.847L12.233,15.08l-1.847-1.318L10.013,16H5.986l-.373-2.237L3.767,15.08.919,12.233l1.319-1.847L0,10.013V5.986l2.238-.373L.919,3.767,3.768.919,5.613,2.238,5.986,0h4.028l.372,2.238L12.233.919,15.08,3.768,13.762,5.613,16,5.986Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M12.876,9.521,15,9.167V6.834L12.879,6.48a5.12,5.12,0,0,0-.354-.854l1.25-1.75-1.65-1.65L10.373,3.477c-.137-.072-.262-.159-.408-.219s-.3-.087-.444-.133L9.167,1H6.834L6.48,3.121a5.118,5.118,0,0,0-.854.354l-1.75-1.25-1.65,1.65L3.477,5.627c-.072.137-.159.262-.219.408s-.087.3-.133.444L1,6.833V9.166l2.121.354a5.122,5.122,0,0,0,.354.854l-1.25,1.75,1.65,1.65,1.752-1.252c.137.072.262.159.408.22s.3.087.444.133L6.833,15H9.166l.354-2.121a5.121,5.121,0,0,0,.854-.354l1.75,1.25,1.65-1.65-1.252-1.752c.072-.137.159-.263.219-.409S12.83,9.669,12.876,9.521ZM8,10.212A2.212,2.212,0,1,1,10.212,8,2.212,2.212,0,0,1,8,10.212Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>configure</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M16,10.015l-2.238.372,1.318,1.847L12.233,15.08l-1.847-1.318L10.013,16H5.986l-.373-2.237L3.767,15.08.919,12.233l1.319-1.847L0,10.013V5.986l2.238-.373L.919,3.767,3.768.919,5.613,2.238,5.986,0h4.028l.372,2.238L12.233.919,15.08,3.768,13.762,5.613,16,5.986Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M12.876,9.521,15,9.167V6.834L12.879,6.48a5.12,5.12,0,0,0-.354-.854l1.25-1.75-1.65-1.65L10.373,3.477c-.137-.072-.262-.159-.408-.219s-.3-.087-.444-.133L9.167,1H6.834L6.48,3.121a5.118,5.118,0,0,0-.854.354l-1.75-1.25-1.65,1.65L3.477,5.627c-.072.137-.159.262-.219.408s-.087.3-.133.444L1,6.833V9.166l2.121.354a5.122,5.122,0,0,0,.354.854l-1.25,1.75,1.65,1.65,1.752-1.252c.137.072.262.159.408.22s.3.087.444.133L6.833,15H9.166l.354-2.121a5.121,5.121,0,0,0,.854-.354l1.75,1.25,1.65-1.65-1.252-1.752c.072-.137.159-.263.219-.409S12.83,9.669,12.876,9.521ZM8,10.212A2.212,2.212,0,1,1,10.212,8,2.212,2.212,0,0,1,8,10.212Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/vs/workbench/contrib/debug/browser/media/continue-inverse.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-action-blue{fill:#75beff;}</style></defs><title>continue</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M15.64,8l-7.8,6H2V2H7.84Z"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M3,3H5V13H3ZM7.5,3V13L14,8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 502 B |
1
src/vs/workbench/contrib/debug/browser/media/continue.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-action-blue{fill:#00539c;}</style></defs><title>continue</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M15.64,8l-7.8,6H2V2H7.84Z"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M3,3H5V13H3ZM7.5,3V13L14,8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-yellow{fill:#fc0;}.icon-vs-red{fill:#e51400;}</style></defs><title>current-and-breakpoint</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.829,8,9.454,13H4V3H9.454Z"/></g><g id="iconBg"><path class="icon-vs-yellow" d="M12.5,8,9,12H5V4H9Z"/><path class="icon-vs-red" d="M10.5,8A2.5,2.5,0,1,1,8,5.5,2.5,2.5,0,0,1,10.5,8Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 607 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-yellow{fill:#fc0;}</style></defs><title>current-arrow</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13.829,8,9.454,13H4V3H9.454Z"/></g><g id="iconBg"><path class="icon-vs-yellow" d="M12.5,8,9,12H5V4H9Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 490 B |
@@ -0,0 +1 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M15.1673 18.0687V23.0247C15.1673 23.5637 15.2723 24.5 14.7315 24.5H12.8328V23.3327H14V19.6122L13.7988 19.4022C13.0604 20.0803 12.1008 20.4669 11.0986 20.49C10.0964 20.5132 9.11994 20.1714 8.351 19.5282C7 18.1737 7.13826 16.3327 8.60475 14H4.66726V15.1672H3.50001V13.2685C3.50001 12.7277 4.43626 12.8327 4.97526 12.8327H9.76326L15.1673 18.0687ZM11.6673 5.83275H10.5V4.66725H12.775C13.3123 4.66725 14 4.9245 14 5.4635V9.366L14.8593 10.3862C14.927 9.83979 15.1906 9.33644 15.6013 8.96958C16.0119 8.60271 16.5416 8.39723 17.0923 8.39125C17.2298 8.37945 17.3684 8.39492 17.5 8.43675V5.83275H18.6673V8.88825C18.703 8.99154 18.7618 9.08536 18.8391 9.16265C18.9164 9.23995 19.0102 9.29871 19.1135 9.3345H22.1673V10.5H19.5615C19.593 10.5 19.6105 10.675 19.6105 10.85C19.6058 11.4034 19.4011 11.9365 19.0341 12.3508C18.6671 12.7651 18.1626 13.0326 17.6138 13.104L18.634 14H22.5383C23.0773 14 23.3345 14.6807 23.3345 15.225V17.5H22.1673V16.3327H19.2273L11.6673 8.98275V5.83275ZM14 0C11.2311 0 8.52431 0.821086 6.22202 2.35943C3.91973 3.89776 2.12532 6.08426 1.06569 8.64243C0.00606593 11.2006 -0.271181 14.0155 0.269012 16.7313C0.809205 19.447 2.14258 21.9416 4.10051 23.8995C6.05845 25.8574 8.55301 27.1908 11.2687 27.731C13.9845 28.2712 16.7994 27.9939 19.3576 26.9343C21.9157 25.8747 24.1022 24.0803 25.6406 21.778C27.1789 19.4757 28 16.7689 28 14C28 10.287 26.525 6.72601 23.8995 4.1005C21.274 1.475 17.713 0 14 0V0ZM25.6673 14C25.6692 16.6908 24.7364 19.2988 23.0283 21.378L6.622 4.97175C8.33036 3.57269 10.4009 2.68755 12.5927 2.41935C14.7845 2.15115 17.0074 2.51091 19.0027 3.45676C20.998 4.40262 22.6836 5.89567 23.8635 7.76217C25.0433 9.62867 25.6689 11.7919 25.6673 14ZM2.33276 14C2.33066 11.3091 3.26351 8.70111 4.97176 6.622L21.378 23.03C19.6693 24.4284 17.5987 25.313 15.407 25.5807C13.2153 25.8485 10.9926 25.4884 8.99754 24.5425C7.00244 23.5965 5.31693 22.1036 4.13708 20.2373C2.95722 18.3709 2.33152 16.208 2.33276 14Z" fill="white"/></g><defs><clipPath id="clip0"><rect width="28" height="28" fill="white"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Activity Bar */
|
||||
.monaco-workbench .activitybar .monaco-action-bar .action-label.debug {
|
||||
-webkit-mask: url('debug-dark.svg') no-repeat 50% 50%;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-top-stack-frame-column::before {
|
||||
background: url('current-arrow.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-hint {
|
||||
background: url('breakpoint-hint.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-disabled,
|
||||
.monaco-editor .debug-breakpoint-column.debug-breakpoint-disabled-column::before {
|
||||
background: url('breakpoint-disabled.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-unverified,
|
||||
.monaco-editor .debug-breakpoint-column.debug-breakpoint-unverified-column::before {
|
||||
background: url('breakpoint-unverified.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-top-stack-frame {
|
||||
background: url('current-arrow.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-focused-stack-frame {
|
||||
background: url('stackframe-arrow.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint,
|
||||
.monaco-editor .debug-breakpoint-column::before {
|
||||
background: url('breakpoint.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-breakpoint-column::before,
|
||||
.monaco-editor .debug-top-stack-frame-column::before {
|
||||
content: " ";
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
margin-right: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.debug-function-breakpoint {
|
||||
background: url('breakpoint-function.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-function-breakpoint-unverified {
|
||||
background: url('breakpoint-function-unverified.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-function-breakpoint-disabled {
|
||||
background: url('breakpoint-function-disabled.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-conditional,
|
||||
.monaco-editor .debug-breakpoint-column.debug-breakpoint-conditional-column::before {
|
||||
background: url('breakpoint-conditional.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-log,
|
||||
.monaco-editor .debug-breakpoint-column.debug-breakpoint-log-column::before {
|
||||
background: url('breakpoint-log.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-log-disabled,
|
||||
.monaco-editor .debug-breakpoint-log-disabled-column::before {
|
||||
background: url('breakpoint-log-disabled.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-log-unverified,
|
||||
.monaco-editor .debug-breakpoint-log-unverified-column::before {
|
||||
background: url('breakpoint-log-unverified.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.debug-breakpoint-unsupported,
|
||||
.monaco-editor .debug-breakpoint-column.debug-breakpoint-unsupported-column::before {
|
||||
background: url('breakpoint-unsupported.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-top-stack-frame.debug-breakpoint,
|
||||
.monaco-editor .debug-top-stack-frame.debug-breakpoint-conditional,
|
||||
.monaco-editor .debug-top-stack-frame.debug-breakpoint-log,
|
||||
.monaco-editor .debug-breakpoint-column.debug-breakpoint-column.debug-top-stack-frame-column::before {
|
||||
background: url('current-and-breakpoint.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-focused-stack-frame.debug-breakpoint,
|
||||
.monaco-editor .debug-focused-stack-frame.debug-breakpoint-conditional,
|
||||
.monaco-editor .debug-focused-stack-frame.debug-breakpoint-log {
|
||||
background: url('stackframe-and-breakpoint.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
/* Error editor */
|
||||
.debug-error-editor:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.debug-error-editor {
|
||||
padding: 5px 0 0 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Debug status */
|
||||
/* A very precise css rule to overwrite the display set in statusbar.css */
|
||||
.monaco-workbench .part.statusbar > .statusbar-item > .debug-statusbar-item > a {
|
||||
display: flex;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.statusbar .debug-statusbar-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.statusbar .debug-statusbar-item .icon {
|
||||
-webkit-mask: url('start.svg') no-repeat 50% 50%;
|
||||
-webkit-mask-size: 16px;
|
||||
display: inline-block;
|
||||
padding-right: 2px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.monaco-workbench .debug-view-content .monaco-list-row .monaco-tl-contents {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Expressions */
|
||||
|
||||
.monaco-workbench .debug-viewlet .monaco-list-row .expression,
|
||||
.monaco-workbench .debug-hover-widget .monaco-list-row .expression {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
|
||||
.monaco-workbench.mac .debug-viewlet .monaco-list-row .expression,
|
||||
.monaco-workbench.mac .debug-hover-widget .monaco-list-row .expression {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .value {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* White color when element is selected and list is focused. White looks better on blue selection background. */
|
||||
.monaco-workbench .monaco-list:focus .monaco-list-row.selected .expression .name,
|
||||
.monaco-workbench .monaco-list:focus .monaco-list-row.selected .expression .value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .name {
|
||||
color: #9B46B0;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .name.virtual {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.monaco-workbench > .monaco-list-row .expression .value {
|
||||
color: rgba(108, 108, 108, 0.8);
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .unavailable {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .error {
|
||||
color: #E51400;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .value.number {
|
||||
color: #09885A;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .value.boolean {
|
||||
color: #0000FF;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-list-row .expression .value.string {
|
||||
color: #A31515;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .monaco-list-row .expression .value {
|
||||
color: rgba(204, 204, 204, 0.6);
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .monaco-list-row .expression .error {
|
||||
color: #F48771;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .monaco-list-row .expression .value.number {
|
||||
color: #B5CEA8;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .monaco-list-row .expression .value.number {
|
||||
color: #89d185;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .monaco-list-row .expression .value.boolean {
|
||||
color: #75bdfe;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .monaco-list-row .expression .value.string {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .monaco-list-row .expression .value.boolean {
|
||||
color: #4E94CE;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .monaco-list-row .expression .value.string {
|
||||
color: #CE9178;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .monaco-list-row .expression .error {
|
||||
color: #F48771;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
|
||||
.vs-dark .monaco-workbench .monaco-list-row .expression .name {
|
||||
color: #C586C0;
|
||||
}
|
||||
|
||||
/* High Contrast Theming */
|
||||
|
||||
.hc-black .monaco-workbench .monaco-list-row .expression .name {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.hc-black .monaco-editor .debug-remove-token-colors {
|
||||
color:black;
|
||||
}
|
||||
113
src/vs/workbench/contrib/debug/browser/media/debugHover.css
Normal file
@@ -0,0 +1,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .debug-hover-widget {
|
||||
position: absolute;
|
||||
margin-top: -1px;
|
||||
z-index: 50;
|
||||
animation-duration: 0.15s;
|
||||
animation-name: fadeIn;
|
||||
user-select: text;
|
||||
word-break: break-all;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .complex-value {
|
||||
width: 324px;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .complex-value .title {
|
||||
padding-left: 15px;
|
||||
padding-right: 2px;
|
||||
font-size: 11px;
|
||||
word-break: normal;
|
||||
text-overflow: ellipsis;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .debug-hover-tree {
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-row .monaco-tl-contents {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* Disable tree highlight in debug hover tree. */
|
||||
.monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-rows .monaco-list-row:hover:not(.highlighted):not(.selected):not(.focused) {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .debugHoverHighlight {
|
||||
background-color: rgba(173, 214, 255, 0.15);
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .value {
|
||||
white-space: pre-wrap;
|
||||
color: rgba(108, 108, 108, 0.8);
|
||||
overflow: auto;
|
||||
font-family: var(--monaco-monospace-font);
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .monaco-tl-contents .value {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .error {
|
||||
color: #E51400;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .value.number {
|
||||
color: #09885A;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .value.boolean {
|
||||
color: #0000FF;
|
||||
}
|
||||
|
||||
.monaco-editor .debug-hover-widget .value.string {
|
||||
color: #A31515;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
|
||||
.monaco-editor.vs-dark .debug-hover-widget .value,
|
||||
.monaco-editor.hc-black .debug-hover-widget .value {
|
||||
color: rgba(204, 204, 204, 0.6);
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .debug-hover-widget .error,
|
||||
.monaco-editor.hc-black .debug-hover-widget .error {
|
||||
color: #F48771;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .debug-hover-widget .value.number,
|
||||
.monaco-editor.hc-black .debug-hover-widget .value.number {
|
||||
color: #B5CEA8;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .debug-hover-widget .value.boolean,
|
||||
.monaco-editor.hc-black .debug-hover-widget .value.boolean {
|
||||
color: #4E94CE;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .debug-hover-widget .value.string,
|
||||
.monaco-editor.hc-black .debug-hover-widget .value.string {
|
||||
color: #CE9178;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .debugHoverHighlight,
|
||||
.monaco-editor.hc-theme .debugHoverHighlight {
|
||||
background-color: rgba(38, 79, 120, 0.25);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .debug-toolbar {
|
||||
position: absolute;
|
||||
z-index: 200;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
.monaco-workbench .debug-toolbar .monaco-action-bar .action-item {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.monaco-workbench .debug-toolbar .monaco-action-bar .action-item.select-container {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.monaco-workbench .debug-toolbar .drag-area {
|
||||
cursor: -webkit-grab;
|
||||
height: 32px;
|
||||
width: 16px;
|
||||
background: url('drag.svg') center center no-repeat;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.monaco-workbench .debug-toolbar .drag-area.dragged {
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.monaco-workbench .debug-toolbar .monaco-action-bar .action-item > .action-label {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 0;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||